Store.getRange() silently ignores options.transaction: range reads never see the transaction's staged writes or snapshot
Maintainers usually reply within 1 day
@kriszyp is already working on this.
Since Sep 4, 2026.
Assessment
This issue has not been assessed yet.
Description
Summary
Store.getRange() builds its native iterator from this._context and never reads
options.transaction. get/getSync/remove route the same option through getTxnId(options).
Because RocksDatabase extends DBI<DBITransactional>, db.getRange({ transaction }) type-checks —
so the option is accepted, silently dropped, and the iteration runs against the database instead of
the transaction.
Consequences for any caller that passes it:
- the transaction's own staged writes are invisible to the iteration (they are in its write batch,
which onlyTransaction::GetIteratormerges); - the iteration is not on the transaction's snapshot either, so it observes writes other
transactions commit while it is open.
There is no error and no warning. A point read and a range read on the same handle, in the same
transaction, disagree about the contents of the database.
Reproduction
import { RocksDatabase, Transaction } from '@harperfast/rocksdb-js';
const db = RocksDatabase.open(path);
await db.put('committed', 'before');
const txn = new Transaction(db.store);
txn.put('staged', 'in-batch');
const viaOptions = [...db.getRange({ start: 'a', end: 'z', transaction: txn })].map((e) => e.key);
const viaContext = [...txn.getRange({ start: 'a', end: 'z' })].map((e) => e.key);
get(key, {transaction}) sees staged : true
getRange({transaction}) sees staged : false ← ['committed']
txn.getRange() sees staged : true ← ['committed', 'staged']
The correct path exists and works: iterating with the transaction AS the context returns a
BaseDeltaIterator that merges the write batch. Only the options.transaction spelling is inert.
Where it is
src/store.ts—getRange()passescontexttoNativeIterator
and setsITERATOR_CONTEXT_IS_TRANSACTION_FLAGfromcontext !== this.db.options.transaction
is never consulted.getKeys()/getKeysCount()/getCount()delegate here, so they inherit it.src/binding/iterator/db_iterator.cpp—
the native constructor takes itsTransactionHandleonly from the context argument, trusting the
JS layer's flag; there is no options path for it to read.src/dbi.ts—getRange(options?: IteratorOptions & T)with
RocksDatabase extends DBI<DBITransactional>is what makes the dropped option type-check.
Impact
This is the root cause of HarperFast/harper#2506: Harper's query engine passes
{ transaction } to every index and primary-store range read, so no search()/query() in Harper
runs inside the request transaction. Reported from a live 5.2.7 deployment as "indexed search()
cannot see the row this request just wrote", with an application-level consequence (a per-account
row cap enforced by write-then-count overshooting under concurrency).
Fix shape
Route options.transaction into the iterator context, so it means what get() means:
const iterContext = (options as DBITransactional)?.transaction?._context ?? context;
with the ITERATOR_CONTEXT_IS_TRANSACTION_FLAG derived from iterContext.
Not a one-liner in practice: DBIteratorHandle calls txnHandle->registerIterator(), so honouring
the option makes every such range read pin the transaction for as long as the iterator is open.
That interacts directly with Harper's read-transaction lifetime accounting
(DatabaseTransaction.readTxnsUsed, and the "read iterators held a committed transaction's
snapshot past the open-transaction limit" path), which is why the Harper-side issue is tracked
separately rather than folded in here.
Whatever the resolution, a silently-ignored option in the public type surface should not survive
it: if the transaction is deliberately not supported on range reads, getRange should reject the
option rather than accept and drop it.
Versions
Present on main (86757a84). Nothing in the git history suggests it was ever otherwise; the
get/getRange asymmetry predates the transaction-log work.
- Dominant language
- C++
- Stars
- 24
- Forks
- 3
- Avg merge
- 1d 7h
- Merged PRs (30d)
- 49
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from HarperFast/rocksdb-js
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
HarperFast/rocksdb-js#945 ·
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 55/100
HarperFast/rocksdb-js#956 ·
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 25/100
HarperFast/rocksdb-js#955 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 45/100
HarperFast/rocksdb-js#952 ·
Maintainers usually reply within 1 day
-
Difficulty 4/5 1-2 days Newbie friendliness 45/100
HarperFast/rocksdb-js#946 ·
Maintainers usually reply within 1 day
All issues in HarperFast/rocksdb-js
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
OpenSpace/OpenSpace#4353 · 1 comment ·
Maintainers usually reply within 5 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
macOS: a double-click on a notarized altairsim is refused, and the docs give only the offline casePossibly taken @anhaoXie claimed this today. Opendocumentation
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
mutationpp/Mutationpp#337 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
bulletphysics/bullet3#4819 ·