Skip to content

fix(rust, C++): fix potential issues - #58

Merged
HideBa merged 12 commits into
developfrom
fix/upstream-findings-remaining
Aug 14, 2026
Merged

fix(rust, C++): fix potential issues#58
HideBa merged 12 commits into
developfrom
fix/upstream-findings-remaining

Conversation

@HideBa

@HideBa HideBa commented Aug 14, 2026

Copy link
Copy Markdown
Member

No description provided.

HideBa added 12 commits August 13, 2026 22:18
The writer builds a real B+tree index over i64 values for a Long column,
but neither of attr_query.rs's column-type matches (nor the HTTP reader's)
had a Long arm, so every such query fell through to UnsupportedColumnType.
Adds the arm to all three sites and a regression test covering the
seekable and sequential query paths.
…ream finding #24)

Both the HTTP path (http_reader select_query_paged) and the seekable file
path (reader select_query) passed the compile-time DEFAULT_NODE_SIZE to
the R-tree traversal instead of the per-file header.index_node_size(), so
any file written with a non-default node size was walked over the wrong
node ranges. The file-path twin was undocumented — found while testing
the recorded HTTP defect. select_query_seq was never affected.

The mock HTTP range client now truncates a range past EOF instead of
panicking, matching real server behaviour; this is what makes in-crate
HTTP tests over small fixtures possible at all.
…leaf debug_assert (upstream finding #26)

The leaf-level range extension (+1 node, fetched only to size the last
feature of a batch) had no guard excluding that node from candidate
evaluation. An oracle sweep (~950 boundary-targeted and random queries
against brute force) shows the double emission the finding predicted is
NOT reachable — adjacent leaf ranges always take the overlap-merge
branch — so the guard is defensive: it encodes the invariant and closes
the latent worst case of an unbounded RangeFrom emitted for a
sizing-node hit at the end of a range.

The sweep did expose a second, real defect in both query arms:
debug_assert_eq!(node_pos, num_items - 1) compares a position within
the fetched range against the global item count and panics in debug
builds whenever the final feature is hit via a non-initial node range.
Corrected to assert against the last leaf of the level.

Both fixes applied to the BBox and PointIntersects arms; three oracle
tests added over a mocked HTTP client.
…(upstream finding #2a)

The writer stores Byte as u8 and the index path already decodes u8;
the feature-value path was the one remaining site reading i8, turning
a stored 200 into -56. Adds the first direct decode_attributes unit
test, pinning Byte, UByte and Binary against the writer's wire format.
…xed Rust reader

The rejection cited Rust's unreachable!() — removed long ago; Rust now
decodes all three (Byte unsigned as of finding #2a's fix). Byte/UByte
join the fixed-width struct table as '<B'; Binary is a u32 LE length
plus raw bytes, surfaced as a list of ints — the JSON array-of-numbers
shape Rust emits, and JSON-serializable where bytes would not be.
…d Binary JSON shape

The doc block claimed the Rust reader decodes Byte as i8 — fixed there
this session (finding #2a), so all four implementations now agree on u8.
Binary's Array.from conversion at the CityJSON boundary already existed
but was untested; both new tests were validated by mutation (removing
Array.from / making Byte signed each fails loudly).
…ixed Rust reader

The rejection cited Rust's unreachable!() — removed long ago; Rust now
decodes all three (Byte unsigned as of finding #2a's fix this session).
Byte/UByte decode as unsigned single bytes, Binary as a u32-LE-length-
prefixed byte string emitted as a JSON array of numbers (verified at
runtime against nlohmann). key.cpp's mirror-image divergence comment is
retired: Rust agrees on both the index and value paths now.
…ffset subtraction (upstream finding #5)

Gt/Lt/Ne were lowered as 'inclusive range minus find_exact', subtracting
FEATURE OFFSETS — but one feature can be indexed under several keys (its
CityObjects carry different values of the indexed attribute), so the
subtraction deleted features that genuinely match via another key.

All three tree traversal families (in-memory, streaming, HTTP) gain
strict-bound range variants sharing one in_bounds leaf predicate — the
design the C++ reader proved out; the inclusive methods remain as thin
wrappers. All four lowering sites (memory, stream, both http cfg copies)
now do one traversal for Gt/Lt and two half-open traversals for Ne.
First-ever Ne coverage; multi-key-feature expectations run through the
memory, stream and http paths.

Also fixes a pre-existing cursor bug the new tests exposed:
StreamMultiIndex::query's empty-result early return skipped the cursor
restore, so the NEXT query resolved index offsets from the wrong base.
…g Rust (#20.11, #19)

Rust reads every schema-optional header string through an Option
accessor: present-but-empty emits "", only genuinely absent omits the
key. C++ flattened both to an empty std::string and Python to "", then
gated on emptiness/truthiness — silently dropping keys the oracle keeps.

C++: nine FileInfo strings become std::optional<std::string> (breaking
API change, intended); has_poc_email is deleted, subsumed by
poc_email.has_value(). crs (derived; #20.10 stays open) and
poc_address_* (non-emptiness IS Rust's own rule there) deliberately
stay plain strings. Python: FileInfo.identifier/.title become
str | None with presence gates. TypeScript was already correct and is
now pinned by test, mutation-verified.

Each implementation pins both halves against a hand-built header, since
no corpus fixture can express a present-but-empty string.
Marks #2a, #5, #17, #24 and #26 FIXED with their fix records; #24 gains
the undocumented file-reader twin; #26's premise is corrected honestly
(double emission shown unreachable; guard added defensively; the real
defect its reproduction exposed — the wrong last-leaf debug_assert — is
recorded). Item 3's stale-comment NOTE is resolved; §19 bullet 2 is
struck as stale on both premises (address model is all-strings; the one
remaining from_chars already matches Rust including '+'); §20.1's Byte
family is closed; §20.10 (referenceSystem) is explicitly recorded as
deferred pending a maintainer decision. The TS 'four deliberate
divergences' notes (query.ts, key.ts, README, docs/ts.md) re-badge the
Byte item as resolved — its old claim conflated Rust's index and value
paths.
…d the sizing leaf (finding #26, final-review wave)

The wrong debug_assert fixed in the BBox and PointIntersects arms had a
third instance in PointNearest. Correcting it immediately exposed a
reachable defect the review had judged latent: with no sizing-tail
guard, a nearest hit landing on a range's borrowed sizing leaf (last of
its fetched batch) found no next node to size against and emitted an
unbounded RangeFrom — right feature, read to EOF, on ~8% of boundary
queries in a 1506-query sweep. The guard now covers all three arms;
brute-force nearest oracle tests added, including one forcing the
level's last leaf through a non-initial range.
The Byte/UByte/Binary decode test ended with the Binary record, so a
wrong u32 length read had nothing left to desynchronise. A trailing
Int record now follows the payload and is reachable only if the walk
resumed on the right byte; mutation-verified (length 2 -> 3 fails at
the resumption point, not just the bounds check).

Changelog note for the decode fix this pins (a578e69): the C++ reader
previously REJECTED Byte/UByte/Binary attribute columns outright, so
any file whose writer emitted them was unreadable by the C++ reader;
that is un-broken — all three now decode, matching the Rust reader.
@HideBa HideBa changed the title fix(rust): query indexed Long columns (upstream finding #17) fix(rust, C++): fix potential issues Aug 14, 2026
@HideBa
HideBa merged commit 6d459ec into develop Aug 14, 2026
9 checks passed
@HideBa
HideBa deleted the fix/upstream-findings-remaining branch August 14, 2026 08:42
HideBa added a commit that referenced this pull request Aug 14, 2026
* fix(rust): query indexed Long columns (upstream finding #17)

The writer builds a real B+tree index over i64 values for a Long column,
but neither of attr_query.rs's column-type matches (nor the HTTP reader's)
had a Long arm, so every such query fell through to UnsupportedColumnType.
Adds the arm to all three sites and a regression test covering the
seekable and sequential query paths.

* fix(rust): honour header index_node_size on spatial query paths (upstream finding #24)

Both the HTTP path (http_reader select_query_paged) and the seekable file
path (reader select_query) passed the compile-time DEFAULT_NODE_SIZE to
the R-tree traversal instead of the per-file header.index_node_size(), so
any file written with a non-default node size was walked over the wrong
node ranges. The file-path twin was undocumented — found while testing
the recorded HTTP defect. select_query_seq was never affected.

The mock HTTP range client now truncates a range past EOF instead of
panicking, matching real server behaviour; this is what makes in-crate
HTTP tests over small fixtures possible at all.

* fix(rust): guard the sizing leaf in http_stream_search; fix its last-leaf debug_assert (upstream finding #26)

The leaf-level range extension (+1 node, fetched only to size the last
feature of a batch) had no guard excluding that node from candidate
evaluation. An oracle sweep (~950 boundary-targeted and random queries
against brute force) shows the double emission the finding predicted is
NOT reachable — adjacent leaf ranges always take the overlap-merge
branch — so the guard is defensive: it encodes the invariant and closes
the latent worst case of an unbounded RangeFrom emitted for a
sizing-node hit at the end of a range.

The sweep did expose a second, real defect in both query arms:
debug_assert_eq!(node_pos, num_items - 1) compares a position within
the fetched range against the global item count and panics in debug
builds whenever the final feature is hit via a non-initial node range.
Corrected to assert against the last leaf of the level.

Both fixes applied to the BBox and PointIntersects arms; three oracle
tests added over a mocked HTTP client.

* fix(rust): decode Byte feature attributes as u8, matching the writer (upstream finding #2a)

The writer stores Byte as u8 and the index path already decodes u8;
the feature-value path was the one remaining site reading i8, turning
a stored 200 into -56. Adds the first direct decode_attributes unit
test, pinning Byte, UByte and Binary against the writer's wire format.

* fix(py): decode Byte/UByte/Binary feature attributes, matching the fixed Rust reader

The rejection cited Rust's unreachable!() — removed long ago; Rust now
decodes all three (Byte unsigned as of finding #2a's fix). Byte/UByte
join the fixed-width struct table as '<B'; Binary is a u32 LE length
plus raw bytes, surfaced as a list of ints — the JSON array-of-numbers
shape Rust emits, and JSON-serializable where bytes would not be.

* test(ts): close the Byte divergence notes; pin UByte/Binary decode and Binary JSON shape

The doc block claimed the Rust reader decodes Byte as i8 — fixed there
this session (finding #2a), so all four implementations now agree on u8.
Binary's Array.from conversion at the CityJSON boundary already existed
but was untested; both new tests were validated by mutation (removing
Array.from / making Byte signed each fails loudly).

* fix(cpp): decode Byte/UByte/Binary feature attributes, matching the fixed Rust reader

The rejection cited Rust's unreachable!() — removed long ago; Rust now
decodes all three (Byte unsigned as of finding #2a's fix this session).
Byte/UByte decode as unsigned single bytes, Binary as a u32-LE-length-
prefixed byte string emitted as a JSON array of numbers (verified at
runtime against nlohmann). key.cpp's mirror-image divergence comment is
retired: Rust agrees on both the index and value paths now.

* fix(rust): evaluate Gt/Lt/Ne with strict bounds at the leaf, not by offset subtraction (upstream finding #5)

Gt/Lt/Ne were lowered as 'inclusive range minus find_exact', subtracting
FEATURE OFFSETS — but one feature can be indexed under several keys (its
CityObjects carry different values of the indexed attribute), so the
subtraction deleted features that genuinely match via another key.

All three tree traversal families (in-memory, streaming, HTTP) gain
strict-bound range variants sharing one in_bounds leaf predicate — the
design the C++ reader proved out; the inclusive methods remain as thin
wrappers. All four lowering sites (memory, stream, both http cfg copies)
now do one traversal for Gt/Lt and two half-open traversals for Ne.
First-ever Ne coverage; multi-key-feature expectations run through the
memory, stream and http paths.

Also fixes a pre-existing cursor bug the new tests exposed:
StreamMultiIndex::query's empty-result early return skipped the cursor
restore, so the NEXT query resolved index offsets from the wrong base.

* fix(cpp,py): gate header metadata on presence, not emptiness, matching Rust (#20.11, #19)

Rust reads every schema-optional header string through an Option
accessor: present-but-empty emits "", only genuinely absent omits the
key. C++ flattened both to an empty std::string and Python to "", then
gated on emptiness/truthiness — silently dropping keys the oracle keeps.

C++: nine FileInfo strings become std::optional<std::string> (breaking
API change, intended); has_poc_email is deleted, subsumed by
poc_email.has_value(). crs (derived; #20.10 stays open) and
poc_address_* (non-emptiness IS Rust's own rule there) deliberately
stay plain strings. Python: FileInfo.identifier/.title become
str | None with presence gates. TypeScript was already correct and is
now pinned by test, mutation-verified.

Each implementation pins both halves against a hand-built header, since
no corpus fixture can express a present-but-empty string.

* docs: update upstream-findings statuses after the remaining-fixes pass

Marks #2a, #5, #17, #24 and #26 FIXED with their fix records; #24 gains
the undocumented file-reader twin; #26's premise is corrected honestly
(double emission shown unreachable; guard added defensively; the real
defect its reproduction exposed — the wrong last-leaf debug_assert — is
recorded). Item 3's stale-comment NOTE is resolved; §19 bullet 2 is
struck as stale on both premises (address model is all-strings; the one
remaining from_chars already matches Rust including '+'); §20.1's Byte
family is closed; §20.10 (referenceSystem) is explicitly recorded as
deferred pending a maintainer decision. The TS 'four deliberate
divergences' notes (query.ts, key.ts, README, docs/ts.md) re-badge the
Byte item as resolved — its old claim conflated Rust's index and value
paths.

* fix(rust): PointNearest arm — correct last-leaf debug_assert and guard the sizing leaf (finding #26, final-review wave)

The wrong debug_assert fixed in the BBox and PointIntersects arms had a
third instance in PointNearest. Correcting it immediately exposed a
reachable defect the review had judged latent: with no sizing-tail
guard, a nearest hit landing on a range's borrowed sizing leaf (last of
its fetched batch) found no next node to size against and emitted an
unbounded RangeFrom — right feature, read to EOF, on ~8% of boundary
queries in a 1506-query sweep. The guard now covers all three arms;
brute-force nearest oracle tests added, including one forcing the
level's last leaf through a non-initial range.

* test(cpp): prove the attribute walk re-syncs after a Binary payload

The Byte/UByte/Binary decode test ended with the Binary record, so a
wrong u32 length read had nothing left to desynchronise. A trailing
Int record now follows the payload and is reachable only if the walk
resumed on the right byte; mutation-verified (length 2 -> 3 fails at
the resumption point, not just the bounds check).

Changelog note for the decode fix this pins (a578e69): the C++ reader
previously REJECTED Byte/UByte/Binary attribute columns outright, so
any file whose writer emitted them was unreadable by the C++ reader;
that is un-broken — all three now decode, matching the Rust reader.
HideBa added a commit that referenced this pull request Aug 14, 2026
* fix(rust): query indexed Long columns (upstream finding #17)

The writer builds a real B+tree index over i64 values for a Long column,
but neither of attr_query.rs's column-type matches (nor the HTTP reader's)
had a Long arm, so every such query fell through to UnsupportedColumnType.
Adds the arm to all three sites and a regression test covering the
seekable and sequential query paths.

* fix(rust): honour header index_node_size on spatial query paths (upstream finding #24)

Both the HTTP path (http_reader select_query_paged) and the seekable file
path (reader select_query) passed the compile-time DEFAULT_NODE_SIZE to
the R-tree traversal instead of the per-file header.index_node_size(), so
any file written with a non-default node size was walked over the wrong
node ranges. The file-path twin was undocumented — found while testing
the recorded HTTP defect. select_query_seq was never affected.

The mock HTTP range client now truncates a range past EOF instead of
panicking, matching real server behaviour; this is what makes in-crate
HTTP tests over small fixtures possible at all.

* fix(rust): guard the sizing leaf in http_stream_search; fix its last-leaf debug_assert (upstream finding #26)

The leaf-level range extension (+1 node, fetched only to size the last
feature of a batch) had no guard excluding that node from candidate
evaluation. An oracle sweep (~950 boundary-targeted and random queries
against brute force) shows the double emission the finding predicted is
NOT reachable — adjacent leaf ranges always take the overlap-merge
branch — so the guard is defensive: it encodes the invariant and closes
the latent worst case of an unbounded RangeFrom emitted for a
sizing-node hit at the end of a range.

The sweep did expose a second, real defect in both query arms:
debug_assert_eq!(node_pos, num_items - 1) compares a position within
the fetched range against the global item count and panics in debug
builds whenever the final feature is hit via a non-initial node range.
Corrected to assert against the last leaf of the level.

Both fixes applied to the BBox and PointIntersects arms; three oracle
tests added over a mocked HTTP client.

* fix(rust): decode Byte feature attributes as u8, matching the writer (upstream finding #2a)

The writer stores Byte as u8 and the index path already decodes u8;
the feature-value path was the one remaining site reading i8, turning
a stored 200 into -56. Adds the first direct decode_attributes unit
test, pinning Byte, UByte and Binary against the writer's wire format.

* fix(py): decode Byte/UByte/Binary feature attributes, matching the fixed Rust reader

The rejection cited Rust's unreachable!() — removed long ago; Rust now
decodes all three (Byte unsigned as of finding #2a's fix). Byte/UByte
join the fixed-width struct table as '<B'; Binary is a u32 LE length
plus raw bytes, surfaced as a list of ints — the JSON array-of-numbers
shape Rust emits, and JSON-serializable where bytes would not be.

* test(ts): close the Byte divergence notes; pin UByte/Binary decode and Binary JSON shape

The doc block claimed the Rust reader decodes Byte as i8 — fixed there
this session (finding #2a), so all four implementations now agree on u8.
Binary's Array.from conversion at the CityJSON boundary already existed
but was untested; both new tests were validated by mutation (removing
Array.from / making Byte signed each fails loudly).

* fix(cpp): decode Byte/UByte/Binary feature attributes, matching the fixed Rust reader

The rejection cited Rust's unreachable!() — removed long ago; Rust now
decodes all three (Byte unsigned as of finding #2a's fix this session).
Byte/UByte decode as unsigned single bytes, Binary as a u32-LE-length-
prefixed byte string emitted as a JSON array of numbers (verified at
runtime against nlohmann). key.cpp's mirror-image divergence comment is
retired: Rust agrees on both the index and value paths now.

* fix(rust): evaluate Gt/Lt/Ne with strict bounds at the leaf, not by offset subtraction (upstream finding #5)

Gt/Lt/Ne were lowered as 'inclusive range minus find_exact', subtracting
FEATURE OFFSETS — but one feature can be indexed under several keys (its
CityObjects carry different values of the indexed attribute), so the
subtraction deleted features that genuinely match via another key.

All three tree traversal families (in-memory, streaming, HTTP) gain
strict-bound range variants sharing one in_bounds leaf predicate — the
design the C++ reader proved out; the inclusive methods remain as thin
wrappers. All four lowering sites (memory, stream, both http cfg copies)
now do one traversal for Gt/Lt and two half-open traversals for Ne.
First-ever Ne coverage; multi-key-feature expectations run through the
memory, stream and http paths.

Also fixes a pre-existing cursor bug the new tests exposed:
StreamMultiIndex::query's empty-result early return skipped the cursor
restore, so the NEXT query resolved index offsets from the wrong base.

* fix(cpp,py): gate header metadata on presence, not emptiness, matching Rust (#20.11, #19)

Rust reads every schema-optional header string through an Option
accessor: present-but-empty emits "", only genuinely absent omits the
key. C++ flattened both to an empty std::string and Python to "", then
gated on emptiness/truthiness — silently dropping keys the oracle keeps.

C++: nine FileInfo strings become std::optional<std::string> (breaking
API change, intended); has_poc_email is deleted, subsumed by
poc_email.has_value(). crs (derived; #20.10 stays open) and
poc_address_* (non-emptiness IS Rust's own rule there) deliberately
stay plain strings. Python: FileInfo.identifier/.title become
str | None with presence gates. TypeScript was already correct and is
now pinned by test, mutation-verified.

Each implementation pins both halves against a hand-built header, since
no corpus fixture can express a present-but-empty string.

* docs: update upstream-findings statuses after the remaining-fixes pass

Marks #2a, #5, #17, #24 and #26 FIXED with their fix records; #24 gains
the undocumented file-reader twin; #26's premise is corrected honestly
(double emission shown unreachable; guard added defensively; the real
defect its reproduction exposed — the wrong last-leaf debug_assert — is
recorded). Item 3's stale-comment NOTE is resolved; §19 bullet 2 is
struck as stale on both premises (address model is all-strings; the one
remaining from_chars already matches Rust including '+'); §20.1's Byte
family is closed; §20.10 (referenceSystem) is explicitly recorded as
deferred pending a maintainer decision. The TS 'four deliberate
divergences' notes (query.ts, key.ts, README, docs/ts.md) re-badge the
Byte item as resolved — its old claim conflated Rust's index and value
paths.

* fix(rust): PointNearest arm — correct last-leaf debug_assert and guard the sizing leaf (finding #26, final-review wave)

The wrong debug_assert fixed in the BBox and PointIntersects arms had a
third instance in PointNearest. Correcting it immediately exposed a
reachable defect the review had judged latent: with no sizing-tail
guard, a nearest hit landing on a range's borrowed sizing leaf (last of
its fetched batch) found no next node to size against and emitted an
unbounded RangeFrom — right feature, read to EOF, on ~8% of boundary
queries in a 1506-query sweep. The guard now covers all three arms;
brute-force nearest oracle tests added, including one forcing the
level's last leaf through a non-initial range.

* test(cpp): prove the attribute walk re-syncs after a Binary payload

The Byte/UByte/Binary decode test ended with the Binary record, so a
wrong u32 length read had nothing left to desynchronise. A trailing
Int record now follows the payload and is reachable only if the walk
resumed on the right byte; mutation-verified (length 2 -> 3 fails at
the resumption point, not just the bounds check).

Changelog note for the decode fix this pins (a578e69): the C++ reader
previously REJECTED Byte/UByte/Binary attribute columns outright, so
any file whose writer emitted them was unreadable by the C++ reader;
that is un-broken — all three now decode, matching the Rust reader.
HideBa added a commit that referenced this pull request Aug 14, 2026
* fix(rust): query indexed Long columns (upstream finding #17)

The writer builds a real B+tree index over i64 values for a Long column,
but neither of attr_query.rs's column-type matches (nor the HTTP reader's)
had a Long arm, so every such query fell through to UnsupportedColumnType.
Adds the arm to all three sites and a regression test covering the
seekable and sequential query paths.

* fix(rust): honour header index_node_size on spatial query paths (upstream finding #24)

Both the HTTP path (http_reader select_query_paged) and the seekable file
path (reader select_query) passed the compile-time DEFAULT_NODE_SIZE to
the R-tree traversal instead of the per-file header.index_node_size(), so
any file written with a non-default node size was walked over the wrong
node ranges. The file-path twin was undocumented — found while testing
the recorded HTTP defect. select_query_seq was never affected.

The mock HTTP range client now truncates a range past EOF instead of
panicking, matching real server behaviour; this is what makes in-crate
HTTP tests over small fixtures possible at all.

* fix(rust): guard the sizing leaf in http_stream_search; fix its last-leaf debug_assert (upstream finding #26)

The leaf-level range extension (+1 node, fetched only to size the last
feature of a batch) had no guard excluding that node from candidate
evaluation. An oracle sweep (~950 boundary-targeted and random queries
against brute force) shows the double emission the finding predicted is
NOT reachable — adjacent leaf ranges always take the overlap-merge
branch — so the guard is defensive: it encodes the invariant and closes
the latent worst case of an unbounded RangeFrom emitted for a
sizing-node hit at the end of a range.

The sweep did expose a second, real defect in both query arms:
debug_assert_eq!(node_pos, num_items - 1) compares a position within
the fetched range against the global item count and panics in debug
builds whenever the final feature is hit via a non-initial node range.
Corrected to assert against the last leaf of the level.

Both fixes applied to the BBox and PointIntersects arms; three oracle
tests added over a mocked HTTP client.

* fix(rust): decode Byte feature attributes as u8, matching the writer (upstream finding #2a)

The writer stores Byte as u8 and the index path already decodes u8;
the feature-value path was the one remaining site reading i8, turning
a stored 200 into -56. Adds the first direct decode_attributes unit
test, pinning Byte, UByte and Binary against the writer's wire format.

* fix(py): decode Byte/UByte/Binary feature attributes, matching the fixed Rust reader

The rejection cited Rust's unreachable!() — removed long ago; Rust now
decodes all three (Byte unsigned as of finding #2a's fix). Byte/UByte
join the fixed-width struct table as '<B'; Binary is a u32 LE length
plus raw bytes, surfaced as a list of ints — the JSON array-of-numbers
shape Rust emits, and JSON-serializable where bytes would not be.

* test(ts): close the Byte divergence notes; pin UByte/Binary decode and Binary JSON shape

The doc block claimed the Rust reader decodes Byte as i8 — fixed there
this session (finding #2a), so all four implementations now agree on u8.
Binary's Array.from conversion at the CityJSON boundary already existed
but was untested; both new tests were validated by mutation (removing
Array.from / making Byte signed each fails loudly).

* fix(cpp): decode Byte/UByte/Binary feature attributes, matching the fixed Rust reader

The rejection cited Rust's unreachable!() — removed long ago; Rust now
decodes all three (Byte unsigned as of finding #2a's fix this session).
Byte/UByte decode as unsigned single bytes, Binary as a u32-LE-length-
prefixed byte string emitted as a JSON array of numbers (verified at
runtime against nlohmann). key.cpp's mirror-image divergence comment is
retired: Rust agrees on both the index and value paths now.

* fix(rust): evaluate Gt/Lt/Ne with strict bounds at the leaf, not by offset subtraction (upstream finding #5)

Gt/Lt/Ne were lowered as 'inclusive range minus find_exact', subtracting
FEATURE OFFSETS — but one feature can be indexed under several keys (its
CityObjects carry different values of the indexed attribute), so the
subtraction deleted features that genuinely match via another key.

All three tree traversal families (in-memory, streaming, HTTP) gain
strict-bound range variants sharing one in_bounds leaf predicate — the
design the C++ reader proved out; the inclusive methods remain as thin
wrappers. All four lowering sites (memory, stream, both http cfg copies)
now do one traversal for Gt/Lt and two half-open traversals for Ne.
First-ever Ne coverage; multi-key-feature expectations run through the
memory, stream and http paths.

Also fixes a pre-existing cursor bug the new tests exposed:
StreamMultiIndex::query's empty-result early return skipped the cursor
restore, so the NEXT query resolved index offsets from the wrong base.

* fix(cpp,py): gate header metadata on presence, not emptiness, matching Rust (#20.11, #19)

Rust reads every schema-optional header string through an Option
accessor: present-but-empty emits "", only genuinely absent omits the
key. C++ flattened both to an empty std::string and Python to "", then
gated on emptiness/truthiness — silently dropping keys the oracle keeps.

C++: nine FileInfo strings become std::optional<std::string> (breaking
API change, intended); has_poc_email is deleted, subsumed by
poc_email.has_value(). crs (derived; #20.10 stays open) and
poc_address_* (non-emptiness IS Rust's own rule there) deliberately
stay plain strings. Python: FileInfo.identifier/.title become
str | None with presence gates. TypeScript was already correct and is
now pinned by test, mutation-verified.

Each implementation pins both halves against a hand-built header, since
no corpus fixture can express a present-but-empty string.

* docs: update upstream-findings statuses after the remaining-fixes pass

Marks #2a, #5, #17, #24 and #26 FIXED with their fix records; #24 gains
the undocumented file-reader twin; #26's premise is corrected honestly
(double emission shown unreachable; guard added defensively; the real
defect its reproduction exposed — the wrong last-leaf debug_assert — is
recorded). Item 3's stale-comment NOTE is resolved; §19 bullet 2 is
struck as stale on both premises (address model is all-strings; the one
remaining from_chars already matches Rust including '+'); §20.1's Byte
family is closed; §20.10 (referenceSystem) is explicitly recorded as
deferred pending a maintainer decision. The TS 'four deliberate
divergences' notes (query.ts, key.ts, README, docs/ts.md) re-badge the
Byte item as resolved — its old claim conflated Rust's index and value
paths.

* fix(rust): PointNearest arm — correct last-leaf debug_assert and guard the sizing leaf (finding #26, final-review wave)

The wrong debug_assert fixed in the BBox and PointIntersects arms had a
third instance in PointNearest. Correcting it immediately exposed a
reachable defect the review had judged latent: with no sizing-tail
guard, a nearest hit landing on a range's borrowed sizing leaf (last of
its fetched batch) found no next node to size against and emitted an
unbounded RangeFrom — right feature, read to EOF, on ~8% of boundary
queries in a 1506-query sweep. The guard now covers all three arms;
brute-force nearest oracle tests added, including one forcing the
level's last leaf through a non-initial range.

* test(cpp): prove the attribute walk re-syncs after a Binary payload

The Byte/UByte/Binary decode test ended with the Binary record, so a
wrong u32 length read had nothing left to desynchronise. A trailing
Int record now follows the payload and is reachable only if the walk
resumed on the right byte; mutation-verified (length 2 -> 3 fails at
the resumption point, not just the bounds check).

Changelog note for the decode fix this pins (a578e69): the C++ reader
previously REJECTED Byte/UByte/Binary attribute columns outright, so
any file whose writer emitted them was unreadable by the C++ reader;
that is un-broken — all three now decode, matching the Rust reader.
HideBa added a commit that referenced this pull request Aug 14, 2026
)

* fix(rust, C++): fix potential issues (#58)

* fix(rust): query indexed Long columns (upstream finding #17)

The writer builds a real B+tree index over i64 values for a Long column,
but neither of attr_query.rs's column-type matches (nor the HTTP reader's)
had a Long arm, so every such query fell through to UnsupportedColumnType.
Adds the arm to all three sites and a regression test covering the
seekable and sequential query paths.

* fix(rust): honour header index_node_size on spatial query paths (upstream finding #24)

Both the HTTP path (http_reader select_query_paged) and the seekable file
path (reader select_query) passed the compile-time DEFAULT_NODE_SIZE to
the R-tree traversal instead of the per-file header.index_node_size(), so
any file written with a non-default node size was walked over the wrong
node ranges. The file-path twin was undocumented — found while testing
the recorded HTTP defect. select_query_seq was never affected.

The mock HTTP range client now truncates a range past EOF instead of
panicking, matching real server behaviour; this is what makes in-crate
HTTP tests over small fixtures possible at all.

* fix(rust): guard the sizing leaf in http_stream_search; fix its last-leaf debug_assert (upstream finding #26)

The leaf-level range extension (+1 node, fetched only to size the last
feature of a batch) had no guard excluding that node from candidate
evaluation. An oracle sweep (~950 boundary-targeted and random queries
against brute force) shows the double emission the finding predicted is
NOT reachable — adjacent leaf ranges always take the overlap-merge
branch — so the guard is defensive: it encodes the invariant and closes
the latent worst case of an unbounded RangeFrom emitted for a
sizing-node hit at the end of a range.

The sweep did expose a second, real defect in both query arms:
debug_assert_eq!(node_pos, num_items - 1) compares a position within
the fetched range against the global item count and panics in debug
builds whenever the final feature is hit via a non-initial node range.
Corrected to assert against the last leaf of the level.

Both fixes applied to the BBox and PointIntersects arms; three oracle
tests added over a mocked HTTP client.

* fix(rust): decode Byte feature attributes as u8, matching the writer (upstream finding #2a)

The writer stores Byte as u8 and the index path already decodes u8;
the feature-value path was the one remaining site reading i8, turning
a stored 200 into -56. Adds the first direct decode_attributes unit
test, pinning Byte, UByte and Binary against the writer's wire format.

* fix(py): decode Byte/UByte/Binary feature attributes, matching the fixed Rust reader

The rejection cited Rust's unreachable!() — removed long ago; Rust now
decodes all three (Byte unsigned as of finding #2a's fix). Byte/UByte
join the fixed-width struct table as '<B'; Binary is a u32 LE length
plus raw bytes, surfaced as a list of ints — the JSON array-of-numbers
shape Rust emits, and JSON-serializable where bytes would not be.

* test(ts): close the Byte divergence notes; pin UByte/Binary decode and Binary JSON shape

The doc block claimed the Rust reader decodes Byte as i8 — fixed there
this session (finding #2a), so all four implementations now agree on u8.
Binary's Array.from conversion at the CityJSON boundary already existed
but was untested; both new tests were validated by mutation (removing
Array.from / making Byte signed each fails loudly).

* fix(cpp): decode Byte/UByte/Binary feature attributes, matching the fixed Rust reader

The rejection cited Rust's unreachable!() — removed long ago; Rust now
decodes all three (Byte unsigned as of finding #2a's fix this session).
Byte/UByte decode as unsigned single bytes, Binary as a u32-LE-length-
prefixed byte string emitted as a JSON array of numbers (verified at
runtime against nlohmann). key.cpp's mirror-image divergence comment is
retired: Rust agrees on both the index and value paths now.

* fix(rust): evaluate Gt/Lt/Ne with strict bounds at the leaf, not by offset subtraction (upstream finding #5)

Gt/Lt/Ne were lowered as 'inclusive range minus find_exact', subtracting
FEATURE OFFSETS — but one feature can be indexed under several keys (its
CityObjects carry different values of the indexed attribute), so the
subtraction deleted features that genuinely match via another key.

All three tree traversal families (in-memory, streaming, HTTP) gain
strict-bound range variants sharing one in_bounds leaf predicate — the
design the C++ reader proved out; the inclusive methods remain as thin
wrappers. All four lowering sites (memory, stream, both http cfg copies)
now do one traversal for Gt/Lt and two half-open traversals for Ne.
First-ever Ne coverage; multi-key-feature expectations run through the
memory, stream and http paths.

Also fixes a pre-existing cursor bug the new tests exposed:
StreamMultiIndex::query's empty-result early return skipped the cursor
restore, so the NEXT query resolved index offsets from the wrong base.

* fix(cpp,py): gate header metadata on presence, not emptiness, matching Rust (#20.11, #19)

Rust reads every schema-optional header string through an Option
accessor: present-but-empty emits "", only genuinely absent omits the
key. C++ flattened both to an empty std::string and Python to "", then
gated on emptiness/truthiness — silently dropping keys the oracle keeps.

C++: nine FileInfo strings become std::optional<std::string> (breaking
API change, intended); has_poc_email is deleted, subsumed by
poc_email.has_value(). crs (derived; #20.10 stays open) and
poc_address_* (non-emptiness IS Rust's own rule there) deliberately
stay plain strings. Python: FileInfo.identifier/.title become
str | None with presence gates. TypeScript was already correct and is
now pinned by test, mutation-verified.

Each implementation pins both halves against a hand-built header, since
no corpus fixture can express a present-but-empty string.

* docs: update upstream-findings statuses after the remaining-fixes pass

Marks #2a, #5, #17, #24 and #26 FIXED with their fix records; #24 gains
the undocumented file-reader twin; #26's premise is corrected honestly
(double emission shown unreachable; guard added defensively; the real
defect its reproduction exposed — the wrong last-leaf debug_assert — is
recorded). Item 3's stale-comment NOTE is resolved; §19 bullet 2 is
struck as stale on both premises (address model is all-strings; the one
remaining from_chars already matches Rust including '+'); §20.1's Byte
family is closed; §20.10 (referenceSystem) is explicitly recorded as
deferred pending a maintainer decision. The TS 'four deliberate
divergences' notes (query.ts, key.ts, README, docs/ts.md) re-badge the
Byte item as resolved — its old claim conflated Rust's index and value
paths.

* fix(rust): PointNearest arm — correct last-leaf debug_assert and guard the sizing leaf (finding #26, final-review wave)

The wrong debug_assert fixed in the BBox and PointIntersects arms had a
third instance in PointNearest. Correcting it immediately exposed a
reachable defect the review had judged latent: with no sizing-tail
guard, a nearest hit landing on a range's borrowed sizing leaf (last of
its fetched batch) found no next node to size against and emitted an
unbounded RangeFrom — right feature, read to EOF, on ~8% of boundary
queries in a 1506-query sweep. The guard now covers all three arms;
brute-force nearest oracle tests added, including one forcing the
level's last leaf through a non-initial range.

* test(cpp): prove the attribute walk re-syncs after a Binary payload

The Byte/UByte/Binary decode test ended with the Binary record, so a
wrong u32 length read had nothing left to desynchronise. A trailing
Int record now follows the payload and is reachable only if the walk
resumed on the right byte; mutation-verified (length 2 -> 3 fails at
the resumption point, not just the bounds check).

Changelog note for the decode fix this pins (a578e69): the C++ reader
previously REJECTED Byte/UByte/Binary attribute columns outright, so
any file whose writer emitted them was unreadable by the C++ reader;
that is un-broken — all three now decode, matching the Rust reader.

* release(cpp): 0.8.1 — public encoded-geometry API in the vcpkg port

Bump the C++ project version so the installed
flatcitybufConfigVersion.cmake matches the vcpkg port that ships it.
Since v0.8.0 the library made Feature::raw() public (the encoded
geometry: the five flat count arrays and the boundaries index list)
and gained the geometry_analysis example.

Also drop the square-bracket placeholder around the copyright holder
in LICENSE — vcpkg installs this file verbatim as the port copyright.

The tag for this release is cpp-v0.8.1, joining the npm-v*/python-v*
per-language scheme: a bare v0.8.1 would collide with the tag
release.yml cuts when the Rust crates reach 0.8.1, silently skipping
that release (its TAG_EXISTS guard matches refs/tags/v<version>).
HideBa added a commit that referenced this pull request Aug 14, 2026
The cpp-v0.8.1 tag (579969f) was cut on the release branch before #58
landed, so the shipped 0.8.1 port predates every C++ fix in it. This
bump gives the vcpkg port a tag that carries them:

- Byte/UByte/Binary feature attributes decode as the writer stores them
  (Byte unsigned; Binary a u32-LE-length-prefixed byte string emitted
  as a JSON array of numbers).
- Nine FileInfo strings became std::optional<std::string> and
  has_poc_email was deleted, so header metadata is gated on presence
  rather than emptiness, matching Rust.

Minor, not patch: the FileInfo change breaks the API for anyone
resolving flatcitybuf through the registry.
HideBa added a commit to HideBa/vcpkg that referenced this pull request Aug 14, 2026
Minor, not patch: cityjson/flatcitybuf#58 turned nine FileInfo strings
into std::optional<std::string> and deleted has_poc_email, so header
metadata is gated on presence rather than emptiness. It also fixed
Byte/UByte/Binary feature-attribute decoding.

The cpp-v0.8.1 tag was cut on a release branch before microsoft#58 landed, so
the 0.8.1 port predates every one of those fixes.

relax-flatbuffers-version-check.patch still applies unchanged; the
release build installs a ConfigVersion of 0.9.0, matching this port.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant