@sixb/ducklake 0.1.7 → 0.1.8

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.
package/README.md CHANGED
@@ -534,7 +534,7 @@ Local DuckLake e2e tests:
534
534
  bun --filter @sixb/ducklake test:e2e:local
535
535
  ```
536
536
 
537
- Docker-backed PostgreSQL and MinIO/S3-compatible e2e tests:
537
+ Docker-backed PostgreSQL and SeaweedFS (S3-compatible) e2e tests:
538
538
 
539
539
  ```bash
540
540
  bun --filter @sixb/ducklake test:e2e:remote
@@ -1,5 +1,5 @@
1
1
  import type { DatasetDefinition, DatasetRow, LakeStorageWithSql } from "@sixb/core";
2
- import type { BeginDatasetMergeInput, BeginDatasetWriteInput, DatasetCatalogState, DatasetChanges, DatasetVersion, LakeMergeSession, LakeWriteSession, ReadDatasetChangesInput, ReadDatasetRowsInput } from "@sixb/core/lake-storage";
2
+ import type { BeginDatasetMergeInput, BeginDatasetWriteInput, DatasetCatalogState, DatasetChanges, DatasetVersion, LakeMergeSession, LakeWriteSession, LatestVersionsSince, ListLatestVersionsSinceInput, ReadDatasetChangesInput, ReadDatasetRowsInput } from "@sixb/core/lake-storage";
3
3
  import { DuckLakeSqlExecutor } from "./internal/ducklake-sql-executor";
4
4
  import type { DuckLakeMaintenanceOptions, DuckLakeMaintenanceReport, DuckLakeStorageOptions } from "./types";
5
5
  /**
@@ -33,6 +33,7 @@ export declare class DuckLakeStorage implements LakeStorageWithSql<"duckdb"> {
33
33
  beginMerge(input: BeginDatasetMergeInput): Promise<LakeMergeSession>;
34
34
  getLatestVersion(datasetId: string): Promise<DatasetVersion | null>;
35
35
  getVersion(datasetId: string, versionId: string): Promise<DatasetVersion | null>;
36
+ listLatestVersionsSince(input: ListLatestVersionsSinceInput): Promise<LatestVersionsSince | null>;
36
37
  readRows(input: ReadDatasetRowsInput): AsyncIterable<DatasetRow>;
37
38
  readChanges(input: ReadDatasetChangesInput): Promise<DatasetChanges | null>;
38
39
  runMaintenance(options?: DuckLakeMaintenanceOptions): Promise<DuckLakeMaintenanceReport>;
@@ -1 +1 @@
1
- {"version":3,"file":"ducklake-storage.d.ts","sourceRoot":"","sources":["../src/ducklake-storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AACnF,OAAO,KAAK,EACV,sBAAsB,EACtB,sBAAsB,EACtB,mBAAmB,EACnB,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EAChB,uBAAuB,EACvB,oBAAoB,EACrB,MAAM,yBAAyB,CAAA;AAOhC,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAA;AAEtE,OAAO,KAAK,EACV,0BAA0B,EAC1B,yBAAyB,EACzB,sBAAsB,EACvB,MAAM,SAAS,CAAA;AAEhB;;;;;;GAMG;AACH,qBAAa,eAAgB,YAAW,kBAAkB,CAAC,QAAQ,CAAC;IAClE,QAAQ,CAAC,QAAQ;;;MAA8C;IAC/D,QAAQ,CAAC,GAAG,EAAE,mBAAmB,CAAA;IAEjC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA2B;IACvD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAwB;IACjD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqB;IACjD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAmB;IACxC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsB;IAC9C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAwB;IACvD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0B;gBAErC,OAAO,EAAE,sBAAsB;IAwCrC,aAAa,CAAC,UAAU,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAIxE,kCAAkC,CACtC,WAAW,EAAE,SAAS,iBAAiB,EAAE,GACxC,OAAO,CAAC,IAAI,CAAC;IAIV,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAIhE,YAAY,IAAI,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC;IAIrD,uBAAuB,CAC3B,UAAU,EAAE,SAAS,MAAM,EAAE,GAC5B,OAAO,CAAC,SAAS,mBAAmB,EAAE,CAAC;IAIpC,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,cAAc,EAAE,CAAC;IAInF,UAAU,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAIpE,UAAU,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAIpE,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAInE,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAItF,QAAQ,CAAC,KAAK,EAAE,oBAAoB,GAAG,aAAa,CAAC,UAAU,CAAC;IAIhE,WAAW,CAAC,KAAK,EAAE,uBAAuB,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAIrE,cAAc,CAAC,OAAO,CAAC,EAAE,0BAA0B,GAAG,OAAO,CAAC,yBAAyB,CAAC;IAI9F;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
1
+ {"version":3,"file":"ducklake-storage.d.ts","sourceRoot":"","sources":["../src/ducklake-storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AACnF,OAAO,KAAK,EACV,sBAAsB,EACtB,sBAAsB,EACtB,mBAAmB,EACnB,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EAChB,mBAAmB,EACnB,4BAA4B,EAC5B,uBAAuB,EACvB,oBAAoB,EACrB,MAAM,yBAAyB,CAAA;AAOhC,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAA;AAEtE,OAAO,KAAK,EACV,0BAA0B,EAC1B,yBAAyB,EACzB,sBAAsB,EACvB,MAAM,SAAS,CAAA;AAEhB;;;;;;GAMG;AACH,qBAAa,eAAgB,YAAW,kBAAkB,CAAC,QAAQ,CAAC;IAClE,QAAQ,CAAC,QAAQ;;;MAA8C;IAC/D,QAAQ,CAAC,GAAG,EAAE,mBAAmB,CAAA;IAEjC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA2B;IACvD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAwB;IACjD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqB;IACjD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAmB;IACxC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsB;IAC9C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAwB;IACvD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0B;gBAErC,OAAO,EAAE,sBAAsB;IAwCrC,aAAa,CAAC,UAAU,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAIxE,kCAAkC,CACtC,WAAW,EAAE,SAAS,iBAAiB,EAAE,GACxC,OAAO,CAAC,IAAI,CAAC;IAIV,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAIhE,YAAY,IAAI,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC;IAIrD,uBAAuB,CAC3B,UAAU,EAAE,SAAS,MAAM,EAAE,GAC5B,OAAO,CAAC,SAAS,mBAAmB,EAAE,CAAC;IAIpC,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,cAAc,EAAE,CAAC;IAInF,UAAU,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAIpE,UAAU,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAIpE,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAInE,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAIhF,uBAAuB,CAC3B,KAAK,EAAE,4BAA4B,GAClC,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC;IAItC,QAAQ,CAAC,KAAK,EAAE,oBAAoB,GAAG,aAAa,CAAC,UAAU,CAAC;IAIhE,WAAW,CAAC,KAAK,EAAE,uBAAuB,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAIrE,cAAc,CAAC,OAAO,CAAC,EAAE,0BAA0B,GAAG,OAAO,CAAC,yBAAyB,CAAC;IAI9F;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
package/dist/index.js CHANGED
@@ -158,8 +158,20 @@ function buildSetPostgresPoolSql(options) {
158
158
  return postgresPoolParameters(options.postgresPool).map(({ name, value }) => `SET pg_pool_${name} = ${value}`);
159
159
  }
160
160
  function duckLakeMetadataTableName(options, tableName) {
161
- const schemaName = options.catalog.type === "postgres" ? options.catalog.metadataSchema ?? "main" : "main";
162
- return `${quoteIdentifier(duckLakeMetadataCatalog(options))}.${quoteIdentifier(schemaName)}.${quoteIdentifier(tableName)}`;
161
+ return `${quoteIdentifier(duckLakeMetadataCatalog(options))}.${quoteIdentifier(duckLakeMetadataSchema(options))}.${quoteIdentifier(tableName)}`;
162
+ }
163
+ function buildDuckLakeMetadataQuery(options, build) {
164
+ if (options.catalog.type !== "postgres") {
165
+ return build((tableName) => duckLakeMetadataTableName(options, tableName));
166
+ }
167
+ const schema = options.catalog.metadataSchema;
168
+ const sql = build((tableName) => schema === undefined ? quoteIdentifier(tableName) : `${quoteIdentifier(schema)}.${quoteIdentifier(tableName)}`);
169
+ return `SELECT * FROM postgres_query(${quoteSqlString(duckLakeMetadataCatalog(options))}, ${quoteSqlString(sql)})`;
170
+ }
171
+ function duckLakeMetadataSchema(options) {
172
+ if (options.catalog.type !== "postgres")
173
+ return "main";
174
+ return options.catalog.metadataSchema ?? "public";
163
175
  }
164
176
  function quoteIdentifier(identifier) {
165
177
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
@@ -2311,6 +2323,7 @@ function pageSql(read, limit, offset, cursor) {
2311
2323
  // src/internal/ducklake-snapshot-reader.ts
2312
2324
  import { LakeStorageError as LakeStorageError17 } from "@sixb/core/lake-storage";
2313
2325
  var SNAPSHOT_ROW_BATCH_SIZE = 128;
2326
+ var FEED_PAGE_SIZE = 512;
2314
2327
  var CATALOG_SNAPSHOT_SCAN_LIMIT = 512;
2315
2328
 
2316
2329
  class DuckLakeSnapshotReader {
@@ -2376,6 +2389,135 @@ class DuckLakeSnapshotReader {
2376
2389
  };
2377
2390
  });
2378
2391
  }
2392
+ async listLatestVersionsSince(input) {
2393
+ this.connections.assertOpen();
2394
+ return this.connections.withAttachedRuntime(async (runtime) => {
2395
+ if (input.cursor === null) {
2396
+ return { cursor: await this.queryHeadSnapshotId(runtime), versions: [] };
2397
+ }
2398
+ if (!/^\d+$/.test(input.cursor)) {
2399
+ return null;
2400
+ }
2401
+ const snapshots = await this.querySnapshotsFrom(runtime, input.cursor);
2402
+ if (snapshots[0]?.snapshotId !== input.cursor) {
2403
+ return null;
2404
+ }
2405
+ const newSnapshots = snapshots.slice(1);
2406
+ if (newSnapshots.length === 0) {
2407
+ return { cursor: input.cursor, versions: [] };
2408
+ }
2409
+ const tables = await this.resolveFeedTables(runtime, input.datasetIds);
2410
+ const latest = await this.collectFeedPage(runtime, tables, newSnapshots);
2411
+ return {
2412
+ cursor: newSnapshots[newSnapshots.length - 1]?.snapshotId ?? input.cursor,
2413
+ versions: [...latest].map(([datasetId, row]) => this.snapshotRowToSummary(datasetId, row))
2414
+ };
2415
+ });
2416
+ }
2417
+ async queryHeadSnapshotId(runtime) {
2418
+ const [row] = await runtime.query(buildDuckLakeMetadataQuery(this.options, (table) => `SELECT max(snapshot_id) AS snapshot_id FROM ${table("ducklake_snapshot")}`));
2419
+ if (row?.snapshot_id === null || row?.snapshot_id === undefined) {
2420
+ throw new LakeStorageError17("[SixbDuckLake] DuckLake catalog has no snapshots.");
2421
+ }
2422
+ return String(getBigIntLike(row, "snapshot_id"));
2423
+ }
2424
+ async querySnapshotsFrom(runtime, cursor) {
2425
+ const rows = await runtime.query(buildDuckLakeMetadataQuery(this.options, (table) => `
2426
+ SELECT snapshot_id, snapshot_time
2427
+ FROM ${table("ducklake_snapshot")}
2428
+ WHERE snapshot_id >= ${cursor}
2429
+ ORDER BY snapshot_id
2430
+ LIMIT ${FEED_PAGE_SIZE + 1}
2431
+ `));
2432
+ return rows.map((row) => ({
2433
+ snapshotId: String(getBigIntLike(row, "snapshot_id")),
2434
+ createdAt: getDate(row, "snapshot_time")
2435
+ }));
2436
+ }
2437
+ async resolveFeedTables(runtime, datasetIds) {
2438
+ const requested = datasetIds === undefined ? null : new Set(datasetIds);
2439
+ const rows = await runtime.query(`
2440
+ SELECT table_id, table_name, begin_snapshot, end_snapshot
2441
+ FROM ${duckLakeMetadataTableName(this.options, "ducklake_table")}
2442
+ `);
2443
+ const startByTableId = new Map;
2444
+ const current = [];
2445
+ for (const row of rows) {
2446
+ const tableId = getBigIntLike(row, "table_id");
2447
+ const begin = getBigIntLike(row, "begin_snapshot");
2448
+ const start = startByTableId.get(tableId);
2449
+ if (start === undefined || begin < start) {
2450
+ startByTableId.set(tableId, begin);
2451
+ }
2452
+ if (row.end_snapshot !== null && row.end_snapshot !== undefined) {
2453
+ continue;
2454
+ }
2455
+ const datasetId = decodeDatasetTableName(getString(row, "table_name"));
2456
+ if (datasetId !== null && (requested === null || requested.has(datasetId))) {
2457
+ current.push({ datasetId, tableId });
2458
+ }
2459
+ }
2460
+ return current.map((table) => ({
2461
+ ...table,
2462
+ startSnapshotId: startByTableId.get(table.tableId) ?? 0n
2463
+ }));
2464
+ }
2465
+ async collectFeedPage(runtime, tables, snapshots) {
2466
+ const latest = new Map;
2467
+ const first = snapshots[0]?.snapshotId;
2468
+ const last = snapshots[snapshots.length - 1]?.snapshotId;
2469
+ if (first === undefined || last === undefined || tables.length === 0) {
2470
+ return latest;
2471
+ }
2472
+ const changeRows = await runtime.query(buildDuckLakeMetadataQuery(this.options, (table) => `
2473
+ SELECT snapshot_id, changes_made, commit_extra_info
2474
+ FROM ${table("ducklake_snapshot_changes")}
2475
+ WHERE snapshot_id >= ${first} AND snapshot_id <= ${last}
2476
+ `));
2477
+ const fileFlags = await this.queryFileChangeFlags(runtime, snapshots.map((snapshot) => snapshot.snapshotId), tables.map((table) => table.tableId));
2478
+ const changesBySnapshotId = new Map(changeRows.map((row) => [String(getBigIntLike(row, "snapshot_id")), row]));
2479
+ const tablesByTableId = new Map(tables.map((table) => [table.tableId.toString(), table]));
2480
+ const tablesByDatasetId = new Map(tables.map((table) => [table.datasetId, table]));
2481
+ for (const snapshot of snapshots) {
2482
+ const changes = changesBySnapshotId.get(snapshot.snapshotId);
2483
+ if (changes === undefined) {
2484
+ continue;
2485
+ }
2486
+ const changesMade = getString(changes, "changes_made");
2487
+ const metadata = parseCommitMetadata(changes.commit_extra_info);
2488
+ const touched = new Set;
2489
+ for (const change of changesMade.split(",")) {
2490
+ const table = tablesByTableId.get(change.split(":")[1] ?? "");
2491
+ if (table)
2492
+ touched.add(table);
2493
+ }
2494
+ for (const table of tables) {
2495
+ if (fileFlags.has(fileChangeKey(snapshot.snapshotId, table.tableId)))
2496
+ touched.add(table);
2497
+ }
2498
+ const tagged = metadata === undefined ? undefined : tablesByDatasetId.get(metadata.datasetId);
2499
+ if (tagged)
2500
+ touched.add(tagged);
2501
+ for (const table of touched) {
2502
+ if (BigInt(snapshot.snapshotId) < table.startSnapshotId) {
2503
+ continue;
2504
+ }
2505
+ const flags = fileFlags.get(fileChangeKey(snapshot.snapshotId, table.tableId));
2506
+ const row = this.candidateToSnapshotRow(table.datasetId, table.tableId, {
2507
+ snapshotId: snapshot.snapshotId,
2508
+ createdAt: snapshot.createdAt,
2509
+ changesMade,
2510
+ hasFileChange: flags?.hasFileChange ?? false,
2511
+ hasFileDeleteChange: flags?.hasFileDeleteChange ?? false,
2512
+ ...metadata !== undefined ? { metadata } : {}
2513
+ });
2514
+ if (row && row.mode !== "schema") {
2515
+ latest.set(table.datasetId, row);
2516
+ }
2517
+ }
2518
+ }
2519
+ return latest;
2520
+ }
2379
2521
  snapshotRowToSummary(datasetId, row) {
2380
2522
  return {
2381
2523
  datasetId,
@@ -2639,6 +2781,7 @@ class DuckLakeSnapshotReader {
2639
2781
  let beforeSnapshotId = input.beforeSnapshotId;
2640
2782
  while (input.visibleRowLimit === undefined || snapshots.length < input.visibleRowLimit) {
2641
2783
  const candidates = await this.querySnapshotCandidates(runtime, {
2784
+ datasetId: input.datasetId,
2642
2785
  tableId: input.tableId,
2643
2786
  exactSnapshotId: input.exactSnapshotId,
2644
2787
  beforeSnapshotId,
@@ -2648,6 +2791,9 @@ class DuckLakeSnapshotReader {
2648
2791
  break;
2649
2792
  }
2650
2793
  for (const candidate of candidates) {
2794
+ if ("missing" in candidate) {
2795
+ continue;
2796
+ }
2651
2797
  const snapshot = this.candidateToSnapshotRow(input.datasetId, input.tableId, candidate);
2652
2798
  if (snapshot) {
2653
2799
  snapshots.push(snapshot);
@@ -2664,90 +2810,118 @@ class DuckLakeSnapshotReader {
2664
2810
  return snapshots;
2665
2811
  }
2666
2812
  async querySnapshotCandidates(runtime, input) {
2667
- if (input.limit !== undefined && input.limit <= 0) {
2813
+ const tableId = input.tableId.toString();
2814
+ const limit = Math.max(0, Math.trunc(input.limit));
2815
+ if (limit === 0) {
2668
2816
  return [];
2669
2817
  }
2670
- const ducklakeTable = duckLakeMetadataTableName(this.options, "ducklake_table");
2671
- const where = [
2672
- `snapshot.snapshot_id >= (SELECT min(begin_snapshot) FROM ${ducklakeTable} WHERE table_id = ${input.tableId})`
2673
- ];
2674
2818
  if (input.exactSnapshotId !== undefined) {
2675
2819
  assertDuckLakeSnapshotId2(input.exactSnapshotId);
2676
- where.push(`snapshot.snapshot_id = ${input.exactSnapshotId}`);
2677
2820
  }
2678
2821
  if (input.beforeSnapshotId !== undefined) {
2679
2822
  assertDuckLakeSnapshotId2(input.beforeSnapshotId);
2680
- where.push(`snapshot.snapshot_id < ${input.beforeSnapshotId}`);
2681
2823
  }
2682
- const whereSql = `WHERE ${where.join(" AND ")}`;
2683
- const limitSql = input.limit === undefined ? "" : `LIMIT ${Math.max(0, Math.trunc(input.limit))}`;
2684
- const ducklakeSnapshot = duckLakeMetadataTableName(this.options, "ducklake_snapshot");
2685
- const ducklakeSnapshotChanges = duckLakeMetadataTableName(this.options, "ducklake_snapshot_changes");
2686
- const ducklakeDataFile = duckLakeMetadataTableName(this.options, "ducklake_data_file");
2687
- const ducklakeDeleteFile = duckLakeMetadataTableName(this.options, "ducklake_delete_file");
2688
- const rows = await runtime.query(`
2689
- WITH candidate_snapshots AS (
2690
- -- Recent DuckLake snapshots to inspect; Sixb visibility is filtered later.
2691
- SELECT
2692
- snapshot.snapshot_id,
2693
- snapshot.snapshot_time,
2694
- changes.changes_made,
2695
- changes.commit_extra_info
2696
- FROM ${ducklakeSnapshot} snapshot
2697
- JOIN ${ducklakeSnapshotChanges} changes
2698
- ON changes.snapshot_id = snapshot.snapshot_id
2699
- ${whereSql}
2700
- ORDER BY snapshot.snapshot_id DESC
2701
- ${limitSql}
2702
- ),
2703
- file_changes AS (
2704
- -- File metadata tells whether this table changed in each candidate snapshot.
2705
- SELECT begin_snapshot AS snapshot_id, false AS is_delete_change
2706
- FROM ${ducklakeDataFile}
2707
- WHERE table_id = ${input.tableId}
2708
- AND begin_snapshot IN (SELECT snapshot_id FROM candidate_snapshots)
2709
- UNION ALL
2710
- SELECT end_snapshot AS snapshot_id, true AS is_delete_change
2711
- FROM ${ducklakeDataFile}
2712
- WHERE table_id = ${input.tableId}
2713
- AND end_snapshot IN (SELECT snapshot_id FROM candidate_snapshots)
2714
- UNION ALL
2715
- SELECT begin_snapshot AS snapshot_id, true AS is_delete_change
2716
- FROM ${ducklakeDeleteFile}
2717
- WHERE table_id = ${input.tableId}
2718
- AND begin_snapshot IN (SELECT snapshot_id FROM candidate_snapshots)
2719
- UNION ALL
2720
- SELECT end_snapshot AS snapshot_id, true AS is_delete_change
2721
- FROM ${ducklakeDeleteFile}
2722
- WHERE table_id = ${input.tableId}
2723
- AND end_snapshot IN (SELECT snapshot_id FROM candidate_snapshots)
2724
- ),
2725
- file_changes_by_snapshot AS (
2726
- -- Collapse file-level changes into one row per snapshot.
2727
- SELECT
2728
- snapshot_id,
2729
- count(*) > 0 AS has_file_change,
2730
- count(*) FILTER (WHERE is_delete_change) > 0 AS has_file_delete_change
2731
- FROM file_changes
2732
- GROUP BY snapshot_id
2733
- )
2734
- SELECT
2735
- -- Keep metadata-only candidates; Sixb filters them with commit_extra_info.
2736
- candidate.snapshot_id,
2737
- candidate.snapshot_time,
2738
- candidate.changes_made,
2739
- candidate.commit_extra_info,
2740
- coalesce(file_changes.has_file_change, false) AS has_file_change,
2741
- coalesce(file_changes.has_file_delete_change, false) AS has_file_delete_change
2742
- FROM candidate_snapshots candidate
2743
- LEFT JOIN file_changes_by_snapshot file_changes
2744
- ON file_changes.snapshot_id = candidate.snapshot_id
2745
- ORDER BY candidate.snapshot_id DESC
2746
- `);
2824
+ const rows = await runtime.query(buildDuckLakeMetadataQuery(this.options, (table) => {
2825
+ const tableStart = `(SELECT min(begin_snapshot) FROM ${table("ducklake_table")} WHERE table_id = ${tableId})`;
2826
+ const candidateIds = input.exactSnapshotId !== undefined ? `SELECT CAST(${input.exactSnapshotId} AS BIGINT) AS snapshot_id
2827
+ WHERE ${input.exactSnapshotId} >= ${tableStart}` : `SELECT DISTINCT snapshot_id
2828
+ FROM (
2829
+ SELECT begin_snapshot AS snapshot_id
2830
+ FROM ${table("ducklake_data_file")} WHERE table_id = ${tableId}
2831
+ UNION ALL
2832
+ SELECT end_snapshot
2833
+ FROM ${table("ducklake_data_file")}
2834
+ WHERE table_id = ${tableId} AND end_snapshot IS NOT NULL
2835
+ UNION ALL
2836
+ SELECT begin_snapshot
2837
+ FROM ${table("ducklake_delete_file")} WHERE table_id = ${tableId}
2838
+ UNION ALL
2839
+ SELECT end_snapshot
2840
+ FROM ${table("ducklake_delete_file")}
2841
+ WHERE table_id = ${tableId} AND end_snapshot IS NOT NULL
2842
+ UNION ALL
2843
+ SELECT snapshot_id
2844
+ FROM ${table("ducklake_snapshot_changes")}
2845
+ WHERE strpos(',' || changes_made || ',', ${quoteSqlString(`:${tableId},`)}) > 0
2846
+ OR strpos(
2847
+ commit_extra_info,
2848
+ ${quoteSqlString(`"datasetId":${JSON.stringify(input.datasetId)}`)}
2849
+ ) > 0
2850
+ ) touched
2851
+ WHERE snapshot_id >= ${tableStart}
2852
+ ${input.beforeSnapshotId === undefined ? "" : `AND snapshot_id < ${input.beforeSnapshotId}`}
2853
+ ORDER BY snapshot_id DESC
2854
+ LIMIT ${limit}`;
2855
+ return `
2856
+ WITH candidate_ids AS (
2857
+ ${candidateIds}
2858
+ ),
2859
+ candidate_snapshots AS (
2860
+ SELECT
2861
+ snapshot.snapshot_id,
2862
+ snapshot.snapshot_time,
2863
+ changes.changes_made,
2864
+ changes.commit_extra_info
2865
+ FROM ${table("ducklake_snapshot")} snapshot
2866
+ JOIN ${table("ducklake_snapshot_changes")} changes
2867
+ ON changes.snapshot_id = snapshot.snapshot_id
2868
+ WHERE snapshot.snapshot_id IN (SELECT snapshot_id FROM candidate_ids)
2869
+ ),
2870
+ file_changes AS (
2871
+ -- File metadata tells whether this table changed in each candidate snapshot.
2872
+ SELECT begin_snapshot AS snapshot_id, false AS is_delete_change
2873
+ FROM ${table("ducklake_data_file")}
2874
+ WHERE table_id = ${tableId}
2875
+ AND begin_snapshot IN (SELECT snapshot_id FROM candidate_ids)
2876
+ UNION ALL
2877
+ SELECT end_snapshot AS snapshot_id, true AS is_delete_change
2878
+ FROM ${table("ducklake_data_file")}
2879
+ WHERE table_id = ${tableId}
2880
+ AND end_snapshot IN (SELECT snapshot_id FROM candidate_ids)
2881
+ UNION ALL
2882
+ SELECT begin_snapshot AS snapshot_id, true AS is_delete_change
2883
+ FROM ${table("ducklake_delete_file")}
2884
+ WHERE table_id = ${tableId}
2885
+ AND begin_snapshot IN (SELECT snapshot_id FROM candidate_ids)
2886
+ UNION ALL
2887
+ SELECT end_snapshot AS snapshot_id, true AS is_delete_change
2888
+ FROM ${table("ducklake_delete_file")}
2889
+ WHERE table_id = ${tableId}
2890
+ AND end_snapshot IN (SELECT snapshot_id FROM candidate_ids)
2891
+ ),
2892
+ file_changes_by_snapshot AS (
2893
+ -- Collapse file-level changes into one row per snapshot.
2894
+ SELECT
2895
+ snapshot_id,
2896
+ count(*) > 0 AS has_file_change,
2897
+ count(*) FILTER (WHERE is_delete_change) > 0 AS has_file_delete_change
2898
+ FROM file_changes
2899
+ GROUP BY snapshot_id
2900
+ )
2901
+ SELECT
2902
+ -- Keep metadata-only candidates; Sixb filters them with commit_extra_info.
2903
+ candidate_ids.snapshot_id,
2904
+ candidate.snapshot_time,
2905
+ candidate.changes_made,
2906
+ candidate.commit_extra_info,
2907
+ coalesce(file_changes.has_file_change, false) AS has_file_change,
2908
+ coalesce(file_changes.has_file_delete_change, false) AS has_file_delete_change
2909
+ FROM candidate_ids
2910
+ LEFT JOIN candidate_snapshots candidate
2911
+ ON candidate.snapshot_id = candidate_ids.snapshot_id
2912
+ LEFT JOIN file_changes_by_snapshot file_changes
2913
+ ON file_changes.snapshot_id = candidate_ids.snapshot_id
2914
+ ORDER BY candidate_ids.snapshot_id DESC
2915
+ `;
2916
+ }));
2747
2917
  return rows.map((row) => {
2918
+ const snapshotId = String(getBigIntLike(row, "snapshot_id"));
2919
+ if (row.snapshot_time === null || row.snapshot_time === undefined) {
2920
+ return { snapshotId, missing: true };
2921
+ }
2748
2922
  const metadata = parseCommitMetadata(row.commit_extra_info);
2749
2923
  return {
2750
- snapshotId: String(getBigIntLike(row, "snapshot_id")),
2924
+ snapshotId,
2751
2925
  createdAt: getDate(row, "snapshot_time"),
2752
2926
  changesMade: getString(row, "changes_made"),
2753
2927
  hasFileChange: getBoolean(row, "has_file_change"),
@@ -4237,6 +4411,9 @@ class DuckLakeStorage {
4237
4411
  async getVersion(datasetId, versionId) {
4238
4412
  return this.snapshotReader.getVersion(datasetId, versionId);
4239
4413
  }
4414
+ async listLatestVersionsSince(input) {
4415
+ return this.snapshotReader.listLatestVersionsSince(input);
4416
+ }
4240
4417
  readRows(input) {
4241
4418
  return this.rows.readRows(input);
4242
4419
  }
@@ -4254,4 +4431,4 @@ export {
4254
4431
  DuckLakeStorage
4255
4432
  };
4256
4433
 
4257
- //# debugId=35ABBEC5A94F36A964756E2164756E21
4434
+ //# debugId=99E61447F4C74C9E64756E2164756E21