Skip to content

feat: support structured_query in the directory namespace query_table#7592

Open
hamersaw wants to merge 2 commits into
lance-format:mainfrom
hamersaw:bug/dir-namespace-fts-structured-query
Open

feat: support structured_query in the directory namespace query_table#7592
hamersaw wants to merge 2 commits into
lance-format:mainfrom
hamersaw:bug/dir-namespace-fts-structured-query

Conversation

@hamersaw

@hamersaw hamersaw commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Summary

The directory namespace's query_table only handled string_query for full-text search — a structured_query was silently ignored, so the scan ran with no FTS filter and returned all rows. This wires structured_query through to the engine, so the local query_table path honors structured FTS the same way the JNI scanner and fragment.new_scan(full_text_query) already do. There was a literal // For now, we only support string_query TODO at the change site.

What changed

rust/lance-namespace-impls/src/dir.rs:

  • Added a structured_query branch in query_table that runs FullTextSearchQuery::new_query(...) built from the namespace model.
  • build_engine_fts_query maps models::FtsQuery → engine FtsQuery for all variants (match, phrase, multi_match, and recursively boolean/boost), mirroring the mapping in lance-jni/src/blocking_scanner.rs.
  • build_engine_match_query copies terms / column / boost / fuzziness / max_expansions / operator / prefix_length, parsing the operator string via Operator::try_from.
  • An FtsQuery with no variant set is now rejected with a clear error instead of silently ignored.
  • string_query behavior is unchanged.

Tests

Unit tests for the mapping — test_build_engine_fts_query_match (all MatchQuery options round-trip into the engine query) and test_build_engine_fts_query_requires_a_variant (empty query is rejected). cargo test -p lance-namespace-impls, cargo check -p lance-namespace-impls, and cargo fmt --check all pass.

Motivation

Downstream connectors (e.g. lance-spark) route full-text search to the namespace query_table and build the richer structured_query form (fuzziness / operator / phrase / multi_match). Against a directory namespace that dropped structured_query, those queries silently returned every row instead of filtering. This makes the dir namespace honor the structured API — which is the canonical FTS query direction (the engine's FullTextSearchQuery is structured internally; string_query is the legacy convenience form, and LanceDB's remote path already prefers structured).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for structured full-text search queries when querying tables.
    • Supports match, phrase, multi-match, Boolean, and boosted search expressions.
  • Bug Fixes

    • Structured search requests are now applied correctly instead of being ignored.
    • Invalid search query formats now return consistent errors.

The directory namespace's query_table only handled string_query; a
structured_query was silently ignored, so the scan ran without any full-text
filter and returned all rows. Map the namespace structured FtsQuery model
into the engine FtsQuery for all variants — match, phrase, multi_match, and
(recursively) boolean and boost — and run it via
FullTextSearchQuery::new_query, mirroring the mapping the JNI scanner already
performs. An FtsQuery with no variant set is now rejected rather than
silently ignored. string_query behavior is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added A-namespace Namespace impls enhancement New feature or request labels Jul 2, 2026
@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.39456% with 20 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
rust/lance-namespace-impls/src/dir.rs 86.39% 11 Missing and 9 partials ⚠️

📢 Thoughts on this report? Let us know!

@wjones127 wjones127 self-requested a review July 6, 2026 23:39

@wjones127 wjones127 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly just want a little more test coverage.

Comment on lines +5385 to +5387
fn build_engine_match_query(
m: &lance_namespace::models::MatchQuery,
) -> std::result::Result<MatchQuery, NamespaceError> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Should build_engine_match_query really by a TryFrom implementation instead?

@hamersaw hamersaw Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately the same issue as the other, where TryFrom can't operate on foreign types.

Comment on lines +5335 to +5337
fn build_engine_fts_query(
query: &lance_namespace::models::FtsQuery,
) -> std::result::Result<FtsQuery, NamespaceError> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: should build_engine_fts_query be a TryFrom impl?

@hamersaw hamersaw Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to do this, but TIL TryFrom is a foreign trait so it can't be used on types that are foreign to this crate. The source (ie. lance_namespace::models::FtsQuery) and the target (ie. lance_index::scalar::inverted::query::FtsQuery) both live elsewhere, so it won't compile with TryFrom. The options would be to define TryFrom on these types in either the lance_namespace or lance_index crates, but that feels like the wrong approach?

.map(build_engine_match_query)
.collect::<std::result::Result<Vec<_>, _>>()?;
Ok(FtsQuery::MultiMatch(MultiMatchQuery { match_queries }))
} else if let Some(ref b) = query.boolean {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue(blocking): could you add test coverage for the phrase / boolean / boost query roundtrip? Looks like you just have match query.

@hamersaw hamersaw Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great call, added round trip tests for all of the FtsQuery variants.

Extends the structured_query mapping tests beyond the match variant to
exercise every FtsQuery variant build_engine_fts_query maps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Structured full-text search

Layer / File(s) Summary
FTS query mapping and validation
rust/lance-namespace-impls/src/dir.rs
Structured namespace FTS variants are translated into engine queries, with validation and unit tests covering supported variants and invalid empty queries.
query_table scanner integration
rust/lance-namespace-impls/src/dir.rs
structured_query is mapped and applied to the scanner, with mapping failures returned as NamespaceError::InvalidInput.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant query_table
  participant build_engine_fts_query
  participant Scanner
  Client->>query_table: submit structured_query
  query_table->>build_engine_fts_query: translate query
  build_engine_fts_query-->>query_table: return engine FtsQuery
  query_table->>Scanner: apply FtsQuery
  Scanner-->>Client: return query results
Loading

Suggested reviewers: jackye1995, xuqianjin-stars

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding structured_query support to directory namespace query_table.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
rust/lance-namespace-impls/src/dir.rs (1)

4886-4917: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Validate string_query/structured_query mutual exclusivity; dedupe the apply-to-scanner call.

If a caller sets both fts_query.string_query and fts_query.structured_query, this silently applies only string_query and drops structured_query with no error. The else if also duplicates the scanner.full_text_search(fts).map_err(...) block from the if branch.

🛠️ Proposed fix: reject ambiguous input and apply once
         if let Some(ref fts_query) = request.full_text_query {
+            if fts_query.string_query.is_some() && fts_query.structured_query.is_some() {
+                return Err(NamespaceError::InvalidInput {
+                    message: "full_text_query must set exactly one of string_query or structured_query".to_string(),
+                }
+                .into());
+            }
             // Handle string_query (simple string FTS)
-            if let Some(ref string_query) = fts_query.string_query {
+            let fts = if let Some(ref string_query) = fts_query.string_query {
                 let mut fts = FullTextSearchQuery::new(string_query.query.clone());
                 if let Some(ref columns) = string_query.columns
                     && !columns.is_empty()
                 {
                     fts = fts
                         .with_columns(columns)
                         .map_err(|e| NamespaceError::InvalidInput {
                             message: format!("Invalid FTS columns: {:?}", e),
                         })?;
                 }
-                scanner
-                    .full_text_search(fts)
-                    .map_err(|e| NamespaceError::InvalidInput {
-                        message: format!("Invalid full text search: {:?}", e),
-                    })?;
+                Some(fts)
             } else if let Some(ref structured_query) = fts_query.structured_query {
-                // Structured FTS: map the namespace query model into the engine FtsQuery.
                 let engine_query = build_engine_fts_query(&structured_query.query)?;
-                let fts = FullTextSearchQuery::new_query(engine_query);
+                Some(FullTextSearchQuery::new_query(engine_query))
+            } else {
+                None
+            };
+            if let Some(fts) = fts {
                 scanner
                     .full_text_search(fts)
                     .map_err(|e| NamespaceError::InvalidInput {
                         message: format!("Invalid full text search: {:?}", e),
                     })?;
             }
         }

As per coding guidelines, "Validate mutually exclusive builder or configuration options and report a clear error when both are set" and "Extract logic repeated in two or more places into a shared helper."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance-namespace-impls/src/dir.rs` around lines 4886 - 4917, Update the
`fts_query` handling to reject requests where both `string_query` and
`structured_query` are set, returning a clear `NamespaceError::InvalidInput`;
otherwise build one `FullTextSearchQuery` from the selected variant and call
`scanner.full_text_search(...).map_err(...)` once after the branching logic.
Preserve the existing column validation and structured-query conversion
behavior.

Source: Coding guidelines

🧹 Nitpick comments (1)
rust/lance-namespace-impls/src/dir.rs (1)

5332-5552: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting the FTS query mapping (+ its tests) into a dedicated submodule.

dir.rs is already ~13,900 lines. build_engine_fts_query/build_engine_match_query and their tests form a self-contained unit with minimal coupling to the rest of the file and would fit well in their own module (e.g. dir/fts_query.rs).

As per coding guidelines, "Extract substantial new logic such as bin packing or scheduling into dedicated submodules instead of inlining it into large files."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance-namespace-impls/src/dir.rs` around lines 5332 - 5552, Extract
build_engine_fts_query, build_engine_match_query, and their associated tests
into a dedicated fts_query submodule under dir; update module declarations and
imports so dir.rs continues using the mapping functions without changing
behavior or test coverage.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rust/lance-namespace-impls/src/dir.rs`:
- Around line 5335-5383: Update build_engine_fts_query to validate that exactly
one of match, phrase, multi_match, boolean, or boost is set before constructing
the engine query. Reject inputs with multiple variants using the existing
NamespaceError::InvalidInput path and a clear message, while preserving the
current conversion logic for valid single-variant queries and the existing error
for none set.
- Around line 5547-5552: Strengthen
test_build_engine_fts_query_requires_a_variant by matching the returned error as
NamespaceError::InvalidInput and asserting its message contains the expected
missing-variant text. Replace the is_err-only assertion while preserving the
existing empty FtsQuery setup.
- Around line 5340-5348: Validate signed namespace-model fields before
converting them in the query conversion logic around PhraseQuery and the
fuzzy-query construction: reject negative slop, fuzziness, max_expansions, and
prefix_length with descriptive errors, then perform the existing unsigned
conversions only after validation. Confirm the actual field types in
lance_namespace::models and preserve valid-value behavior while preventing
negative inputs from wrapping.

---

Outside diff comments:
In `@rust/lance-namespace-impls/src/dir.rs`:
- Around line 4886-4917: Update the `fts_query` handling to reject requests
where both `string_query` and `structured_query` are set, returning a clear
`NamespaceError::InvalidInput`; otherwise build one `FullTextSearchQuery` from
the selected variant and call `scanner.full_text_search(...).map_err(...)` once
after the branching logic. Preserve the existing column validation and
structured-query conversion behavior.

---

Nitpick comments:
In `@rust/lance-namespace-impls/src/dir.rs`:
- Around line 5332-5552: Extract build_engine_fts_query,
build_engine_match_query, and their associated tests into a dedicated fts_query
submodule under dir; update module declarations and imports so dir.rs continues
using the mapping functions without changing behavior or test coverage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c856ff0-ac42-4923-8779-f6683f73e4e8

📥 Commits

Reviewing files that changed from the base of the PR and between 9644d16 and 19e4488.

📒 Files selected for processing (1)
  • rust/lance-namespace-impls/src/dir.rs

Comment on lines +5335 to +5383
fn build_engine_fts_query(
query: &lance_namespace::models::FtsQuery,
) -> std::result::Result<FtsQuery, NamespaceError> {
if let Some(ref m) = query.r#match {
Ok(FtsQuery::Match(build_engine_match_query(m)?))
} else if let Some(ref p) = query.phrase {
let mut phrase = PhraseQuery::new(p.terms.clone());
if let Some(ref column) = p.column {
phrase = phrase.with_column(Some(column.clone()));
}
if let Some(slop) = p.slop {
phrase = phrase.with_slop(slop as u32);
}
Ok(FtsQuery::Phrase(phrase))
} else if let Some(ref mm) = query.multi_match {
let match_queries = mm
.match_queries
.iter()
.map(build_engine_match_query)
.collect::<std::result::Result<Vec<_>, _>>()?;
Ok(FtsQuery::MultiMatch(MultiMatchQuery { match_queries }))
} else if let Some(ref b) = query.boolean {
let mut clauses: Vec<(Occur, FtsQuery)> = Vec::new();
for clause in &b.must {
clauses.push((Occur::Must, build_engine_fts_query(clause)?));
}
for clause in &b.should {
clauses.push((Occur::Should, build_engine_fts_query(clause)?));
}
for clause in &b.must_not {
clauses.push((Occur::MustNot, build_engine_fts_query(clause)?));
}
Ok(FtsQuery::Boolean(BooleanQuery::new(clauses)))
} else if let Some(ref boost) = query.boost {
let positive = build_engine_fts_query(&boost.positive)?;
let negative = build_engine_fts_query(&boost.negative)?;
Ok(FtsQuery::Boost(BoostQuery::new(
positive,
negative,
boost.negative_boost,
)))
} else {
Err(NamespaceError::InvalidInput {
message: "structured_query.query must set exactly one of match, phrase, multi_match, \
boolean, or boost"
.to_string(),
})
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

build_engine_fts_query silently picks a variant instead of rejecting ambiguous input.

The if/else if chain over match/phrase/multi_match/boolean/boost only rejects the "none set" case (line 5376-5382). If a caller sets more than one variant (e.g. both match and phrase), the function silently uses whichever is checked first and drops the rest — even though the error message on line 5378-5380 states "must set exactly one of...". This contradicts the stated contract and can silently produce a query that doesn't match what the caller intended.

🛠️ Proposed fix: count set variants and reject >1
 fn build_engine_fts_query(
     query: &lance_namespace::models::FtsQuery,
 ) -> std::result::Result<FtsQuery, NamespaceError> {
+    let set_count = [
+        query.r#match.is_some(),
+        query.phrase.is_some(),
+        query.multi_match.is_some(),
+        query.boolean.is_some(),
+        query.boost.is_some(),
+    ]
+    .into_iter()
+    .filter(|set| *set)
+    .count();
+    if set_count > 1 {
+        return Err(NamespaceError::InvalidInput {
+            message: "structured_query.query must set exactly one of match, phrase, multi_match, \
+                      boolean, or boost, but multiple were set"
+                .to_string(),
+        });
+    }
     if let Some(ref m) = query.r#match {

As per coding guidelines, "Validate mutually exclusive builder or configuration options and report a clear error when both are set."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn build_engine_fts_query(
query: &lance_namespace::models::FtsQuery,
) -> std::result::Result<FtsQuery, NamespaceError> {
if let Some(ref m) = query.r#match {
Ok(FtsQuery::Match(build_engine_match_query(m)?))
} else if let Some(ref p) = query.phrase {
let mut phrase = PhraseQuery::new(p.terms.clone());
if let Some(ref column) = p.column {
phrase = phrase.with_column(Some(column.clone()));
}
if let Some(slop) = p.slop {
phrase = phrase.with_slop(slop as u32);
}
Ok(FtsQuery::Phrase(phrase))
} else if let Some(ref mm) = query.multi_match {
let match_queries = mm
.match_queries
.iter()
.map(build_engine_match_query)
.collect::<std::result::Result<Vec<_>, _>>()?;
Ok(FtsQuery::MultiMatch(MultiMatchQuery { match_queries }))
} else if let Some(ref b) = query.boolean {
let mut clauses: Vec<(Occur, FtsQuery)> = Vec::new();
for clause in &b.must {
clauses.push((Occur::Must, build_engine_fts_query(clause)?));
}
for clause in &b.should {
clauses.push((Occur::Should, build_engine_fts_query(clause)?));
}
for clause in &b.must_not {
clauses.push((Occur::MustNot, build_engine_fts_query(clause)?));
}
Ok(FtsQuery::Boolean(BooleanQuery::new(clauses)))
} else if let Some(ref boost) = query.boost {
let positive = build_engine_fts_query(&boost.positive)?;
let negative = build_engine_fts_query(&boost.negative)?;
Ok(FtsQuery::Boost(BoostQuery::new(
positive,
negative,
boost.negative_boost,
)))
} else {
Err(NamespaceError::InvalidInput {
message: "structured_query.query must set exactly one of match, phrase, multi_match, \
boolean, or boost"
.to_string(),
})
}
}
fn build_engine_fts_query(
query: &lance_namespace::models::FtsQuery,
) -> std::result::Result<FtsQuery, NamespaceError> {
let set_count = [
query.r#match.is_some(),
query.phrase.is_some(),
query.multi_match.is_some(),
query.boolean.is_some(),
query.boost.is_some(),
]
.into_iter()
.filter(|set| *set)
.count();
if set_count > 1 {
return Err(NamespaceError::InvalidInput {
message: "structured_query.query must set exactly one of match, phrase, multi_match, \
boolean, or boost, but multiple were set"
.to_string(),
});
}
if let Some(ref m) = query.r#match {
Ok(FtsQuery::Match(build_engine_match_query(m)?))
} else if let Some(ref p) = query.phrase {
let mut phrase = PhraseQuery::new(p.terms.clone());
if let Some(ref column) = p.column {
phrase = phrase.with_column(Some(column.clone()));
}
if let Some(slop) = p.slop {
phrase = phrase.with_slop(slop as u32);
}
Ok(FtsQuery::Phrase(phrase))
} else if let Some(ref mm) = query.multi_match {
let match_queries = mm
.match_queries
.iter()
.map(build_engine_match_query)
.collect::<std::result::Result<Vec<_>, _>>()?;
Ok(FtsQuery::MultiMatch(MultiMatchQuery { match_queries }))
} else if let Some(ref b) = query.boolean {
let mut clauses: Vec<(Occur, FtsQuery)> = Vec::new();
for clause in &b.must {
clauses.push((Occur::Must, build_engine_fts_query(clause)?));
}
for clause in &b.should {
clauses.push((Occur::Should, build_engine_fts_query(clause)?));
}
for clause in &b.must_not {
clauses.push((Occur::MustNot, build_engine_fts_query(clause)?));
}
Ok(FtsQuery::Boolean(BooleanQuery::new(clauses)))
} else if let Some(ref boost) = query.boost {
let positive = build_engine_fts_query(&boost.positive)?;
let negative = build_engine_fts_query(&boost.negative)?;
Ok(FtsQuery::Boost(BoostQuery::new(
positive,
negative,
boost.negative_boost,
)))
} else {
Err(NamespaceError::InvalidInput {
message: "structured_query.query must set exactly one of match, phrase, multi_match, \
boolean, or boost"
.to_string(),
})
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance-namespace-impls/src/dir.rs` around lines 5335 - 5383, Update
build_engine_fts_query to validate that exactly one of match, phrase,
multi_match, boolean, or boost is set before constructing the engine query.
Reject inputs with multiple variants using the existing
NamespaceError::InvalidInput path and a clear message, while preserving the
current conversion logic for valid single-variant queries and the existing error
for none set.

Source: Coding guidelines

Comment on lines +5340 to +5348
} else if let Some(ref p) = query.phrase {
let mut phrase = PhraseQuery::new(p.terms.clone());
if let Some(ref column) = p.column {
phrase = phrase.with_column(Some(column.clone()));
}
if let Some(slop) = p.slop {
phrase = phrase.with_slop(slop as u32);
}
Ok(FtsQuery::Phrase(phrase))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Unchecked signed→unsigned casts on slop/fuzziness/max_expansions/prefix_length.

p.slop as u32 (line 5346), fuzziness as u32 (line 5396), max_expansions as usize (line 5399), and prefix_length as u32 (line 5409) all cast without checking for negative input first. If these namespace-model fields are signed integers (as the presence of as casts suggests) and a caller passes e.g. -1, the value silently wraps to a huge unsigned number (u32::MAX) instead of erroring — producing nonsensical fuzzy-match/slop behavior rather than a clear validation error. Note this file already validates signed→unsigned conversions elsewhere in the same PR-adjacent code (e.g. from_version in create_table_branch, version <= 0 checks in tag/version handlers, request.k > 0 before as usize), so this is inconsistent with the rest of the file.

🛠️ Example fix pattern (repeat for each field)
     if let Some(slop) = p.slop {
-        phrase = phrase.with_slop(slop as u32);
+        let slop = u32::try_from(slop).map_err(|_| NamespaceError::InvalidInput {
+            message: format!("phrase.slop must be non-negative, got {}", slop),
+        })?;
+        phrase = phrase.with_slop(slop);
     }

As per coding guidelines, "Validate inputs and reject invalid values with descriptive errors at API boundaries; never silently clamp or adjust values." Please confirm the actual signedness of these lance_namespace::models fields.

#!/bin/bash
rg -nP 'struct (MatchQuery|PhraseQuery)\b' -A 25
rg -nP '\bfuzziness\b|\bmax_expansions\b|\bprefix_length\b|\bslop\b' -g '*.rs'

Also applies to: 5385-5412

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance-namespace-impls/src/dir.rs` around lines 5340 - 5348, Validate
signed namespace-model fields before converting them in the query conversion
logic around PhraseQuery and the fuzzy-query construction: reject negative slop,
fuzziness, max_expansions, and prefix_length with descriptive errors, then
perform the existing unsigned conversions only after validation. Confirm the
actual field types in lance_namespace::models and preserve valid-value behavior
while preventing negative inputs from wrapping.

Comment on lines +5547 to +5552
#[test]
fn test_build_engine_fts_query_requires_a_variant() {
// An FtsQuery with no variant set is rejected rather than silently ignored.
let empty = lance_namespace::models::FtsQuery::new();
assert!(build_engine_fts_query(&empty).is_err());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Test only checks is_err(), not the error variant/message.

test_build_engine_fts_query_requires_a_variant asserts build_engine_fts_query(&empty).is_err() but never verifies it's NamespaceError::InvalidInput with the expected message.

✅ Proposed fix
     #[test]
     fn test_build_engine_fts_query_requires_a_variant() {
         // An FtsQuery with no variant set is rejected rather than silently ignored.
         let empty = lance_namespace::models::FtsQuery::new();
-        assert!(build_engine_fts_query(&empty).is_err());
+        let err = build_engine_fts_query(&empty).unwrap_err();
+        assert!(matches!(err, NamespaceError::InvalidInput { .. }));
+        assert!(err.to_string().contains("must set exactly one of"));
     }

As per coding guidelines, "Assert on both the error variant and the message content in tests; do not check only is_err()."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[test]
fn test_build_engine_fts_query_requires_a_variant() {
// An FtsQuery with no variant set is rejected rather than silently ignored.
let empty = lance_namespace::models::FtsQuery::new();
assert!(build_engine_fts_query(&empty).is_err());
}
#[test]
fn test_build_engine_fts_query_requires_a_variant() {
// An FtsQuery with no variant set is rejected rather than silently ignored.
let empty = lance_namespace::models::FtsQuery::new();
let err = build_engine_fts_query(&empty).unwrap_err();
assert!(matches!(err, NamespaceError::InvalidInput { .. }));
assert!(err.to_string().contains("must set exactly one of"));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance-namespace-impls/src/dir.rs` around lines 5547 - 5552, Strengthen
test_build_engine_fts_query_requires_a_variant by matching the returned error as
NamespaceError::InvalidInput and asserting its message contains the expected
missing-variant text. Replace the is_err-only assertion while preserving the
existing empty FtsQuery setup.

Source: Coding guidelines

@hamersaw hamersaw requested a review from wjones127 July 15, 2026 13:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-namespace Namespace impls enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants