FmIndex is trained over the _rowaddr column — TrainingCriteria::new(TrainingOrdering::None).with_row_addr() at rust/lance-index/src/scalar/fmindex.rs:1666 and :2264, and the training batches are read from ROW_ADDR at :1783. Its search results are therefore physical row addresses.
However, FmIndex does not override ScalarIndex::results_are_row_addresses(), so it inherits the default false from rust/lance-index-core/src/scalar.rs:568. Only bloomfilter.rs:508, zonemap.rs:720, and scalar_logical.rs:166 declare the address domain today.
On a dataset without stable row ids this is harmless, because _rowid == _rowaddr. On a dataset with stable row ids the two domains diverge, and the dataset layer skips the address-to-row-id translation it would otherwise apply. The addresses are then treated as row ids, so the query returns the wrong rows — or no rows — with no error.
The same missing declaration also affects index maintenance: rust/lance-table/src/transaction/index_maintenance.rs:42 and rust/lance-table/src/transaction/manifest_build.rs:819 branch on results_are_row_addrs() to decide how fragment coverage is maintained, and take the row-id-domain branch for FM indices.
Reachable from Python via create_scalar_index(..., "FM") on a dataset created with enable_stable_row_ids=True.
The fix is to override results_are_row_addresses() to return true, plus a regression test that builds an FM index on a stable-row-id dataset with more than one fragment and asserts the returned rows match an unindexed scan.
FmIndexis trained over the_rowaddrcolumn —TrainingCriteria::new(TrainingOrdering::None).with_row_addr()atrust/lance-index/src/scalar/fmindex.rs:1666and:2264, and the training batches are read fromROW_ADDRat:1783. Its search results are therefore physical row addresses.However,
FmIndexdoes not overrideScalarIndex::results_are_row_addresses(), so it inherits the defaultfalsefromrust/lance-index-core/src/scalar.rs:568. Onlybloomfilter.rs:508,zonemap.rs:720, andscalar_logical.rs:166declare the address domain today.On a dataset without stable row ids this is harmless, because
_rowid == _rowaddr. On a dataset with stable row ids the two domains diverge, and the dataset layer skips the address-to-row-id translation it would otherwise apply. The addresses are then treated as row ids, so the query returns the wrong rows — or no rows — with no error.The same missing declaration also affects index maintenance:
rust/lance-table/src/transaction/index_maintenance.rs:42andrust/lance-table/src/transaction/manifest_build.rs:819branch onresults_are_row_addrs()to decide how fragment coverage is maintained, and take the row-id-domain branch for FM indices.Reachable from Python via
create_scalar_index(..., "FM")on a dataset created withenable_stable_row_ids=True.The fix is to override
results_are_row_addresses()to returntrue, plus a regression test that builds an FM index on a stable-row-id dataset with more than one fragment and asserts the returned rows match an unindexed scan.