lakesync 0.1.0

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.
Files changed (69) hide show
  1. package/README.md +74 -0
  2. package/dist/adapter.d.ts +369 -0
  3. package/dist/adapter.js +39 -0
  4. package/dist/adapter.js.map +1 -0
  5. package/dist/analyst.d.ts +268 -0
  6. package/dist/analyst.js +495 -0
  7. package/dist/analyst.js.map +1 -0
  8. package/dist/auth-CAVutXzx.d.ts +30 -0
  9. package/dist/base-poller-Qo_SmCZs.d.ts +82 -0
  10. package/dist/catalogue.d.ts +65 -0
  11. package/dist/catalogue.js +17 -0
  12. package/dist/catalogue.js.map +1 -0
  13. package/dist/chunk-4ARO6KTJ.js +257 -0
  14. package/dist/chunk-4ARO6KTJ.js.map +1 -0
  15. package/dist/chunk-5YOFCJQ7.js +1115 -0
  16. package/dist/chunk-5YOFCJQ7.js.map +1 -0
  17. package/dist/chunk-7D4SUZUM.js +38 -0
  18. package/dist/chunk-7D4SUZUM.js.map +1 -0
  19. package/dist/chunk-BNJOGBYK.js +335 -0
  20. package/dist/chunk-BNJOGBYK.js.map +1 -0
  21. package/dist/chunk-ICNT7I3K.js +1180 -0
  22. package/dist/chunk-ICNT7I3K.js.map +1 -0
  23. package/dist/chunk-P5DRFKIT.js +413 -0
  24. package/dist/chunk-P5DRFKIT.js.map +1 -0
  25. package/dist/chunk-X3RO5SYJ.js +880 -0
  26. package/dist/chunk-X3RO5SYJ.js.map +1 -0
  27. package/dist/client.d.ts +428 -0
  28. package/dist/client.js +2048 -0
  29. package/dist/client.js.map +1 -0
  30. package/dist/compactor.d.ts +342 -0
  31. package/dist/compactor.js +793 -0
  32. package/dist/compactor.js.map +1 -0
  33. package/dist/coordinator-CxckTzYW.d.ts +396 -0
  34. package/dist/db-types-BR6Kt4uf.d.ts +29 -0
  35. package/dist/gateway-D5SaaMvT.d.ts +337 -0
  36. package/dist/gateway-server.d.ts +306 -0
  37. package/dist/gateway-server.js +4663 -0
  38. package/dist/gateway-server.js.map +1 -0
  39. package/dist/gateway.d.ts +196 -0
  40. package/dist/gateway.js +79 -0
  41. package/dist/gateway.js.map +1 -0
  42. package/dist/hlc-DiD8QNG3.d.ts +70 -0
  43. package/dist/index.d.ts +245 -0
  44. package/dist/index.js +102 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/json-dYtqiL0F.d.ts +18 -0
  47. package/dist/nessie-client-DrNikVXy.d.ts +160 -0
  48. package/dist/parquet.d.ts +78 -0
  49. package/dist/parquet.js +15 -0
  50. package/dist/parquet.js.map +1 -0
  51. package/dist/proto.d.ts +434 -0
  52. package/dist/proto.js +67 -0
  53. package/dist/proto.js.map +1 -0
  54. package/dist/react.d.ts +147 -0
  55. package/dist/react.js +224 -0
  56. package/dist/react.js.map +1 -0
  57. package/dist/resolver-C3Wphi6O.d.ts +10 -0
  58. package/dist/result-CojzlFE2.d.ts +64 -0
  59. package/dist/src-QU2YLPZY.js +383 -0
  60. package/dist/src-QU2YLPZY.js.map +1 -0
  61. package/dist/src-WYBF5LOI.js +102 -0
  62. package/dist/src-WYBF5LOI.js.map +1 -0
  63. package/dist/src-WZNPHANQ.js +426 -0
  64. package/dist/src-WZNPHANQ.js.map +1 -0
  65. package/dist/types-Bs-QyOe-.d.ts +143 -0
  66. package/dist/types-DAQL_vU_.d.ts +118 -0
  67. package/dist/types-DSC_EiwR.d.ts +45 -0
  68. package/dist/types-V_jVu2sA.d.ts +73 -0
  69. package/package.json +119 -0
@@ -0,0 +1,495 @@
1
+ import {
2
+ Err,
3
+ LakeSyncError,
4
+ Ok
5
+ } from "./chunk-ICNT7I3K.js";
6
+ import "./chunk-7D4SUZUM.js";
7
+
8
+ // ../analyst/src/duckdb.ts
9
+ var DuckDBClient = class {
10
+ _config;
11
+ /* eslint-disable @typescript-eslint/no-explicit-any */
12
+ _db = null;
13
+ _conn = null;
14
+ _closed = false;
15
+ constructor(config) {
16
+ this._config = config ?? {};
17
+ }
18
+ /**
19
+ * Initialise the DuckDB-Wasm instance and open a connection.
20
+ *
21
+ * Uses the blocking Node.js bindings when running in Node/Bun,
22
+ * which avoids the need for Worker threads.
23
+ *
24
+ * @returns A Result indicating success or failure with a LakeSyncError
25
+ */
26
+ async init() {
27
+ try {
28
+ const duckdb = await import("@duckdb/duckdb-wasm/blocking");
29
+ const path = await import("path");
30
+ const { createRequire } = await import("module");
31
+ const require2 = createRequire(import.meta.url);
32
+ const blockingEntry = require2.resolve("@duckdb/duckdb-wasm/dist/duckdb-node-blocking.cjs");
33
+ const duckdbPkgPath = path.dirname(path.dirname(blockingEntry));
34
+ const bundles = {
35
+ mvp: {
36
+ mainModule: path.join(duckdbPkgPath, "dist", "duckdb-mvp.wasm"),
37
+ mainWorker: path.join(duckdbPkgPath, "dist", "duckdb-node-mvp.worker.cjs")
38
+ },
39
+ eh: {
40
+ mainModule: path.join(duckdbPkgPath, "dist", "duckdb-eh.wasm"),
41
+ mainWorker: path.join(duckdbPkgPath, "dist", "duckdb-node-eh.worker.cjs")
42
+ }
43
+ };
44
+ const logger = this._config.logger ? new duckdb.ConsoleLogger() : new duckdb.VoidLogger();
45
+ const db = await duckdb.createDuckDB(bundles, logger, duckdb.NODE_RUNTIME);
46
+ await db.instantiate(() => {
47
+ });
48
+ if (this._config.threads !== void 0) {
49
+ db.open({ maximumThreads: this._config.threads });
50
+ }
51
+ const conn = db.connect();
52
+ this._db = db;
53
+ this._conn = conn;
54
+ this._closed = false;
55
+ return Ok(void 0);
56
+ } catch (err) {
57
+ const cause = err instanceof Error ? err : new Error(String(err));
58
+ return Err(
59
+ new LakeSyncError(
60
+ `Failed to initialise DuckDB-Wasm: ${cause.message}`,
61
+ "ANALYST_ERROR",
62
+ cause
63
+ )
64
+ );
65
+ }
66
+ }
67
+ /**
68
+ * Execute a SQL query and return the results as an array of objects.
69
+ *
70
+ * @param sql - The SQL statement to execute
71
+ * @param _params - Reserved for future use (parameterised queries)
72
+ * @returns A Result containing the query results or a LakeSyncError
73
+ */
74
+ async query(sql, _params) {
75
+ try {
76
+ if (this._closed || !this._conn) {
77
+ return Err(
78
+ new LakeSyncError(
79
+ "Cannot query: DuckDB connection is closed or not initialised",
80
+ "ANALYST_ERROR"
81
+ )
82
+ );
83
+ }
84
+ const arrowTable = this._conn.query(sql);
85
+ const rows = arrowTable.toArray();
86
+ const plainRows = rows.map((row) => ({ ...row }));
87
+ return Ok(plainRows);
88
+ } catch (err) {
89
+ const cause = err instanceof Error ? err : new Error(String(err));
90
+ return Err(
91
+ new LakeSyncError(`DuckDB query failed: ${cause.message}`, "ANALYST_ERROR", cause)
92
+ );
93
+ }
94
+ }
95
+ /**
96
+ * Register an in-memory Parquet file as a named table that can be
97
+ * queried using `SELECT * FROM '<name>'`.
98
+ *
99
+ * @param name - The virtual file name (e.g. "deltas.parquet")
100
+ * @param data - The Parquet file contents as a Uint8Array
101
+ * @returns A Result indicating success or failure
102
+ */
103
+ async registerParquetBuffer(name, data) {
104
+ try {
105
+ if (this._closed || !this._db) {
106
+ return Err(
107
+ new LakeSyncError(
108
+ "Cannot register Parquet buffer: DuckDB is closed or not initialised",
109
+ "ANALYST_ERROR"
110
+ )
111
+ );
112
+ }
113
+ this._db.registerFileBuffer(name, data);
114
+ return Ok(void 0);
115
+ } catch (err) {
116
+ const cause = err instanceof Error ? err : new Error(String(err));
117
+ return Err(
118
+ new LakeSyncError(
119
+ `Failed to register Parquet buffer "${name}": ${cause.message}`,
120
+ "ANALYST_ERROR",
121
+ cause
122
+ )
123
+ );
124
+ }
125
+ }
126
+ /**
127
+ * Register a remote Parquet file by URL so it can be queried using
128
+ * `SELECT * FROM '<name>'`.
129
+ *
130
+ * @param name - The virtual file name (e.g. "remote.parquet")
131
+ * @param url - The URL pointing to the Parquet file
132
+ * @returns A Result indicating success or failure
133
+ */
134
+ async registerParquetUrl(name, url) {
135
+ try {
136
+ if (this._closed || !this._db) {
137
+ return Err(
138
+ new LakeSyncError(
139
+ "Cannot register Parquet URL: DuckDB is closed or not initialised",
140
+ "ANALYST_ERROR"
141
+ )
142
+ );
143
+ }
144
+ const HTTP_PROTOCOL = 1;
145
+ this._db.registerFileURL(name, url, HTTP_PROTOCOL, false);
146
+ return Ok(void 0);
147
+ } catch (err) {
148
+ const cause = err instanceof Error ? err : new Error(String(err));
149
+ return Err(
150
+ new LakeSyncError(
151
+ `Failed to register Parquet URL "${name}": ${cause.message}`,
152
+ "ANALYST_ERROR",
153
+ cause
154
+ )
155
+ );
156
+ }
157
+ }
158
+ /**
159
+ * Tear down the DuckDB connection and database instance.
160
+ *
161
+ * After calling close(), any subsequent query or registration calls
162
+ * will return an error Result.
163
+ */
164
+ async close() {
165
+ if (this._conn) {
166
+ try {
167
+ this._conn.close();
168
+ } catch {
169
+ }
170
+ this._conn = null;
171
+ }
172
+ this._db = null;
173
+ this._closed = true;
174
+ }
175
+ };
176
+
177
+ // ../analyst/src/time-travel.ts
178
+ var SYSTEM_COLUMNS = /* @__PURE__ */ new Set(["op", "table", "rowId", "clientId", "hlc", "deltaId"]);
179
+ var TimeTraveller = class {
180
+ _config;
181
+ _sources = [];
182
+ constructor(config) {
183
+ this._config = config;
184
+ }
185
+ /**
186
+ * Register one or more Parquet buffers containing delta data.
187
+ *
188
+ * Each buffer is registered with DuckDB and can subsequently be
189
+ * queried via the time-travel methods.
190
+ *
191
+ * @param parquetBuffers - Array of named Parquet file buffers to register
192
+ * @returns A Result indicating success or a LakeSyncError on failure
193
+ */
194
+ async registerDeltas(parquetBuffers) {
195
+ try {
196
+ for (const buf of parquetBuffers) {
197
+ const regResult = await this._config.duckdb.registerParquetBuffer(buf.name, buf.data);
198
+ if (!regResult.ok) {
199
+ return regResult;
200
+ }
201
+ this._sources.push(buf.name);
202
+ }
203
+ return Ok(void 0);
204
+ } catch (err) {
205
+ const cause = err instanceof Error ? err : new Error(String(err));
206
+ return Err(
207
+ new LakeSyncError(
208
+ `Failed to register delta data: ${cause.message}`,
209
+ "ANALYST_ERROR",
210
+ cause
211
+ )
212
+ );
213
+ }
214
+ }
215
+ /**
216
+ * Query the materialised state as of the given HLC timestamp.
217
+ *
218
+ * Filters all deltas where `hlc <= asOfHlc`, then materialises the latest
219
+ * state per (table, rowId) using column-level LWW (highest HLC wins per
220
+ * column). The user's SQL is applied on top of the materialised view,
221
+ * which is exposed as the CTE `_state`.
222
+ *
223
+ * Deleted rows (where the latest operation is DELETE) are excluded from
224
+ * the materialised view.
225
+ *
226
+ * @param asOfHlc - The HLC timestamp representing the point in time to query
227
+ * @param sql - SQL to apply on the materialised view (use `_state` as the table name)
228
+ * @returns A Result containing the query results or a LakeSyncError
229
+ */
230
+ async queryAsOf(asOfHlc, sql) {
231
+ try {
232
+ if (this._sources.length === 0) {
233
+ return Ok([]);
234
+ }
235
+ const userColumns = await this._discoverUserColumns();
236
+ if (!userColumns.ok) {
237
+ return Err(userColumns.error);
238
+ }
239
+ const materialiseSql = this._buildMaterialiseSql(userColumns.value, BigInt(asOfHlc));
240
+ const finalSql = `WITH _state AS (${materialiseSql}) ${sql}`;
241
+ const result = await this._config.duckdb.query(finalSql);
242
+ if (!result.ok) {
243
+ return Err(result.error);
244
+ }
245
+ return Ok(result.value);
246
+ } catch (err) {
247
+ const cause = err instanceof Error ? err : new Error(String(err));
248
+ return Err(
249
+ new LakeSyncError(`Time-travel queryAsOf failed: ${cause.message}`, "ANALYST_ERROR", cause)
250
+ );
251
+ }
252
+ }
253
+ /**
254
+ * Query raw deltas within a time range.
255
+ *
256
+ * Filters deltas where `fromHlc < hlc <= toHlc` and returns them as raw
257
+ * (unmaterialised) rows. Useful for audit trails and changelog views.
258
+ *
259
+ * The user's SQL is applied on top of the filtered deltas, which are
260
+ * exposed as the CTE `_deltas`.
261
+ *
262
+ * @param fromHlc - The exclusive lower bound HLC timestamp
263
+ * @param toHlc - The inclusive upper bound HLC timestamp
264
+ * @param sql - SQL to apply on the filtered deltas (use `_deltas` as the table name)
265
+ * @returns A Result containing the query results or a LakeSyncError
266
+ */
267
+ async queryBetween(fromHlc, toHlc, sql) {
268
+ try {
269
+ if (this._sources.length === 0) {
270
+ return Ok([]);
271
+ }
272
+ const unionSql = this._buildUnionSql();
273
+ const fromBigint = BigInt(fromHlc);
274
+ const toBigint = BigInt(toHlc);
275
+ const filteredSql = `
276
+ SELECT * FROM (${unionSql}) AS _all
277
+ WHERE CAST(hlc AS BIGINT) > ${fromBigint}
278
+ AND CAST(hlc AS BIGINT) <= ${toBigint}
279
+ `;
280
+ const finalSql = `WITH _deltas AS (${filteredSql}) ${sql}`;
281
+ const result = await this._config.duckdb.query(finalSql);
282
+ if (!result.ok) {
283
+ return Err(result.error);
284
+ }
285
+ return Ok(result.value);
286
+ } catch (err) {
287
+ const cause = err instanceof Error ? err : new Error(String(err));
288
+ return Err(
289
+ new LakeSyncError(
290
+ `Time-travel queryBetween failed: ${cause.message}`,
291
+ "ANALYST_ERROR",
292
+ cause
293
+ )
294
+ );
295
+ }
296
+ }
297
+ /**
298
+ * Materialise the full state at a point in time, returning all rows.
299
+ *
300
+ * Equivalent to `queryAsOf(asOfHlc, "SELECT * FROM _state")` but provided
301
+ * as a convenience method.
302
+ *
303
+ * @param asOfHlc - The HLC timestamp representing the point in time to materialise
304
+ * @returns A Result containing all materialised rows or a LakeSyncError
305
+ */
306
+ async materialiseAsOf(asOfHlc) {
307
+ return this.queryAsOf(asOfHlc, "SELECT * FROM _state");
308
+ }
309
+ /**
310
+ * Build a UNION ALL SQL expression covering all registered Parquet sources.
311
+ */
312
+ _buildUnionSql() {
313
+ return this._sources.map((s) => `SELECT * FROM '${s}'`).join(" UNION ALL BY NAME ");
314
+ }
315
+ /**
316
+ * Discover user-defined column names from the registered Parquet data.
317
+ *
318
+ * Reads the column names from the first registered source and filters
319
+ * out system columns to identify user-defined columns.
320
+ */
321
+ async _discoverUserColumns() {
322
+ if (this._sources.length === 0) {
323
+ return Ok([]);
324
+ }
325
+ const result = await this._config.duckdb.query(
326
+ `SELECT column_name FROM (DESCRIBE SELECT * FROM '${this._sources[0]}')`
327
+ );
328
+ if (!result.ok) {
329
+ return Err(result.error);
330
+ }
331
+ const userCols = result.value.map((r) => r.column_name).filter((name) => !SYSTEM_COLUMNS.has(name));
332
+ return Ok(userCols);
333
+ }
334
+ /**
335
+ * Build the materialisation SQL that reconstructs per-row state using
336
+ * column-level LWW semantics.
337
+ *
338
+ * Strategy:
339
+ * 1. Filter deltas by HLC <= asOfHlc
340
+ * 2. For each (table, rowId), determine the latest operation (by max HLC)
341
+ * 3. For each user column in each row, pick the value from the delta with
342
+ * the highest HLC (where that column is not null)
343
+ * 4. Exclude rows where the latest operation is DELETE
344
+ *
345
+ * @param userColumns - Names of user-defined columns
346
+ * @param asOfHlc - The HLC timestamp cutoff as a bigint
347
+ * @returns SQL string producing the materialised view
348
+ */
349
+ _buildMaterialiseSql(userColumns, asOfHlc) {
350
+ const unionSql = this._buildUnionSql();
351
+ if (userColumns.length === 0) {
352
+ return `
353
+ SELECT "table", "rowId"
354
+ FROM (
355
+ SELECT *,
356
+ ROW_NUMBER() OVER (
357
+ PARTITION BY "table", "rowId"
358
+ ORDER BY CAST(hlc AS BIGINT) DESC
359
+ ) AS _rn
360
+ FROM (${unionSql}) AS _all
361
+ WHERE CAST(hlc AS BIGINT) <= ${asOfHlc}
362
+ ) AS _ranked
363
+ WHERE _rn = 1 AND op != 'DELETE'
364
+ `;
365
+ }
366
+ const columnSelects = userColumns.map((col) => {
367
+ return `LAST_VALUE("${col}" IGNORE NULLS) OVER (
368
+ PARTITION BY "table", "rowId"
369
+ ORDER BY CAST(hlc AS BIGINT) ASC
370
+ ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
371
+ ) AS "${col}"`;
372
+ });
373
+ const latestOpExpr = `LAST_VALUE(op) OVER (
374
+ PARTITION BY "table", "rowId"
375
+ ORDER BY CAST(hlc AS BIGINT) ASC
376
+ ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
377
+ ) AS _latest_op`;
378
+ const rowNumExpr = `ROW_NUMBER() OVER (
379
+ PARTITION BY "table", "rowId"
380
+ ORDER BY CAST(hlc AS BIGINT) DESC
381
+ ) AS _rn`;
382
+ return `
383
+ SELECT "table", "rowId", ${userColumns.map((c) => `"${c}"`).join(", ")}
384
+ FROM (
385
+ SELECT
386
+ "table",
387
+ "rowId",
388
+ ${columnSelects.join(",\n ")},
389
+ ${latestOpExpr},
390
+ ${rowNumExpr}
391
+ FROM (${unionSql}) AS _all
392
+ WHERE CAST(hlc AS BIGINT) <= ${asOfHlc}
393
+ ) AS _materialised
394
+ WHERE _rn = 1 AND _latest_op != 'DELETE'
395
+ `;
396
+ }
397
+ };
398
+
399
+ // ../analyst/src/union-read.ts
400
+ var UnionReader = class {
401
+ _config;
402
+ _coldSources = [];
403
+ _hotCounter = 0;
404
+ constructor(config) {
405
+ this._config = config;
406
+ }
407
+ /**
408
+ * Register one or more Parquet buffers as cold data sources.
409
+ *
410
+ * Each buffer is registered with DuckDB and can subsequently be
411
+ * queried alongside hot data via {@link query}.
412
+ *
413
+ * @param parquetBuffers - Array of named Parquet file buffers to register
414
+ * @returns A Result indicating success or a LakeSyncError on failure
415
+ */
416
+ async registerColdData(parquetBuffers) {
417
+ try {
418
+ for (const buf of parquetBuffers) {
419
+ const regResult = await this._config.duckdb.registerParquetBuffer(buf.name, buf.data);
420
+ if (!regResult.ok) {
421
+ return regResult;
422
+ }
423
+ this._coldSources.push(buf.name);
424
+ }
425
+ return Ok(void 0);
426
+ } catch (err) {
427
+ const cause = err instanceof Error ? err : new Error(String(err));
428
+ return Err(
429
+ new LakeSyncError(`Failed to register cold data: ${cause.message}`, "ANALYST_ERROR", cause)
430
+ );
431
+ }
432
+ }
433
+ /**
434
+ * Execute a SQL query that unions hot in-memory rows with cold Parquet data.
435
+ *
436
+ * The caller's SQL is wrapped around a UNION ALL of cold and hot sources.
437
+ * The unioned data is available as `_union` in the SQL statement.
438
+ *
439
+ * If `hotRows` is empty or not provided, only cold data is queried.
440
+ * If no cold sources are registered and `hotRows` is provided, only hot data is queried.
441
+ *
442
+ * @param sql - SQL to apply on top of the unioned data (use `_union` as the table name)
443
+ * @param hotRows - Optional array of in-memory row objects to include in the union
444
+ * @returns A Result containing the query results or a LakeSyncError
445
+ */
446
+ async query(sql, hotRows) {
447
+ try {
448
+ const hasHot = hotRows !== void 0 && hotRows.length > 0;
449
+ const hasCold = this._coldSources.length > 0;
450
+ if (!hasHot && !hasCold) {
451
+ return Ok([]);
452
+ }
453
+ const unionParts = [];
454
+ if (hasCold) {
455
+ for (const source of this._coldSources) {
456
+ unionParts.push(`SELECT * FROM '${source}'`);
457
+ }
458
+ }
459
+ if (hasHot) {
460
+ const hotBufferName = `_hot_${this._config.tableName}_${this._hotCounter++}.json`;
461
+ const jsonBytes = new TextEncoder().encode(JSON.stringify(hotRows));
462
+ const regResult = await this._config.duckdb.registerParquetBuffer(hotBufferName, jsonBytes);
463
+ if (!regResult.ok) {
464
+ return Err(regResult.error);
465
+ }
466
+ unionParts.push(`SELECT * FROM read_json_auto('${hotBufferName}')`);
467
+ }
468
+ const unionSql = unionParts.join(" UNION ALL BY NAME ");
469
+ const finalSql = `WITH _union AS (${unionSql}) ${sql}`;
470
+ const result = await this._config.duckdb.query(finalSql);
471
+ if (!result.ok) {
472
+ return Err(result.error);
473
+ }
474
+ return Ok(result.value);
475
+ } catch (err) {
476
+ const cause = err instanceof Error ? err : new Error(String(err));
477
+ return Err(new LakeSyncError(`Union query failed: ${cause.message}`, "ANALYST_ERROR", cause));
478
+ }
479
+ }
480
+ /**
481
+ * Query only cold (Parquet) data without any hot rows.
482
+ *
483
+ * @param sql - SQL to execute against cold data (use `_union` as the table name)
484
+ * @returns A Result containing the query results or a LakeSyncError
485
+ */
486
+ async queryColdOnly(sql) {
487
+ return this.query(sql);
488
+ }
489
+ };
490
+ export {
491
+ DuckDBClient,
492
+ TimeTraveller,
493
+ UnionReader
494
+ };
495
+ //# sourceMappingURL=analyst.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../analyst/src/duckdb.ts","../../analyst/src/time-travel.ts","../../analyst/src/union-read.ts"],"sourcesContent":["import { Err, LakeSyncError, Ok, type Result } from \"@lakesync/core\";\n\n/**\n * Configuration options for the DuckDB-Wasm client.\n */\nexport interface DuckDBClientConfig {\n\t/** Whether to enable console logging from DuckDB. Defaults to false. */\n\tlogger?: boolean;\n\t/** Maximum number of threads for DuckDB. Defaults to 1. */\n\tthreads?: number;\n}\n\n/**\n * Wrapper around DuckDB-Wasm that provides a simplified, Result-based API\n * for executing SQL queries and registering Parquet data sources.\n *\n * Works in both Node.js/Bun (using the blocking bindings) and browser\n * environments (using the async worker-based bindings).\n *\n * @example\n * ```ts\n * const client = new DuckDBClient({ logger: false });\n * const initResult = await client.init();\n * if (!initResult.ok) { console.error(initResult.error); return; }\n *\n * const result = await client.query<{ answer: number }>(\"SELECT 42 AS answer\");\n * if (result.ok) console.log(result.value); // [{ answer: 42 }]\n *\n * await client.close();\n * ```\n */\nexport class DuckDBClient {\n\tprivate readonly _config: DuckDBClientConfig;\n\t/* eslint-disable @typescript-eslint/no-explicit-any */\n\tprivate _db: {\n\t\tregisterFileBuffer(name: string, buffer: Uint8Array): void;\n\t\tregisterFileURL(name: string, url: string, proto: number, directIO: boolean): void;\n\t} | null = null;\n\tprivate _conn: {\n\t\tquery<T>(text: string): { toArray(): T[] };\n\t\tclose(): void;\n\t} | null = null;\n\tprivate _closed = false;\n\n\tconstructor(config?: DuckDBClientConfig) {\n\t\tthis._config = config ?? {};\n\t}\n\n\t/**\n\t * Initialise the DuckDB-Wasm instance and open a connection.\n\t *\n\t * Uses the blocking Node.js bindings when running in Node/Bun,\n\t * which avoids the need for Worker threads.\n\t *\n\t * @returns A Result indicating success or failure with a LakeSyncError\n\t */\n\tasync init(): Promise<Result<void, LakeSyncError>> {\n\t\ttry {\n\t\t\t// Dynamic import of the blocking Node bindings\n\t\t\tconst duckdb = await import(\"@duckdb/duckdb-wasm/blocking\");\n\t\t\tconst path = await import(\"node:path\");\n\t\t\tconst { createRequire } = await import(\"node:module\");\n\t\t\t// Resolve WASM bundle paths relative to the duckdb-wasm package.\n\t\t\t// We resolve via the exported blocking entry point and walk up to the\n\t\t\t// package root, since `./package.json` is not in the exports map.\n\t\t\tconst require = createRequire(import.meta.url);\n\t\t\tconst blockingEntry = require.resolve(\"@duckdb/duckdb-wasm/dist/duckdb-node-blocking.cjs\");\n\t\t\tconst duckdbPkgPath = path.dirname(path.dirname(blockingEntry));\n\n\t\t\tconst bundles: {\n\t\t\t\tmvp: { mainModule: string; mainWorker: string };\n\t\t\t\teh?: { mainModule: string; mainWorker: string };\n\t\t\t} = {\n\t\t\t\tmvp: {\n\t\t\t\t\tmainModule: path.join(duckdbPkgPath, \"dist\", \"duckdb-mvp.wasm\"),\n\t\t\t\t\tmainWorker: path.join(duckdbPkgPath, \"dist\", \"duckdb-node-mvp.worker.cjs\"),\n\t\t\t\t},\n\t\t\t\teh: {\n\t\t\t\t\tmainModule: path.join(duckdbPkgPath, \"dist\", \"duckdb-eh.wasm\"),\n\t\t\t\t\tmainWorker: path.join(duckdbPkgPath, \"dist\", \"duckdb-node-eh.worker.cjs\"),\n\t\t\t\t},\n\t\t\t};\n\n\t\t\tconst logger = this._config.logger ? new duckdb.ConsoleLogger() : new duckdb.VoidLogger();\n\n\t\t\tconst db = await duckdb.createDuckDB(bundles, logger, duckdb.NODE_RUNTIME);\n\t\t\tawait db.instantiate(() => {});\n\n\t\t\tif (this._config.threads !== undefined) {\n\t\t\t\tdb.open({ maximumThreads: this._config.threads });\n\t\t\t}\n\n\t\t\tconst conn = db.connect();\n\n\t\t\t// Store references using structural typing (avoids importing concrete types)\n\t\t\tthis._db = db as unknown as typeof this._db;\n\t\t\tthis._conn = conn as unknown as typeof this._conn;\n\t\t\tthis._closed = false;\n\n\t\t\treturn Ok(undefined);\n\t\t} catch (err) {\n\t\t\tconst cause = err instanceof Error ? err : new Error(String(err));\n\t\t\treturn Err(\n\t\t\t\tnew LakeSyncError(\n\t\t\t\t\t`Failed to initialise DuckDB-Wasm: ${cause.message}`,\n\t\t\t\t\t\"ANALYST_ERROR\",\n\t\t\t\t\tcause,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Execute a SQL query and return the results as an array of objects.\n\t *\n\t * @param sql - The SQL statement to execute\n\t * @param _params - Reserved for future use (parameterised queries)\n\t * @returns A Result containing the query results or a LakeSyncError\n\t */\n\tasync query<T>(sql: string, _params?: unknown[]): Promise<Result<T[], LakeSyncError>> {\n\t\ttry {\n\t\t\tif (this._closed || !this._conn) {\n\t\t\t\treturn Err(\n\t\t\t\t\tnew LakeSyncError(\n\t\t\t\t\t\t\"Cannot query: DuckDB connection is closed or not initialised\",\n\t\t\t\t\t\t\"ANALYST_ERROR\",\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst arrowTable = this._conn.query<T>(sql);\n\t\t\tconst rows = arrowTable.toArray();\n\n\t\t\t// Convert Arrow StructRow proxies to plain JS objects\n\t\t\tconst plainRows = rows.map((row) => ({ ...row })) as T[];\n\t\t\treturn Ok(plainRows);\n\t\t} catch (err) {\n\t\t\tconst cause = err instanceof Error ? err : new Error(String(err));\n\t\t\treturn Err(\n\t\t\t\tnew LakeSyncError(`DuckDB query failed: ${cause.message}`, \"ANALYST_ERROR\", cause),\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Register an in-memory Parquet file as a named table that can be\n\t * queried using `SELECT * FROM '<name>'`.\n\t *\n\t * @param name - The virtual file name (e.g. \"deltas.parquet\")\n\t * @param data - The Parquet file contents as a Uint8Array\n\t * @returns A Result indicating success or failure\n\t */\n\tasync registerParquetBuffer(\n\t\tname: string,\n\t\tdata: Uint8Array,\n\t): Promise<Result<void, LakeSyncError>> {\n\t\ttry {\n\t\t\tif (this._closed || !this._db) {\n\t\t\t\treturn Err(\n\t\t\t\t\tnew LakeSyncError(\n\t\t\t\t\t\t\"Cannot register Parquet buffer: DuckDB is closed or not initialised\",\n\t\t\t\t\t\t\"ANALYST_ERROR\",\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tthis._db.registerFileBuffer(name, data);\n\t\t\treturn Ok(undefined);\n\t\t} catch (err) {\n\t\t\tconst cause = err instanceof Error ? err : new Error(String(err));\n\t\t\treturn Err(\n\t\t\t\tnew LakeSyncError(\n\t\t\t\t\t`Failed to register Parquet buffer \"${name}\": ${cause.message}`,\n\t\t\t\t\t\"ANALYST_ERROR\",\n\t\t\t\t\tcause,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Register a remote Parquet file by URL so it can be queried using\n\t * `SELECT * FROM '<name>'`.\n\t *\n\t * @param name - The virtual file name (e.g. \"remote.parquet\")\n\t * @param url - The URL pointing to the Parquet file\n\t * @returns A Result indicating success or failure\n\t */\n\tasync registerParquetUrl(name: string, url: string): Promise<Result<void, LakeSyncError>> {\n\t\ttry {\n\t\t\tif (this._closed || !this._db) {\n\t\t\t\treturn Err(\n\t\t\t\t\tnew LakeSyncError(\n\t\t\t\t\t\t\"Cannot register Parquet URL: DuckDB is closed or not initialised\",\n\t\t\t\t\t\t\"ANALYST_ERROR\",\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// DuckDBDataProtocol.HTTP = 1\n\t\t\tconst HTTP_PROTOCOL = 1;\n\t\t\tthis._db.registerFileURL(name, url, HTTP_PROTOCOL, false);\n\t\t\treturn Ok(undefined);\n\t\t} catch (err) {\n\t\t\tconst cause = err instanceof Error ? err : new Error(String(err));\n\t\t\treturn Err(\n\t\t\t\tnew LakeSyncError(\n\t\t\t\t\t`Failed to register Parquet URL \"${name}\": ${cause.message}`,\n\t\t\t\t\t\"ANALYST_ERROR\",\n\t\t\t\t\tcause,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Tear down the DuckDB connection and database instance.\n\t *\n\t * After calling close(), any subsequent query or registration calls\n\t * will return an error Result.\n\t */\n\tasync close(): Promise<void> {\n\t\tif (this._conn) {\n\t\t\ttry {\n\t\t\t\tthis._conn.close();\n\t\t\t} catch {\n\t\t\t\t// Ignore errors during cleanup\n\t\t\t}\n\t\t\tthis._conn = null;\n\t\t}\n\t\tthis._db = null;\n\t\tthis._closed = true;\n\t}\n}\n","import type { HLCTimestamp } from \"@lakesync/core\";\nimport { Err, LakeSyncError, Ok, type Result } from \"@lakesync/core\";\nimport type { DuckDBClient } from \"./duckdb\";\n\n/** System columns present in every delta Parquet file. */\nconst SYSTEM_COLUMNS = new Set([\"op\", \"table\", \"rowId\", \"clientId\", \"hlc\", \"deltaId\"]);\n\n/**\n * Configuration for the TimeTraveller.\n */\nexport interface TimeTravelConfig {\n\t/** The DuckDB client used for executing time-travel queries. */\n\tduckdb: DuckDBClient;\n}\n\n/**\n * Provides time-travel query capabilities over delta Parquet files.\n *\n * Allows querying the materialised state of data as it existed at a specific\n * HLC timestamp, or inspecting raw deltas within a time range. Uses DuckDB\n * SQL with window functions to perform column-level LWW materialisation\n * entirely in-engine.\n *\n * Delta Parquet files contain flattened rows with system columns (`op`, `table`,\n * `rowId`, `clientId`, `hlc`, `deltaId`) and user-defined columns (e.g. `title`,\n * `completed`). The materialisation reconstructs per-row state by selecting the\n * latest value for each column based on HLC ordering, then excluding deleted rows.\n *\n * @example\n * ```ts\n * const traveller = new TimeTraveller({ duckdb: client });\n * await traveller.registerDeltas([{ name: \"batch-1.parquet\", data: bytes }]);\n *\n * const result = await traveller.queryAsOf(hlcTimestamp, \"SELECT * FROM _state WHERE completed = true\");\n * if (result.ok) console.log(result.value);\n * ```\n */\nexport class TimeTraveller {\n\tprivate readonly _config: TimeTravelConfig;\n\tprivate readonly _sources: string[] = [];\n\n\tconstructor(config: TimeTravelConfig) {\n\t\tthis._config = config;\n\t}\n\n\t/**\n\t * Register one or more Parquet buffers containing delta data.\n\t *\n\t * Each buffer is registered with DuckDB and can subsequently be\n\t * queried via the time-travel methods.\n\t *\n\t * @param parquetBuffers - Array of named Parquet file buffers to register\n\t * @returns A Result indicating success or a LakeSyncError on failure\n\t */\n\tasync registerDeltas(\n\t\tparquetBuffers: Array<{ name: string; data: Uint8Array }>,\n\t): Promise<Result<void, LakeSyncError>> {\n\t\ttry {\n\t\t\tfor (const buf of parquetBuffers) {\n\t\t\t\tconst regResult = await this._config.duckdb.registerParquetBuffer(buf.name, buf.data);\n\t\t\t\tif (!regResult.ok) {\n\t\t\t\t\treturn regResult;\n\t\t\t\t}\n\t\t\t\tthis._sources.push(buf.name);\n\t\t\t}\n\t\t\treturn Ok(undefined);\n\t\t} catch (err) {\n\t\t\tconst cause = err instanceof Error ? err : new Error(String(err));\n\t\t\treturn Err(\n\t\t\t\tnew LakeSyncError(\n\t\t\t\t\t`Failed to register delta data: ${cause.message}`,\n\t\t\t\t\t\"ANALYST_ERROR\",\n\t\t\t\t\tcause,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Query the materialised state as of the given HLC timestamp.\n\t *\n\t * Filters all deltas where `hlc <= asOfHlc`, then materialises the latest\n\t * state per (table, rowId) using column-level LWW (highest HLC wins per\n\t * column). The user's SQL is applied on top of the materialised view,\n\t * which is exposed as the CTE `_state`.\n\t *\n\t * Deleted rows (where the latest operation is DELETE) are excluded from\n\t * the materialised view.\n\t *\n\t * @param asOfHlc - The HLC timestamp representing the point in time to query\n\t * @param sql - SQL to apply on the materialised view (use `_state` as the table name)\n\t * @returns A Result containing the query results or a LakeSyncError\n\t */\n\tasync queryAsOf(\n\t\tasOfHlc: HLCTimestamp,\n\t\tsql: string,\n\t): Promise<Result<Record<string, unknown>[], LakeSyncError>> {\n\t\ttry {\n\t\t\tif (this._sources.length === 0) {\n\t\t\t\treturn Ok([]);\n\t\t\t}\n\n\t\t\tconst userColumns = await this._discoverUserColumns();\n\t\t\tif (!userColumns.ok) {\n\t\t\t\treturn Err(userColumns.error);\n\t\t\t}\n\n\t\t\tconst materialiseSql = this._buildMaterialiseSql(userColumns.value, BigInt(asOfHlc));\n\t\t\tconst finalSql = `WITH _state AS (${materialiseSql}) ${sql}`;\n\n\t\t\tconst result = await this._config.duckdb.query<Record<string, unknown>>(finalSql);\n\t\t\tif (!result.ok) {\n\t\t\t\treturn Err(result.error);\n\t\t\t}\n\t\t\treturn Ok(result.value);\n\t\t} catch (err) {\n\t\t\tconst cause = err instanceof Error ? err : new Error(String(err));\n\t\t\treturn Err(\n\t\t\t\tnew LakeSyncError(`Time-travel queryAsOf failed: ${cause.message}`, \"ANALYST_ERROR\", cause),\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Query raw deltas within a time range.\n\t *\n\t * Filters deltas where `fromHlc < hlc <= toHlc` and returns them as raw\n\t * (unmaterialised) rows. Useful for audit trails and changelog views.\n\t *\n\t * The user's SQL is applied on top of the filtered deltas, which are\n\t * exposed as the CTE `_deltas`.\n\t *\n\t * @param fromHlc - The exclusive lower bound HLC timestamp\n\t * @param toHlc - The inclusive upper bound HLC timestamp\n\t * @param sql - SQL to apply on the filtered deltas (use `_deltas` as the table name)\n\t * @returns A Result containing the query results or a LakeSyncError\n\t */\n\tasync queryBetween(\n\t\tfromHlc: HLCTimestamp,\n\t\ttoHlc: HLCTimestamp,\n\t\tsql: string,\n\t): Promise<Result<Record<string, unknown>[], LakeSyncError>> {\n\t\ttry {\n\t\t\tif (this._sources.length === 0) {\n\t\t\t\treturn Ok([]);\n\t\t\t}\n\n\t\t\tconst unionSql = this._buildUnionSql();\n\t\t\tconst fromBigint = BigInt(fromHlc);\n\t\t\tconst toBigint = BigInt(toHlc);\n\n\t\t\tconst filteredSql = `\n\t\t\t\tSELECT * FROM (${unionSql}) AS _all\n\t\t\t\tWHERE CAST(hlc AS BIGINT) > ${fromBigint}\n\t\t\t\t AND CAST(hlc AS BIGINT) <= ${toBigint}\n\t\t\t`;\n\n\t\t\tconst finalSql = `WITH _deltas AS (${filteredSql}) ${sql}`;\n\n\t\t\tconst result = await this._config.duckdb.query<Record<string, unknown>>(finalSql);\n\t\t\tif (!result.ok) {\n\t\t\t\treturn Err(result.error);\n\t\t\t}\n\t\t\treturn Ok(result.value);\n\t\t} catch (err) {\n\t\t\tconst cause = err instanceof Error ? err : new Error(String(err));\n\t\t\treturn Err(\n\t\t\t\tnew LakeSyncError(\n\t\t\t\t\t`Time-travel queryBetween failed: ${cause.message}`,\n\t\t\t\t\t\"ANALYST_ERROR\",\n\t\t\t\t\tcause,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Materialise the full state at a point in time, returning all rows.\n\t *\n\t * Equivalent to `queryAsOf(asOfHlc, \"SELECT * FROM _state\")` but provided\n\t * as a convenience method.\n\t *\n\t * @param asOfHlc - The HLC timestamp representing the point in time to materialise\n\t * @returns A Result containing all materialised rows or a LakeSyncError\n\t */\n\tasync materialiseAsOf(\n\t\tasOfHlc: HLCTimestamp,\n\t): Promise<Result<Record<string, unknown>[], LakeSyncError>> {\n\t\treturn this.queryAsOf(asOfHlc, \"SELECT * FROM _state\");\n\t}\n\n\t/**\n\t * Build a UNION ALL SQL expression covering all registered Parquet sources.\n\t */\n\tprivate _buildUnionSql(): string {\n\t\treturn this._sources.map((s) => `SELECT * FROM '${s}'`).join(\" UNION ALL BY NAME \");\n\t}\n\n\t/**\n\t * Discover user-defined column names from the registered Parquet data.\n\t *\n\t * Reads the column names from the first registered source and filters\n\t * out system columns to identify user-defined columns.\n\t */\n\tprivate async _discoverUserColumns(): Promise<Result<string[], LakeSyncError>> {\n\t\tif (this._sources.length === 0) {\n\t\t\treturn Ok([]);\n\t\t}\n\n\t\tconst result = await this._config.duckdb.query<{ column_name: string }>(\n\t\t\t`SELECT column_name FROM (DESCRIBE SELECT * FROM '${this._sources[0]}')`,\n\t\t);\n\t\tif (!result.ok) {\n\t\t\treturn Err(result.error);\n\t\t}\n\n\t\tconst userCols = result.value\n\t\t\t.map((r) => r.column_name)\n\t\t\t.filter((name) => !SYSTEM_COLUMNS.has(name));\n\n\t\treturn Ok(userCols);\n\t}\n\n\t/**\n\t * Build the materialisation SQL that reconstructs per-row state using\n\t * column-level LWW semantics.\n\t *\n\t * Strategy:\n\t * 1. Filter deltas by HLC <= asOfHlc\n\t * 2. For each (table, rowId), determine the latest operation (by max HLC)\n\t * 3. For each user column in each row, pick the value from the delta with\n\t * the highest HLC (where that column is not null)\n\t * 4. Exclude rows where the latest operation is DELETE\n\t *\n\t * @param userColumns - Names of user-defined columns\n\t * @param asOfHlc - The HLC timestamp cutoff as a bigint\n\t * @returns SQL string producing the materialised view\n\t */\n\tprivate _buildMaterialiseSql(userColumns: string[], asOfHlc: bigint): string {\n\t\tconst unionSql = this._buildUnionSql();\n\n\t\t// If there are no user columns, we still need to return rowId and table\n\t\tif (userColumns.length === 0) {\n\t\t\treturn `\n\t\t\t\tSELECT \"table\", \"rowId\"\n\t\t\t\tFROM (\n\t\t\t\t\tSELECT *,\n\t\t\t\t\t\tROW_NUMBER() OVER (\n\t\t\t\t\t\t\tPARTITION BY \"table\", \"rowId\"\n\t\t\t\t\t\t\tORDER BY CAST(hlc AS BIGINT) DESC\n\t\t\t\t\t\t) AS _rn\n\t\t\t\t\tFROM (${unionSql}) AS _all\n\t\t\t\t\tWHERE CAST(hlc AS BIGINT) <= ${asOfHlc}\n\t\t\t\t) AS _ranked\n\t\t\t\tWHERE _rn = 1 AND op != 'DELETE'\n\t\t\t`;\n\t\t}\n\n\t\t// Build per-column LAST_VALUE expressions.\n\t\t// For each user column, we want the value from the row with the highest HLC\n\t\t// where that column is NOT NULL. We use LAST_VALUE with IGNORE NULLS over\n\t\t// an HLC-ordered window partition.\n\t\tconst columnSelects = userColumns.map((col) => {\n\t\t\treturn `LAST_VALUE(\"${col}\" IGNORE NULLS) OVER (\n\t\t\t\tPARTITION BY \"table\", \"rowId\"\n\t\t\t\tORDER BY CAST(hlc AS BIGINT) ASC\n\t\t\t\tROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING\n\t\t\t) AS \"${col}\"`;\n\t\t});\n\n\t\t// We need the latest op per (table, rowId) to filter out DELETEs.\n\t\t// Use LAST_VALUE on op ordered by HLC to get the most recent operation.\n\t\tconst latestOpExpr = `LAST_VALUE(op) OVER (\n\t\t\tPARTITION BY \"table\", \"rowId\"\n\t\t\tORDER BY CAST(hlc AS BIGINT) ASC\n\t\t\tROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING\n\t\t) AS _latest_op`;\n\n\t\t// Use ROW_NUMBER to deduplicate — we only need one output row per (table, rowId).\n\t\tconst rowNumExpr = `ROW_NUMBER() OVER (\n\t\t\tPARTITION BY \"table\", \"rowId\"\n\t\t\tORDER BY CAST(hlc AS BIGINT) DESC\n\t\t) AS _rn`;\n\n\t\treturn `\n\t\t\tSELECT \"table\", \"rowId\", ${userColumns.map((c) => `\"${c}\"`).join(\", \")}\n\t\t\tFROM (\n\t\t\t\tSELECT\n\t\t\t\t\t\"table\",\n\t\t\t\t\t\"rowId\",\n\t\t\t\t\t${columnSelects.join(\",\\n\\t\\t\\t\\t\\t\")},\n\t\t\t\t\t${latestOpExpr},\n\t\t\t\t\t${rowNumExpr}\n\t\t\t\tFROM (${unionSql}) AS _all\n\t\t\t\tWHERE CAST(hlc AS BIGINT) <= ${asOfHlc}\n\t\t\t) AS _materialised\n\t\t\tWHERE _rn = 1 AND _latest_op != 'DELETE'\n\t\t`;\n\t}\n}\n","import { Err, LakeSyncError, Ok, type Result } from \"@lakesync/core\";\nimport type { DuckDBClient } from \"./duckdb\";\n\n/**\n * Configuration for the UnionReader.\n */\nexport interface UnionReadConfig {\n\t/** The DuckDB client used to query cold (Parquet) data. */\n\tduckdb: DuckDBClient;\n\t/** The logical table name being queried. */\n\ttableName: string;\n}\n\n/**\n * Merges \"hot\" in-memory rows with \"cold\" Parquet data via DuckDB.\n *\n * Cold data is registered as Parquet file buffers in DuckDB. Hot data is\n * serialised to JSON and loaded via `read_json_auto`. The two sources are\n * combined with `UNION ALL` and the caller's SQL is applied on top.\n *\n * The union is exposed as a CTE named `_union`, so the caller's SQL should\n * reference `_union` as the table name.\n *\n * @example\n * ```ts\n * const reader = new UnionReader({ duckdb: client, tableName: \"todos\" });\n * await reader.registerColdData([{ name: \"batch-1.parquet\", data: parquetBytes }]);\n *\n * const result = await reader.query(\n * \"SELECT * FROM _union WHERE completed = true\",\n * [{ id: \"row-3\", title: \"New task\", completed: false }],\n * );\n * ```\n */\nexport class UnionReader {\n\tprivate readonly _config: UnionReadConfig;\n\tprivate readonly _coldSources: string[] = [];\n\tprivate _hotCounter = 0;\n\n\tconstructor(config: UnionReadConfig) {\n\t\tthis._config = config;\n\t}\n\n\t/**\n\t * Register one or more Parquet buffers as cold data sources.\n\t *\n\t * Each buffer is registered with DuckDB and can subsequently be\n\t * queried alongside hot data via {@link query}.\n\t *\n\t * @param parquetBuffers - Array of named Parquet file buffers to register\n\t * @returns A Result indicating success or a LakeSyncError on failure\n\t */\n\tasync registerColdData(\n\t\tparquetBuffers: Array<{ name: string; data: Uint8Array }>,\n\t): Promise<Result<void, LakeSyncError>> {\n\t\ttry {\n\t\t\tfor (const buf of parquetBuffers) {\n\t\t\t\tconst regResult = await this._config.duckdb.registerParquetBuffer(buf.name, buf.data);\n\t\t\t\tif (!regResult.ok) {\n\t\t\t\t\treturn regResult;\n\t\t\t\t}\n\t\t\t\tthis._coldSources.push(buf.name);\n\t\t\t}\n\t\t\treturn Ok(undefined);\n\t\t} catch (err) {\n\t\t\tconst cause = err instanceof Error ? err : new Error(String(err));\n\t\t\treturn Err(\n\t\t\t\tnew LakeSyncError(`Failed to register cold data: ${cause.message}`, \"ANALYST_ERROR\", cause),\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Execute a SQL query that unions hot in-memory rows with cold Parquet data.\n\t *\n\t * The caller's SQL is wrapped around a UNION ALL of cold and hot sources.\n\t * The unioned data is available as `_union` in the SQL statement.\n\t *\n\t * If `hotRows` is empty or not provided, only cold data is queried.\n\t * If no cold sources are registered and `hotRows` is provided, only hot data is queried.\n\t *\n\t * @param sql - SQL to apply on top of the unioned data (use `_union` as the table name)\n\t * @param hotRows - Optional array of in-memory row objects to include in the union\n\t * @returns A Result containing the query results or a LakeSyncError\n\t */\n\tasync query(\n\t\tsql: string,\n\t\thotRows?: Record<string, unknown>[],\n\t): Promise<Result<Record<string, unknown>[], LakeSyncError>> {\n\t\ttry {\n\t\t\tconst hasHot = hotRows !== undefined && hotRows.length > 0;\n\t\t\tconst hasCold = this._coldSources.length > 0;\n\n\t\t\tif (!hasHot && !hasCold) {\n\t\t\t\treturn Ok([]);\n\t\t\t}\n\n\t\t\t// Build the inner union query parts\n\t\t\tconst unionParts: string[] = [];\n\n\t\t\t// Add cold sources (each Parquet file is a separate SELECT)\n\t\t\tif (hasCold) {\n\t\t\t\tfor (const source of this._coldSources) {\n\t\t\t\t\tunionParts.push(`SELECT * FROM '${source}'`);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Add hot source via JSON buffer registered with DuckDB\n\t\t\tif (hasHot) {\n\t\t\t\tconst hotBufferName = `_hot_${this._config.tableName}_${this._hotCounter++}.json`;\n\t\t\t\tconst jsonBytes = new TextEncoder().encode(JSON.stringify(hotRows));\n\n\t\t\t\tconst regResult = await this._config.duckdb.registerParquetBuffer(hotBufferName, jsonBytes);\n\t\t\t\tif (!regResult.ok) {\n\t\t\t\t\treturn Err(regResult.error);\n\t\t\t\t}\n\n\t\t\t\tunionParts.push(`SELECT * FROM read_json_auto('${hotBufferName}')`);\n\t\t\t}\n\n\t\t\t// Combine all parts with UNION ALL and expose as CTE named `_union`\n\t\t\t// Use UNION ALL BY NAME so columns are matched by name, not position.\n\t\t\t// This handles differing column orders between Parquet and JSON sources.\n\t\t\tconst unionSql = unionParts.join(\" UNION ALL BY NAME \");\n\t\t\tconst finalSql = `WITH _union AS (${unionSql}) ${sql}`;\n\n\t\t\tconst result = await this._config.duckdb.query<Record<string, unknown>>(finalSql);\n\t\t\tif (!result.ok) {\n\t\t\t\treturn Err(result.error);\n\t\t\t}\n\n\t\t\treturn Ok(result.value);\n\t\t} catch (err) {\n\t\t\tconst cause = err instanceof Error ? err : new Error(String(err));\n\t\t\treturn Err(new LakeSyncError(`Union query failed: ${cause.message}`, \"ANALYST_ERROR\", cause));\n\t\t}\n\t}\n\n\t/**\n\t * Query only cold (Parquet) data without any hot rows.\n\t *\n\t * @param sql - SQL to execute against cold data (use `_union` as the table name)\n\t * @returns A Result containing the query results or a LakeSyncError\n\t */\n\tasync queryColdOnly(sql: string): Promise<Result<Record<string, unknown>[], LakeSyncError>> {\n\t\treturn this.query(sql);\n\t}\n}\n"],"mappings":"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","names":["require"]}
@@ -0,0 +1,30 @@
1
+ import { R as Result } from './result-CojzlFE2.js';
2
+
3
+ /** Claims extracted from a verified JWT token */
4
+ interface AuthClaims {
5
+ /** Client identifier (from JWT `sub` claim) */
6
+ clientId: string;
7
+ /** Authorised gateway ID (from JWT `gw` claim) */
8
+ gatewayId: string;
9
+ /** Role for route-level access control (from JWT `role` claim, defaults to "client") */
10
+ role: string;
11
+ /** Non-standard JWT claims for sync rule evaluation */
12
+ customClaims: Record<string, string | string[]>;
13
+ }
14
+ /** Authentication error returned when JWT verification fails */
15
+ declare class AuthError extends Error {
16
+ constructor(message: string);
17
+ }
18
+ /**
19
+ * Verify a JWT token signed with HMAC-SHA256 and extract authentication claims.
20
+ *
21
+ * Uses the Web Crypto API exclusively (no external dependencies), making it
22
+ * suitable for Cloudflare Workers and other edge runtimes.
23
+ *
24
+ * @param token - The raw JWT string (header.payload.signature)
25
+ * @param secret - The HMAC-SHA256 secret key
26
+ * @returns A Result containing AuthClaims on success, or AuthError on failure
27
+ */
28
+ declare function verifyToken(token: string, secret: string): Promise<Result<AuthClaims, AuthError>>;
29
+
30
+ export { type AuthClaims as A, AuthError as a, verifyToken as v };
@@ -0,0 +1,82 @@
1
+ import { b as SyncPush, R as RowDelta } from './types-V_jVu2sA.js';
2
+ import { H as HLC } from './hlc-DiD8QNG3.js';
3
+ import { R as Result, F as FlushError } from './result-CojzlFE2.js';
4
+
5
+ /** Minimal interface for a push target (avoids depending on @lakesync/gateway). */
6
+ interface PushTarget {
7
+ handlePush(push: SyncPush): unknown;
8
+ }
9
+ /**
10
+ * Extended push target that supports flush and buffer inspection.
11
+ * Implemented by SyncGateway so pollers can trigger flushes to relieve memory pressure.
12
+ */
13
+ interface IngestTarget extends PushTarget {
14
+ flush(): Promise<Result<void, FlushError>>;
15
+ shouldFlush(): boolean;
16
+ readonly bufferStats: {
17
+ logSize: number;
18
+ indexSize: number;
19
+ byteSize: number;
20
+ };
21
+ }
22
+ /** Type guard: returns true if the target supports flush/shouldFlush/bufferStats. */
23
+ declare function isIngestTarget(target: PushTarget): target is IngestTarget;
24
+ /** Memory configuration for the streaming accumulator. */
25
+ interface PollerMemoryConfig {
26
+ /** Number of deltas per push chunk (default 500). */
27
+ chunkSize?: number;
28
+ /** Approximate memory budget in bytes — triggers flush at 70% (default: no limit). */
29
+ memoryBudgetBytes?: number;
30
+ /** Proportion of memoryBudgetBytes at which to trigger a flush (default 0.7). */
31
+ flushThreshold?: number;
32
+ }
33
+ /**
34
+ * Base class for source pollers that poll an external API and push deltas
35
+ * to a SyncGateway. Handles lifecycle (start/stop/schedule), and push.
36
+ */
37
+ declare abstract class BaseSourcePoller {
38
+ protected readonly gateway: PushTarget;
39
+ protected readonly hlc: HLC;
40
+ protected readonly clientId: string;
41
+ private readonly intervalMs;
42
+ private timer;
43
+ private running;
44
+ private readonly chunkSize;
45
+ private readonly memoryBudgetBytes;
46
+ private readonly flushThreshold;
47
+ private pendingDeltas;
48
+ constructor(config: {
49
+ name: string;
50
+ intervalMs: number;
51
+ gateway: PushTarget;
52
+ memory?: PollerMemoryConfig;
53
+ });
54
+ /** Start the polling loop. */
55
+ start(): void;
56
+ /** Stop the polling loop. */
57
+ stop(): void;
58
+ /** Whether the poller is currently running. */
59
+ get isRunning(): boolean;
60
+ /** Execute a single poll cycle. Subclasses implement their specific polling logic. */
61
+ abstract poll(): Promise<void>;
62
+ /** Push collected deltas to the gateway (single-shot, backward compat). */
63
+ protected pushDeltas(deltas: RowDelta[]): void;
64
+ /**
65
+ * Accumulate a single delta. When `chunkSize` is reached, the pending
66
+ * deltas are automatically pushed (and flushed if needed).
67
+ */
68
+ protected accumulateDelta(delta: RowDelta): Promise<void>;
69
+ /** Flush any remaining accumulated deltas. Call at the end of `poll()`. */
70
+ protected flushAccumulator(): Promise<void>;
71
+ /**
72
+ * Push a chunk of pending deltas. If the gateway is an IngestTarget,
73
+ * checks memory pressure and flushes before/after push when needed.
74
+ * On backpressure, flushes once and retries.
75
+ */
76
+ private pushPendingChunk;
77
+ private pushChunkWithFlush;
78
+ private shouldFlushTarget;
79
+ private schedulePoll;
80
+ }
81
+
82
+ export { BaseSourcePoller as B, type IngestTarget as I, type PollerMemoryConfig as P, type PushTarget as a, isIngestTarget as i };