arrowbase 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,191 @@
1
+ // src/pushdown.ts
2
+ function pushDown(expr, knownColumns) {
3
+ if (!expr) return null;
4
+ return translate(expr, knownColumns);
5
+ }
6
+ function translate(expr, knownColumns) {
7
+ switch (expr.type) {
8
+ case "val":
9
+ return null;
10
+ case "ref":
11
+ return toRefPredicate(expr, knownColumns);
12
+ case "func": {
13
+ const f = expr;
14
+ return translateFunc(f, knownColumns);
15
+ }
16
+ default:
17
+ return null;
18
+ }
19
+ }
20
+ function translateFunc(f, knownColumns) {
21
+ const name = f.name;
22
+ const args = f.args;
23
+ if (name === "eq" && args.length === 2) {
24
+ return translateEq(args[0], args[1], knownColumns);
25
+ }
26
+ if (name === "and" || name === "or") {
27
+ const children = [];
28
+ for (const arg of args) {
29
+ const c = translate(arg, knownColumns);
30
+ if (!c) return null;
31
+ children.push(c);
32
+ }
33
+ if (name === "and") {
34
+ return (row) => children.every((c) => c(row));
35
+ }
36
+ return (row) => children.some((c) => c(row));
37
+ }
38
+ if (name === "not" && args.length === 1) {
39
+ const inner = translate(args[0], knownColumns);
40
+ if (!inner) return null;
41
+ return (row) => !inner(row);
42
+ }
43
+ return null;
44
+ }
45
+ function translateEq(a, b, knownColumns) {
46
+ let ref = null;
47
+ let lit = null;
48
+ if (a.type === "ref" && b.type === "val") {
49
+ ref = a;
50
+ lit = b;
51
+ } else if (a.type === "val" && b.type === "ref") {
52
+ ref = b;
53
+ lit = a;
54
+ } else {
55
+ return null;
56
+ }
57
+ const col = resolveRef(ref, knownColumns);
58
+ if (!col) return null;
59
+ const literal = lit.value;
60
+ return (row) => row[col] === literal;
61
+ }
62
+ function toRefPredicate(ref, knownColumns) {
63
+ const col = resolveRef(ref, knownColumns);
64
+ if (!col) return null;
65
+ return (row) => row[col] === true;
66
+ }
67
+ function resolveRef(ref, knownColumns) {
68
+ if (!ref.path || ref.path.length === 0) return null;
69
+ let col;
70
+ if (ref.path.length === 1) {
71
+ col = ref.path[0];
72
+ } else if (ref.path.length === 2) {
73
+ col = ref.path[1];
74
+ } else {
75
+ return null;
76
+ }
77
+ if (!knownColumns.has(col)) return null;
78
+ return col;
79
+ }
80
+
81
+ // src/tanstack.ts
82
+ function toTanstackCollection(abColl, options = {}) {
83
+ const id = options.id ?? `arrowbase:${abColl.schema.name}`;
84
+ const pkName = abColl.schema.primaryKey;
85
+ const includeSoftDeleted = options.includeSoftDeleted ?? false;
86
+ const sync = {
87
+ sync: (params) => {
88
+ const { begin, write, commit, markReady, truncate } = params;
89
+ begin();
90
+ for (const row of abColl.rows({ includeSoftDeleted })) {
91
+ write({ value: row, type: "insert" });
92
+ }
93
+ commit();
94
+ markReady();
95
+ const unsubscribe = abColl.subscribe((event) => {
96
+ if (event.op === "compact" || event.op === "rollback") {
97
+ begin();
98
+ truncate();
99
+ for (const row of abColl.rows({ includeSoftDeleted })) {
100
+ write({ value: row, type: "insert" });
101
+ }
102
+ commit();
103
+ return;
104
+ }
105
+ translateEvent(abColl, event, pkName, includeSoftDeleted, {
106
+ begin,
107
+ write,
108
+ commit
109
+ });
110
+ });
111
+ return {
112
+ cleanup: () => unsubscribe(),
113
+ // Push-down hook: when TanStack DB requests a subset with an
114
+ // eq/and/or/not filter over known columns, we translate it to
115
+ // an ArrowBase predicate and emit only matching rows. This
116
+ // module keeps behaviour correct regardless — TanStack DB also
117
+ // re-evaluates the predicate against materialized rows, so an
118
+ // over-approximation here just means extra work, never wrong
119
+ // results.
120
+ loadSubset: (opts) => {
121
+ const knownCols = new Set(abColl.columnNames());
122
+ const predicate = pushDown(opts.where, knownCols);
123
+ if (!predicate) {
124
+ return true;
125
+ }
126
+ begin();
127
+ for (const row of abColl.rows({ includeSoftDeleted })) {
128
+ if (!predicate(row)) continue;
129
+ write({ value: row, type: "update" });
130
+ }
131
+ commit();
132
+ return true;
133
+ }
134
+ };
135
+ },
136
+ // Full row snapshots keep the adapter stateless. TanStack DB's
137
+ // default is 'partial'; we override because ArrowBase always
138
+ // produces a full row on read.
139
+ rowUpdateMode: "full"
140
+ };
141
+ return {
142
+ id,
143
+ getKey: (item) => item[pkName],
144
+ sync,
145
+ onInsert: async ({ transaction }) => {
146
+ for (const m of transaction.mutations) {
147
+ abColl.insert(m.modified);
148
+ }
149
+ },
150
+ onUpdate: async ({ transaction }) => {
151
+ for (const m of transaction.mutations) {
152
+ const key = m.key;
153
+ abColl.update(key, m.changes);
154
+ }
155
+ },
156
+ onDelete: async ({ transaction }) => {
157
+ for (const m of transaction.mutations) {
158
+ abColl.delete(m.key);
159
+ }
160
+ }
161
+ };
162
+ }
163
+ function translateEvent(abColl, event, _pkName, includeSoftDeleted, port) {
164
+ const key = event.rowId;
165
+ if (event.op === "delete") {
166
+ if (!includeSoftDeleted) {
167
+ port.begin();
168
+ port.write({ key, type: "delete" });
169
+ port.commit();
170
+ return;
171
+ }
172
+ const row2 = abColl.get(key);
173
+ if (!row2) return;
174
+ port.begin();
175
+ port.write({ value: row2, type: "update" });
176
+ port.commit();
177
+ return;
178
+ }
179
+ const row = abColl.get(key);
180
+ if (!row) return;
181
+ port.begin();
182
+ port.write({
183
+ value: row,
184
+ type: event.op === "insert" ? "insert" : "update"
185
+ });
186
+ port.commit();
187
+ }
188
+
189
+ export { toTanstackCollection };
190
+ //# sourceMappingURL=tanstack.js.map
191
+ //# sourceMappingURL=tanstack.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/pushdown.ts","../src/tanstack.ts"],"names":["row"],"mappings":";AAuCO,SAAS,QAAA,CACd,MACA,YAAA,EACkB;AAClB,EAAA,IAAI,CAAC,MAAM,OAAO,IAAA;AAClB,EAAA,OAAO,SAAA,CAAU,MAAM,YAAY,CAAA;AACrC;AAEA,SAAS,SAAA,CACP,MACA,YAAA,EACkB;AAClB,EAAA,QAAQ,KAAK,IAAA;AAAM,IACjB,KAAK,KAAA;AAGH,MAAA,OAAO,IAAA;AAAA,IACT,KAAK,KAAA;AAGH,MAAA,OAAO,cAAA,CAAe,MAAiB,YAAY,CAAA;AAAA,IACrD,KAAK,MAAA,EAAQ;AACX,MAAA,MAAM,CAAA,GAAI,IAAA;AACV,MAAA,OAAO,aAAA,CAAc,GAAG,YAAY,CAAA;AAAA,IACtC;AAAA,IACA;AACE,MAAA,OAAO,IAAA;AAAA;AAEb;AAEA,SAAS,aAAA,CACP,GACA,YAAA,EACkB;AAClB,EAAA,MAAM,OAAO,CAAA,CAAE,IAAA;AACf,EAAA,MAAM,OAAO,CAAA,CAAE,IAAA;AAEf,EAAA,IAAI,IAAA,KAAS,IAAA,IAAQ,IAAA,CAAK,MAAA,KAAW,CAAA,EAAG;AACtC,IAAA,OAAO,YAAY,IAAA,CAAK,CAAC,GAAI,IAAA,CAAK,CAAC,GAAI,YAAY,CAAA;AAAA,EACrD;AAEA,EAAA,IAAI,IAAA,KAAS,KAAA,IAAS,IAAA,KAAS,IAAA,EAAM;AACnC,IAAA,MAAM,WAAwB,EAAC;AAC/B,IAAA,KAAA,MAAW,OAAO,IAAA,EAAM;AACtB,MAAA,MAAM,CAAA,GAAI,SAAA,CAAU,GAAA,EAAK,YAAY,CAAA;AACrC,MAAA,IAAI,CAAC,GAAG,OAAO,IAAA;AACf,MAAA,QAAA,CAAS,KAAK,CAAC,CAAA;AAAA,IACjB;AACA,IAAA,IAAI,SAAS,KAAA,EAAO;AAClB,MAAA,OAAO,CAAC,QAAQ,QAAA,CAAS,KAAA,CAAM,CAAC,CAAA,KAAM,CAAA,CAAE,GAAG,CAAC,CAAA;AAAA,IAC9C;AACA,IAAA,OAAO,CAAC,QAAQ,QAAA,CAAS,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,GAAG,CAAC,CAAA;AAAA,EAC7C;AAEA,EAAA,IAAI,IAAA,KAAS,KAAA,IAAS,IAAA,CAAK,MAAA,KAAW,CAAA,EAAG;AACvC,IAAA,MAAM,KAAA,GAAQ,SAAA,CAAU,IAAA,CAAK,CAAC,GAAI,YAAY,CAAA;AAC9C,IAAA,IAAI,CAAC,OAAO,OAAO,IAAA;AACnB,IAAA,OAAO,CAAC,GAAA,KAAQ,CAAC,KAAA,CAAM,GAAG,CAAA;AAAA,EAC5B;AAEA,EAAA,OAAO,IAAA;AACT;AAEA,SAAS,WAAA,CACP,CAAA,EACA,CAAA,EACA,YAAA,EACkB;AAElB,EAAA,IAAI,GAAA,GAAsB,IAAA;AAC1B,EAAA,IAAI,GAAA,GAAwB,IAAA;AAC5B,EAAA,IAAI,CAAA,CAAE,IAAA,KAAS,KAAA,IAAS,CAAA,CAAE,SAAS,KAAA,EAAO;AACxC,IAAA,GAAA,GAAM,CAAA;AACN,IAAA,GAAA,GAAM,CAAA;AAAA,EACR,WAAW,CAAA,CAAE,IAAA,KAAS,KAAA,IAAS,CAAA,CAAE,SAAS,KAAA,EAAO;AAC/C,IAAA,GAAA,GAAM,CAAA;AACN,IAAA,GAAA,GAAM,CAAA;AAAA,EACR,CAAA,MAAO;AACL,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,MAAM,GAAA,GAAM,UAAA,CAAW,GAAA,EAAK,YAAY,CAAA;AACxC,EAAA,IAAI,CAAC,KAAK,OAAO,IAAA;AACjB,EAAA,MAAM,UAAU,GAAA,CAAI,KAAA;AACpB,EAAA,OAAO,CAAC,GAAA,KAAQ,GAAA,CAAI,GAAG,CAAA,KAAM,OAAA;AAC/B;AAEA,SAAS,cAAA,CACP,KACA,YAAA,EACkB;AAClB,EAAA,MAAM,GAAA,GAAM,UAAA,CAAW,GAAA,EAAK,YAAY,CAAA;AACxC,EAAA,IAAI,CAAC,KAAK,OAAO,IAAA;AACjB,EAAA,OAAO,CAAC,GAAA,KAAQ,GAAA,CAAI,GAAG,CAAA,KAAM,IAAA;AAC/B;AAEA,SAAS,UAAA,CACP,KACA,YAAA,EACe;AACf,EAAA,IAAI,CAAC,GAAA,CAAI,IAAA,IAAQ,IAAI,IAAA,CAAK,MAAA,KAAW,GAAG,OAAO,IAAA;AAI/C,EAAA,IAAI,GAAA;AACJ,EAAA,IAAI,GAAA,CAAI,IAAA,CAAK,MAAA,KAAW,CAAA,EAAG;AACzB,IAAA,GAAA,GAAM,GAAA,CAAI,KAAK,CAAC,CAAA;AAAA,EAClB,CAAA,MAAA,IAAW,GAAA,CAAI,IAAA,CAAK,MAAA,KAAW,CAAA,EAAG;AAChC,IAAA,GAAA,GAAM,GAAA,CAAI,KAAK,CAAC,CAAA;AAAA,EAClB,CAAA,MAAO;AACL,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,IAAI,CAAC,YAAA,CAAa,GAAA,CAAI,GAAG,GAAG,OAAO,IAAA;AACnC,EAAA,OAAO,GAAA;AACT;;;AChGO,SAAS,oBAAA,CACd,MAAA,EACA,OAAA,GAA6B,EAAC,EACM;AACpC,EAAA,MAAM,KAAK,OAAA,CAAQ,EAAA,IAAM,CAAA,UAAA,EAAa,MAAA,CAAO,OAAO,IAAI,CAAA,CAAA;AACxD,EAAA,MAAM,MAAA,GAAS,OAAO,MAAA,CAAO,UAAA;AAC7B,EAAA,MAAM,kBAAA,GAAqB,QAAQ,kBAAA,IAAsB,KAAA;AAEzD,EAAA,MAAM,IAAA,GAAqC;AAAA,IACzC,IAAA,EAAM,CAAC,MAAA,KAAW;AAChB,MAAA,MAAM,EAAE,KAAA,EAAO,KAAA,EAAO,MAAA,EAAQ,SAAA,EAAW,UAAS,GAAI,MAAA;AAGtD,MAAA,KAAA,EAAM;AACN,MAAA,KAAA,MAAW,OAAO,MAAA,CAAO,IAAA,CAAK,EAAE,kBAAA,EAAoB,CAAA,EAAG;AAGrD,QAAA,KAAA,CAAM,EAAE,KAAA,EAAO,GAAA,EAAK,IAAA,EAAM,UAAU,CAAA;AAAA,MACtC;AACA,MAAA,MAAA,EAAO;AACP,MAAA,SAAA,EAAU;AAGV,MAAA,MAAM,WAAA,GAAc,MAAA,CAAO,SAAA,CAAU,CAAC,KAAA,KAAU;AAG9C,QAAA,IAAI,KAAA,CAAM,EAAA,KAAO,SAAA,IAAa,KAAA,CAAM,OAAO,UAAA,EAAY;AACrD,UAAA,KAAA,EAAM;AACN,UAAA,QAAA,EAAS;AACT,UAAA,KAAA,MAAW,OAAO,MAAA,CAAO,IAAA,CAAK,EAAE,kBAAA,EAAoB,CAAA,EAAG;AACrD,YAAA,KAAA,CAAM,EAAE,KAAA,EAAO,GAAA,EAAK,IAAA,EAAM,UAAU,CAAA;AAAA,UACtC;AACA,UAAA,MAAA,EAAO;AACP,UAAA;AAAA,QACF;AAEA,QAAA,cAAA,CAAe,MAAA,EAAQ,KAAA,EAAO,MAAA,EAAQ,kBAAA,EAAoB;AAAA,UACxD,KAAA;AAAA,UACA,KAAA;AAAA,UACA;AAAA,SACD,CAAA;AAAA,MACH,CAAC,CAAA;AAED,MAAA,OAAO;AAAA,QACL,OAAA,EAAS,MAAM,WAAA,EAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAQ3B,UAAA,EAAY,CAAC,IAAA,KAA4B;AACvC,UAAA,MAAM,SAAA,GAAY,IAAI,GAAA,CAAI,MAAA,CAAO,aAAa,CAAA;AAC9C,UAAA,MAAM,SAAA,GAAY,QAAA,CAAS,IAAA,CAAK,KAAA,EAAO,SAAS,CAAA;AAChD,UAAA,IAAI,CAAC,SAAA,EAAW;AAId,YAAA,OAAO,IAAA;AAAA,UACT;AACA,UAAA,KAAA,EAAM;AACN,UAAA,KAAA,MAAW,OAAO,MAAA,CAAO,IAAA,CAAK,EAAE,kBAAA,EAAoB,CAAA,EAAG;AACrD,YAAA,IAAI,CAAC,SAAA,CAAU,GAAG,CAAA,EAAG;AACrB,YAAA,KAAA,CAAM,EAAE,KAAA,EAAO,GAAA,EAAK,IAAA,EAAM,UAAU,CAAA;AAAA,UACtC;AACA,UAAA,MAAA,EAAO;AACP,UAAA,OAAO,IAAA;AAAA,QACT;AAAA,OACF;AAAA,IACF,CAAA;AAAA;AAAA;AAAA;AAAA,IAIA,aAAA,EAAe;AAAA,GACjB;AAEA,EAAA,OAAO;AAAA,IACL,EAAA;AAAA,IACA,MAAA,EAAQ,CAAC,IAAA,KAAmB,IAAA,CAAK,MAAM,CAAA;AAAA,IACvC,IAAA;AAAA,IACA,QAAA,EAAU,OAAO,EAAE,WAAA,EAAY,KAAM;AACnC,MAAA,KAAA,MAAW,CAAA,IAAK,YAAY,SAAA,EAAW;AACrC,QAAA,MAAA,CAAO,MAAA,CAAO,EAAE,QAAoB,CAAA;AAAA,MACtC;AAAA,IACF,CAAA;AAAA,IACA,QAAA,EAAU,OAAO,EAAE,WAAA,EAAY,KAAM;AACnC,MAAA,KAAA,MAAW,CAAA,IAAK,YAAY,SAAA,EAAW;AACrC,QAAA,MAAM,MAAM,CAAA,CAAE,GAAA;AAId,QAAA,MAAA,CAAO,MAAA,CAAO,GAAA,EAAK,CAAA,CAAE,OAA4B,CAAA;AAAA,MACnD;AAAA,IACF,CAAA;AAAA,IACA,QAAA,EAAU,OAAO,EAAE,WAAA,EAAY,KAAM;AACnC,MAAA,KAAA,MAAW,CAAA,IAAK,YAAY,SAAA,EAAW;AACrC,QAAA,MAAA,CAAO,MAAA,CAAO,EAAE,GAAa,CAAA;AAAA,MAC/B;AAAA,IACF;AAAA,GACF;AACF;AAgBA,SAAS,cAAA,CACP,MAAA,EACA,KAAA,EACA,OAAA,EACA,oBACA,IAAA,EACM;AACN,EAAA,MAAM,MAAM,KAAA,CAAM,KAAA;AAElB,EAAA,IAAI,KAAA,CAAM,OAAO,QAAA,EAAU;AAIzB,IAAA,IAAI,CAAC,kBAAA,EAAoB;AACvB,MAAA,IAAA,CAAK,KAAA,EAAM;AACX,MAAA,IAAA,CAAK,KAAA,CAAM,EAAE,GAAA,EAAK,IAAA,EAAM,UAAU,CAAA;AAClC,MAAA,IAAA,CAAK,MAAA,EAAO;AACZ,MAAA;AAAA,IACF;AAGA,IAAA,MAAMA,IAAAA,GAAM,MAAA,CAAO,GAAA,CAAI,GAAG,CAAA;AAC1B,IAAA,IAAI,CAACA,IAAAA,EAAK;AACV,IAAA,IAAA,CAAK,KAAA,EAAM;AACX,IAAA,IAAA,CAAK,MAAM,EAAE,KAAA,EAAOA,IAAAA,EAAK,IAAA,EAAM,UAAU,CAAA;AACzC,IAAA,IAAA,CAAK,MAAA,EAAO;AACZ,IAAA;AAAA,EACF;AAGA,EAAA,MAAM,GAAA,GAAM,MAAA,CAAO,GAAA,CAAI,GAAG,CAAA;AAC1B,EAAA,IAAI,CAAC,GAAA,EAAK;AACV,EAAA,IAAA,CAAK,KAAA,EAAM;AACX,EAAA,IAAA,CAAK,KAAA,CAAM;AAAA,IACT,KAAA,EAAO,GAAA;AAAA,IACP,IAAA,EAAM,KAAA,CAAM,EAAA,KAAO,QAAA,GAAW,QAAA,GAAW;AAAA,GAC1C,CAAA;AACD,EAAA,IAAA,CAAK,MAAA,EAAO;AACd","file":"tanstack.js","sourcesContent":["/**\n * TanStack DB → ArrowBase push-down: translate a `BasicExpression<boolean>`\n * into an ArrowBase predicate `(row) => boolean` when possible.\n *\n * v1 recognises:\n * - eq(prop, literal) → row[col] === lit\n * - eq(literal, prop) → row[col] === lit\n * - and(a, b, ...) → every translated arg\n * - or(a, b, ...) → some translated arg\n * - not(a) → !translated arg\n *\n * Any expression containing a node type that is not one of the above\n * causes the translator to return `null`. Callers should treat `null`\n * as \"not pushable\" and either fall back to full scan or let TanStack\n * DB's own evaluator run against the materialized rows.\n *\n * `PropRef` paths are taken as `[alias, column]` when length === 2, or\n * `[column]` when length === 1. Anything deeper (nested struct fields)\n * is not supported in v1.\n */\n\nimport type { RowValue } from './collection/collection.js';\n\n// TanStack DB's IR lives under the `IR` namespace on the package root.\n// We only depend on the types at compile time; runtime type-checks use\n// the `type` field on each node so no runtime IR import is needed.\nimport type { IR } from '@tanstack/db';\n\ntype BasicExpression<T = unknown> = IR.BasicExpression<T>;\ntype PropRef<T = unknown> = IR.PropRef<T>;\ntype ValueExpr<T = unknown> = IR.Value<T>;\ntype Func<T = unknown> = IR.Func<T>;\n\nexport type Predicate = (row: RowValue) => boolean;\n\n/**\n * Attempt to translate a TanStack DB expression into an ArrowBase\n * predicate. Returns `null` if the expression is not pushable.\n */\nexport function pushDown(\n expr: BasicExpression<boolean> | undefined,\n knownColumns: ReadonlySet<string>,\n): Predicate | null {\n if (!expr) return null;\n return translate(expr, knownColumns);\n}\n\nfunction translate(\n expr: BasicExpression,\n knownColumns: ReadonlySet<string>,\n): Predicate | null {\n switch (expr.type) {\n case 'val':\n // Literal at the root of a where doesn't make sense; TanStack\n // would have short-circuited.\n return null;\n case 'ref':\n // A ref at the root means boolean-column projection\n // (e.g. where(row.active)). Supported.\n return toRefPredicate(expr as PropRef, knownColumns);\n case 'func': {\n const f = expr as Func;\n return translateFunc(f, knownColumns);\n }\n default:\n return null;\n }\n}\n\nfunction translateFunc(\n f: Func,\n knownColumns: ReadonlySet<string>,\n): Predicate | null {\n const name = f.name;\n const args = f.args;\n\n if (name === 'eq' && args.length === 2) {\n return translateEq(args[0]!, args[1]!, knownColumns);\n }\n\n if (name === 'and' || name === 'or') {\n const children: Predicate[] = [];\n for (const arg of args) {\n const c = translate(arg, knownColumns);\n if (!c) return null;\n children.push(c);\n }\n if (name === 'and') {\n return (row) => children.every((c) => c(row));\n }\n return (row) => children.some((c) => c(row));\n }\n\n if (name === 'not' && args.length === 1) {\n const inner = translate(args[0]!, knownColumns);\n if (!inner) return null;\n return (row) => !inner(row);\n }\n\n return null;\n}\n\nfunction translateEq(\n a: BasicExpression,\n b: BasicExpression,\n knownColumns: ReadonlySet<string>,\n): Predicate | null {\n // eq(ref, value) or eq(value, ref). Anything else → not pushable.\n let ref: PropRef | null = null;\n let lit: ValueExpr | null = null;\n if (a.type === 'ref' && b.type === 'val') {\n ref = a as PropRef;\n lit = b as ValueExpr;\n } else if (a.type === 'val' && b.type === 'ref') {\n ref = b as PropRef;\n lit = a as ValueExpr;\n } else {\n return null;\n }\n\n const col = resolveRef(ref, knownColumns);\n if (!col) return null;\n const literal = lit.value;\n return (row) => row[col] === literal;\n}\n\nfunction toRefPredicate(\n ref: PropRef,\n knownColumns: ReadonlySet<string>,\n): Predicate | null {\n const col = resolveRef(ref, knownColumns);\n if (!col) return null;\n return (row) => row[col] === true;\n}\n\nfunction resolveRef(\n ref: PropRef,\n knownColumns: ReadonlySet<string>,\n): string | null {\n if (!ref.path || ref.path.length === 0) return null;\n // TanStack DB encodes refs as [alias, column] inside a join/from,\n // but for a single collection the alias is dropped. We accept both\n // shapes: [col] or [alias, col]. Anything deeper is rejected.\n let col: string;\n if (ref.path.length === 1) {\n col = ref.path[0]!;\n } else if (ref.path.length === 2) {\n col = ref.path[1]!;\n } else {\n return null;\n }\n if (!knownColumns.has(col)) return null;\n return col;\n}\n","/**\n * TanStack DB adapter for ArrowBase.\n *\n * Exposes an ArrowBase `Collection` as a standard `@tanstack/db`\n * `createCollection({ sync, onInsert, onUpdate, onDelete, getKey })` config\n * so the two stores can be joined via TanStack DB's live query engine.\n *\n * Design notes:\n * - `@tanstack/db` is an optional peer dependency. This module only\n * imports its *types* (erased at build time) and its value exports\n * are resolved by the consumer. Import this module via the\n * `arrowbase/tanstack` subpath only when you intend to use TanStack\n * DB; the main `arrowbase` entry stays TanStack-free.\n * - Rows handed to TanStack DB are plain JS objects (a snapshot from\n * ArrowBase). The zero-copy proxy idea from PRD v2.0 was dropped in\n * v2.1 because TanStack caches values internally.\n * - Row-update mode is 'full' — every sync write carries a full row\n * snapshot. `partial` would require TanStack DB to merge deltas\n * against its cached value, but we always have the full current\n * value from ArrowBase, so 'full' is simpler and correct.\n */\n\nimport type {\n CollectionConfig,\n DeleteKeyMessage,\n SyncConfig,\n LoadSubsetOptions,\n} from '@tanstack/db';\n\nimport type { Collection, RowValue } from './collection/collection.js';\nimport type { ChangeEvent } from './collection/events.js';\nimport { pushDown } from './pushdown.js';\n\n/** Options for the adapter. */\nexport interface ToTanstackOptions {\n /**\n * TanStack DB collection id. Defaults to `arrowbase:<schema.name>`.\n */\n id?: string;\n /**\n * When true, include soft-deleted rows in sync. Default false (the\n * adapter filters them out, matching `Collection.rows()` semantics).\n */\n includeSoftDeleted?: boolean;\n}\n\n/**\n * Convert an ArrowBase collection into a TanStack DB `CollectionConfig`.\n *\n * Usage:\n * ```ts\n * import { createCollection } from '@tanstack/db';\n * import { toTanstackCollection } from 'arrowbase/tanstack';\n *\n * const features = createCollection(toTanstackCollection(abFeatures));\n * ```\n */\nexport function toTanstackCollection(\n abColl: Collection,\n options: ToTanstackOptions = {},\n): CollectionConfig<RowValue, number> {\n const id = options.id ?? `arrowbase:${abColl.schema.name}`;\n const pkName = abColl.schema.primaryKey;\n const includeSoftDeleted = options.includeSoftDeleted ?? false;\n\n const sync: SyncConfig<RowValue, number> = {\n sync: (params) => {\n const { begin, write, commit, markReady, truncate } = params;\n\n // ---- Initial backfill ---------------------------------------\n begin();\n for (const row of abColl.rows({ includeSoftDeleted })) {\n // TanStack DB derives the key from the value via getKey when\n // the `key` field is absent, per the sync protocol.\n write({ value: row, type: 'insert' });\n }\n commit();\n markReady();\n\n // ---- Live change feed ---------------------------------------\n const unsubscribe = abColl.subscribe((event) => {\n // compact + rollback are wholesale changes: truncate then\n // replay every live row.\n if (event.op === 'compact' || event.op === 'rollback') {\n begin();\n truncate();\n for (const row of abColl.rows({ includeSoftDeleted })) {\n write({ value: row, type: 'insert' });\n }\n commit();\n return;\n }\n\n translateEvent(abColl, event, pkName, includeSoftDeleted, {\n begin,\n write,\n commit,\n });\n });\n\n return {\n cleanup: () => unsubscribe(),\n // Push-down hook: when TanStack DB requests a subset with an\n // eq/and/or/not filter over known columns, we translate it to\n // an ArrowBase predicate and emit only matching rows. This\n // module keeps behaviour correct regardless — TanStack DB also\n // re-evaluates the predicate against materialized rows, so an\n // over-approximation here just means extra work, never wrong\n // results.\n loadSubset: (opts: LoadSubsetOptions) => {\n const knownCols = new Set(abColl.columnNames());\n const predicate = pushDown(opts.where, knownCols);\n if (!predicate) {\n // Not pushable: TanStack DB already has the full dataset\n // from the initial eager backfill. Returning `true` signals\n // \"already loaded\" — no additional work required.\n return true;\n }\n begin();\n for (const row of abColl.rows({ includeSoftDeleted })) {\n if (!predicate(row)) continue;\n write({ value: row, type: 'update' });\n }\n commit();\n return true;\n },\n };\n },\n // Full row snapshots keep the adapter stateless. TanStack DB's\n // default is 'partial'; we override because ArrowBase always\n // produces a full row on read.\n rowUpdateMode: 'full',\n };\n\n return {\n id,\n getKey: (item: RowValue) => item[pkName] as number,\n sync,\n onInsert: async ({ transaction }) => {\n for (const m of transaction.mutations) {\n abColl.insert(m.modified as RowValue);\n }\n },\n onUpdate: async ({ transaction }) => {\n for (const m of transaction.mutations) {\n const key = m.key as number;\n // TanStack's `changes` is the field-level diff; forward only\n // the changed columns so we do not bump unchanged column\n // versions unnecessarily.\n abColl.update(key, m.changes as Partial<RowValue>);\n }\n },\n onDelete: async ({ transaction }) => {\n for (const m of transaction.mutations) {\n abColl.delete(m.key as number);\n }\n },\n };\n}\n\n// ---------------------------------------------------------------------\n// helpers\n// ---------------------------------------------------------------------\n\ninterface SyncPort {\n begin: (options?: { immediate?: boolean }) => void;\n write: (\n msg:\n | { value: RowValue; type: 'insert' | 'update' }\n | DeleteKeyMessage<number>,\n ) => void;\n commit: () => void;\n}\n\nfunction translateEvent(\n abColl: Collection,\n event: ChangeEvent,\n _pkName: string,\n includeSoftDeleted: boolean,\n port: SyncPort,\n): void {\n const key = event.rowId;\n\n if (event.op === 'delete') {\n // Hard delete: row is gone. Soft delete: row still exists but\n // `__deleted=true`. TanStack DB should see a delete in either\n // case unless the caller opted in to soft-deleted visibility.\n if (!includeSoftDeleted) {\n port.begin();\n port.write({ key, type: 'delete' });\n port.commit();\n return;\n }\n // Soft delete with includeSoftDeleted=true: emit as update so the\n // __deleted=true field propagates to consumers.\n const row = abColl.get(key);\n if (!row) return;\n port.begin();\n port.write({ value: row, type: 'update' });\n port.commit();\n return;\n }\n\n // Insert / update: read the full current row and emit.\n const row = abColl.get(key);\n if (!row) return; // e.g. the row was deleted between the emit and here\n port.begin();\n port.write({\n value: row,\n type: event.op === 'insert' ? 'insert' : 'update',\n });\n port.commit();\n}\n"]}
@@ -0,0 +1,50 @@
1
+ import { R as RowValue } from './collection-DGlKgOGi.js';
2
+
3
+ /**
4
+ * Delta types exchanged by SyncAdapters.
5
+ *
6
+ * A `Delta` carries a single primary-key change with its outcome:
7
+ * - insert: full row payload, with `version` = globalVersion after insert
8
+ * - update: full row payload (we send post-image for simplicity in v1)
9
+ * - delete: no payload
10
+ *
11
+ * v1 uses `version` as a monotonic clock. Senders are responsible for
12
+ * producing deltas in version order; receivers apply blindly. No
13
+ * per-row version tracking yet — last-writer-wins emerges from
14
+ * deterministic version ordering at the sender.
15
+ */
16
+
17
+ type DeltaOp = 'insert' | 'update' | 'delete';
18
+ interface Delta {
19
+ readonly op: DeltaOp;
20
+ readonly key: number;
21
+ /** Required for insert/update, absent for delete. */
22
+ readonly value?: RowValue;
23
+ /**
24
+ * Monotonic version at the moment of the mutation (globalVersion).
25
+ * Consumers use this to resume from a cursor.
26
+ */
27
+ readonly version: number;
28
+ }
29
+ /**
30
+ * Interface implemented by pluggable sync transports (REST, WebSocket,
31
+ * CRDT, …). Adapters shuttle deltas between a local ArrowBase collection
32
+ * and a remote source.
33
+ */
34
+ interface SyncAdapter {
35
+ /** Apply remote deltas to the local collection. */
36
+ apply(deltas: readonly Delta[]): Promise<void>;
37
+ /**
38
+ * Pull deltas strictly newer than `sinceVersion`. Returns the deltas
39
+ * and the new cursor (`upTo`) that callers should pass back on the
40
+ * next pull.
41
+ */
42
+ pull(sinceVersion: number): Promise<{
43
+ deltas: Delta[];
44
+ upTo: number;
45
+ }>;
46
+ /** Push local deltas to the remote. */
47
+ push(deltas: readonly Delta[]): Promise<void>;
48
+ }
49
+
50
+ export type { Delta as D, SyncAdapter as S, DeltaOp as a };
package/package.json ADDED
@@ -0,0 +1,139 @@
1
+ {
2
+ "name": "arrowbase",
3
+ "version": "0.1.1",
4
+ "private": false,
5
+ "description": "Columnar Arrow store with an @tanstack/db-compatible API.",
6
+ "keywords": [
7
+ "arrow",
8
+ "columnar",
9
+ "tanstack-db",
10
+ "local-first",
11
+ "sharedarraybuffer",
12
+ "reactive-query"
13
+ ],
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+ssh://git@github.com/natanelia/arrowbase.git"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/natanelia/arrowbase/issues"
21
+ },
22
+ "homepage": "https://natanelia.github.io/arrowbase/",
23
+ "type": "module",
24
+ "sideEffects": false,
25
+ "main": "./dist/index.js",
26
+ "module": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js"
32
+ },
33
+ "./tanstack": {
34
+ "types": "./dist/tanstack.d.ts",
35
+ "import": "./dist/tanstack.js"
36
+ },
37
+ "./react": {
38
+ "types": "./dist/react.d.ts",
39
+ "import": "./dist/react.js"
40
+ },
41
+ "./arrow": {
42
+ "types": "./dist/arrow.d.ts",
43
+ "import": "./dist/arrow.js"
44
+ },
45
+ "./idb": {
46
+ "types": "./dist/idb.d.ts",
47
+ "import": "./dist/idb.js"
48
+ },
49
+ "./broadcast": {
50
+ "types": "./dist/broadcast.d.ts",
51
+ "import": "./dist/broadcast.js"
52
+ }
53
+ },
54
+ "files": [
55
+ "dist",
56
+ "README.md",
57
+ "MIGRATING.md",
58
+ "COMPATIBILITY.md",
59
+ "CHANGELOG.md",
60
+ "RELEASE_NOTES.md",
61
+ "LICENSE"
62
+ ],
63
+ "publishConfig": {
64
+ "access": "public",
65
+ "provenance": true
66
+ },
67
+ "scripts": {
68
+ "build": "tsup",
69
+ "typecheck": "tsc --noEmit",
70
+ "test": "vitest run -c vitest.config.ts",
71
+ "test:watch": "vitest -c vitest.config.ts",
72
+ "test:differential": "vitest run -c vitest.differential.config.ts",
73
+ "test:golden": "vitest run -c vitest.golden.config.ts",
74
+ "test:showcase-docs": "vitest run -c vitest.config.ts tests/showcase-docs.test.ts",
75
+ "test:public-api": "vitest run -c vitest.config.ts tests/public-api-contract.test.ts",
76
+ "verify:package-exports": "tsx scripts/verify-package-exports.ts",
77
+ "lint": "tsc --noEmit",
78
+ "bench": "node --expose-gc --import tsx bench/index.ts",
79
+ "bench:all": "tsx bench/all.ts",
80
+ "bench:ivm": "tsx bench/ivm-live.ts",
81
+ "bench:operations": "node --expose-gc --import tsx bench/query-operations.ts",
82
+ "bench:spatial": "node --expose-gc --import tsx bench/spatial.ts",
83
+ "bench:vs-tanstack": "node --expose-gc --import tsx bench/vs-tanstack.ts",
84
+ "release:verify": "node scripts/verify-release.mjs",
85
+ "release:pack": "pnpm build && pnpm pack --pack-destination .release",
86
+ "playground:build:pages": "pnpm --filter @arrowbase/showcase build:pages",
87
+ "release:tag": "git tag v$npm_package_version",
88
+ "prepublishOnly": "pnpm release:verify && pnpm check && pnpm build && pnpm verify:package-exports",
89
+ "check": "pnpm typecheck && pnpm test:public-api && pnpm test:differential && pnpm test:golden && pnpm test:showcase-docs && pnpm test",
90
+ "playground:dev": "pnpm --filter @arrowbase/showcase dev",
91
+ "playground:build": "pnpm --filter @arrowbase/showcase build",
92
+ "playground:test": "pnpm --filter @arrowbase/showcase test",
93
+ "playground:e2e": "pnpm --filter @arrowbase/showcase e2e",
94
+ "playground:typecheck": "pnpm --filter @arrowbase/showcase typecheck",
95
+ "playground:check": "pnpm playground:typecheck && pnpm playground:test && pnpm playground:build",
96
+ "bench:showcase": "pnpm --filter @arrowbase/showcase bench:report"
97
+ },
98
+ "engines": {
99
+ "node": ">=20"
100
+ },
101
+ "peerDependencies": {
102
+ "@tanstack/db": "^0.6.16",
103
+ "apache-arrow": "^19.0.0 || ^20.0.0 || ^21.0.0",
104
+ "react": "^18.0.0 || ^19.0.0"
105
+ },
106
+ "peerDependenciesMeta": {
107
+ "@tanstack/db": {
108
+ "optional": true
109
+ },
110
+ "apache-arrow": {
111
+ "optional": true
112
+ },
113
+ "react": {
114
+ "optional": true
115
+ }
116
+ },
117
+ "devDependencies": {
118
+ "@tanstack/db": "^0.6.16",
119
+ "@testing-library/react": "^16.3.2",
120
+ "@types/node": "^22.10.0",
121
+ "@types/react": "^19.2.14",
122
+ "@types/react-dom": "^19.2.3",
123
+ "apache-arrow": "^21.1.0",
124
+ "fake-indexeddb": "^6.2.5",
125
+ "jsdom": "^25.0.1",
126
+ "react": "^19.2.5",
127
+ "react-dom": "^19.2.5",
128
+ "tinybench": "^6.0.1",
129
+ "tsup": "^8.3.5",
130
+ "tsx": "^4.21.0",
131
+ "typescript": "^5.6.3",
132
+ "vitest": "^2.1.8"
133
+ },
134
+ "packageManager": "pnpm@10.26.2",
135
+ "dependencies": {
136
+ "@tanstack/db-ivm": "0.1.18",
137
+ "flatbush": "^4.5.1"
138
+ }
139
+ }