Skip to content

refactor: read JSON through one accessor in the JSON semantic module - #9479

Merged
Xuanwo merged 9 commits into
mainfrom
xuanwo/semantic-type-json-accessor
Sep 23, 2026
Merged

Xuanwo merged 9 commits into
mainfrom
xuanwo/semantic-type-json-accessor

Conversation

@Xuanwo

@Xuanwo Xuanwo commented Sep 22, 2026 •

Copy link
Copy Markdown
Member

Part of #7073. Builds on #9478.

Problem

Code that computes on JSON decided whether a column holds JSON by matching lance.json over LargeBinary itself. The same JSON column was therefore tokenized as JSON when it came from storage but as plain text when it arrived as Arrow JSON text (arrow.json over Utf8), and the JSON scalar index could not read Arrow JSON text at all. JSONPath selection was also written twice: JsonArray::json_path kept only the first match, while json_extract returns every match as one JSON array.

Behavior

  • lance_arrow::json is the one place that encodes and decodes JSONB and selects JSONPath values. JsonEncoding::of_field recognizes a JSON field in either representation, and JsonValues reads its values as JSONB or as text, whichever the consumer needs, regardless of the representation they arrived in.
  • FTS index builds and unindexed FTS input read JSON documents through JsonValues, so both representations are tokenized as JSON. The JSON scalar index accepts Arrow JSON text and indexes the same values it would from JSONB.
  • The SQL JSON functions use the shared JSONPath selection. JsonArray::json_path now returns every match of a multi-match path as one JSON array, the same as json_extract.
  • Breaking API changes: lance_index::scalar::inverted::json::JsonTextStream::new is replaced by the fallible try_new, which rejects a column that does not hold JSON, and jsonb_to_json is removed in favor of JsonValues::to_text.
  • No format change, and blob behavior is unchanged. JsonValues is an internal Rust API, not a plugin extension point.

Tradeoffs

  • dataset/sql.rs, lance-datafusion/src/projection.rs and io/exec/projection.rs do not inspect JSON metadata themselves: SQL only converts its output to Arrow JSON through SchemaAdapter, and the projections carry field metadata through unchanged. They are left as they are; choosing the user-facing output encoding belongs to the read path.
  • An FTS index records its tokenizer when it is created, so existing indices keep theirs. The document-type change only affects new indices built from Arrow JSON text.

…ndary

Data file writers now reject any array whose Arrow type or extension
differs from the file schema before encoding, returning a structured
FieldTypeMismatch. Dataset writes convert Arrow JSON to Lance JSONB in the
one current-format data file writer instead of at each call site.
JSON encoding detection, JSONB/text conversion and JSONPath selection now
live in lance_arrow::json. FTS and the JSON scalar index read values
through JsonValues, so they treat stored JSONB and Arrow JSON text alike.
@github-actions github-actions Bot added the A-index Vector index, linalg, tokenizer label Sep 22, 2026
Build the expected Arrow fields once per writer, match batch columns by
position before falling back to a name search, and skip batches whose
schema equals the last accepted one.
@Xuanwo
Xuanwo marked this pull request as ready for review September 23, 2026 03:44
lance-gatekeeper[bot]

This comment was marked as outdated.

@lance-gatekeeper lance-gatekeeper Bot added the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 23, 2026
FieldTypeMismatch now carries the expected and actual Arrow fields instead
of a separate type-and-extension struct, and the check walks nested
fields without tracking a path stack.
@lance-gatekeeper lance-gatekeeper Bot removed the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 23, 2026
lance-gatekeeper[bot]

This comment was marked as outdated.

@lance-gatekeeper lance-gatekeeper Bot added the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 23, 2026
do_write_fragments_impl hands each batch to both the data file writer and
the index seed observers. Convert it once before both, so appending view
or Arrow JSON input to a seeded column no longer fails in the seed writer.
@lance-gatekeeper lance-gatekeeper Bot removed the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 23, 2026
lance-gatekeeper[bot]

This comment was marked as outdated.

@lance-gatekeeper lance-gatekeeper Bot added the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 23, 2026
Base automatically changed from xuanwo/semantic-type-write-boundary to main September 23, 2026 04:58
@lance-gatekeeper lance-gatekeeper Bot added K-approved Latest Gatekeeper recommendation permits acceptance. and removed K-approved Latest Gatekeeper recommendation permits acceptance. labels Sep 23, 2026

@lance-gatekeeper lance-gatekeeper Bot 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.

✅ Gate recommendation: approve.

The shared JSON accessor is preferable to per-consumer metadata branching: FTS, JSON scalar indexing, and JSONPath evaluation apply the same representation semantics to stored JSONB and Arrow JSON text, while existing index tokenizer metadata remains intentionally preserved. The affected accessor, UDF, and index paths have focused regression coverage, including multi-match JSONPath selection.

@lance-gatekeeper lance-gatekeeper Bot added the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 23, 2026
@Xuanwo
Xuanwo merged commit d45b546 into main Sep 23, 2026
40 of 41 checks passed
@Xuanwo
Xuanwo deleted the xuanwo/semantic-type-json-accessor branch September 23, 2026 05:28
sbrunk added a commit to sbrunk/lance that referenced this pull request Sep 25, 2026
The flat `combined_fields` path wraps its input in one `JsonTextStream` per
JSON target column, so it depends on that stream declaring the schema its
batches actually have. Upstream lance-format#9479 made `JsonTextStream` derive one schema
from its input and use it for both `schema()` and the batches, which fixes the
mismatch this branch originally patched. What remains are regression tests for
the caller that the mismatch broke.

Two `combined_fields` scans over `arrow.json` columns with mixed index
coverage, so the plan carries a flat child: one over two JSON columns, which
needs name resolution to survive wrapping the stream twice, and one mixing a
JSON column with a plain text column, where the JSON column is not at
position 0 so a positional read would tokenize the wrong data. Both fail if
`JsonTextStream::schema()` lists only the JSON column.
sbrunk added a commit to sbrunk/lance that referenced this pull request Sep 25, 2026
The flat `combined_fields` path wraps its input in one `JsonTextStream` per
JSON target column, so it depends on that stream declaring the schema its
batches actually have. Upstream lance-format#9479 made `JsonTextStream` derive one schema
from its input and use it for both `schema()` and the batches, which fixes the
mismatch this branch originally patched. What remains are regression tests for
the caller that the mismatch broke.

Two `combined_fields` scans over `arrow.json` columns with mixed index
coverage, so the plan carries a flat child: one over two JSON columns, which
needs name resolution to survive wrapping the stream twice, and one mixing a
JSON column with a plain text column, where the JSON column is not at
position 0 so a positional read would tokenize the wrong data. Both fail if
`JsonTextStream::schema()` lists only the JSON column.
sbrunk added a commit to sbrunk/lance that referenced this pull request Sep 25, 2026
The flat `combined_fields` path wraps its input in one `JsonTextStream` per
JSON target column, so it depends on that stream declaring the schema its
batches actually have. Upstream lance-format#9479 made `JsonTextStream` derive one schema
from its input and use it for both `schema()` and the batches, which fixes the
mismatch this branch originally patched. What remains are regression tests for
the caller that the mismatch broke.

Two `combined_fields` scans over `arrow.json` columns with mixed index
coverage, so the plan carries a flat child: one over two JSON columns, which
needs name resolution to survive wrapping the stream twice, and one mixing a
JSON column with a plain text column, where the JSON column is not at
position 0 so a positional read would tokenize the wrong data. Both fail if
`JsonTextStream::schema()` lists only the JSON column.
sbrunk added a commit to sbrunk/lance that referenced this pull request Sep 26, 2026
The flat `combined_fields` path wraps its input in one `JsonTextStream` per
JSON target column, so it depends on that stream declaring the schema its
batches actually have. Upstream lance-format#9479 made `JsonTextStream` derive one schema
from its input and use it for both `schema()` and the batches, which fixes the
mismatch this branch originally patched. What remains are regression tests for
the caller that the mismatch broke.

Two `combined_fields` scans over `arrow.json` columns with mixed index
coverage, so the plan carries a flat child: one over two JSON columns, which
needs name resolution to survive wrapping the stream twice, and one mixing a
JSON column with a plain text column, where the JSON column is not at
position 0 so a positional read would tokenize the wrong data. Both fail if
`JsonTextStream::schema()` lists only the JSON column.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-index Vector index, linalg, tokenizer breaking-change K-approved Latest Gatekeeper recommendation permits acceptance.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants