/** * Money-aware `where()` comparison. * * Query clauses evaluate against RAW stored records — money decode * happens on output only — so a money field's stored form is a * scaled-integer digit string (`'1000000'`) while the caller naturally * writes the operand in major units (`10000`, `'10000.00'`). Without a * rewrite the comparison is silently wrong by the scale factor, and a * string-vs-number comparison is excluded by `isComparable` anyway. * * Two halves: * * - {@link moneyFieldClause} runs at QUERY BUILD time: it quantizes the * caller's major-unit operand into stored scaled-int space via the * same `parseToScaledInt` path as writes, so a malformed operand * throws at `.where()` — not silently filters everything out. * - {@link evaluateMoneyClause} runs per record and compares * `BigInt`-exact in scaled space (exact past 2^53, like the rest of * the money service). * * Currency semantics (multi mode): an operand carries one currency — * explicit via `{ amount, currency }`, or the descriptor's sole allowed * currency for a bare amount. A record in a DIFFERENT currency has no * defined order against the operand: it matches `!=` and nothing else. */ import { type MoneyDescriptor } from './descriptor.js'; import type { Operator } from '../../kernel/query/predicate.js'; /** One quantized operand value: scaled digit string + its currency. */ interface MoneyOperandEntry { readonly scaled: string; readonly currency: string; } /** * The opaque `via` clause payload for a `where()` over a declared money * field (see `ViaBinding.buildClause` in kernel/via/index.ts). `entries` holds * one element for comparison ops, two for `between` (lo, hi — same * currency), N for `in`. */ export interface MoneyWhereOperand { readonly mode: 'fixed' | 'multi'; readonly entries: ReadonlyArray; } /** * Build the `via` clause payload ({@link MoneyWhereOperand}) for a * `where()` over a declared money field — the `ViaBinding.buildClause` * implementation for money. The operand is quantized into stored * scaled-int space NOW — build time — so typos throw at the call site. */ export declare function moneyFieldClause(field: string, op: Operator, value: unknown, desc: MoneyDescriptor): MoneyWhereOperand; /** * The `via` index-probe operand for a `where()` over a declared money * field (see `ViaBinding.indexProbe` in kernel/via/index.ts) — the STORED-form * value `CollectionIndexes.lookupEqual`/`lookupIn` (`with-lookup/indexing/ * eager-indexes.ts`) can bucket directly. * * Only FIXED-mode `==`/`in` are sound: * - fixed-mode stores a bare scaled-int digit string (no currency * component), and `entries[i].scaled` is built by {@link parseOperand} * via the exact same `parseToScaledInt(...).value.toString()` path * `quantizeMoneyFields` (`via-money/normalize.ts#quantizeAmount`) uses * to produce the STORED value — so the two strings are byte-identical * by construction, and `stringifyKey` (which passes a string through * unchanged) buckets them the same way. * - multi-mode stores `{ amount, currency }` — the index would stringify * that object to its no-match `'\0OBJECT\0'` sentinel — so multi-mode * always returns `undefined` (no sound probe). * - every op besides `==`/`in` (range comparisons, `between`) has no * single stored-form value the hash index can serve, so they return * `undefined` too — the caller falls back to a scan. * * MIXED-ERA DATA (#672, fixed — including the #672 review's C1 finding): a * record whose stored value predates the field's `money()` declaration, or * otherwise bypassed `quantizeMoneyFields`, may hold a NON-canonical scaled * string (e.g. `'0100'` instead of `'100'`). `CollectionIndexes` no longer * buckets it under that raw string verbatim at ANY bucket-mutation site — * `build`, `upsert`, and `remove` all consult the same registered * canonicalizer (`ViaPipeline.canonicalizeIndexKey`; money's implementation: * `canonicalizeMoneyIndexKey`, `via/money/normalize.ts`), which re-parses the * stored value the same way the scan's `evaluateMoneyClause`/`readStored` * does (`BigInt(actual).toString()`). So the legacy record lands in the SAME * bucket a canonical write produces, an `==`/`in` probe for `'100'` finds it, * and — because add and remove now agree on that bucket — a later `put()` * (update) or `delete()` of that same legacy record cleans up the correct * bucket instead of stranding the id under a bucket the removal could never * reach. The two-string byte-identity argument above still explains WHY a * canonical write's probe hits directly (no canonicalization needed for the * common case); this note covers the pre-declaration tail. * * Boundary — CLOSED for the index layer (#677): LAZY mode's * `PersistedCollectionIndex` (`with-lookup/indexing/persisted-indexes.ts`) * now consults the SAME `ViaPipeline.canonicalizeIndexKey` at every * bucket-mutation site (`ingest`/`upsert`/`remove`) as `CollectionIndexes` * does, and its query path (`with-lookup/indexing/lazy-builder.ts`'s * `resolveCandidateIds()`) canonicalizes the `==`/`in` probe value before * calling `lookupEqual`/`lookupIn` — mirroring `candidateRecords()` above * resolving `indexValue` before its own `lookupEqual`/`lookupIn` call. A * lazy-mode mixed-era record's bucket and a canonicalized probe now agree, * the same as eager mode. * * Residual boundary CLOSED by #684: `LazyQuery.where()` now builds * `clause.via` exactly like `Query.where()`/`ScanBuilder.where()` do, and * `LazyQuery`'s post-filter (`lazy-builder.ts`'s `toArray()`) runs it * against the RAW record (a dedicated raw-fetch seam on `Collection`, split * out of `get()`) instead of the DECODED one — `clause.via.evaluate` (this * file's {@link evaluateMoneyClause}) now sees the same stored-form * operand/actual-value space eager's `filterRecords` does, at any `scale`. * Only survivors are decoded (`present()`) on the way out. Money RANGE * clauses additionally route around `PersistedCollectionIndex.lookupRange` * (raw/typed, wrong space for scaled-int money) — `resolveCandidateIds()` * enumerates the field's indexed ids instead and lets this now-via-aware * post-filter apply the correct BigInt-exact comparison. `lazyQuery(). * where()` on a money field now agrees with `scan().where()` / * `query().where()` at every `scale` — for `.where()` results specifically. * `orderBy` ordering is a separate, still-open lazy-vs-eager divergence, * tracked in #695. */ export declare function moneyIndexProbe(op: Operator, payload: MoneyWhereOperand): unknown | undefined; /** * Per-record evaluation of a money clause: BigInt-exact comparison in * scaled-integer space. `actual` is the RAW stored field value. * * Missing/malformed stored values and cross-currency comparisons match * `!=` only — consistent with the generic clause semantics where an * absent field is "not equal" and has no defined order. */ export declare function evaluateMoneyClause(actual: unknown, op: Operator, operand: MoneyWhereOperand): boolean; export {};