/** * Engine 3 — scalable uniqueness (detection + repair) via external memory. * * Engine 1 makes tuples unique with an O(N²) in-RAM swap-repair. Engine 3 does * the same job past RAM by sorting on disk. Stages: * 2. duplicate detection (findDuplicateGroups / countDuplicates) * 3. exact-% construction + verification (arrangeExactUniq): generate each * column with EXACT percentages seekably (Feistel quota, O(1) memory), * then verify uniqueness externally. No collisions → done (the common, * ample-slack case) with byte-exact marginals. * 4. repair (repairExactUniq): when the tight construction DID collide, the * collisions are FEW (birthday bound). Collect them + donor rows into a * small pool, learn (one pass) which present tuples the pool could clash * with, and rearrange the pool into distinct tuples avoiding those — all * in RAM, preserving each column's multiset so marginals stay exact. * Only a pathologically tight pool that can't be solved throws * `ExactUniqRepairNeeded` (→ caller falls back to the in-memory engine). * * Assumes tuple keys contain no NUL byte (char code 0); the builder that forms * keys controls the join, so this holds. */ import type { Sequence } from './types.js'; /** * Rows past which the in-memory engine is NOT a fallback. * * The catch-and-fall-back exists for small tight configs, where the whole * table in RAM genuinely can arrange what the bounded repair cannot. Past this * many rows the in-memory engine cannot hold the table at all — the fallback * would not fail fast, it would fail after half an hour of materialising, out * of memory, having written nothing. Refusing with a sentence is the honest * outcome. The number is conservative: engine 1 at this count already needs * gigabytes. */ export declare const IN_MEMORY_FALLBACK_MAX_ROWS = 20000000; export interface DuplicateScanOptions { /** Records held in RAM per sort run (passed to externalSort). */ readonly chunkSize?: number; /** Directory for temp files. */ readonly tmpDir?: string; /** * The colliding rows, already found somewhere else. * * A pile can be sorted and scanned by anything that can read a file — it * needs no config, no packs and no registry, only the records. So the piles * can be handed to threads that know nothing about the run, and what comes * back is this: the rows that have to move. Given it, the scan is skipped * altogether. * * Must be every excess row, sorted ascending — the same list the scan would * have produced, or the repair will move the wrong rows. */ readonly knownExcess?: readonly number[]; /** * Hunt duplicates by FINGERPRINT instead of by tuple text — Engine 5. * * The scan writes 13-byte records (64-bit hash + row index) into piles and * sorts numbers instead of strings; groups sharing a hash are candidates, * verified by recomputing the true tuples for those few rows. The sorted * piles then stay on disk and answer the repair's "is this tuple taken?" by * binary search — no in-memory structure over the run at all. * * The value is the number of piles. Absent or <2 leaves Engine 3's text * path in charge. */ readonly fingerprintBuckets?: number; /** * Sorted fingerprint pile files computed elsewhere (the parallel * coordinator), pile order. With `knownExcess` this skips the scan entirely * and goes straight to the arrangement, reading nothing but these. */ readonly fingerprintFiles?: readonly string[]; /** Called as the scan and the pile sort advance — the uniq half of `RenderProgress`. */ readonly onProgress?: (progress: { phase: 'uniq-scan' | 'uniq-sort' | 'uniq-repair' | 'render'; done: number; total: number; }) => void; } /** A row and the key of its tuple (the concatenation of its uniq'd columns). */ export interface KeyedRow { readonly index: number; readonly key: string; } /** * Stream groups of row indices whose tuple key is identical — size ≥ 2, i.e. * exactly the collisions to repair. Input `(index, key)` in any order; scales * past RAM via external sort. Groups come in sorted-key order, indices within a * group ascending (deterministic). */ export declare function findDuplicateGroups(rows: Iterable, options?: DuplicateScanOptions): Generator; /** Total DUPLICATE rows (rows beyond the first in each colliding group). */ export declare function countDuplicates(rows: Iterable, options?: DuplicateScanOptions): number; /** One uniq column: registry id, its value pool, and per-value percentages. */ export interface ExactUniqField { readonly id: string; readonly values: readonly string[]; readonly percents: readonly number[]; } /** * Signals that the exact-% construction collided AND the bounded-memory repair * couldn't place every row (a pathologically tight pool). * * A caller that CHOSE a bounded-memory engine for the user catches this and * falls back to the in-memory engine, which has the whole table to work with. * A caller the user forced into stream mode lets it through instead: loading * the whole table is the one thing that user asked not to happen, so the text * says what to change rather than claiming a fallback happened. */ /** * A refusal's message with its "— and therefore…" tail cut off. * * The messages here are written in two halves: WHAT could not be done, an em * dash, then what the caller may do about it. A caller that has a better second * half — it knows the row count, or that the engine was named — keeps the first * and writes its own. * * Read by hand rather than with `/ — .*$/`. That regex has to try `.*$` again * at every place ` — ` could start, which is quadratic in the length of a * message whose middle is a value out of someone's config. CodeQL called it * three times over, and it was the same expression copied three times, so it * lives here once. */ export declare function withoutTail(message: string): string; export declare class ExactUniqRepairNeeded extends Error { readonly collisions: number; constructor(collisions: number, label: string, /** * The count is a floor, not a total — say so rather than round it down. * * The scan that finds repeats stops as soon as it is past the cap, because * nothing it could find afterwards changes the answer. What it gives up is * the exact figure, and a number that is quietly 20,001 where the truth is * 1,618,803 is worse than no number: it invites someone to widen a column * by a little. */ atLeast?: boolean); } /** * Build seekable exact-% resolvers for the uniq columns and VERIFY the tuples * are unique (externally, bounded memory). On success returns the resolvers — * O(1) memory, byte-exact per-column marginals, verified distinct. Throws a * feasibility error if `count` exceeds the provable capacity, or * `ExactUniqRepairNeeded` if construction collides (tight config). */ export declare function arrangeExactUniq(fields: readonly ExactUniqField[], count: number, seed: string, label: string, options?: DuplicateScanOptions): Record; /** A uniq column resolver: its registry id and its exact-% value(i). */ export interface UniqResolver { readonly id: string; readonly resolve: (i: number) => string; } /** * Compute rows `[from, to)` and route each tuple's fingerprint straight into * its pile file. * * The unit a scan thread runs: each thread takes a contiguous range and writes * `buckets` files, and pile `b`'s records end up spread over the threads' b-th * files — regrouping them is a rearrangement of paths, no bytes move. Returns * one path per pile, in pile order. */ export declare function writeFingerprintPiles(resolvers: readonly UniqResolver[], from: number, to: number, dir: string, prefix: string, buckets: number, onProgress?: (progress: { phase: 'uniq-scan' | 'uniq-sort' | 'uniq-repair' | 'render'; done: number; total: number; }) => void): string[]; /** * Keep only the rows whose tuples GENUINELY repeat, lowest row of each group * spared — exactly the rule the text scan applies. */ /** * One rising scale for the whole `uniq-repair` phase. * * The repair is several steps with different units: candidate groups to check * here, pool rows to prepare there, then a deal repeated per sweep. Reported * straight, each step would restart the counter at zero, and a bar drawn from * the phase would jump backwards every time one ended — which reads as a bug, * not as progress. * * So the steps are added up. Each declares its size, the phase's total grows * to hold it, and `done` only ever rises. The total is not known in advance * and is not meant to be: it is what has been taken on so far. */ export interface RepairReport { /** Take on a step of `size` units. Ends the previous one. */ step(size: number): void; /** `done` units into the current step. */ at(done: number): void; /** Close the phase full, so a watcher sees it end rather than stall. */ finish(): void; } export declare function repairReport(onProgress?: (progress: { phase: 'uniq-scan' | 'uniq-sort' | 'uniq-repair' | 'render'; done: number; total: number; }) => void): RepairReport; export declare function verifyCandidates(resolvers: readonly UniqResolver[], candidates: readonly (readonly number[])[], report?: RepairReport, /** * Stop once the excess is past this, because the answer cannot change. * * Above the repair cap the run falls back to the in-memory engine whatever * the exact figure is, and finding it out costs a tuple recomputed per row * for every remaining candidate group: measured on a config that misses the * cap by two orders of magnitude — 1,618,803 rows against 20,000 — that was * 1,298,015 groups and 37 seconds, spent to learn something already known * after the first twenty thousand. */ stopAfter?: number): number[]; export declare function tupleKeyAt(resolvers: readonly UniqResolver[], i: number): string; /** * Verify uniqueness and, if the exact-% construction left collisions, repair * them while preserving each column's multiset (so marginals stay exact). * * The collisions are FEW (birthday bound: capacity ≫ count² → none; between * count and count² → ~count²/2·capacity). So: * 1. dedup (external) → the "excess" rows that must move (small). * 2. one pass over all rows → the present tuples that lie inside the excess * rows' value space (the only tuples a repaired row could clash with; small). * 3. re-arrange the excess rows' freed values into DISTINCT tuples that avoid * those present ones — all in RAM. An override map (small) layers over the * seekable resolvers. * If the tiny sub-problem can't be solved (pathologically tight), throw * `ExactUniqRepairNeeded` so the caller falls back to the in-memory engine. */ export declare function repairExactUniq(resolvers: readonly UniqResolver[], count: number, label: string, options?: DuplicateScanOptions, /** * Rows that may trade values with each other, named by a key per row. * * A `` member draws from a different list depending on another * column, so a male row's first name is not a value a female row may hold. * The repair rearranges values among pool rows, and without this it would * happily put `Megan` on a `Male` row: the tuple stays unique and the record * stops making sense. With it, the pool is arranged one block at a time and * donors come from the row's own block. * * Absent means one block holding everything, which is the ordinary case. */ blockOf?: (row: number) => string, /** * How the answer travels between threads. * * The analysis is the expensive half of a uniq run and it is a function of * the config and the seed alone, so it is worth doing ONCE. `onComputed` * hands the result out; `preset` hands it back in, and a caller holding one * skips the analysis entirely. That is what lets several threads render * different row ranges of the same uniq config: one thread works out which * rows move where, the rest are told. * * The result is small — only the rows that actually moved, a few thousand on * a run of a hundred million — so it crosses a thread boundary cheaply. */ plan?: { readonly preset?: Readonly> | undefined; readonly onComputed?: ((moved: Record) => void) | undefined; }): Record; //# sourceMappingURL=exact-uniq.d.ts.map