@sixb/ducklake 0.1.7 → 0.1.9

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 (72) hide show
  1. package/README.md +60 -5
  2. package/dist/ducklake-storage.d.ts +3 -1
  3. package/dist/ducklake-storage.d.ts.map +1 -1
  4. package/dist/index.js +1963 -480
  5. package/dist/index.js.map +25 -15
  6. package/dist/internal/dataset-row-commit.d.ts +10 -0
  7. package/dist/internal/dataset-row-commit.d.ts.map +1 -1
  8. package/dist/internal/dataset-row-merge.d.ts.map +1 -1
  9. package/dist/internal/ducklake-dataset-catalog.d.ts +20 -3
  10. package/dist/internal/ducklake-dataset-catalog.d.ts.map +1 -1
  11. package/dist/internal/ducklake-dataset-table-ref.d.ts +13 -0
  12. package/dist/internal/ducklake-dataset-table-ref.d.ts.map +1 -1
  13. package/dist/internal/ducklake-maintenance.d.ts +21 -1
  14. package/dist/internal/ducklake-maintenance.d.ts.map +1 -1
  15. package/dist/internal/ducklake-snapshot-reader.d.ts +81 -9
  16. package/dist/internal/ducklake-snapshot-reader.d.ts.map +1 -1
  17. package/dist/internal/ducklake-sql-executor.d.ts +9 -1
  18. package/dist/internal/ducklake-sql-executor.d.ts.map +1 -1
  19. package/dist/internal/ducklake-version-transaction.d.ts +54 -0
  20. package/dist/internal/ducklake-version-transaction.d.ts.map +1 -0
  21. package/dist/internal/ducklake-write-coordinator.d.ts +30 -3
  22. package/dist/internal/ducklake-write-coordinator.d.ts.map +1 -1
  23. package/dist/internal/lineage-encoding.d.ts +8 -0
  24. package/dist/internal/lineage-encoding.d.ts.map +1 -0
  25. package/dist/internal/sql-lineage/analysis.d.ts +32 -0
  26. package/dist/internal/sql-lineage/analysis.d.ts.map +1 -0
  27. package/dist/internal/sql-lineage/expressions.d.ts +9 -0
  28. package/dist/internal/sql-lineage/expressions.d.ts.map +1 -0
  29. package/dist/internal/sql-lineage/plan-json.d.ts +34 -0
  30. package/dist/internal/sql-lineage/plan-json.d.ts.map +1 -0
  31. package/dist/internal/sql-lineage/plan-relation.d.ts +28 -0
  32. package/dist/internal/sql-lineage/plan-relation.d.ts.map +1 -0
  33. package/dist/internal/sql-lineage/plan-walker.d.ts +18 -0
  34. package/dist/internal/sql-lineage/plan-walker.d.ts.map +1 -0
  35. package/dist/internal/sql-lineage/rejection.d.ts +11 -0
  36. package/dist/internal/sql-lineage/rejection.d.ts.map +1 -0
  37. package/dist/internal/sql-lineage/statement-gate.d.ts +21 -0
  38. package/dist/internal/sql-lineage/statement-gate.d.ts.map +1 -0
  39. package/dist/internal/sql-lineage/table-functions.d.ts +6 -0
  40. package/dist/internal/sql-lineage/table-functions.d.ts.map +1 -0
  41. package/dist/internal/sql-transform-relations.d.ts +20 -5
  42. package/dist/internal/sql-transform-relations.d.ts.map +1 -1
  43. package/dist/internal/sql.d.ts +11 -2
  44. package/dist/internal/sql.d.ts.map +1 -1
  45. package/dist/internal/versions.d.ts +10 -1
  46. package/dist/internal/versions.d.ts.map +1 -1
  47. package/dist/types.d.ts +10 -1
  48. package/dist/types.d.ts.map +1 -1
  49. package/package.json +5 -5
  50. package/src/ducklake-storage.ts +17 -2
  51. package/src/internal/dataset-row-commit.ts +28 -2
  52. package/src/internal/dataset-row-merge.ts +4 -0
  53. package/src/internal/ducklake-dataset-catalog.ts +162 -49
  54. package/src/internal/ducklake-dataset-table-ref.ts +75 -17
  55. package/src/internal/ducklake-maintenance.ts +110 -48
  56. package/src/internal/ducklake-snapshot-reader.ts +549 -254
  57. package/src/internal/ducklake-sql-executor.ts +214 -14
  58. package/src/internal/ducklake-version-transaction.ts +224 -0
  59. package/src/internal/ducklake-write-coordinator.ts +226 -148
  60. package/src/internal/lineage-encoding.ts +136 -0
  61. package/src/internal/sql-lineage/analysis.ts +257 -0
  62. package/src/internal/sql-lineage/expressions.ts +208 -0
  63. package/src/internal/sql-lineage/plan-json.ts +130 -0
  64. package/src/internal/sql-lineage/plan-relation.ts +59 -0
  65. package/src/internal/sql-lineage/plan-walker.ts +727 -0
  66. package/src/internal/sql-lineage/rejection.ts +16 -0
  67. package/src/internal/sql-lineage/statement-gate.ts +164 -0
  68. package/src/internal/sql-lineage/table-functions.ts +11 -0
  69. package/src/internal/sql-transform-relations.ts +90 -40
  70. package/src/internal/sql.ts +35 -6
  71. package/src/internal/versions.ts +37 -14
  72. package/src/types.ts +11 -1
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // src/internal/ducklake-change-reader.ts
3
3
  import { randomUUID } from "crypto";
4
4
  import {
5
- LakeStorageError as LakeStorageError7,
5
+ LakeStorageError as LakeStorageError8,
6
6
  resolveDatasetChangeColumns
7
7
  } from "@sixb/core/lake-storage";
8
8
 
@@ -63,6 +63,9 @@ function getDate(row, key) {
63
63
  throw new LakeStorageError(`[SixbDuckLake] Expected DuckDB column '${key}' to be a date, got ${typeof value}.`);
64
64
  }
65
65
 
66
+ // src/internal/ducklake-dataset-table-ref.ts
67
+ import { LakeStorageError as LakeStorageError4 } from "@sixb/core/lake-storage";
68
+
66
69
  // src/internal/names.ts
67
70
  import { LakeStorageError as LakeStorageError2 } from "@sixb/core/lake-storage";
68
71
  var DATASET_TABLE_PREFIX = "sixb__ds__";
@@ -158,8 +161,20 @@ function buildSetPostgresPoolSql(options) {
158
161
  return postgresPoolParameters(options.postgresPool).map(({ name, value }) => `SET pg_pool_${name} = ${value}`);
159
162
  }
160
163
  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)}`;
164
+ return `${quoteIdentifier(duckLakeMetadataCatalog(options))}.${quoteIdentifier(duckLakeMetadataSchema(options))}.${quoteIdentifier(tableName)}`;
165
+ }
166
+ function buildDuckLakeMetadataQuery(options, build) {
167
+ if (options.catalog.type !== "postgres") {
168
+ return build((tableName) => duckLakeMetadataTableName(options, tableName));
169
+ }
170
+ const schema = options.catalog.metadataSchema;
171
+ const sql = build((tableName) => schema === undefined ? quoteIdentifier(tableName) : `${quoteIdentifier(schema)}.${quoteIdentifier(tableName)}`);
172
+ return `SELECT * FROM postgres_query(${quoteSqlString(duckLakeMetadataCatalog(options))}, ${quoteSqlString(sql)})`;
173
+ }
174
+ function duckLakeMetadataSchema(options) {
175
+ if (options.catalog.type !== "postgres")
176
+ return "main";
177
+ return options.catalog.metadataSchema ?? "public";
163
178
  }
164
179
  function quoteIdentifier(identifier) {
165
180
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
@@ -400,24 +415,56 @@ function pathScheme(path) {
400
415
 
401
416
  // src/internal/ducklake-dataset-table-ref.ts
402
417
  async function resolveDatasetTableRef(options, runtime, datasetId) {
403
- const tableName = encodeDatasetTableName(datasetId);
404
- const ducklakeTable = duckLakeMetadataTableName(options, "ducklake_table");
405
- const [row] = await runtime.query(`
406
- SELECT table_id
407
- FROM ${ducklakeTable}
408
- WHERE table_name = ${quoteSqlString(tableName)}
409
- AND end_snapshot IS NULL
410
- LIMIT 1
418
+ return tableRefFromRows(datasetId, await runtime.query(datasetTablesSql(options, datasetId)));
419
+ }
420
+ async function resolveDatasetTableRefAtCurrentSnapshot(options, runtime, datasetId) {
421
+ const rows = await runtime.query(`
422
+ SELECT current.id AS current_snapshot, tables.table_id, tables.table_name
423
+ FROM ${quoteIdentifier(duckLakeAlias(options))}.current_snapshot() current
424
+ LEFT JOIN (${datasetTablesSql(options, datasetId)}) tables ON true
411
425
  `);
412
- return row === undefined ? null : {
426
+ const [first] = rows;
427
+ if (first === undefined) {
428
+ throw new LakeStorageError4("[SixbDuckLake] DuckLake did not return a current snapshot.");
429
+ }
430
+ return {
431
+ snapshotId: String(getBigIntLike(first, "current_snapshot")),
432
+ table: tableRefFromRows(datasetId, rows.filter((row) => row.table_id !== null && row.table_id !== undefined))
433
+ };
434
+ }
435
+ function datasetTableIds(table) {
436
+ return table.sequenceTableId === undefined ? [table.tableId] : [table.tableId, table.sequenceTableId];
437
+ }
438
+ function datasetTablesSql(options, datasetId) {
439
+ const tableNames = [encodeDatasetTableName(datasetId), encodeDatasetSequenceTableName(datasetId)];
440
+ return `
441
+ SELECT table_id, table_name
442
+ FROM ${duckLakeMetadataTableName(options, "ducklake_table")}
443
+ WHERE table_name IN (${tableNames.map((name) => quoteSqlString(name)).join(", ")})
444
+ AND end_snapshot IS NULL
445
+ `;
446
+ }
447
+ function tableRefFromRows(datasetId, rows) {
448
+ const tableName = encodeDatasetTableName(datasetId);
449
+ const tableIdOf = (name) => {
450
+ const row = rows.find((candidate) => candidate.table_name === name);
451
+ return row === undefined ? undefined : getBigIntLike(row, "table_id");
452
+ };
453
+ const tableId = tableIdOf(tableName);
454
+ if (tableId === undefined) {
455
+ return null;
456
+ }
457
+ const sequenceTableId = tableIdOf(encodeDatasetSequenceTableName(datasetId));
458
+ return {
413
459
  datasetId,
414
460
  tableName,
415
- tableId: getBigIntLike(row, "table_id")
461
+ tableId,
462
+ ...sequenceTableId === undefined ? {} : { sequenceTableId }
416
463
  };
417
464
  }
418
465
 
419
466
  // src/internal/schema.ts
420
- import { LakeStorageError as LakeStorageError5 } from "@sixb/core/lake-storage";
467
+ import { LakeStorageError as LakeStorageError6 } from "@sixb/core/lake-storage";
421
468
 
422
469
  // src/internal/duckdb-column-types.ts
423
470
  function normalizeDuckDbTypeSql(typeSql) {
@@ -459,23 +506,23 @@ function findDatasetColumnTypeForDuckDbSql(typeSql) {
459
506
 
460
507
  // src/internal/duckdb-decimal.ts
461
508
  import { normalizeDecimalValue } from "@sixb/core";
462
- import { LakeStorageError as LakeStorageError4 } from "@sixb/core/lake-storage";
509
+ import { LakeStorageError as LakeStorageError5 } from "@sixb/core/lake-storage";
463
510
  var decimalType = DUCKDB_COLUMN_TYPES.decimal;
464
511
  var maxIntegerDigits = decimalType.precision - decimalType.scale;
465
512
  function normalizeDuckDbDecimalValue(value, columnName) {
466
513
  if (typeof value !== "string") {
467
- throw new LakeStorageError4(`[SixbDuckLake] Decimal column '${columnName}' must be an exact decimal string.`);
514
+ throw new LakeStorageError5(`[SixbDuckLake] Decimal column '${columnName}' must be an exact decimal string.`);
468
515
  }
469
516
  let normalized;
470
517
  try {
471
518
  normalized = normalizeDecimalValue(value);
472
519
  } catch {
473
- throw new LakeStorageError4(`[SixbDuckLake] Decimal column '${columnName}' must be an exact decimal string.`);
520
+ throw new LakeStorageError5(`[SixbDuckLake] Decimal column '${columnName}' must be an exact decimal string.`);
474
521
  }
475
522
  const unsigned = normalized.startsWith("-") ? normalized.slice(1) : normalized;
476
523
  const [integer, fraction = ""] = unsigned.split(".");
477
524
  if (integer.length > maxIntegerDigits || fraction.length > decimalType.scale) {
478
- throw new LakeStorageError4(`[SixbDuckLake] Decimal column '${columnName}' cannot be represented exactly as ${decimalType.sql}; expected at most ${maxIntegerDigits} integer digits and ${decimalType.scale} fractional digits.`);
525
+ throw new LakeStorageError5(`[SixbDuckLake] Decimal column '${columnName}' cannot be represented exactly as ${decimalType.sql}; expected at most ${maxIntegerDigits} integer digits and ${decimalType.scale} fractional digits.`);
479
526
  }
480
527
  return normalized;
481
528
  }
@@ -489,7 +536,7 @@ function duckDbTypeToDatasetColumnType(typeSql) {
489
536
  if (type) {
490
537
  return type;
491
538
  }
492
- throw new LakeStorageError5(`[SixbDuckLake] DuckDB column type '${typeSql}' cannot be mapped to a Sixb dataset column type.`);
539
+ throw new LakeStorageError6(`[SixbDuckLake] DuckDB column type '${typeSql}' cannot be mapped to a Sixb dataset column type.`);
493
540
  }
494
541
  function datasetColumnToDuckDbSql(column) {
495
542
  const nullableSql = column.nullable ? "" : " NOT NULL";
@@ -527,7 +574,7 @@ function normalizeReadValue(value, column) {
527
574
  }
528
575
  function normalizeFileRef(value) {
529
576
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
530
- throw new LakeStorageError5("[SixbDuckLake] DuckDB fileRef value must be a struct.");
577
+ throw new LakeStorageError6("[SixbDuckLake] DuckDB fileRef value must be a struct.");
531
578
  }
532
579
  const record = value;
533
580
  return {
@@ -542,7 +589,7 @@ function normalizeFileRef(value) {
542
589
  function getStringField(record, key) {
543
590
  const value = record[key];
544
591
  if (typeof value !== "string") {
545
- throw new LakeStorageError5(`[SixbDuckLake] DuckDB fileRef field '${key}' must be a string.`);
592
+ throw new LakeStorageError6(`[SixbDuckLake] DuckDB fileRef field '${key}' must be a string.`);
546
593
  }
547
594
  return value;
548
595
  }
@@ -552,7 +599,7 @@ function getOptionalStringField(record, key) {
552
599
  return;
553
600
  }
554
601
  if (typeof value !== "string") {
555
- throw new LakeStorageError5(`[SixbDuckLake] DuckDB fileRef field '${key}' must be a string.`);
602
+ throw new LakeStorageError6(`[SixbDuckLake] DuckDB fileRef field '${key}' must be a string.`);
556
603
  }
557
604
  return value;
558
605
  }
@@ -564,18 +611,18 @@ function getIntegerField(record, key) {
564
611
  if (typeof value === "bigint") {
565
612
  return Number(value);
566
613
  }
567
- throw new LakeStorageError5(`[SixbDuckLake] DuckDB fileRef field '${key}' must be an integer.`);
614
+ throw new LakeStorageError6(`[SixbDuckLake] DuckDB fileRef field '${key}' must be an integer.`);
568
615
  }
569
616
 
570
617
  // src/internal/versions.ts
571
- import { LakeStorageError as LakeStorageError6 } from "@sixb/core/lake-storage";
618
+ import { LakeStorageError as LakeStorageError7 } from "@sixb/core/lake-storage";
572
619
  function parseVersionId(versionId) {
573
620
  if (!versionId.startsWith("ducklake:")) {
574
- throw new LakeStorageError6(`[SixbDuckLake] Invalid DuckLake version id '${versionId}'.`);
621
+ throw new LakeStorageError7(`[SixbDuckLake] Invalid DuckLake version id '${versionId}'.`);
575
622
  }
576
623
  const snapshotId = versionId.slice("ducklake:".length);
577
624
  if (!/^\d+$/.test(snapshotId)) {
578
- throw new LakeStorageError6(`[SixbDuckLake] Invalid DuckLake version id '${versionId}'.`);
625
+ throw new LakeStorageError7(`[SixbDuckLake] Invalid DuckLake version id '${versionId}'.`);
579
626
  }
580
627
  return snapshotId;
581
628
  }
@@ -597,6 +644,7 @@ function parseCommitMetadata(value) {
597
644
  }
598
645
  const mode = parsed.sixb.mode;
599
646
  const commitId = parsed.sixb.commitId;
647
+ const producer = parseDatasetProducer(parsed.sixb.producer);
600
648
  const rowCount = parsed.sixb.rowCount;
601
649
  const validatedPrimaryKeyColumns = parsed.sixb.validatedPrimaryKeyColumns;
602
650
  return {
@@ -604,14 +652,19 @@ function parseCommitMetadata(value) {
604
652
  datasetId: parsed.sixb.datasetId,
605
653
  ...typeof commitId === "string" ? { commitId } : {},
606
654
  ...mode === "snapshot" || mode === "append" || mode === "merge" || mode === "schema" ? { mode } : {},
607
- ...isDatasetProducer(parsed.sixb.producer) ? { producer: parsed.sixb.producer } : {},
655
+ ...producer === undefined ? {} : { producer },
608
656
  ...isDatasetVersionRefs(parsed.sixb.inputs) ? { inputs: parsed.sixb.inputs } : {},
609
657
  ...typeof commitId === "string" && isRowCount(rowCount) ? { rowCount } : {},
610
658
  ...typeof commitId === "string" && isValidatedPrimaryKeyColumns(validatedPrimaryKeyColumns) ? { validatedPrimaryKeyColumns } : {},
611
- ...isSchemaChangeMetadata(parsed.sixb.schemaChange) ? { schemaChange: parsed.sixb.schemaChange } : {}
659
+ ...isSchemaChangeMetadata(parsed.sixb.schemaChange) ? { schemaChange: parsed.sixb.schemaChange } : {},
660
+ ...parsed.sixb.lineage === undefined ? {} : { lineage: parsed.sixb.lineage }
612
661
  };
613
662
  }
614
- function parseInlineDataChange(changesMade, tableId) {
663
+ function changesTouchTables(changesMade, tableIds) {
664
+ const ids = new Set(tableIds.map((tableId) => tableId.toString()));
665
+ return changesMade.split(",").some((change) => ids.has(change.split(":")[1] ?? ""));
666
+ }
667
+ function parseTableDataChange(changesMade, tableId) {
615
668
  const tableIdText = tableId.toString();
616
669
  let hasDataChange = false;
617
670
  let hasDeleteChange = false;
@@ -633,8 +686,16 @@ function parseInlineDataChange(changesMade, tableId) {
633
686
  function isRecord(value) {
634
687
  return typeof value === "object" && value !== null && !Array.isArray(value);
635
688
  }
636
- function isDatasetProducer(value) {
637
- return isRecord(value) && (value.kind === "sync" || value.kind === "pipeline" || value.kind === "ingest") && optionalString(value.id) && optionalString(value.runId);
689
+ function parseDatasetProducer(value) {
690
+ if (!isRecord(value) || value.kind !== "sync" && value.kind !== "pipeline" && value.kind !== "ingest") {
691
+ return;
692
+ }
693
+ return {
694
+ kind: value.kind,
695
+ ...typeof value.id === "string" ? { id: value.id } : {},
696
+ ...typeof value.runId === "string" ? { runId: value.runId } : {},
697
+ ...typeof value.stepId === "string" ? { stepId: value.stepId } : {}
698
+ };
638
699
  }
639
700
  function isDatasetVersionRefs(value) {
640
701
  return Array.isArray(value) && value.every((item) => isRecord(item) && typeof item.datasetId === "string" && typeof item.versionId === "string");
@@ -642,9 +703,6 @@ function isDatasetVersionRefs(value) {
642
703
  function isSchemaChangeMetadata(value) {
643
704
  return isRecord(value) && (value.addColumns === undefined || Array.isArray(value.addColumns) && value.addColumns.every((columnName) => typeof columnName === "string"));
644
705
  }
645
- function optionalString(value) {
646
- return value === undefined || typeof value === "string";
647
- }
648
706
  function isRowCount(value) {
649
707
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
650
708
  }
@@ -717,11 +775,11 @@ class DuckLakeChangeReader {
717
775
  while (delivered < changeCount) {
718
776
  input.signal?.throwIfAborted();
719
777
  if (closePromise)
720
- throw new LakeStorageError7("[SixbDuckLake] Change reader is closed.");
778
+ throw new LakeStorageError8("[SixbDuckLake] Change reader is closed.");
721
779
  const page = await runtime.query(`SELECT rowid AS position, * FROM ${temporary}
722
780
  WHERE rowid > ${cursor} ORDER BY rowid LIMIT ${PAGE_ROWS}`);
723
781
  if (page.length === 0)
724
- throw new LakeStorageError7("[SixbDuckLake] Change read ended before its declared count.");
782
+ throw new LakeStorageError8("[SixbDuckLake] Change read ended before its declared count.");
725
783
  for (const row of page) {
726
784
  input.signal?.throwIfAborted();
727
785
  delivered++;
@@ -760,7 +818,7 @@ async function uniqueRowCount(runtime, relation, keys) {
760
818
  function safeCount(row, name) {
761
819
  const count = Number(getBigIntLike(row, name));
762
820
  if (!Number.isSafeInteger(count) || count < 0)
763
- throw new LakeStorageError7("[SixbDuckLake] Change count exceeds the supported integer range.");
821
+ throw new LakeStorageError8("[SixbDuckLake] Change count exceeds the supported integer range.");
764
822
  return count;
765
823
  }
766
824
  function rowImage(row, prefix, columns) {
@@ -771,7 +829,7 @@ function rowImage(row, prefix, columns) {
771
829
  }
772
830
 
773
831
  // src/internal/ducklake-connection-manager.ts
774
- import { LakeStorageError as LakeStorageError11 } from "@sixb/core/lake-storage";
832
+ import { LakeStorageError as LakeStorageError12 } from "@sixb/core/lake-storage";
775
833
 
776
834
  // src/internal/catalog-key.ts
777
835
  import { resolve } from "path";
@@ -796,10 +854,10 @@ import { join, resolve as resolve2 } from "path";
796
854
  import {
797
855
  DuckDBInstance
798
856
  } from "@duckdb/node-api";
799
- import { LakeStorageError as LakeStorageError10 } from "@sixb/core/lake-storage";
857
+ import { LakeStorageError as LakeStorageError11 } from "@sixb/core/lake-storage";
800
858
 
801
859
  // src/internal/duckdb-reader.ts
802
- import { LakeStorageError as LakeStorageError9 } from "@sixb/core/lake-storage";
860
+ import { LakeStorageError as LakeStorageError10 } from "@sixb/core/lake-storage";
803
861
 
804
862
  // src/internal/duckdb-value-converter.ts
805
863
  import {
@@ -807,14 +865,14 @@ import {
807
865
  DuckDBTypeId,
808
866
  JSDuckDBValueConverter
809
867
  } from "@duckdb/node-api";
810
- import { LakeStorageError as LakeStorageError8 } from "@sixb/core/lake-storage";
868
+ import { LakeStorageError as LakeStorageError9 } from "@sixb/core/lake-storage";
811
869
  var sixbDuckDbValueConverter = (value, type, converter) => {
812
870
  if (value === null) {
813
871
  return null;
814
872
  }
815
873
  if (type.typeId === DuckDBTypeId.DECIMAL) {
816
874
  if (!(value instanceof DuckDBDecimalValue)) {
817
- throw new LakeStorageError8("[SixbDuckLake] DuckDB returned an invalid DECIMAL value.");
875
+ throw new LakeStorageError9("[SixbDuckLake] DuckDB returned an invalid DECIMAL value.");
818
876
  }
819
877
  return value.toString();
820
878
  }
@@ -856,7 +914,7 @@ class NodeDuckDbReader {
856
914
  }
857
915
  async* rows() {
858
916
  if (this.consumed)
859
- throw new LakeStorageError9("[SixbDuckLake] Reader can only be consumed once.");
917
+ throw new LakeStorageError10("[SixbDuckLake] Reader can only be consumed once.");
860
918
  this.consumed = true;
861
919
  try {
862
920
  this.assertOpen();
@@ -919,7 +977,7 @@ class NodeDuckDbReader {
919
977
  assertOpen() {
920
978
  this.options.signal?.throwIfAborted();
921
979
  if (this.closePromise)
922
- throw new LakeStorageError9("[SixbDuckLake] Reader is closed.");
980
+ throw new LakeStorageError10("[SixbDuckLake] Reader is closed.");
923
981
  }
924
982
  }
925
983
 
@@ -1030,12 +1088,12 @@ class NodeDuckDbRuntime {
1030
1088
  }
1031
1089
  assertAcceptingOperations() {
1032
1090
  if (this.closing || this.closed) {
1033
- throw new LakeStorageError10("[SixbDuckLake] DuckDB runtime is closed.");
1091
+ throw new LakeStorageError11("[SixbDuckLake] DuckDB runtime is closed.");
1034
1092
  }
1035
1093
  }
1036
1094
  assertNotClosed() {
1037
1095
  if (this.closed) {
1038
- throw new LakeStorageError10("[SixbDuckLake] DuckDB runtime is closed.");
1096
+ throw new LakeStorageError11("[SixbDuckLake] DuckDB runtime is closed.");
1039
1097
  }
1040
1098
  }
1041
1099
  }
@@ -1227,7 +1285,7 @@ class DuckLakeConnectionManager {
1227
1285
  this.observedLocalCatalogGeneration = currentLocalCatalogGeneration(options);
1228
1286
  this.maxStreamingReads = options.maxStreamingReads ?? 4;
1229
1287
  if (!Number.isSafeInteger(this.maxStreamingReads) || this.maxStreamingReads < 1) {
1230
- throw new LakeStorageError11("[SixbDuckLake] maxStreamingReads must be a positive integer.");
1288
+ throw new LakeStorageError12("[SixbDuckLake] maxStreamingReads must be a positive integer.");
1231
1289
  }
1232
1290
  }
1233
1291
  async acquireRead(signal) {
@@ -1281,7 +1339,7 @@ class DuckLakeConnectionManager {
1281
1339
  }
1282
1340
  assertOpen() {
1283
1341
  if (this.closed) {
1284
- throw new LakeStorageError11("[SixbDuckLake] DuckLakeStorage is closed.");
1342
+ throw new LakeStorageError12("[SixbDuckLake] DuckLakeStorage is closed.");
1285
1343
  }
1286
1344
  }
1287
1345
  async runtime() {
@@ -1469,7 +1527,7 @@ class DuckLakeConnectionManager {
1469
1527
  }
1470
1528
  async finishClose() {
1471
1529
  this.closed = true;
1472
- this.shutdown.abort(new LakeStorageError11("[SixbDuckLake] DuckLakeStorage is closed."));
1530
+ this.shutdown.abort(new LakeStorageError12("[SixbDuckLake] DuckLakeStorage is closed."));
1473
1531
  const runtimePromise = this.runtimePromise;
1474
1532
  this.runtimePromise = undefined;
1475
1533
  await this.closeRuntimePromise(runtimePromise, {
@@ -1631,12 +1689,13 @@ function incrementLocalCatalogGeneration(options) {
1631
1689
  import { randomUUID as randomUUID2 } from "crypto";
1632
1690
  import {
1633
1691
  assertDatasetDefinition,
1634
- LakeStorageError as LakeStorageError14,
1692
+ LakeStorageError as LakeStorageError16,
1693
+ lineageAtCommit,
1635
1694
  planDatasetDefinitionUpdate
1636
1695
  } from "@sixb/core/lake-storage";
1637
1696
 
1638
1697
  // src/internal/ducklake-catalog-schema.ts
1639
- import { LakeStorageError as LakeStorageError12 } from "@sixb/core/lake-storage";
1698
+ import { LakeStorageError as LakeStorageError13 } from "@sixb/core/lake-storage";
1640
1699
  var SIXB_PRIMARY_KEY_COMMENT_NAMESPACE = "sixb:primary-key:";
1641
1700
  var DATASET_SEQUENCE_COLUMN_COMMENT = "sixb.sequenceBy.v1";
1642
1701
  var SIXB_PRIMARY_KEY_COMMENT_PREFIX = `${SIXB_PRIMARY_KEY_COMMENT_NAMESPACE}v1:`;
@@ -1687,14 +1746,14 @@ function duckLakeCatalogColumnsToDatasetPrimaryKey(tableName, rows) {
1687
1746
  throwInvalidPrimaryKeyMetadata(tableName, column, comment);
1688
1747
  }
1689
1748
  if (seenOrdinals.has(ordinal)) {
1690
- throw new LakeStorageError12(`[SixbDuckLake] Dataset table '${tableName}' has duplicate primary-key ordinal '${ordinal}'.`);
1749
+ throw new LakeStorageError13(`[SixbDuckLake] Dataset table '${tableName}' has duplicate primary-key ordinal '${ordinal}'.`);
1691
1750
  }
1692
1751
  seenOrdinals.add(ordinal);
1693
1752
  if (duckDbTypeToDatasetColumnType(column.columnType) !== "string") {
1694
- throw new LakeStorageError12(`[SixbDuckLake] Dataset table '${tableName}' primary-key column '${column.columnName}' must map to type 'string'.`);
1753
+ throw new LakeStorageError13(`[SixbDuckLake] Dataset table '${tableName}' primary-key column '${column.columnName}' must map to type 'string'.`);
1695
1754
  }
1696
1755
  if (column.nullsAllowed) {
1697
- throw new LakeStorageError12(`[SixbDuckLake] Dataset table '${tableName}' primary-key column '${column.columnName}' must not be nullable.`);
1756
+ throw new LakeStorageError13(`[SixbDuckLake] Dataset table '${tableName}' primary-key column '${column.columnName}' must not be nullable.`);
1698
1757
  }
1699
1758
  keyedColumns.push({ column, ordinal });
1700
1759
  }
@@ -1704,7 +1763,7 @@ function duckLakeCatalogColumnsToDatasetPrimaryKey(tableName, rows) {
1704
1763
  keyedColumns.sort((left, right) => left.ordinal - right.ordinal);
1705
1764
  for (const [expectedOrdinal, keyedColumn] of keyedColumns.entries()) {
1706
1765
  if (keyedColumn.ordinal !== expectedOrdinal) {
1707
- throw new LakeStorageError12(`[SixbDuckLake] Dataset table '${tableName}' primary-key ordinals must be contiguous from 0.`);
1766
+ throw new LakeStorageError13(`[SixbDuckLake] Dataset table '${tableName}' primary-key ordinals must be contiguous from 0.`);
1708
1767
  }
1709
1768
  }
1710
1769
  const columnNames = keyedColumns.map(({ column }) => column.columnName);
@@ -1714,7 +1773,7 @@ function duckLakeCatalogColumnsToDatasetPrimaryKey(tableName, rows) {
1714
1773
  return columnNames;
1715
1774
  }
1716
1775
  function throwInvalidPrimaryKeyMetadata(tableName, column, comment) {
1717
- throw new LakeStorageError12(`[SixbDuckLake] Dataset table '${tableName}' has malformed primary-key metadata '${comment}' on column '${column.columnName}'.`);
1776
+ throw new LakeStorageError13(`[SixbDuckLake] Dataset table '${tableName}' has malformed primary-key metadata '${comment}' on column '${column.columnName}'.`);
1718
1777
  }
1719
1778
  function duckLakeCatalogColumnToDefinition(tableName, column, childrenByParentId) {
1720
1779
  const children = [...childrenByParentId.get(column.columnId) ?? []].sort(compareDuckLakeCatalogColumnOrder);
@@ -1731,7 +1790,7 @@ function duckLakeCatalogColumnTypeToDatasetType(tableName, column, children) {
1731
1790
  if (isFileRefStruct(children)) {
1732
1791
  return "fileRef";
1733
1792
  }
1734
- throw new LakeStorageError12(`[SixbDuckLake] DuckDB column type '${formatDuckLakeCatalogColumnType(column, children)}' in dataset table '${tableName}' cannot be mapped to a Sixb dataset column type.`);
1793
+ throw new LakeStorageError13(`[SixbDuckLake] DuckDB column type '${formatDuckLakeCatalogColumnType(column, children)}' in dataset table '${tableName}' cannot be mapped to a Sixb dataset column type.`);
1735
1794
  }
1736
1795
  function compareDuckLakeCatalogColumnOrder(left, right) {
1737
1796
  return Number(left.columnOrder - right.columnOrder);
@@ -1753,7 +1812,7 @@ function formatDuckLakeCatalogColumnType(column, children) {
1753
1812
  }
1754
1813
 
1755
1814
  // src/internal/ducklake-current-dataset-definitions.ts
1756
- import { LakeStorageError as LakeStorageError13 } from "@sixb/core/lake-storage";
1815
+ import { LakeStorageError as LakeStorageError14 } from "@sixb/core/lake-storage";
1757
1816
  async function readCurrentDatasetDefinitions(input) {
1758
1817
  if (input.datasetIds?.length === 0) {
1759
1818
  return new Map;
@@ -1880,7 +1939,7 @@ async function readCurrentDatasetPartitions(input, tableNames) {
1880
1939
  transform: getString(row, "transform")
1881
1940
  };
1882
1941
  if (partition.transform !== "identity") {
1883
- throw new LakeStorageError13(`[SixbDuckLake] Dataset table '${partition.tableName}' uses unsupported DuckLake partition transform '${partition.transform}'.`);
1942
+ throw new LakeStorageError14(`[SixbDuckLake] Dataset table '${partition.tableName}' uses unsupported DuckLake partition transform '${partition.transform}'.`);
1884
1943
  }
1885
1944
  return partition;
1886
1945
  });
@@ -1904,6 +1963,188 @@ function groupPartitionsByTableName(rows) {
1904
1963
  return result;
1905
1964
  }
1906
1965
 
1966
+ // src/internal/ducklake-version-transaction.ts
1967
+ import { LakeConcurrencyError, LakeStorageError as LakeStorageError15 } from "@sixb/core/lake-storage";
1968
+ var VERSION_RERUNS = { maxAttempts: 16, baseDelayMillis: 10, maxDelayMillis: 1000 };
1969
+
1970
+ class CatalogMovedError extends LakeConcurrencyError {
1971
+ }
1972
+ async function beginTransactionOnLatestVersion(input) {
1973
+ if (!isSqliteCatalog(input.options.catalog)) {
1974
+ await input.runtime.run("BEGIN TRANSACTION");
1975
+ return (await input.readLatestVersion()).latestVersion;
1976
+ }
1977
+ input.signal?.throwIfAborted();
1978
+ const { snapshotId, latestVersion } = await input.readLatestVersion();
1979
+ await input.runtime.run("BEGIN TRANSACTION");
1980
+ if (await currentSnapshotId(input.options, input.runtime) !== snapshotId) {
1981
+ await input.runtime.run("ROLLBACK");
1982
+ throw new CatalogMovedError(`[SixbDuckLake] Catalog changed while a write to dataset '${input.datasetId}' was prepared.`);
1983
+ }
1984
+ return latestVersion;
1985
+ }
1986
+ async function commitVersionTransaction(runtime, datasetId, onReadSnapshot) {
1987
+ if (onReadSnapshot) {
1988
+ await runtime.run("SET ducklake_max_retry_count = 0");
1989
+ }
1990
+ try {
1991
+ await runtime.run("COMMIT");
1992
+ } catch (error) {
1993
+ throw catalogMovedError(error, datasetId) ?? error;
1994
+ } finally {
1995
+ if (onReadSnapshot) {
1996
+ await runtime.run("RESET ducklake_max_retry_count").catch(() => {
1997
+ onReadSnapshot.poisonRuntime();
1998
+ });
1999
+ }
2000
+ }
2001
+ }
2002
+ async function rerunWhenCatalogMoves(datasetId, transaction, signal) {
2003
+ for (let attempt = 1;; attempt++) {
2004
+ try {
2005
+ return await transaction(attempt);
2006
+ } catch (error) {
2007
+ if (!(error instanceof CatalogMovedError)) {
2008
+ throw error;
2009
+ }
2010
+ if (attempt === VERSION_RERUNS.maxAttempts) {
2011
+ throw new LakeConcurrencyError(`[SixbDuckLake] Concurrent writers moved the catalog during ${attempt} attempts to commit dataset '${datasetId}'. Retry the write.`, { cause: error });
2012
+ }
2013
+ }
2014
+ await waitBeforeRerun(rerunDelayMillis(attempt), signal);
2015
+ }
2016
+ }
2017
+ function rerunDelayMillis(rerun) {
2018
+ if (rerun === 1) {
2019
+ return 0;
2020
+ }
2021
+ const window = Math.min(VERSION_RERUNS.maxDelayMillis, VERSION_RERUNS.baseDelayMillis * 2 ** (rerun - 1));
2022
+ return Math.random() * window;
2023
+ }
2024
+ function waitBeforeRerun(millis, signal) {
2025
+ signal?.throwIfAborted();
2026
+ if (millis === 0) {
2027
+ return Promise.resolve();
2028
+ }
2029
+ return new Promise((resolve, reject) => {
2030
+ const abort = () => {
2031
+ clearTimeout(timer);
2032
+ reject(signal?.reason);
2033
+ };
2034
+ const timer = setTimeout(() => {
2035
+ signal?.removeEventListener("abort", abort);
2036
+ resolve();
2037
+ }, millis);
2038
+ signal?.addEventListener("abort", abort, { once: true });
2039
+ });
2040
+ }
2041
+ function isSqliteCatalog(catalog) {
2042
+ return catalog.type === "sqlite" || catalog.type === "custom" && catalog.uri.toLowerCase().startsWith("sqlite:");
2043
+ }
2044
+ function catalogMovedError(error, datasetId) {
2045
+ if (error instanceof Error && error.message.startsWith("TransactionContext Error: Failed to commit: Failed to commit DuckLake transaction.") && (error.message.includes("Transaction conflict - ") || error.message.includes("Exceeded the maximum retry count"))) {
2046
+ return new CatalogMovedError(`[SixbDuckLake] Concurrent catalog commit for dataset '${datasetId}'.`, { cause: error });
2047
+ }
2048
+ return;
2049
+ }
2050
+ async function currentSnapshotId(options, runtime) {
2051
+ const [row] = await runtime.query(`SELECT id FROM ${quoteIdentifier(duckLakeAlias(options))}.current_snapshot()`);
2052
+ if (row === undefined) {
2053
+ throw new LakeStorageError15("[SixbDuckLake] DuckLake did not return a current snapshot.");
2054
+ }
2055
+ return String(getBigIntLike(row, "id"));
2056
+ }
2057
+
2058
+ // src/internal/lineage-encoding.ts
2059
+ function encodeLineage(lineage) {
2060
+ if ("unknown" in lineage) {
2061
+ return JSON.stringify({ unknown: lineage.unknown });
2062
+ }
2063
+ const strings = [];
2064
+ const indexes = new Map;
2065
+ const indexOf = (value) => {
2066
+ let index = indexes.get(value);
2067
+ if (index === undefined) {
2068
+ index = strings.length;
2069
+ strings.push(value);
2070
+ indexes.set(value, index);
2071
+ }
2072
+ return index;
2073
+ };
2074
+ const encodeRefs = (refs) => refs.flatMap((ref) => [indexOf(ref.datasetId), indexOf(ref.column)]);
2075
+ const encoded = {
2076
+ v: 1,
2077
+ strings,
2078
+ rows: encodeRefs(lineage.rows),
2079
+ columns: Object.entries(lineage.columns).map(([column, refs]) => [
2080
+ indexOf(column),
2081
+ ...encodeRefs(refs)
2082
+ ])
2083
+ };
2084
+ return JSON.stringify(encoded);
2085
+ }
2086
+ function decodeLineage(datasetId, stored) {
2087
+ const parsed = typeof stored === "string" ? parseJson(stored) : undefined;
2088
+ if (isRecord2(parsed) && Object.hasOwn(parsed, "unknown")) {
2089
+ return typeof parsed.unknown === "string" ? { unknown: parsed.unknown } : unreadable(datasetId);
2090
+ }
2091
+ return decodeColumnLineage(parsed) ?? unreadable(datasetId);
2092
+ }
2093
+ function decodeColumnLineage(value) {
2094
+ if (!isRecord2(value) || value.v !== 1 || !isStringList(value.strings)) {
2095
+ return;
2096
+ }
2097
+ const { strings } = value;
2098
+ const decodeRefs = (flat, from) => {
2099
+ if ((flat.length - from) % 2 !== 0) {
2100
+ return;
2101
+ }
2102
+ const refs = [];
2103
+ for (let index = from;index < flat.length; index += 2) {
2104
+ const datasetId = stringAt(strings, flat[index]);
2105
+ const column = stringAt(strings, flat[index + 1]);
2106
+ if (datasetId === undefined || column === undefined) {
2107
+ return;
2108
+ }
2109
+ refs.push({ datasetId, column });
2110
+ }
2111
+ return refs;
2112
+ };
2113
+ const rows = Array.isArray(value.rows) ? decodeRefs(value.rows, 0) : undefined;
2114
+ if (rows === undefined || !Array.isArray(value.columns)) {
2115
+ return;
2116
+ }
2117
+ const columns = new Map;
2118
+ for (const entry of value.columns) {
2119
+ const name = Array.isArray(entry) ? stringAt(strings, entry[0]) : undefined;
2120
+ const refs = name === undefined ? undefined : decodeRefs(entry, 1);
2121
+ if (name === undefined || refs === undefined || columns.has(name)) {
2122
+ return;
2123
+ }
2124
+ columns.set(name, refs);
2125
+ }
2126
+ return { rows, columns: Object.fromEntries(columns) };
2127
+ }
2128
+ function unreadable(datasetId) {
2129
+ return { unknown: `a version of '${datasetId}' records a column lineage Sixb cannot read` };
2130
+ }
2131
+ function parseJson(text) {
2132
+ try {
2133
+ return JSON.parse(text);
2134
+ } catch {
2135
+ return;
2136
+ }
2137
+ }
2138
+ function stringAt(strings, index) {
2139
+ return typeof index === "number" && Number.isInteger(index) ? strings[index] : undefined;
2140
+ }
2141
+ function isStringList(value) {
2142
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
2143
+ }
2144
+ function isRecord2(value) {
2145
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2146
+ }
2147
+
1907
2148
  // src/internal/ducklake-dataset-catalog.ts
1908
2149
  class DuckLakeDatasetCatalog {
1909
2150
  options;
@@ -1912,13 +2153,22 @@ class DuckLakeDatasetCatalog {
1912
2153
  this.options = options;
1913
2154
  this.connections = connections;
1914
2155
  }
1915
- async createDataset(definition) {
2156
+ async createDataset(definition, readLatestVersion) {
1916
2157
  this.assertSchema(definition);
1917
2158
  const existing = await this.getDataset(definition.id);
1918
2159
  if (existing) {
1919
- const plan = planDatasetDefinitionUpdate(existing, definition);
2160
+ const plan = await rerunWhenCatalogMoves(definition.id, async (attempt) => {
2161
+ const current = attempt === 1 ? existing : await this.getDataset(definition.id);
2162
+ if (!current) {
2163
+ throw new LakeStorageError16(`[SixbDuckLake] Dataset '${definition.id}' was removed while its definition was updated.`);
2164
+ }
2165
+ const plan = planDatasetDefinitionUpdate(current, definition);
2166
+ if (plan.changed) {
2167
+ await this.applyExistingDatasetUpdate(plan, readLatestVersion);
2168
+ }
2169
+ return plan;
2170
+ });
1920
2171
  if (plan.changed) {
1921
- await this.applyExistingDatasetUpdate(plan);
1922
2172
  this.connections.markLocalCatalogChanged();
1923
2173
  await this.connections.resetRuntime();
1924
2174
  }
@@ -1982,7 +2232,7 @@ class DuckLakeDatasetCatalog {
1982
2232
  if (failures.length > 0) {
1983
2233
  const details = failures.join(`
1984
2234
  `);
1985
- throw new LakeStorageError14(`[SixbLake] Lake dataset definition check failed for ${failures.length} dataset(s).
2235
+ throw new LakeStorageError16(`[SixbLake] Lake dataset definition check failed for ${failures.length} dataset(s).
1986
2236
  ${details}`);
1987
2237
  }
1988
2238
  }
@@ -2014,32 +2264,50 @@ ${details}`);
2014
2264
  datasetIds: options?.datasetIds
2015
2265
  });
2016
2266
  }
2017
- async getDatasetSchemaAtSnapshot(runtime, datasetId, tableName, tableId, snapshotId) {
2018
- assertDuckLakeSnapshotId(snapshotId);
2019
- const columns = await this.readDatasetColumnsAtSnapshot(runtime, tableName, tableId, snapshotId);
2020
- if (columns.length === 0) {
2021
- throw new LakeStorageError14(`[SixbDuckLake] Could not reconstruct schema for dataset '${datasetId}' at DuckLake snapshot '${snapshotId}'.`);
2267
+ async getDatasetSchemasAtSnapshots(runtime, requests) {
2268
+ if (requests.length === 0) {
2269
+ return [];
2270
+ }
2271
+ for (const request of requests) {
2272
+ assertDuckLakeSnapshotId(request.snapshotId);
2022
2273
  }
2023
- return duckLakeCatalogColumnsToDatasetSchema(tableName, columns);
2274
+ const snapshots = requests.map((request) => BigInt(request.snapshotId));
2275
+ const rows = await this.readDatasetColumnsBetweenSnapshots(runtime, requests.map((request) => request.table), snapshots.reduce((oldest, snapshot) => snapshot < oldest ? snapshot : oldest), snapshots.reduce((newest, snapshot) => snapshot > newest ? snapshot : newest));
2276
+ return requests.map(({ table, snapshotId }, index) => {
2277
+ const snapshot = snapshots[index];
2278
+ const columns = rows.filter((row) => row.tableId === table.tableId && row.beginSnapshot <= snapshot && (row.endSnapshot === undefined || row.endSnapshot > snapshot)).map((row) => ({ tableName: table.tableName, ...row.column }));
2279
+ if (columns.length === 0) {
2280
+ throw new LakeStorageError16(`[SixbDuckLake] Could not reconstruct schema for dataset '${table.datasetId}' at DuckLake snapshot '${snapshotId}'.`);
2281
+ }
2282
+ return duckLakeCatalogColumnsToDatasetSchema(table.tableName, columns);
2283
+ });
2024
2284
  }
2025
2285
  assertSchema(definition) {
2026
- assertDatasetDefinition(definition, (message) => new LakeStorageError14(`[SixbDuckLake] ${message}`));
2286
+ assertDatasetDefinition(definition, (message) => new LakeStorageError16(`[SixbDuckLake] ${message}`));
2027
2287
  }
2028
- async applyExistingDatasetUpdate(plan) {
2288
+ async applyExistingDatasetUpdate(plan, readLatestVersion) {
2029
2289
  const tableName = encodeDatasetTableName(plan.definition.id);
2030
2290
  const qualifiedName = qualifiedTableName(this.options, tableName);
2291
+ const { schema } = plan;
2292
+ if (schema.kind === "none") {
2293
+ await this.connections.withExclusiveAttached((runtime) => this.applyMetadataDelta(runtime, qualifiedName, plan));
2294
+ return;
2295
+ }
2031
2296
  await this.connections.withExclusiveAttached(async (runtime) => {
2032
- if (plan.schema.kind === "none") {
2033
- await this.applyMetadataDelta(runtime, qualifiedName, plan);
2034
- return;
2035
- }
2036
- await runtime.run("BEGIN TRANSACTION");
2037
2297
  let committed = false;
2038
2298
  try {
2039
- await this.applySchemaEvolutionDdl(runtime, qualifiedName, plan.schema);
2299
+ const parent = await beginTransactionOnLatestVersion({
2300
+ options: this.options,
2301
+ runtime,
2302
+ datasetId: plan.definition.id,
2303
+ readLatestVersion: () => readLatestVersion(runtime, plan.definition)
2304
+ });
2305
+ await this.applySchemaEvolutionDdl(runtime, qualifiedName, schema);
2040
2306
  await this.applyMetadataDelta(runtime, qualifiedName, plan);
2041
- await this.setSchemaEvolutionCommitMetadata(runtime, plan.definition.id, plan.schema);
2042
- await runtime.run("COMMIT");
2307
+ await this.setSchemaEvolutionCommitMetadata(runtime, plan.definition.id, schema, lineageAtCommit({ mode: "schema", schema: plan.definition.schema, parent }));
2308
+ await commitVersionTransaction(runtime, plan.definition.id, {
2309
+ poisonRuntime: () => this.connections.poisonRuntime()
2310
+ });
2043
2311
  committed = true;
2044
2312
  } catch (error) {
2045
2313
  if (!committed) {
@@ -2070,7 +2338,7 @@ ${details}`);
2070
2338
  await runtime.run(`ALTER TABLE ${qualifiedName} SET PARTITIONED BY (${definition.partitionBy.map((columnName) => quoteIdentifier(columnName)).join(", ")})`);
2071
2339
  } catch (error) {
2072
2340
  const message = error instanceof Error ? error.message : String(error);
2073
- throw new LakeStorageError14(`[SixbDuckLake] Dataset '${definition.id}' cannot apply partitionBy because DuckLake rejected ALTER TABLE SET PARTITIONED BY: ${message}`);
2341
+ throw new LakeStorageError16(`[SixbDuckLake] Dataset '${definition.id}' cannot apply partitionBy because DuckLake rejected ALTER TABLE SET PARTITIONED BY: ${message}`);
2074
2342
  }
2075
2343
  }
2076
2344
  async applyPrimaryKey(runtime, qualifiedName, definition) {
@@ -2082,7 +2350,7 @@ ${details}`);
2082
2350
  await runtime.run(`COMMENT ON COLUMN ${qualifiedName}.${quoteIdentifier(columnName)} IS ${quoteSqlString(duckLakePrimaryKeyColumnComment(ordinal))}`);
2083
2351
  }
2084
2352
  }
2085
- async setSchemaEvolutionCommitMetadata(runtime, datasetId, plan) {
2353
+ async setSchemaEvolutionCommitMetadata(runtime, datasetId, plan, lineage) {
2086
2354
  await runtime.run(`CALL ${quoteIdentifier(duckLakeAlias(this.options))}.set_commit_message(${quoteSqlString("Sixb")}, ${quoteSqlString(`evolve dataset ${datasetId} schema`)}, extra_info => ${quoteSqlString(JSON.stringify({
2087
2355
  sixb: {
2088
2356
  kind: "datasetVersion",
@@ -2091,7 +2359,8 @@ ${details}`);
2091
2359
  mode: "schema",
2092
2360
  schemaChange: {
2093
2361
  addColumns: plan.columns.map((column) => column.name)
2094
- }
2362
+ },
2363
+ ...lineage === undefined ? {} : { lineage: encodeLineage(lineage) }
2095
2364
  }
2096
2365
  }))})`);
2097
2366
  }
@@ -2100,63 +2369,71 @@ ${details}`);
2100
2369
  await runtime.run("ROLLBACK");
2101
2370
  } catch {}
2102
2371
  }
2103
- async readDatasetColumnsAtSnapshot(runtime, tableName, tableId, snapshotId) {
2372
+ async readDatasetColumnsBetweenSnapshots(runtime, tables, oldestSnapshot, newestSnapshot) {
2373
+ const tableIds = new Set(tables.map((table) => table.tableId));
2104
2374
  const ducklakeColumn = duckLakeMetadataTableName(this.options, "ducklake_column");
2105
2375
  const rows = await runtime.query(`
2106
2376
  SELECT
2377
+ table_id,
2107
2378
  column_id,
2108
2379
  column_order,
2109
2380
  column_name,
2110
2381
  column_type,
2111
2382
  CAST(nulls_allowed AS BOOLEAN) AS nulls_allowed,
2112
- parent_column
2383
+ parent_column,
2384
+ begin_snapshot,
2385
+ end_snapshot
2113
2386
  FROM ${ducklakeColumn}
2114
- WHERE table_id = ${tableId}
2115
- AND begin_snapshot <= ${snapshotId}
2116
- AND (end_snapshot IS NULL OR end_snapshot > ${snapshotId})
2117
- ORDER BY column_order
2387
+ WHERE table_id IN (${[...tableIds].join(", ")})
2388
+ AND begin_snapshot <= ${newestSnapshot}
2389
+ AND (end_snapshot IS NULL OR end_snapshot > ${oldestSnapshot})
2390
+ ORDER BY column_order, column_id
2118
2391
  `);
2119
2392
  return rows.map((row) => ({
2120
- tableName,
2121
- columnId: getBigIntLike(row, "column_id"),
2122
- columnOrder: getBigIntLike(row, "column_order"),
2123
- columnName: getString(row, "column_name"),
2124
- columnType: getString(row, "column_type"),
2125
- nullsAllowed: getBoolean(row, "nulls_allowed"),
2126
- parentColumnId: getOptionalBigIntLike(row, "parent_column")
2393
+ tableId: getBigIntLike(row, "table_id"),
2394
+ beginSnapshot: getBigIntLike(row, "begin_snapshot"),
2395
+ endSnapshot: getOptionalBigIntLike(row, "end_snapshot"),
2396
+ column: {
2397
+ columnId: getBigIntLike(row, "column_id"),
2398
+ columnOrder: getBigIntLike(row, "column_order"),
2399
+ columnName: getString(row, "column_name"),
2400
+ columnType: getString(row, "column_type"),
2401
+ nullsAllowed: getBoolean(row, "nulls_allowed"),
2402
+ parentColumnId: getOptionalBigIntLike(row, "parent_column")
2403
+ }
2127
2404
  }));
2128
2405
  }
2129
2406
  }
2130
2407
  function assertDuckLakeSnapshotId(snapshotId) {
2131
2408
  if (!/^\d+$/.test(snapshotId)) {
2132
- throw new LakeStorageError14(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`);
2409
+ throw new LakeStorageError16(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`);
2133
2410
  }
2134
2411
  }
2135
2412
 
2136
2413
  // src/internal/ducklake-maintenance.ts
2137
- import { LakeStorageError as LakeStorageError15 } from "@sixb/core/lake-storage";
2414
+ import { LakeStorageError as LakeStorageError17 } from "@sixb/core/lake-storage";
2138
2415
  var DEFAULT_RETENTION = "7 days";
2139
2416
 
2140
2417
  class DuckLakeMaintenance {
2141
2418
  options;
2142
2419
  connections;
2143
- constructor(options, connections) {
2420
+ snapshots;
2421
+ constructor(options, connections, snapshots) {
2144
2422
  this.options = options;
2145
2423
  this.connections = connections;
2424
+ this.snapshots = snapshots;
2146
2425
  }
2147
2426
  async runMaintenance(options = {}) {
2148
2427
  this.connections.assertOpen();
2149
2428
  const expireOlderThan = options.expireOlderThan ?? DEFAULT_RETENTION;
2150
2429
  const deleteOlderThan = options.deleteOlderThan ?? expireOlderThan;
2151
2430
  const dryRun = options.dryRun ?? false;
2431
+ const protect = (options.protect ?? []).map(protectedSnapshotId);
2152
2432
  if (this.options.readOnly && !dryRun) {
2153
- throw new LakeStorageError15("[SixbDuckLake] DuckLake maintenance cannot run in read-only mode.");
2433
+ throw new LakeStorageError17("[SixbDuckLake] DuckLake maintenance cannot run in read-only mode.");
2154
2434
  }
2155
2435
  return this.connections.withMaintenanceRuntime(async (runtime) => {
2156
- const counts = await runMaintenanceProcedures(runtime, this.options, {
2157
- expireOlderThan,
2158
- deleteOlderThan
2159
- }, dryRun);
2436
+ const counts = await this.runMaintenanceProcedures(runtime, { expireOlderThan, deleteOlderThan }, protect, dryRun);
2160
2437
  if (!dryRun) {
2161
2438
  this.connections.markLocalCatalogChanged();
2162
2439
  }
@@ -2168,26 +2445,55 @@ class DuckLakeMaintenance {
2168
2445
  };
2169
2446
  });
2170
2447
  }
2448
+ async runMaintenanceProcedures(runtime, retention, protect, dryRun) {
2449
+ const aliasSql = quoteSqlString(duckLakeAlias(this.options));
2450
+ const oldSnapshotIds = await this.snapshotsOlderThan(runtime, retention.expireOlderThan);
2451
+ const needed = new Set([
2452
+ ...await this.snapshots.latestVersionSnapshotIds(runtime),
2453
+ ...protect
2454
+ ]);
2455
+ const expirable = oldSnapshotIds.filter((snapshotId) => !needed.has(snapshotId));
2456
+ const snapshots = expirable.length === 0 ? [] : await runtime.query(maintenanceProcedureSql("ducklake_expire_snapshots", aliasSql, `versions => [${expirable.join(", ")}]`, dryRun));
2457
+ const oldFiles = await runtime.query(maintenanceProcedureSql("ducklake_cleanup_old_files", aliasSql, `older_than => ${olderThanSql(retention.deleteOlderThan)}`, dryRun));
2458
+ const orphanedFiles = await runtime.query(maintenanceProcedureSql("ducklake_delete_orphaned_files", aliasSql, `older_than => ${olderThanSql(retention.deleteOlderThan)}`, dryRun));
2459
+ return {
2460
+ snapshots: snapshots.length,
2461
+ keptSnapshots: oldSnapshotIds.length - expirable.length,
2462
+ oldFiles: oldFiles.length,
2463
+ orphanedFiles: orphanedFiles.length
2464
+ };
2465
+ }
2466
+ async snapshotsOlderThan(runtime, olderThan) {
2467
+ const ducklakeSnapshot = duckLakeMetadataTableName(this.options, "ducklake_snapshot");
2468
+ const rows = await runtime.query(`
2469
+ SELECT snapshot_id
2470
+ FROM ${ducklakeSnapshot}
2471
+ WHERE CAST(snapshot_time AS TIMESTAMPTZ) < ${olderThanSql(olderThan)}
2472
+ AND snapshot_id < (
2473
+ SELECT snapshot_id FROM ${ducklakeSnapshot} ORDER BY snapshot_id DESC LIMIT 1
2474
+ )
2475
+ ORDER BY snapshot_id
2476
+ `);
2477
+ return rows.map((row) => String(getBigIntLike(row, "snapshot_id")));
2478
+ }
2171
2479
  }
2172
- async function runMaintenanceProcedures(runtime, options, retention, dryRun) {
2173
- const aliasSql = quoteSqlString(duckLakeAlias(options));
2174
- const snapshots = await runtime.query(maintenanceProcedureSql("ducklake_expire_snapshots", aliasSql, retention.expireOlderThan, dryRun));
2175
- const oldFiles = await runtime.query(maintenanceProcedureSql("ducklake_cleanup_old_files", aliasSql, retention.deleteOlderThan, dryRun));
2176
- const orphanedFiles = await runtime.query(maintenanceProcedureSql("ducklake_delete_orphaned_files", aliasSql, retention.deleteOlderThan, dryRun));
2177
- return {
2178
- snapshots: snapshots.length,
2179
- oldFiles: oldFiles.length,
2180
- orphanedFiles: orphanedFiles.length
2181
- };
2480
+ function protectedSnapshotId(ref) {
2481
+ try {
2482
+ return parseVersionId(ref.versionId);
2483
+ } catch (error) {
2484
+ throw new LakeStorageError17(`[SixbDuckLake] Cannot protect version '${ref.versionId}' of dataset '${ref.datasetId}' from maintenance: it is not a DuckLake version id.`, { cause: error });
2485
+ }
2182
2486
  }
2183
- function maintenanceProcedureSql(procedure, aliasSql, olderThan, dryRun) {
2184
- const olderThanSql = quoteSqlString(olderThan);
2487
+ function olderThanSql(olderThan) {
2488
+ return `now() - INTERVAL ${quoteSqlString(olderThan)}`;
2489
+ }
2490
+ function maintenanceProcedureSql(procedure, aliasSql, selection, dryRun) {
2185
2491
  const dryRunSql = dryRun ? "dry_run => true, " : "";
2186
- return `CALL ${procedure}(${aliasSql}, ${dryRunSql}` + `older_than => now() - INTERVAL ${olderThanSql})`;
2492
+ return `CALL ${procedure}(${aliasSql}, ${dryRunSql}${selection})`;
2187
2493
  }
2188
2494
 
2189
2495
  // src/internal/ducklake-row-reader.ts
2190
- import { LakeStorageError as LakeStorageError16 } from "@sixb/core/lake-storage";
2496
+ import { LakeStorageError as LakeStorageError18 } from "@sixb/core/lake-storage";
2191
2497
  var READ_PAGE_ROWS = 5000;
2192
2498
  var PHYSICAL_ROW_ID_ALIAS = "__sixb_physical_row_id";
2193
2499
 
@@ -2208,7 +2514,7 @@ class DuckLakeRowReader {
2208
2514
  read.signal.throwIfAborted();
2209
2515
  const tableRef = await resolveDatasetTableRef(this.options, runtime, input.datasetId);
2210
2516
  if (!tableRef) {
2211
- throw new LakeStorageError16(`[SixbDuckLake] Unknown dataset '${input.datasetId}'.`);
2517
+ throw new LakeStorageError18(`[SixbDuckLake] Unknown dataset '${input.datasetId}'.`);
2212
2518
  }
2213
2519
  const version = await this.resolveVersion(runtime, tableRef, input.versionId);
2214
2520
  const columns = this.resolveReadColumns(tableRef.datasetId, version.schema, input.columns);
@@ -2293,13 +2599,13 @@ class DuckLakeRowReader {
2293
2599
  return columns.map((columnName) => {
2294
2600
  const column = columnsByName.get(columnName);
2295
2601
  if (!column) {
2296
- throw new LakeStorageError16(`[SixbDuckLake] Dataset '${datasetId}' does not have column '${columnName}' at the requested version.`);
2602
+ throw new LakeStorageError18(`[SixbDuckLake] Dataset '${datasetId}' does not have column '${columnName}' at the requested version.`);
2297
2603
  }
2298
2604
  return column;
2299
2605
  });
2300
2606
  }
2301
2607
  throwNoCommittedVersion(datasetId) {
2302
- throw new LakeStorageError16(`[SixbDuckLake] No committed version found for dataset '${datasetId}'.`);
2608
+ throw new LakeStorageError18(`[SixbDuckLake] No committed version found for dataset '${datasetId}'.`);
2303
2609
  }
2304
2610
  }
2305
2611
  function pageSql(read, limit, offset, cursor) {
@@ -2309,8 +2615,10 @@ function pageSql(read, limit, offset, cursor) {
2309
2615
  }
2310
2616
 
2311
2617
  // src/internal/ducklake-snapshot-reader.ts
2312
- import { LakeStorageError as LakeStorageError17 } from "@sixb/core/lake-storage";
2618
+ import { LakeStorageError as LakeStorageError19 } from "@sixb/core/lake-storage";
2313
2619
  var SNAPSHOT_ROW_BATCH_SIZE = 128;
2620
+ var FEED_PAGE_SIZE = 512;
2621
+ var SHORT_READ_BATCH_SIZE = 8;
2314
2622
  var CATALOG_SNAPSHOT_SCAN_LIMIT = 512;
2315
2623
 
2316
2624
  class DuckLakeSnapshotReader {
@@ -2362,10 +2670,10 @@ class DuckLakeSnapshotReader {
2362
2670
  }
2363
2671
  async collectDatasetCatalogState(runtime, datasetIds) {
2364
2672
  const uniqueIds = [...new Set(datasetIds)];
2365
- const tableIdsByDatasetId = await this.resolveDatasetTableIds(runtime, uniqueIds);
2366
- const latestRowByDatasetId = await this.resolveLatestSnapshotRows(runtime, tableIdsByDatasetId);
2673
+ const tablesByDatasetId = await this.resolveDatasetTables(runtime, uniqueIds);
2674
+ const latestRowByDatasetId = await this.resolveLatestSnapshotRows(runtime, tablesByDatasetId);
2367
2675
  return uniqueIds.map((datasetId) => {
2368
- if (!tableIdsByDatasetId.has(datasetId)) {
2676
+ if (!tablesByDatasetId.has(datasetId)) {
2369
2677
  return { datasetId, materialized: false, latestVersion: null };
2370
2678
  }
2371
2679
  const row = latestRowByDatasetId.get(datasetId);
@@ -2376,6 +2684,139 @@ class DuckLakeSnapshotReader {
2376
2684
  };
2377
2685
  });
2378
2686
  }
2687
+ async listLatestVersionsSince(input) {
2688
+ this.connections.assertOpen();
2689
+ return this.connections.withAttachedRuntime(async (runtime) => {
2690
+ if (input.cursor === null) {
2691
+ return { cursor: await this.queryHeadSnapshotId(runtime), versions: [] };
2692
+ }
2693
+ if (!/^\d+$/.test(input.cursor)) {
2694
+ return null;
2695
+ }
2696
+ const snapshots = await this.querySnapshotsFrom(runtime, input.cursor);
2697
+ if (snapshots[0]?.snapshotId !== input.cursor) {
2698
+ return null;
2699
+ }
2700
+ const newSnapshots = snapshots.slice(1);
2701
+ if (newSnapshots.length === 0) {
2702
+ return { cursor: input.cursor, versions: [] };
2703
+ }
2704
+ const tables = await this.resolveFeedTables(runtime, input.datasetIds);
2705
+ const latest = await this.collectFeedPage(runtime, tables, newSnapshots);
2706
+ return {
2707
+ cursor: newSnapshots[newSnapshots.length - 1]?.snapshotId ?? input.cursor,
2708
+ versions: [...latest].map(([datasetId, row]) => this.snapshotRowToSummary(datasetId, row))
2709
+ };
2710
+ });
2711
+ }
2712
+ async queryHeadSnapshotId(runtime) {
2713
+ const [row] = await runtime.query(buildDuckLakeMetadataQuery(this.options, (table) => `SELECT max(snapshot_id) AS snapshot_id FROM ${table("ducklake_snapshot")}`));
2714
+ if (row?.snapshot_id === null || row?.snapshot_id === undefined) {
2715
+ throw new LakeStorageError19("[SixbDuckLake] DuckLake catalog has no snapshots.");
2716
+ }
2717
+ return String(getBigIntLike(row, "snapshot_id"));
2718
+ }
2719
+ async querySnapshotsFrom(runtime, cursor) {
2720
+ const rows = await runtime.query(buildDuckLakeMetadataQuery(this.options, (table) => `
2721
+ SELECT snapshot_id, snapshot_time
2722
+ FROM ${table("ducklake_snapshot")}
2723
+ WHERE snapshot_id >= ${cursor}
2724
+ ORDER BY snapshot_id
2725
+ LIMIT ${FEED_PAGE_SIZE + 1}
2726
+ `));
2727
+ return rows.map((row) => ({
2728
+ snapshotId: String(getBigIntLike(row, "snapshot_id")),
2729
+ createdAt: getDate(row, "snapshot_time")
2730
+ }));
2731
+ }
2732
+ async resolveFeedTables(runtime, datasetIds) {
2733
+ const requested = datasetIds === undefined ? null : new Set(datasetIds);
2734
+ const rows = await runtime.query(`
2735
+ SELECT table_id, table_name, begin_snapshot, end_snapshot
2736
+ FROM ${duckLakeMetadataTableName(this.options, "ducklake_table")}
2737
+ `);
2738
+ const startByTableId = new Map;
2739
+ const current = [];
2740
+ for (const row of rows) {
2741
+ const tableId = getBigIntLike(row, "table_id");
2742
+ const begin = getBigIntLike(row, "begin_snapshot");
2743
+ const start = startByTableId.get(tableId);
2744
+ if (start === undefined || begin < start) {
2745
+ startByTableId.set(tableId, begin);
2746
+ }
2747
+ if (row.end_snapshot !== null && row.end_snapshot !== undefined) {
2748
+ continue;
2749
+ }
2750
+ const datasetId = decodeDatasetTableName(getString(row, "table_name"));
2751
+ if (datasetId !== null && (requested === null || requested.has(datasetId))) {
2752
+ current.push({ datasetId, tableId });
2753
+ }
2754
+ }
2755
+ return current.map((table) => ({
2756
+ ...table,
2757
+ startSnapshotId: startByTableId.get(table.tableId) ?? 0n
2758
+ }));
2759
+ }
2760
+ async collectFeedPage(runtime, tables, snapshots) {
2761
+ const latest = new Map;
2762
+ const first = snapshots[0]?.snapshotId;
2763
+ const last = snapshots[snapshots.length - 1]?.snapshotId;
2764
+ if (first === undefined || last === undefined || tables.length === 0) {
2765
+ return latest;
2766
+ }
2767
+ const changeRows = await runtime.query(buildDuckLakeMetadataQuery(this.options, (table) => `
2768
+ SELECT snapshot_id, changes_made, commit_extra_info
2769
+ FROM ${table("ducklake_snapshot_changes")}
2770
+ WHERE snapshot_id >= ${first} AND snapshot_id <= ${last}
2771
+ `));
2772
+ const changesBySnapshotId = new Map(changeRows.map((row) => [String(getBigIntLike(row, "snapshot_id")), row]));
2773
+ const tablesByTableId = new Map(tables.map((table) => [table.tableId.toString(), table]));
2774
+ for (const snapshot of snapshots) {
2775
+ const changes = changesBySnapshotId.get(snapshot.snapshotId);
2776
+ if (changes === undefined) {
2777
+ continue;
2778
+ }
2779
+ const changesMade = getString(changes, "changes_made");
2780
+ const metadata = parseCommitMetadata(changes.commit_extra_info);
2781
+ const touched = new Set;
2782
+ for (const change of changesMade.split(",")) {
2783
+ const table = tablesByTableId.get(change.split(":")[1] ?? "");
2784
+ if (table)
2785
+ touched.add(table);
2786
+ }
2787
+ for (const table of touched) {
2788
+ if (BigInt(snapshot.snapshotId) < table.startSnapshotId) {
2789
+ continue;
2790
+ }
2791
+ const row = this.candidateToSnapshotRow(table.datasetId, table.tableId, {
2792
+ snapshotId: snapshot.snapshotId,
2793
+ createdAt: snapshot.createdAt,
2794
+ changesMade,
2795
+ ...metadata !== undefined ? { metadata } : {}
2796
+ });
2797
+ if (row && row.mode !== "schema") {
2798
+ latest.set(table.datasetId, row);
2799
+ }
2800
+ }
2801
+ }
2802
+ return latest;
2803
+ }
2804
+ async latestVersionSnapshotIds(runtime) {
2805
+ const tables = await runtime.query(`
2806
+ SELECT table_name
2807
+ FROM ${duckLakeMetadataTableName(this.options, "ducklake_table")}
2808
+ WHERE end_snapshot IS NULL
2809
+ `);
2810
+ const datasetIds = tables.flatMap((table) => {
2811
+ const datasetId = decodeDatasetTableName(getString(table, "table_name"));
2812
+ return datasetId === null ? [] : [datasetId];
2813
+ });
2814
+ if (datasetIds.length === 0) {
2815
+ return [];
2816
+ }
2817
+ const latest = await this.resolveLatestSnapshotRows(runtime, await this.resolveDatasetTables(runtime, datasetIds), Number.POSITIVE_INFINITY);
2818
+ return [...latest.values()].map((row) => row.snapshotId);
2819
+ }
2379
2820
  snapshotRowToSummary(datasetId, row) {
2380
2821
  return {
2381
2822
  datasetId,
@@ -2385,54 +2826,69 @@ class DuckLakeSnapshotReader {
2385
2826
  ...row.metadata?.rowCount !== undefined ? { rowCount: row.metadata.rowCount } : {}
2386
2827
  };
2387
2828
  }
2388
- async resolveDatasetTableIds(runtime, datasetIds) {
2829
+ async resolveDatasetTables(runtime, datasetIds) {
2389
2830
  const datasetIdByTableName = new Map;
2831
+ const datasetIdBySequenceTableName = new Map;
2390
2832
  for (const datasetId of datasetIds) {
2391
2833
  datasetIdByTableName.set(encodeDatasetTableName(datasetId), datasetId);
2834
+ datasetIdBySequenceTableName.set(encodeDatasetSequenceTableName(datasetId), datasetId);
2392
2835
  }
2393
2836
  const ducklakeTable = duckLakeMetadataTableName(this.options, "ducklake_table");
2394
- const tableNameList = [...datasetIdByTableName.keys()].map((name) => quoteSqlString(name)).join(", ");
2837
+ const tableNameList = [...datasetIdByTableName.keys(), ...datasetIdBySequenceTableName.keys()].map((name) => quoteSqlString(name)).join(", ");
2395
2838
  const rows = await runtime.query(`
2396
2839
  SELECT table_id, table_name
2397
2840
  FROM ${ducklakeTable}
2398
2841
  WHERE table_name IN (${tableNameList})
2399
2842
  AND end_snapshot IS NULL
2400
2843
  `);
2401
- const tableIdsByDatasetId = new Map;
2844
+ const tablesByDatasetId = new Map;
2402
2845
  for (const row of rows) {
2403
- const datasetId = datasetIdByTableName.get(getString(row, "table_name"));
2846
+ const tableName = getString(row, "table_name");
2847
+ const datasetId = datasetIdByTableName.get(tableName);
2404
2848
  if (datasetId !== undefined) {
2405
- tableIdsByDatasetId.set(datasetId, getBigIntLike(row, "table_id"));
2849
+ tablesByDatasetId.set(datasetId, {
2850
+ datasetId,
2851
+ tableName,
2852
+ tableId: getBigIntLike(row, "table_id")
2853
+ });
2854
+ }
2855
+ }
2856
+ for (const row of rows) {
2857
+ const datasetId = datasetIdBySequenceTableName.get(getString(row, "table_name"));
2858
+ const table = datasetId === undefined ? undefined : tablesByDatasetId.get(datasetId);
2859
+ if (datasetId !== undefined && table !== undefined) {
2860
+ tablesByDatasetId.set(datasetId, {
2861
+ ...table,
2862
+ sequenceTableId: getBigIntLike(row, "table_id")
2863
+ });
2406
2864
  }
2407
2865
  }
2408
- return tableIdsByDatasetId;
2866
+ return tablesByDatasetId;
2409
2867
  }
2410
- async resolveLatestSnapshotRows(runtime, tableIdsByDatasetId) {
2868
+ async resolveLatestSnapshotRows(runtime, tablesByDatasetId, scanLimit = CATALOG_SNAPSHOT_SCAN_LIMIT) {
2411
2869
  const result = new Map;
2412
- if (tableIdsByDatasetId.size === 0) {
2870
+ if (tablesByDatasetId.size === 0) {
2413
2871
  return result;
2414
2872
  }
2415
- const unresolved = new Map(tableIdsByDatasetId);
2416
- const tableIds = [...new Set(tableIdsByDatasetId.values())];
2873
+ const unresolved = new Map(tablesByDatasetId);
2417
2874
  let beforeSnapshotId;
2418
2875
  let scanned = 0;
2419
- while (unresolved.size > 0 && scanned < CATALOG_SNAPSHOT_SCAN_LIMIT) {
2876
+ while (unresolved.size > 0 && scanned < scanLimit) {
2420
2877
  const candidates = await this.queryCatalogSnapshotBatch(runtime, beforeSnapshotId, SNAPSHOT_ROW_BATCH_SIZE);
2421
2878
  if (candidates.length === 0) {
2422
2879
  break;
2423
2880
  }
2424
- const fileFlags = await this.queryFileChangeFlags(runtime, candidates.map((candidate) => candidate.snapshotId), tableIds);
2881
+ const tables = [...unresolved.values()];
2882
+ const metadata = await this.readCommitMetadata(runtime, candidates.filter((candidate) => tables.some((table) => changesTouchTables(candidate.changesMade, datasetTableIds(table)))).map((candidate) => candidate.snapshotId));
2425
2883
  for (const candidate of candidates) {
2426
2884
  scanned += 1;
2427
- for (const [datasetId, tableId] of [...unresolved]) {
2428
- const flags = fileFlags.get(fileChangeKey(candidate.snapshotId, tableId));
2429
- const row = this.candidateToSnapshotRow(datasetId, tableId, {
2885
+ const candidateMetadata = metadata.get(candidate.snapshotId);
2886
+ for (const [datasetId, table] of [...unresolved]) {
2887
+ const row = this.candidateToSnapshotRow(datasetId, table.tableId, {
2430
2888
  snapshotId: candidate.snapshotId,
2431
2889
  createdAt: candidate.createdAt,
2432
2890
  changesMade: candidate.changesMade,
2433
- hasFileChange: flags?.hasFileChange ?? false,
2434
- hasFileDeleteChange: flags?.hasFileDeleteChange ?? false,
2435
- ...candidate.metadata !== undefined ? { metadata: candidate.metadata } : {}
2891
+ ...candidateMetadata !== undefined ? { metadata: candidateMetadata } : {}
2436
2892
  });
2437
2893
  if (row) {
2438
2894
  result.set(datasetId, row);
@@ -2462,67 +2918,22 @@ class DuckLakeSnapshotReader {
2462
2918
  SELECT
2463
2919
  snapshot.snapshot_id,
2464
2920
  snapshot.snapshot_time,
2465
- changes.changes_made,
2466
- changes.commit_extra_info
2921
+ changes.changes_made
2467
2922
  FROM ${ducklakeSnapshot} snapshot
2468
2923
  JOIN ${ducklakeSnapshotChanges} changes ON changes.snapshot_id = snapshot.snapshot_id
2469
2924
  ${whereSql}
2470
2925
  ORDER BY snapshot.snapshot_id DESC
2471
2926
  LIMIT ${Math.max(0, Math.trunc(limit))}
2472
2927
  `);
2473
- return rows.map((row) => {
2474
- const metadata = parseCommitMetadata(getOptionalString(row, "commit_extra_info"));
2475
- return {
2476
- snapshotId: String(getBigIntLike(row, "snapshot_id")),
2477
- createdAt: getDate(row, "snapshot_time"),
2478
- changesMade: getString(row, "changes_made"),
2479
- ...metadata !== undefined ? { metadata } : {}
2480
- };
2481
- });
2482
- }
2483
- async queryFileChangeFlags(runtime, snapshotIds, tableIds) {
2484
- const flags = new Map;
2485
- if (snapshotIds.length === 0 || tableIds.length === 0) {
2486
- return flags;
2487
- }
2488
- for (const snapshotId of snapshotIds) {
2489
- assertDuckLakeSnapshotId2(snapshotId);
2490
- }
2491
- const snapshotIdList = snapshotIds.join(", ");
2492
- const tableIdList = tableIds.map((tableId) => tableId.toString()).join(", ");
2493
- const ducklakeDataFile = duckLakeMetadataTableName(this.options, "ducklake_data_file");
2494
- const ducklakeDeleteFile = duckLakeMetadataTableName(this.options, "ducklake_delete_file");
2495
- const rows = await runtime.query(`
2496
- WITH file_changes AS (
2497
- SELECT begin_snapshot AS snapshot_id, table_id, false AS is_delete
2498
- FROM ${ducklakeDataFile}
2499
- WHERE table_id IN (${tableIdList}) AND begin_snapshot IN (${snapshotIdList})
2500
- UNION ALL
2501
- SELECT end_snapshot, table_id, true
2502
- FROM ${ducklakeDataFile}
2503
- WHERE table_id IN (${tableIdList}) AND end_snapshot IN (${snapshotIdList})
2504
- UNION ALL
2505
- SELECT begin_snapshot, table_id, true
2506
- FROM ${ducklakeDeleteFile}
2507
- WHERE table_id IN (${tableIdList}) AND begin_snapshot IN (${snapshotIdList})
2508
- UNION ALL
2509
- SELECT end_snapshot, table_id, true
2510
- FROM ${ducklakeDeleteFile}
2511
- WHERE table_id IN (${tableIdList}) AND end_snapshot IN (${snapshotIdList})
2512
- )
2513
- SELECT snapshot_id, table_id, bool_or(is_delete) AS has_delete
2514
- FROM file_changes
2515
- GROUP BY snapshot_id, table_id
2516
- `);
2517
- for (const row of rows) {
2518
- const key = fileChangeKey(String(getBigIntLike(row, "snapshot_id")), getBigIntLike(row, "table_id"));
2519
- flags.set(key, { hasFileChange: true, hasFileDeleteChange: getBoolean(row, "has_delete") });
2520
- }
2521
- return flags;
2928
+ return rows.map((row) => ({
2929
+ snapshotId: String(getBigIntLike(row, "snapshot_id")),
2930
+ createdAt: getDate(row, "snapshot_time"),
2931
+ changesMade: getString(row, "changes_made")
2932
+ }));
2522
2933
  }
2523
2934
  assertNoMetadataConflict(snapshotId, datasetId, metadata) {
2524
2935
  if (metadata !== undefined && metadata.datasetId !== datasetId) {
2525
- throw new LakeStorageError17(`[SixbDuckLake] DuckLake snapshot '${snapshotId}' changed dataset '${datasetId}' but Sixb commit metadata references dataset '${metadata.datasetId}'.`);
2936
+ throw new LakeStorageError19(`[SixbDuckLake] DuckLake snapshot '${snapshotId}' changed dataset '${datasetId}' but Sixb commit metadata references dataset '${metadata.datasetId}'.`);
2526
2937
  }
2527
2938
  }
2528
2939
  async getLatestVersionForDefinition(runtime, dataset) {
@@ -2530,22 +2941,39 @@ class DuckLakeSnapshotReader {
2530
2941
  return tableRef ? this.getLatestVersionForTableRef(runtime, tableRef) : null;
2531
2942
  }
2532
2943
  async getLatestVersionRefForDefinition(runtime, dataset) {
2533
- const summary = await this.getLatestVersionSummaryForDefinition(runtime, dataset);
2534
- return summary ? { datasetId: summary.datasetId, versionId: summary.versionId } : null;
2535
- }
2536
- async getLatestVersionSummaryForDefinition(runtime, dataset) {
2537
2944
  const tableRef = await resolveDatasetTableRef(this.options, runtime, dataset.id);
2538
2945
  if (!tableRef) {
2539
2946
  return null;
2540
2947
  }
2948
+ const row = await this.getLatestSnapshotRowForTableRef(runtime, tableRef);
2949
+ return row ? tableVersionRef(tableRef, row) : null;
2950
+ }
2951
+ async getLatestVersionSummaryForDefinition(runtime, dataset) {
2952
+ const tableRef = await resolveDatasetTableRef(this.options, runtime, dataset.id);
2953
+ return tableRef ? this.getLatestVersionSummaryForTableRef(runtime, tableRef) : null;
2954
+ }
2955
+ async getLatestVersionSummaryAtCurrentSnapshot(runtime, dataset) {
2956
+ const { snapshotId, table } = await resolveDatasetTableRefAtCurrentSnapshot(this.options, runtime, dataset.id);
2957
+ return {
2958
+ snapshotId,
2959
+ latestVersion: table ? await this.getLatestVersionSummaryForTableRef(runtime, table) : null
2960
+ };
2961
+ }
2962
+ async getLatestVersionSummaryForTableRef(runtime, tableRef) {
2541
2963
  const row = await this.getLatestSnapshotRowForTableRef(runtime, tableRef);
2542
2964
  if (!row) {
2543
2965
  return null;
2544
2966
  }
2967
+ const [schema] = await this.datasets.getDatasetSchemasAtSnapshots(runtime, [
2968
+ { table: tableRef, snapshotId: row.snapshotId }
2969
+ ]);
2545
2970
  return {
2546
2971
  datasetId: tableRef.datasetId,
2547
2972
  versionId: toVersionId(row.snapshotId),
2973
+ schema,
2974
+ producer: row.metadata?.producer,
2548
2975
  inputs: row.metadata?.inputs,
2976
+ ...storedLineage(tableRef.datasetId, row),
2549
2977
  ...row.metadata?.rowCount !== undefined ? { rowCount: row.metadata.rowCount } : {},
2550
2978
  ...row.metadata?.validatedPrimaryKeyColumns !== undefined ? { validatedPrimaryKeyColumns: row.metadata.validatedPrimaryKeyColumns } : {}
2551
2979
  };
@@ -2559,7 +2987,7 @@ class DuckLakeSnapshotReader {
2559
2987
  const match = await this.getSnapshotRowForTableRef(runtime, tableRef, snapshotId, {
2560
2988
  includeParent: true
2561
2989
  });
2562
- return match ? this.snapshotToVersion(runtime, tableRef, match) : null;
2990
+ return match ? (await this.snapshotsToVersions(runtime, tableRef, [match]))[0] : null;
2563
2991
  }
2564
2992
  async getVersionRefForSnapshot(runtime, dataset, snapshotId) {
2565
2993
  assertDuckLakeSnapshotId2(snapshotId);
@@ -2570,7 +2998,11 @@ class DuckLakeSnapshotReader {
2570
2998
  const row = await this.getSnapshotRowForTableRef(runtime, tableRef, snapshotId, {
2571
2999
  includeParent: false
2572
3000
  });
2573
- return row ? { datasetId: tableRef.datasetId, versionId: toVersionId(row.snapshotId) } : null;
3001
+ return row ? tableVersionRef(tableRef, row) : null;
3002
+ }
3003
+ async getVersionsForRefs(runtime, refs) {
3004
+ const schemas = await this.datasets.getDatasetSchemasAtSnapshots(runtime, refs.map((ref) => ({ table: ref.table, snapshotId: ref.snapshot.snapshotId })));
3005
+ return refs.map((ref, index) => snapshotToVersion(ref.table, ref.snapshot, schemas[index]));
2574
3006
  }
2575
3007
  async listVersionsForDefinition(runtime, dataset, limit) {
2576
3008
  const tableRef = await resolveDatasetTableRef(this.options, runtime, dataset.id);
@@ -2578,11 +3010,7 @@ class DuckLakeSnapshotReader {
2578
3010
  }
2579
3011
  async listVersionsForTableRef(runtime, tableRef, limit) {
2580
3012
  const limitedRows = await this.getSnapshotRowsForTableRef(runtime, tableRef, limit);
2581
- const versions = [];
2582
- for (const row of limitedRows) {
2583
- versions.push(await this.snapshotToVersion(runtime, tableRef, row));
2584
- }
2585
- return versions;
3013
+ return this.snapshotsToVersions(runtime, tableRef, limitedRows);
2586
3014
  }
2587
3015
  async getLatestVersionForTableRef(runtime, tableRef) {
2588
3016
  const [latest] = await this.listVersionsForTableRef(runtime, tableRef, 1);
@@ -2590,8 +3018,7 @@ class DuckLakeSnapshotReader {
2590
3018
  }
2591
3019
  async getLatestSnapshotRowForTableRef(runtime, tableRef) {
2592
3020
  const [row] = await this.collectVisibleSnapshotRows(runtime, {
2593
- datasetId: tableRef.datasetId,
2594
- tableId: tableRef.tableId,
3021
+ table: tableRef,
2595
3022
  visibleRowLimit: 1
2596
3023
  });
2597
3024
  return row ?? null;
@@ -2599,8 +3026,7 @@ class DuckLakeSnapshotReader {
2599
3026
  async getSnapshotRowForTableRef(runtime, tableRef, snapshotId, options) {
2600
3027
  assertDuckLakeSnapshotId2(snapshotId);
2601
3028
  const [row] = await this.collectVisibleSnapshotRows(runtime, {
2602
- datasetId: tableRef.datasetId,
2603
- tableId: tableRef.tableId,
3029
+ table: tableRef,
2604
3030
  exactSnapshotId: snapshotId,
2605
3031
  visibleRowLimit: 1
2606
3032
  });
@@ -2611,8 +3037,7 @@ class DuckLakeSnapshotReader {
2611
3037
  return row;
2612
3038
  }
2613
3039
  const [parent] = await this.collectVisibleSnapshotRows(runtime, {
2614
- datasetId: tableRef.datasetId,
2615
- tableId: tableRef.tableId,
3040
+ table: tableRef,
2616
3041
  beforeSnapshotId: snapshotId,
2617
3042
  visibleRowLimit: 1
2618
3043
  });
@@ -2624,8 +3049,7 @@ class DuckLakeSnapshotReader {
2624
3049
  return [];
2625
3050
  }
2626
3051
  const rows = await this.collectVisibleSnapshotRows(runtime, {
2627
- datasetId: tableRef.datasetId,
2628
- tableId: tableRef.tableId,
3052
+ table: tableRef,
2629
3053
  visibleRowLimit: visibleLimit === undefined ? undefined : visibleLimit + 1
2630
3054
  });
2631
3055
  const rowsWithParents = this.withParentSnapshotIds(rows);
@@ -2637,18 +3061,21 @@ class DuckLakeSnapshotReader {
2637
3061
  }
2638
3062
  const snapshots = [];
2639
3063
  let beforeSnapshotId = input.beforeSnapshotId;
3064
+ let shortRead = input.exactSnapshotId !== undefined || input.visibleRowLimit !== undefined && input.visibleRowLimit <= 2;
2640
3065
  while (input.visibleRowLimit === undefined || snapshots.length < input.visibleRowLimit) {
3066
+ const batchSize = shortRead ? SHORT_READ_BATCH_SIZE : SNAPSHOT_ROW_BATCH_SIZE;
2641
3067
  const candidates = await this.querySnapshotCandidates(runtime, {
2642
- tableId: input.tableId,
3068
+ table: input.table,
2643
3069
  exactSnapshotId: input.exactSnapshotId,
2644
3070
  beforeSnapshotId,
2645
- limit: input.exactSnapshotId === undefined ? SNAPSHOT_ROW_BATCH_SIZE : 1
3071
+ limit: input.exactSnapshotId === undefined ? batchSize : 1
2646
3072
  });
3073
+ shortRead = false;
2647
3074
  if (candidates.length === 0) {
2648
3075
  break;
2649
3076
  }
2650
3077
  for (const candidate of candidates) {
2651
- const snapshot = this.candidateToSnapshotRow(input.datasetId, input.tableId, candidate);
3078
+ const snapshot = this.candidateToSnapshotRow(input.table.datasetId, input.table.tableId, candidate);
2652
3079
  if (snapshot) {
2653
3080
  snapshots.push(snapshot);
2654
3081
  }
@@ -2656,7 +3083,7 @@ class DuckLakeSnapshotReader {
2656
3083
  break;
2657
3084
  }
2658
3085
  }
2659
- if (input.exactSnapshotId !== undefined || candidates.length < SNAPSHOT_ROW_BATCH_SIZE) {
3086
+ if (input.exactSnapshotId !== undefined || candidates.length < batchSize) {
2660
3087
  break;
2661
3088
  }
2662
3089
  beforeSnapshotId = candidates[candidates.length - 1]?.snapshotId;
@@ -2664,107 +3091,85 @@ class DuckLakeSnapshotReader {
2664
3091
  return snapshots;
2665
3092
  }
2666
3093
  async querySnapshotCandidates(runtime, input) {
2667
- if (input.limit !== undefined && input.limit <= 0) {
3094
+ const limit = Math.max(0, Math.trunc(input.limit));
3095
+ if (limit === 0) {
2668
3096
  return [];
2669
3097
  }
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
3098
  if (input.exactSnapshotId !== undefined) {
2675
3099
  assertDuckLakeSnapshotId2(input.exactSnapshotId);
2676
- where.push(`snapshot.snapshot_id = ${input.exactSnapshotId}`);
2677
3100
  }
2678
3101
  if (input.beforeSnapshotId !== undefined) {
2679
3102
  assertDuckLakeSnapshotId2(input.beforeSnapshotId);
2680
- where.push(`snapshot.snapshot_id < ${input.beforeSnapshotId}`);
2681
3103
  }
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
- `);
3104
+ const rows = await runtime.query(buildDuckLakeMetadataQuery(this.options, (table) => {
3105
+ const bounds = [
3106
+ `changes.snapshot_id >= (
3107
+ SELECT min(begin_snapshot)
3108
+ FROM ${table("ducklake_table")}
3109
+ WHERE table_id = ${input.table.tableId}
3110
+ )`
3111
+ ];
3112
+ if (input.exactSnapshotId !== undefined) {
3113
+ bounds.push(`changes.snapshot_id = ${input.exactSnapshotId}`);
3114
+ }
3115
+ if (input.beforeSnapshotId !== undefined) {
3116
+ bounds.push(`changes.snapshot_id < ${input.beforeSnapshotId}`);
3117
+ }
3118
+ const namesDatasetTable = datasetTableIds(input.table).map((tableId) => `strpos(',' || changes.changes_made || ',', ${quoteSqlString(`:${tableId},`)}) > 0`).join(" OR ");
3119
+ return `
3120
+ SELECT
3121
+ changes.snapshot_id,
3122
+ snapshot.snapshot_time,
3123
+ changes.changes_made,
3124
+ changes.commit_extra_info
3125
+ FROM ${table("ducklake_snapshot_changes")} changes
3126
+ JOIN ${table("ducklake_snapshot")} snapshot
3127
+ ON snapshot.snapshot_id = changes.snapshot_id
3128
+ WHERE ${bounds.join(" AND ")}
3129
+ AND (${namesDatasetTable})
3130
+ ORDER BY changes.snapshot_id DESC
3131
+ LIMIT ${limit}
3132
+ `;
3133
+ }));
2747
3134
  return rows.map((row) => {
2748
3135
  const metadata = parseCommitMetadata(row.commit_extra_info);
2749
3136
  return {
2750
3137
  snapshotId: String(getBigIntLike(row, "snapshot_id")),
2751
3138
  createdAt: getDate(row, "snapshot_time"),
2752
3139
  changesMade: getString(row, "changes_made"),
2753
- hasFileChange: getBoolean(row, "has_file_change"),
2754
- hasFileDeleteChange: getBoolean(row, "has_file_delete_change"),
2755
3140
  ...metadata !== undefined ? { metadata } : {}
2756
3141
  };
2757
3142
  });
2758
3143
  }
2759
- candidateToSnapshotRow(datasetId, tableId, candidate) {
2760
- const inlineChange = parseInlineDataChange(candidate.changesMade, tableId);
2761
- const hasDataChange = inlineChange.hasDataChange || candidate.hasFileChange;
2762
- const hasDeleteChange = inlineChange.hasDeleteChange || candidate.hasFileDeleteChange;
2763
- if (!hasDataChange) {
2764
- const metadata = candidate.metadata;
2765
- if (!metadata || metadata.datasetId !== datasetId) {
2766
- return null;
2767
- }
3144
+ async readCommitMetadata(runtime, snapshotIds) {
3145
+ const metadata = new Map;
3146
+ if (snapshotIds.length === 0) {
3147
+ return metadata;
3148
+ }
3149
+ for (const snapshotId of snapshotIds) {
3150
+ assertDuckLakeSnapshotId2(snapshotId);
3151
+ }
3152
+ const ducklakeSnapshotChanges = duckLakeMetadataTableName(this.options, "ducklake_snapshot_changes");
3153
+ const rows = await runtime.query(`
3154
+ SELECT snapshot_id, commit_extra_info
3155
+ FROM ${ducklakeSnapshotChanges}
3156
+ WHERE snapshot_id IN (${snapshotIds.join(", ")})
3157
+ `);
3158
+ for (const row of rows) {
3159
+ const parsed = parseCommitMetadata(getOptionalString(row, "commit_extra_info"));
3160
+ if (parsed !== undefined) {
3161
+ metadata.set(String(getBigIntLike(row, "snapshot_id")), parsed);
3162
+ }
3163
+ }
3164
+ return metadata;
3165
+ }
3166
+ candidateToSnapshotRow(datasetId, tableId, candidate) {
3167
+ const { hasDataChange, hasDeleteChange } = parseTableDataChange(candidate.changesMade, tableId);
3168
+ if (!hasDataChange) {
3169
+ const metadata = candidate.metadata;
3170
+ if (!metadata || metadata.datasetId !== datasetId) {
3171
+ return null;
3172
+ }
2768
3173
  return {
2769
3174
  snapshotId: candidate.snapshotId,
2770
3175
  tableId,
@@ -2782,19 +3187,9 @@ class DuckLakeSnapshotReader {
2782
3187
  ...candidate.metadata !== undefined ? { metadata: candidate.metadata } : {}
2783
3188
  };
2784
3189
  }
2785
- async snapshotToVersion(runtime, tableRef, row) {
2786
- const schemaAtSnapshot = await this.datasets.getDatasetSchemaAtSnapshot(runtime, tableRef.datasetId, tableRef.tableName, tableRef.tableId, row.snapshotId);
2787
- return {
2788
- datasetId: tableRef.datasetId,
2789
- versionId: toVersionId(row.snapshotId),
2790
- parentVersionId: row.parentSnapshotId === undefined ? undefined : toVersionId(row.parentSnapshotId),
2791
- mode: row.mode,
2792
- createdAt: row.createdAt,
2793
- schema: schemaAtSnapshot,
2794
- producer: row.metadata?.producer,
2795
- inputs: row.metadata?.inputs,
2796
- ...row.metadata?.rowCount !== undefined ? { rowCount: row.metadata.rowCount } : {}
2797
- };
3190
+ async snapshotsToVersions(runtime, tableRef, rows) {
3191
+ const schemas = await this.datasets.getDatasetSchemasAtSnapshots(runtime, rows.map((row) => ({ table: tableRef, snapshotId: row.snapshotId })));
3192
+ return rows.map((row, index) => snapshotToVersion(tableRef, row, schemas[index]));
2798
3193
  }
2799
3194
  withParentSnapshotIds(snapshots) {
2800
3195
  return snapshots.map((snapshot, index) => {
@@ -2803,29 +3198,60 @@ class DuckLakeSnapshotReader {
2803
3198
  });
2804
3199
  }
2805
3200
  }
3201
+ function snapshotToVersion(tableRef, row, schema) {
3202
+ return {
3203
+ datasetId: tableRef.datasetId,
3204
+ versionId: toVersionId(row.snapshotId),
3205
+ parentVersionId: row.parentSnapshotId === undefined ? undefined : toVersionId(row.parentSnapshotId),
3206
+ mode: row.mode,
3207
+ createdAt: row.createdAt,
3208
+ schema,
3209
+ producer: row.metadata?.producer,
3210
+ inputs: row.metadata?.inputs,
3211
+ ...storedLineage(tableRef.datasetId, row),
3212
+ ...row.metadata?.rowCount !== undefined ? { rowCount: row.metadata.rowCount } : {}
3213
+ };
3214
+ }
3215
+ function storedLineage(datasetId, row) {
3216
+ const metadata = row.metadata;
3217
+ if (metadata === undefined) {
3218
+ return {
3219
+ lineage: {
3220
+ unknown: `version '${toVersionId(row.snapshotId)}' of '${datasetId}' was written outside Sixb`
3221
+ }
3222
+ };
3223
+ }
3224
+ return metadata.lineage === undefined ? {} : { lineage: decodeLineage(metadata.datasetId, metadata.lineage) };
3225
+ }
3226
+ function tableVersionRef(table, snapshot) {
3227
+ return {
3228
+ datasetId: table.datasetId,
3229
+ versionId: toVersionId(snapshot.snapshotId),
3230
+ table,
3231
+ snapshot
3232
+ };
3233
+ }
2806
3234
  function assertDuckLakeSnapshotId2(snapshotId) {
2807
3235
  if (!/^\d+$/.test(snapshotId)) {
2808
- throw new LakeStorageError17(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`);
3236
+ throw new LakeStorageError19(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`);
2809
3237
  }
2810
3238
  }
2811
- function fileChangeKey(snapshotId, tableId) {
2812
- return `${snapshotId}|${tableId}`;
2813
- }
2814
3239
 
2815
3240
  // src/internal/ducklake-sql-executor.ts
2816
- import { randomUUID as randomUUID4 } from "crypto";
3241
+ import { randomUUID as randomUUID5 } from "crypto";
2817
3242
  import {
2818
- LakeStorageError as LakeStorageError20
3243
+ LakeStorageError as LakeStorageError22,
3244
+ resolveStepLineage
2819
3245
  } from "@sixb/core/lake-storage";
2820
3246
 
2821
3247
  // src/internal/dataset-row-commit.ts
2822
3248
  import {
2823
3249
  getDatasetPrimaryKeyColumns,
2824
- LakeStorageError as LakeStorageError18
3250
+ LakeStorageError as LakeStorageError20
2825
3251
  } from "@sixb/core/lake-storage";
2826
3252
  function assertDatasetWriteMode(mode, operation) {
2827
3253
  if (mode !== "snapshot" && mode !== "append") {
2828
- throw new LakeStorageError18(`[SixbDuckLake] Invalid ${operation} mode '${String(mode)}'. Expected 'snapshot' or 'append'.`);
3254
+ throw new LakeStorageError20(`[SixbDuckLake] Invalid ${operation} mode '${String(mode)}'. Expected 'snapshot' or 'append'.`);
2829
3255
  }
2830
3256
  }
2831
3257
  async function applyDatasetRowsFromRelation(input) {
@@ -2833,6 +3259,7 @@ async function applyDatasetRowsFromRelation(input) {
2833
3259
  const table = qualifiedTableName(input.options, tableName);
2834
3260
  const columnsSql = datasetSchemaColumnNamesSql(input.dataset.schema);
2835
3261
  const primaryKeyColumns = getDatasetPrimaryKeyColumns(input.dataset);
3262
+ let baselineRowCount;
2836
3263
  if (primaryKeyColumns !== null) {
2837
3264
  await inspectUniqueKeyedRelation({
2838
3265
  runtime: input.runtime,
@@ -2841,7 +3268,7 @@ async function applyDatasetRowsFromRelation(input) {
2841
3268
  context: `${input.mode} source`
2842
3269
  });
2843
3270
  if (input.mode === "append") {
2844
- await inspectUniqueKeyedRelation({
3271
+ baselineRowCount = await inspectUniqueKeyedRelation({
2845
3272
  runtime: input.runtime,
2846
3273
  dataset: input.dataset,
2847
3274
  relationSql: table,
@@ -2863,21 +3290,30 @@ async function applyDatasetRowsFromRelation(input) {
2863
3290
  return {
2864
3291
  dataChangeExpected: false,
2865
3292
  sourceRowCount: source.rowCount,
2866
- resultingRowCount: { kind: "exact", value: target.rowCount }
3293
+ resultingRowCount: { kind: "exact", value: target.rowCount },
3294
+ targetWasEmpty: target.rowCount === 0,
3295
+ changedTableRows: false
2867
3296
  };
2868
3297
  }
2869
3298
  await input.runtime.run(`DELETE FROM ${table}`);
2870
3299
  await input.runtime.run(`INSERT INTO ${table} (${columnsSql}) SELECT ${columnsSql} FROM ${input.sourceRelationSql}`);
3300
+ const changedTableRows = source.rowCount > 0 || target.rowCount > 0;
2871
3301
  return {
2872
- dataChangeExpected: source.rowCount > 0 || target.rowCount > 0,
3302
+ dataChangeExpected: changedTableRows,
2873
3303
  sourceRowCount: source.rowCount,
2874
- resultingRowCount: { kind: "exact", value: source.rowCount }
3304
+ resultingRowCount: { kind: "exact", value: source.rowCount },
3305
+ targetWasEmpty: target.rowCount === 0,
3306
+ changedTableRows
2875
3307
  };
2876
3308
  }
2877
3309
  const sourceRowCount = await countRows(input.runtime, input.sourceRelationSql);
3310
+ const knownRowCount = baselineRowCount ?? input.previousRowCount;
3311
+ const targetWasEmpty = knownRowCount === undefined ? await isEmptyRelation(input.runtime, table) : knownRowCount === 0;
2878
3312
  await input.runtime.run(`INSERT INTO ${table} (${columnsSql}) SELECT ${columnsSql} FROM ${input.sourceRelationSql}`);
2879
3313
  return {
2880
3314
  dataChangeExpected: sourceRowCount > 0,
3315
+ targetWasEmpty,
3316
+ changedTableRows: sourceRowCount > 0,
2881
3317
  sourceRowCount,
2882
3318
  resultingRowCount: input.previousRowCount === undefined ? { kind: "unknown" } : { kind: "exact", value: input.previousRowCount + sourceRowCount }
2883
3319
  };
@@ -2885,7 +3321,7 @@ async function applyDatasetRowsFromRelation(input) {
2885
3321
  async function inspectUniqueKeyedRelation(input) {
2886
3322
  const primaryKeyColumns = getDatasetPrimaryKeyColumns(input.dataset);
2887
3323
  if (primaryKeyColumns === null) {
2888
- throw new LakeStorageError18(`[SixbDuckLake] Dataset '${input.dataset.id}' must define a primaryKey before rows can be keyed.`);
3324
+ throw new LakeStorageError20(`[SixbDuckLake] Dataset '${input.dataset.id}' must define a primaryKey before rows can be keyed.`);
2889
3325
  }
2890
3326
  const keysSql = primaryKeyColumns.map((column) => quoteIdentifier(column)).join(", ");
2891
3327
  const [row] = await input.runtime.query(`
@@ -2900,7 +3336,7 @@ async function inspectUniqueKeyedRelation(input) {
2900
3336
  const rowCount = row === undefined ? 0 : Number(getBigIntLike(row, "row_count"));
2901
3337
  const maxKeyCount = row === undefined ? 0 : Number(getBigIntLike(row, "max_key_count"));
2902
3338
  if (maxKeyCount > 1) {
2903
- throw new LakeStorageError18(`[SixbDuckLake] Dataset '${input.dataset.id}' ${input.context} contains duplicate primary key.`);
3339
+ throw new LakeStorageError20(`[SixbDuckLake] Dataset '${input.dataset.id}' ${input.context} contains duplicate primary key.`);
2904
3340
  }
2905
3341
  return rowCount;
2906
3342
  }
@@ -2917,7 +3353,7 @@ async function assertNoPrimaryKeyOverlap(input) {
2917
3353
  LIMIT 1
2918
3354
  `);
2919
3355
  if (collision !== undefined) {
2920
- throw new LakeStorageError18(`[SixbDuckLake] Dataset '${input.dataset.id}' ${input.context} contains duplicate primary key.`);
3356
+ throw new LakeStorageError20(`[SixbDuckLake] Dataset '${input.dataset.id}' ${input.context} contains duplicate primary key.`);
2921
3357
  }
2922
3358
  }
2923
3359
  async function fingerprintRelation(runtime, relationSql, columnsSql) {
@@ -2947,11 +3383,19 @@ async function countRows(runtime, relationSql) {
2947
3383
  }
2948
3384
  return Number(getBigIntLike(row, "row_count"));
2949
3385
  }
3386
+ async function isEmptyRelation(runtime, relationSql) {
3387
+ const rows = await runtime.query(`SELECT 1 AS present FROM ${relationSql} LIMIT 1`);
3388
+ return rows.length === 0;
3389
+ }
3390
+
3391
+ // src/internal/sql-lineage/analysis.ts
3392
+ import { randomUUID as randomUUID4 } from "crypto";
3393
+ import { DuckDBInstance as DuckDBInstance2 } from "@duckdb/node-api";
2950
3394
 
2951
3395
  // src/internal/sql-transform-relations.ts
2952
3396
  import { randomUUID as randomUUID3 } from "crypto";
2953
3397
  import {
2954
- LakeStorageError as LakeStorageError19
3398
+ LakeStorageError as LakeStorageError21
2955
3399
  } from "@sixb/core/lake-storage";
2956
3400
  var RELATION_PLACEHOLDER_PREFIX = "__sixb_sql_transform_relation_";
2957
3401
  var RELATION_PLACEHOLDER_PATTERN = /__sixb_sql_transform_relation_[0-9a-f]{32}_[0-9]+__/g;
@@ -2965,34 +3409,28 @@ class DuckLakeSqlTransformRelation {
2965
3409
  return this.placeholder;
2966
3410
  }
2967
3411
  }
2968
- function renderDuckLakeSqlTransformSql(input) {
2969
- const context = createDuckLakeSqlTransformRelationContext(input);
2970
- const sql = input.sql(context.relations);
2971
- if (typeof sql !== "string") {
2972
- throw new LakeStorageError19("[SixbDuckLake] SQL transform body must return a SQL string.");
2973
- }
2974
- return context.render(sql);
2975
- }
2976
- function createDuckLakeSqlTransformRelationContext(input) {
3412
+ function createDuckLakeSqlTransformTemplate(sourceNames, body) {
2977
3413
  const token = randomUUID3().replaceAll("-", "");
2978
3414
  const relations = Object.create(null);
2979
- const replacements = new Map;
2980
- let index = 0;
2981
- for (const [sourceName, source] of Object.entries(input.sources)) {
3415
+ const sourceNamesByPlaceholder = new Map;
3416
+ sourceNames.forEach((sourceName, index) => {
2982
3417
  const placeholder = `${RELATION_PLACEHOLDER_PREFIX}${token}_${index}__`;
2983
3418
  relations[sourceName] = Object.freeze(new DuckLakeSqlTransformRelation(placeholder));
2984
- replacements.set(placeholder, renderDuckLakeSourceRelation(input.options, source));
2985
- index += 1;
3419
+ sourceNamesByPlaceholder.set(placeholder, sourceName);
3420
+ });
3421
+ const sql = body(Object.freeze(relations));
3422
+ if (typeof sql !== "string") {
3423
+ throw new LakeStorageError21("[SixbDuckLake] SQL transform body must return a SQL string.");
2986
3424
  }
2987
3425
  return {
2988
- relations: Object.freeze(relations),
2989
- render(sql) {
3426
+ render(relationSql) {
2990
3427
  const rendered = sql.replace(RELATION_PLACEHOLDER_PATTERN, (placeholder) => {
2991
- const replacement = replacements.get(placeholder);
2992
- if (replacement === undefined) {
3428
+ const sourceName = sourceNamesByPlaceholder.get(placeholder);
3429
+ const relation = sourceName === undefined ? undefined : relationSql.get(sourceName);
3430
+ if (relation === undefined) {
2993
3431
  throwUnresolvedPlaceholder();
2994
3432
  }
2995
- return replacement;
3433
+ return relation;
2996
3434
  });
2997
3435
  if (rendered.includes(RELATION_PLACEHOLDER_PREFIX)) {
2998
3436
  throwUnresolvedPlaceholder();
@@ -3001,13 +3439,967 @@ function createDuckLakeSqlTransformRelationContext(input) {
3001
3439
  }
3002
3440
  };
3003
3441
  }
3004
- function renderDuckLakeSourceRelation(options, source) {
3005
- const snapshotId = parseVersionId(source.versionId);
3006
- const tableName = encodeDatasetTableName(source.datasetId);
3007
- return `(SELECT * FROM ${qualifiedTableName(options, tableName)} AT (VERSION => ${snapshotId}))`;
3442
+ function renderDuckLakeSourceRelations(options, sources) {
3443
+ return new Map(Object.entries(sources).map(([sourceName, source]) => {
3444
+ const snapshotId = parseVersionId(source.versionId);
3445
+ const table = qualifiedTableName(options, encodeDatasetTableName(source.datasetId));
3446
+ return [
3447
+ sourceName,
3448
+ renderSourceRelation(`${table} AT (VERSION => ${snapshotId})`, source.columns)
3449
+ ];
3450
+ }));
3451
+ }
3452
+ function renderSourceRelation(tableSql, columns) {
3453
+ const columnsSql = columns === undefined ? "*" : columns.map((column) => quoteIdentifier(column)).join(", ");
3454
+ return `(SELECT ${columnsSql} FROM ${tableSql})`;
3455
+ }
3456
+ function pickSourceColumns(sourceName, dataset, columns) {
3457
+ if (columns === undefined) {
3458
+ return dataset.schema.columns;
3459
+ }
3460
+ if (columns.length === 0) {
3461
+ throw new LakeStorageError21(`[SixbDuckLake] SQL transform source '${sourceName}' must pick at least one column.`);
3462
+ }
3463
+ const columnsByName = new Map(dataset.schema.columns.map((column) => [column.name, column]));
3464
+ const picked = new Set;
3465
+ return columns.map((columnName) => {
3466
+ const column = columnsByName.get(columnName);
3467
+ if (column === undefined) {
3468
+ throw new LakeStorageError21(`[SixbDuckLake] SQL transform source '${sourceName}' picks column '${columnName}', which dataset '${dataset.id}' does not have.`);
3469
+ }
3470
+ if (picked.has(columnName)) {
3471
+ throw new LakeStorageError21(`[SixbDuckLake] SQL transform source '${sourceName}' picks column '${columnName}' more than once.`);
3472
+ }
3473
+ picked.add(columnName);
3474
+ return column;
3475
+ });
3008
3476
  }
3009
3477
  function throwUnresolvedPlaceholder() {
3010
- throw new LakeStorageError19("[SixbDuckLake] SQL transform contains an unresolved relation placeholder.");
3478
+ throw new LakeStorageError21("[SixbDuckLake] SQL transform contains an unresolved relation placeholder.");
3479
+ }
3480
+
3481
+ // src/internal/sql-lineage/rejection.ts
3482
+ class SqlLineageRejected extends Error {
3483
+ name = "SqlLineageRejected";
3484
+ }
3485
+ function reject(reason) {
3486
+ throw new SqlLineageRejected(reason);
3487
+ }
3488
+ function rejectPlanFormat(detail) {
3489
+ reject(`unexpected DuckDB plan format: ${detail}`);
3490
+ }
3491
+
3492
+ // src/internal/sql-lineage/plan-json.ts
3493
+ function isJsonObject(value) {
3494
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3495
+ }
3496
+ function asObject(value, what) {
3497
+ if (!isJsonObject(value)) {
3498
+ rejectPlanFormat(`${what} is not an object`);
3499
+ }
3500
+ return value;
3501
+ }
3502
+ function field(object, key) {
3503
+ if (!Object.hasOwn(object, key)) {
3504
+ rejectPlanFormat(`missing field '${key}'`);
3505
+ }
3506
+ return object[key];
3507
+ }
3508
+ function objectField(object, key) {
3509
+ return asObject(field(object, key), `field '${key}'`);
3510
+ }
3511
+ function nullableObjectField(object, key) {
3512
+ const value = field(object, key);
3513
+ return value === null ? null : asObject(value, `field '${key}'`);
3514
+ }
3515
+ function stringField(object, key) {
3516
+ const value = field(object, key);
3517
+ if (typeof value !== "string") {
3518
+ rejectPlanFormat(`field '${key}' is not a string`);
3519
+ }
3520
+ return value;
3521
+ }
3522
+ function booleanField(object, key) {
3523
+ const value = field(object, key);
3524
+ if (typeof value !== "boolean") {
3525
+ rejectPlanFormat(`field '${key}' is not a boolean`);
3526
+ }
3527
+ return value;
3528
+ }
3529
+ function indexField(object, key) {
3530
+ return asIndex(field(object, key), `field '${key}'`);
3531
+ }
3532
+ function asIndex(value, what) {
3533
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
3534
+ rejectPlanFormat(`${what} is not an index`);
3535
+ }
3536
+ return value;
3537
+ }
3538
+ function listField(object, key) {
3539
+ return asList(field(object, key), `field '${key}'`);
3540
+ }
3541
+ function asList(value, what) {
3542
+ if (value === null) {
3543
+ return [];
3544
+ }
3545
+ if (!Array.isArray(value)) {
3546
+ rejectPlanFormat(`${what} is not a list`);
3547
+ }
3548
+ return value;
3549
+ }
3550
+ function objectListField(object, key) {
3551
+ return listField(object, key).map((item) => asObject(item, `an item of '${key}'`));
3552
+ }
3553
+ function indexListField(object, key) {
3554
+ return asIndexList(field(object, key), `field '${key}'`);
3555
+ }
3556
+ function asIndexList(value, what) {
3557
+ return asList(value, what).map((item) => asIndex(item, `an item of ${what}`));
3558
+ }
3559
+ function stringListField(object, key) {
3560
+ return listField(object, key).map((item) => {
3561
+ if (typeof item !== "string") {
3562
+ rejectPlanFormat(`an item of '${key}' is not a string`);
3563
+ }
3564
+ return item;
3565
+ });
3566
+ }
3567
+ function parseSerializerOutput(text, listKey, failure) {
3568
+ const output = asObject(JSON.parse(text), "serializer output");
3569
+ if (booleanField(output, "error")) {
3570
+ reject(`${failure}: ${stringField(output, "error_message")}`);
3571
+ }
3572
+ return listField(output, listKey);
3573
+ }
3574
+
3575
+ // src/internal/sql-lineage/plan-relation.ts
3576
+ var VIRTUAL_COLUMN = -1;
3577
+ function bindingKey(tableIndex, columnIndex) {
3578
+ return `${tableIndex}.${columnIndex}`;
3579
+ }
3580
+ function itemAt(items, index, what) {
3581
+ const item = items[index];
3582
+ if (item === undefined) {
3583
+ rejectPlanFormat(`${what} ${index} is out of range`);
3584
+ }
3585
+ return item;
3586
+ }
3587
+ function unionOf(...sets) {
3588
+ const union = new Set;
3589
+ for (const set of sets) {
3590
+ addAll(union, set);
3591
+ }
3592
+ return union;
3593
+ }
3594
+ function addAll(target, source) {
3595
+ for (const item of source) {
3596
+ target.add(item);
3597
+ }
3598
+ }
3599
+ function sameDependencies(left, right) {
3600
+ return left.size === right.size && [...left].every((item) => right.has(item));
3601
+ }
3602
+
3603
+ // src/internal/sql-lineage/expressions.ts
3604
+ function expressionDependencies(expression, input) {
3605
+ const dependencies = new Set;
3606
+ addExpression(dependencies, expression, input);
3607
+ return dependencies;
3608
+ }
3609
+ function orderExpressions(orders) {
3610
+ return orders.map((order) => field(asObject(order, "sort order"), "expression"));
3611
+ }
3612
+ function nestedOrderExpressions(object, key) {
3613
+ const orderBy = nullableObjectField(object, key);
3614
+ return orderBy === null ? [] : orderExpressions(listField(orderBy, "orders"));
3615
+ }
3616
+ function addExpression(target, value, input) {
3617
+ const expression = asObject(value, "expression");
3618
+ const expressionClass = stringField(expression, "expression_class");
3619
+ switch (expressionClass) {
3620
+ case "BOUND_REF":
3621
+ addAll(target, itemAt(input, indexField(expression, "index"), "column").dependencies);
3622
+ return;
3623
+ case "BOUND_COLUMN_REF":
3624
+ addAll(target, columnByBinding(expression, input).dependencies);
3625
+ return;
3626
+ case "BOUND_CONSTANT":
3627
+ return;
3628
+ case "BOUND_CAST":
3629
+ case "BOUND_UNNEST":
3630
+ addExpression(target, field(expression, "child"), input);
3631
+ return;
3632
+ case "BOUND_FUNCTION":
3633
+ assertNoHiddenExpressions(expression, "lambda_expr");
3634
+ addExpressions(target, listField(expression, "children"), input);
3635
+ return;
3636
+ case "BOUND_AGGREGATE":
3637
+ assertNoHiddenExpressions(expression);
3638
+ addExpressions(target, listField(expression, "children"), input);
3639
+ addOptionalExpression(target, field(expression, "filter"), input);
3640
+ addExpressions(target, nestedOrderExpressions(expression, "order_bys"), input);
3641
+ return;
3642
+ case "BOUND_WINDOW":
3643
+ addWindowExpression(target, expression, input);
3644
+ return;
3645
+ case "BOUND_COMPARISON":
3646
+ addExpression(target, field(expression, "left"), input);
3647
+ addExpression(target, field(expression, "right"), input);
3648
+ return;
3649
+ case "BOUND_BETWEEN":
3650
+ addExpression(target, field(expression, "input"), input);
3651
+ addExpression(target, field(expression, "lower"), input);
3652
+ addExpression(target, field(expression, "upper"), input);
3653
+ return;
3654
+ case "BOUND_CONJUNCTION":
3655
+ case "BOUND_OPERATOR":
3656
+ addExpressions(target, listField(expression, "children"), input);
3657
+ return;
3658
+ case "BOUND_CASE":
3659
+ for (const check of objectListField(expression, "case_checks")) {
3660
+ addExpression(target, field(check, "when_expr"), input);
3661
+ addExpression(target, field(check, "then_expr"), input);
3662
+ }
3663
+ addExpression(target, field(expression, "else_expr"), input);
3664
+ return;
3665
+ default:
3666
+ reject(`expression '${expressionClass}' is not supported`);
3667
+ }
3668
+ }
3669
+ function addWindowExpression(target, expression, input) {
3670
+ if (stringField(expression, "type") === "WINDOW_AGGREGATE") {
3671
+ assertNoHiddenExpressions(expression);
3672
+ }
3673
+ addExpressions(target, listField(expression, "children"), input);
3674
+ addExpressions(target, listField(expression, "partitions"), input);
3675
+ addExpressions(target, orderExpressions(listField(expression, "orders")), input);
3676
+ addExpressions(target, orderExpressions(listField(expression, "arg_orders")), input);
3677
+ for (const key of ["filters", "start_expr", "end_expr", "offset_expr", "default_expr"]) {
3678
+ addOptionalExpression(target, field(expression, key), input);
3679
+ }
3680
+ }
3681
+ function addExpressions(target, expressions, input) {
3682
+ for (const expression of expressions) {
3683
+ addExpression(target, expression, input);
3684
+ }
3685
+ }
3686
+ function addOptionalExpression(target, expression, input) {
3687
+ if (expression !== null) {
3688
+ addExpression(target, expression, input);
3689
+ }
3690
+ }
3691
+ function columnByBinding(expression, input) {
3692
+ if (indexField(expression, "depth") !== 0) {
3693
+ reject("correlated column references that the planner did not flatten are not supported");
3694
+ }
3695
+ const binding = objectField(expression, "binding");
3696
+ const key = bindingKey(indexField(binding, "table_index"), indexField(binding, "column_index"));
3697
+ const column = input.find((candidate) => candidate.binding === key);
3698
+ if (column === undefined) {
3699
+ rejectPlanFormat(`column binding ${key} does not resolve`);
3700
+ }
3701
+ return column;
3702
+ }
3703
+ function assertNoHiddenExpressions(expression, allowedKey) {
3704
+ if (!booleanField(expression, "has_serialize")) {
3705
+ return;
3706
+ }
3707
+ const bindData = field(expression, "function_data");
3708
+ const inspected = isJsonObject(bindData) && allowedKey !== undefined ? Object.entries(bindData).flatMap(([key, value]) => key === allowedKey ? [] : [value]) : [bindData];
3709
+ if (inspected.some(containsExpression)) {
3710
+ reject(`function '${stringField(expression, "name")}' keeps expressions in its bind data, which is not supported`);
3711
+ }
3712
+ }
3713
+ function containsExpression(value) {
3714
+ if (Array.isArray(value)) {
3715
+ return value.some((item) => containsExpression(item));
3716
+ }
3717
+ if (!isJsonObject(value)) {
3718
+ return false;
3719
+ }
3720
+ return Object.hasOwn(value, "expression_class") || Object.values(value).some(containsExpression);
3721
+ }
3722
+
3723
+ // src/internal/sql-lineage/table-functions.ts
3724
+ var PURE_TABLE_FUNCTIONS = new Set([
3725
+ "range",
3726
+ "generate_series",
3727
+ "unnest",
3728
+ "json_each",
3729
+ "json_tree"
3730
+ ]);
3731
+
3732
+ // src/internal/sql-lineage/plan-walker.ts
3733
+ var MAX_RECURSIVE_CTE_ROUNDS = 64;
3734
+ function planLineage(plan, tables) {
3735
+ const root = evaluate(asObject(plan, "plan"), { tables, ctes: new Map, delimColumns: [] });
3736
+ const rows = sortedIds(root.rows);
3737
+ const columns = root.columns.map((column) => sortedIds(column.dependencies));
3738
+ if ([rows, ...columns].some((ids) => ids.includes(VIRTUAL_COLUMN))) {
3739
+ reject("reading virtual columns such as rowid is not supported");
3740
+ }
3741
+ return { rows, columns };
3742
+ }
3743
+ function evaluate(node, context) {
3744
+ const type = stringField(node, "type");
3745
+ switch (type) {
3746
+ case "LOGICAL_GET":
3747
+ return evaluateGet(node, context);
3748
+ case "LOGICAL_DUMMY_SCAN":
3749
+ return evaluateDummyScan(node);
3750
+ case "LOGICAL_EXPRESSION_GET":
3751
+ return evaluateExpressionGet(node, context);
3752
+ case "LOGICAL_PROJECTION":
3753
+ return evaluateProjection(node, context);
3754
+ case "LOGICAL_FILTER":
3755
+ return evaluateFilter(node, context);
3756
+ case "LOGICAL_AGGREGATE_AND_GROUP_BY":
3757
+ return evaluateAggregate(node, context);
3758
+ case "LOGICAL_WINDOW":
3759
+ return evaluateWindow(node, context);
3760
+ case "LOGICAL_UNNEST":
3761
+ return evaluateUnnest(node, context);
3762
+ case "LOGICAL_ORDER_BY":
3763
+ return evaluateOrderBy(node, context);
3764
+ case "LOGICAL_LIMIT":
3765
+ return evaluateLimit(node, context);
3766
+ case "LOGICAL_SAMPLE":
3767
+ return evaluateOnlyChild(node, context);
3768
+ case "LOGICAL_DISTINCT":
3769
+ return evaluateDistinct(node, context);
3770
+ case "LOGICAL_CROSS_PRODUCT":
3771
+ case "LOGICAL_POSITIONAL_JOIN":
3772
+ return evaluateCrossProduct(node, context);
3773
+ case "LOGICAL_COMPARISON_JOIN":
3774
+ case "LOGICAL_ASOF_JOIN":
3775
+ return evaluateComparisonJoin(node, context);
3776
+ case "LOGICAL_DELIM_JOIN":
3777
+ return evaluateDelimJoin(node, context);
3778
+ case "LOGICAL_ANY_JOIN":
3779
+ return evaluateAnyJoin(node, context);
3780
+ case "LOGICAL_DELIM_GET":
3781
+ return evaluateDelimGet(node, context);
3782
+ case "LOGICAL_UNION":
3783
+ case "LOGICAL_EXCEPT":
3784
+ case "LOGICAL_INTERSECT":
3785
+ return evaluateSetOperation(node, context);
3786
+ case "LOGICAL_MATERIALIZED_CTE":
3787
+ return evaluateMaterializedCte(node, context);
3788
+ case "LOGICAL_RECURSIVE_CTE":
3789
+ return evaluateRecursiveCte(node, context);
3790
+ case "LOGICAL_CTE_REF":
3791
+ return evaluateCteRef(node, context);
3792
+ default:
3793
+ reject(`plan node '${type}' is not supported`);
3794
+ }
3795
+ }
3796
+ function evaluateGet(node, context) {
3797
+ if (objectListField(objectField(node, "table_filters"), "filters").length > 0) {
3798
+ rejectPlanFormat("an unoptimized scan has pushed-down filters");
3799
+ }
3800
+ const inputs = objectListField(node, "children");
3801
+ if (inputs.length > 1) {
3802
+ rejectPlanFormat("a scan has several inputs");
3803
+ }
3804
+ const input = inputs[0] === undefined ? undefined : evaluate(inputs[0], context);
3805
+ const functionName = stringField(node, "name");
3806
+ const source = functionName === "seq_scan" ? tableScanSource(node, context, input) : tableFunctionSource(functionName, input);
3807
+ const scanned = objectListField(node, "column_indexes").map((entry) => source.column(indexField(entry, "index")));
3808
+ const tableIndex = indexField(node, "table_index");
3809
+ const projectionIds = indexListField(node, "projection_ids");
3810
+ const positions = projectionIds.length > 0 ? projectionIds : scanned.map((_, index) => index);
3811
+ const columns = positions.map((position) => ({
3812
+ dependencies: itemAt(scanned, position, "scanned column"),
3813
+ binding: bindingKey(tableIndex, position)
3814
+ }));
3815
+ for (const position of indexListField(node, "projected_input")) {
3816
+ if (input === undefined) {
3817
+ rejectPlanFormat("a scan passes through input columns without an input");
3818
+ }
3819
+ columns.push(itemAt(input.columns, position, "input column"));
3820
+ }
3821
+ return { columns, rows: unionOf(input?.rows ?? new Set, source.rows) };
3822
+ }
3823
+ function tableScanSource(node, context, input) {
3824
+ if (input !== undefined) {
3825
+ rejectPlanFormat("a table scan has an input");
3826
+ }
3827
+ const table = stringField(objectField(node, "function_data"), "table");
3828
+ const columnIds = context.tables.get(table);
3829
+ if (columnIds === undefined) {
3830
+ reject(`table '${table}' is not a source of the transform`);
3831
+ }
3832
+ const names = stringListField(node, "names");
3833
+ return {
3834
+ rows: new Set,
3835
+ column: (index) => {
3836
+ const name = names[index];
3837
+ if (name === undefined) {
3838
+ return new Set([VIRTUAL_COLUMN]);
3839
+ }
3840
+ const id = columnIds.get(name);
3841
+ if (id === undefined) {
3842
+ rejectPlanFormat(`table '${table}' has no column '${name}'`);
3843
+ }
3844
+ return new Set([id]);
3845
+ }
3846
+ };
3847
+ }
3848
+ function tableFunctionSource(name, input) {
3849
+ if (!PURE_TABLE_FUNCTIONS.has(name)) {
3850
+ reject(`table function '${name}' is not supported`);
3851
+ }
3852
+ const dependencies = input === undefined ? new Set : unionOf(...input.columns.map((column) => column.dependencies));
3853
+ return { rows: dependencies, column: () => dependencies };
3854
+ }
3855
+ function evaluateDummyScan(node) {
3856
+ assertLeaf(node);
3857
+ return {
3858
+ columns: [{ dependencies: new Set, binding: bindingKey(indexField(node, "table_index"), 0) }],
3859
+ rows: new Set
3860
+ };
3861
+ }
3862
+ function evaluateExpressionGet(node, context) {
3863
+ const input = evaluateOnlyChild(node, context);
3864
+ const width = listField(node, "expr_types").length;
3865
+ const values = Array.from({ length: width }, () => new Set);
3866
+ for (const row of listField(node, "expressions")) {
3867
+ const expressions = asList(row, "a VALUES row");
3868
+ if (expressions.length !== width) {
3869
+ rejectPlanFormat("a VALUES row does not match the VALUES width");
3870
+ }
3871
+ expressions.forEach((expression, index) => {
3872
+ addAll(itemAt(values, index, "VALUES column"), expressionDependencies(expression, input.columns));
3873
+ });
3874
+ }
3875
+ const tableIndex = indexField(node, "table_index");
3876
+ return {
3877
+ columns: values.map((dependencies, index) => ({
3878
+ dependencies,
3879
+ binding: bindingKey(tableIndex, index)
3880
+ })),
3881
+ rows: input.rows
3882
+ };
3883
+ }
3884
+ function evaluateProjection(node, context) {
3885
+ const input = evaluateOnlyChild(node, context);
3886
+ const tableIndex = indexField(node, "table_index");
3887
+ return {
3888
+ columns: listField(node, "expressions").map((expression, index) => ({
3889
+ dependencies: expressionDependencies(expression, input.columns),
3890
+ binding: bindingKey(tableIndex, index)
3891
+ })),
3892
+ rows: input.rows
3893
+ };
3894
+ }
3895
+ function evaluateWindow(node, context) {
3896
+ const input = evaluateOnlyChild(node, context);
3897
+ const windowIndex = indexField(node, "window_index");
3898
+ const rows = new Set(input.rows);
3899
+ const windows = objectListField(node, "expressions").map((expression, index) => {
3900
+ if (stringField(expression, "expression_class") !== "BOUND_WINDOW") {
3901
+ rejectPlanFormat("a window operator computes something other than a window function");
3902
+ }
3903
+ addAll(rows, dependenciesOf(listField(expression, "partitions"), input.columns));
3904
+ addAll(rows, dependenciesOf(orderExpressions(listField(expression, "orders")), input.columns));
3905
+ return {
3906
+ dependencies: expressionDependencies(expression, input.columns),
3907
+ binding: bindingKey(windowIndex, index)
3908
+ };
3909
+ });
3910
+ return { columns: [...input.columns, ...windows], rows };
3911
+ }
3912
+ function evaluateFilter(node, context) {
3913
+ const input = evaluateOnlyChild(node, context);
3914
+ return {
3915
+ columns: projectColumns(input.columns, indexListField(node, "projection_map")),
3916
+ rows: unionOf(input.rows, dependenciesOf(listField(node, "expressions"), input.columns))
3917
+ };
3918
+ }
3919
+ function evaluateAggregate(node, context) {
3920
+ const input = evaluateOnlyChild(node, context);
3921
+ const groups = listField(node, "groups").map((expression) => expressionDependencies(expression, input.columns));
3922
+ const groupIndex = indexField(node, "group_index");
3923
+ const aggregateIndex = indexField(node, "aggregate_index");
3924
+ const groupingsIndex = indexField(node, "groupings_index");
3925
+ const groupColumns = groups.map((dependencies, index) => ({
3926
+ dependencies,
3927
+ binding: bindingKey(groupIndex, index)
3928
+ }));
3929
+ const aggregateColumns = listField(node, "expressions").map((expression, index) => ({
3930
+ dependencies: expressionDependencies(expression, input.columns),
3931
+ binding: bindingKey(aggregateIndex, index)
3932
+ }));
3933
+ const groupingColumns = listField(node, "grouping_functions").map((groupIndexes, index) => ({
3934
+ dependencies: unionOf(...asIndexList(groupIndexes, "a grouping function").map((group) => itemAt(groups, group, "group"))),
3935
+ binding: bindingKey(groupingsIndex, index)
3936
+ }));
3937
+ return {
3938
+ columns: [...groupColumns, ...aggregateColumns, ...groupingColumns],
3939
+ rows: unionOf(input.rows, ...groups)
3940
+ };
3941
+ }
3942
+ function evaluateUnnest(node, context) {
3943
+ const input = evaluateOnlyChild(node, context);
3944
+ const unnestIndex = indexField(node, "unnest_index");
3945
+ const rows = new Set(input.rows);
3946
+ const unnested = listField(node, "expressions").map((expression, index) => {
3947
+ const dependencies = expressionDependencies(expression, input.columns);
3948
+ addAll(rows, dependencies);
3949
+ return { dependencies, binding: bindingKey(unnestIndex, index) };
3950
+ });
3951
+ return { columns: [...input.columns, ...unnested], rows };
3952
+ }
3953
+ function evaluateOrderBy(node, context) {
3954
+ const input = evaluateOnlyChild(node, context);
3955
+ return {
3956
+ columns: projectColumns(input.columns, indexListField(node, "projections")),
3957
+ rows: unionOf(input.rows, dependenciesOf(orderExpressions(listField(node, "orders")), input.columns))
3958
+ };
3959
+ }
3960
+ function evaluateLimit(node, context) {
3961
+ const input = evaluateOnlyChild(node, context);
3962
+ return {
3963
+ columns: input.columns,
3964
+ rows: unionOf(input.rows, limitDependencies(objectField(node, "limit_val"), input.columns), limitDependencies(objectField(node, "offset_val"), input.columns))
3965
+ };
3966
+ }
3967
+ function limitDependencies(limit, input) {
3968
+ const kind = stringField(limit, "type");
3969
+ switch (kind) {
3970
+ case "UNSET":
3971
+ case "CONSTANT_VALUE":
3972
+ case "CONSTANT_PERCENTAGE":
3973
+ return new Set;
3974
+ case "EXPRESSION_VALUE":
3975
+ case "EXPRESSION_PERCENTAGE":
3976
+ return expressionDependencies(field(limit, "expression"), input);
3977
+ default:
3978
+ reject(`LIMIT kind '${kind}' is not supported`);
3979
+ }
3980
+ }
3981
+ function evaluateDistinct(node, context) {
3982
+ const input = evaluateOnlyChild(node, context);
3983
+ return {
3984
+ columns: input.columns,
3985
+ rows: unionOf(input.rows, dependenciesOf(listField(node, "distinct_targets"), input.columns), dependenciesOf(nestedOrderExpressions(node, "order_by"), input.columns))
3986
+ };
3987
+ }
3988
+ function evaluateCrossProduct(node, context) {
3989
+ const [left, right] = evaluateTwoChildren(node, context);
3990
+ return { columns: [...left.columns, ...right.columns], rows: unionOf(left.rows, right.rows) };
3991
+ }
3992
+ function evaluateComparisonJoin(node, context) {
3993
+ const [left, right] = evaluateTwoChildren(node, context);
3994
+ return comparisonJoinOutput(node, left, right);
3995
+ }
3996
+ function evaluateDelimJoin(node, context) {
3997
+ const [first, second] = twoChildren(node);
3998
+ const flipped = booleanField(node, "delim_flipped");
3999
+ const outer = evaluate(flipped ? second : first, context);
4000
+ context.delimColumns.push(listField(node, "duplicate_eliminated_columns").map((expression) => ({
4001
+ dependencies: expressionDependencies(expression, outer.columns)
4002
+ })));
4003
+ const inner = evaluate(flipped ? first : second, context);
4004
+ context.delimColumns.pop();
4005
+ return flipped ? comparisonJoinOutput(node, inner, outer) : comparisonJoinOutput(node, outer, inner);
4006
+ }
4007
+ function comparisonJoinOutput(node, left, right) {
4008
+ const condition = new Set;
4009
+ for (const comparison of objectListField(node, "conditions")) {
4010
+ addAll(condition, expressionDependencies(field(comparison, "left"), left.columns));
4011
+ addAll(condition, expressionDependencies(field(comparison, "right"), right.columns));
4012
+ }
4013
+ const predicate = field(node, "predicate");
4014
+ if (predicate !== null) {
4015
+ addAll(condition, expressionDependencies(predicate, [...left.columns, ...right.columns]));
4016
+ }
4017
+ return joinOutput(node, left, right, condition);
4018
+ }
4019
+ function evaluateAnyJoin(node, context) {
4020
+ const [left, right] = evaluateTwoChildren(node, context);
4021
+ const condition = expressionDependencies(field(node, "condition"), [
4022
+ ...left.columns,
4023
+ ...right.columns
4024
+ ]);
4025
+ return joinOutput(node, left, right, condition);
4026
+ }
4027
+ function joinOutput(node, left, right, condition) {
4028
+ const rows = unionOf(left.rows, right.rows, condition);
4029
+ const leftColumns = projectColumns(left.columns, indexListField(node, "left_projection_map"));
4030
+ const rightColumns = projectColumns(right.columns, indexListField(node, "right_projection_map"));
4031
+ const joinType = stringField(node, "join_type");
4032
+ switch (joinType) {
4033
+ case "INNER":
4034
+ case "LEFT":
4035
+ case "RIGHT":
4036
+ case "FULL":
4037
+ case "SINGLE":
4038
+ return { columns: [...leftColumns, ...rightColumns], rows };
4039
+ case "SEMI":
4040
+ case "ANTI":
4041
+ return { columns: leftColumns, rows };
4042
+ case "RIGHT_SEMI":
4043
+ case "RIGHT_ANTI":
4044
+ return { columns: rightColumns, rows };
4045
+ case "MARK": {
4046
+ const mark = {
4047
+ dependencies: condition,
4048
+ binding: bindingKey(indexField(node, "mark_index"), 0)
4049
+ };
4050
+ return { columns: [...leftColumns, mark], rows };
4051
+ }
4052
+ default:
4053
+ reject(`join type '${joinType}' is not supported`);
4054
+ }
4055
+ }
4056
+ function evaluateDelimGet(node, context) {
4057
+ assertLeaf(node);
4058
+ const delimColumns = context.delimColumns.at(-1);
4059
+ if (delimColumns === undefined) {
4060
+ rejectPlanFormat("a DELIM_GET is outside any DELIM_JOIN");
4061
+ }
4062
+ if (listField(node, "chunk_types").length !== delimColumns.length) {
4063
+ rejectPlanFormat("a DELIM_GET does not match its DELIM_JOIN");
4064
+ }
4065
+ return { columns: rebind(delimColumns, indexField(node, "table_index")), rows: new Set };
4066
+ }
4067
+ function evaluateSetOperation(node, context) {
4068
+ const inputs = objectListField(node, "children").map((child) => evaluate(child, context));
4069
+ const width = indexField(node, "column_count");
4070
+ if (inputs.length < 2 || inputs.some((input) => input.columns.length !== width)) {
4071
+ rejectPlanFormat("set operation inputs do not match");
4072
+ }
4073
+ const tableIndex = indexField(node, "table_index");
4074
+ const rows = unionOf(...inputs.map((input) => input.rows));
4075
+ if (stringField(node, "type") === "LOGICAL_UNION") {
4076
+ const columns = Array.from({ length: width }, (_, index) => ({
4077
+ dependencies: unionOf(...inputs.map((input) => itemAt(input.columns, index, "column").dependencies)),
4078
+ binding: bindingKey(tableIndex, index)
4079
+ }));
4080
+ if (!booleanField(node, "setop_all")) {
4081
+ addAll(rows, unionOf(...columns.map((column) => column.dependencies)));
4082
+ }
4083
+ return { columns, rows };
4084
+ }
4085
+ for (const input of inputs) {
4086
+ addAll(rows, unionOf(...input.columns.map((column) => column.dependencies)));
4087
+ }
4088
+ return { columns: rebind(itemAt(inputs, 0, "input").columns, tableIndex), rows };
4089
+ }
4090
+ function evaluateMaterializedCte(node, context) {
4091
+ const [definition, query] = twoChildren(node);
4092
+ context.ctes.set(indexField(node, "table_index"), evaluate(definition, context));
4093
+ return evaluate(query, context);
4094
+ }
4095
+ function evaluateRecursiveCte(node, context) {
4096
+ const [anchorNode, stepNode] = twoChildren(node);
4097
+ const tableIndex = indexField(node, "table_index");
4098
+ const unionAll = booleanField(node, "union_all");
4099
+ const keyTargets = listField(node, "key_targets");
4100
+ let current = evaluate(anchorNode, context);
4101
+ for (let round = 0;round < MAX_RECURSIVE_CTE_ROUNDS; round += 1) {
4102
+ context.ctes.set(tableIndex, current);
4103
+ const step = evaluate(stepNode, context);
4104
+ if (step.columns.length !== current.columns.length) {
4105
+ rejectPlanFormat("recursive CTE branches have different widths");
4106
+ }
4107
+ const columns = current.columns.map((column, index) => ({
4108
+ dependencies: unionOf(column.dependencies, itemAt(step.columns, index, "column").dependencies),
4109
+ binding: bindingKey(tableIndex, index)
4110
+ }));
4111
+ const rows = unionOf(current.rows, step.rows, dependenciesOf(keyTargets, columns));
4112
+ if (!unionAll) {
4113
+ addAll(rows, unionOf(...columns.map((column) => column.dependencies)));
4114
+ }
4115
+ const next = { columns, rows };
4116
+ if (sameRelation(current, next)) {
4117
+ return next;
4118
+ }
4119
+ current = next;
4120
+ }
4121
+ reject("recursive CTE lineage did not converge");
4122
+ }
4123
+ function evaluateCteRef(node, context) {
4124
+ assertLeaf(node);
4125
+ const definition = context.ctes.get(indexField(node, "cte_index"));
4126
+ if (definition === undefined) {
4127
+ rejectPlanFormat("a CTE_REF reads an undefined CTE");
4128
+ }
4129
+ if (listField(node, "chunk_types").length !== definition.columns.length) {
4130
+ rejectPlanFormat("a CTE_REF does not match its CTE");
4131
+ }
4132
+ return {
4133
+ columns: rebind(definition.columns, indexField(node, "table_index")),
4134
+ rows: definition.rows
4135
+ };
4136
+ }
4137
+ function twoChildren(node) {
4138
+ const children = objectListField(node, "children");
4139
+ const [first, second] = children;
4140
+ if (children.length !== 2 || first === undefined || second === undefined) {
4141
+ rejectPlanFormat(`'${stringField(node, "type")}' should have two inputs`);
4142
+ }
4143
+ return [first, second];
4144
+ }
4145
+ function evaluateTwoChildren(node, context) {
4146
+ const [left, right] = twoChildren(node);
4147
+ return [evaluate(left, context), evaluate(right, context)];
4148
+ }
4149
+ function evaluateOnlyChild(node, context) {
4150
+ const children = objectListField(node, "children");
4151
+ const [child] = children;
4152
+ if (children.length !== 1 || child === undefined) {
4153
+ rejectPlanFormat(`'${stringField(node, "type")}' should have one input`);
4154
+ }
4155
+ return evaluate(child, context);
4156
+ }
4157
+ function assertLeaf(node) {
4158
+ if (objectListField(node, "children").length > 0) {
4159
+ rejectPlanFormat(`'${stringField(node, "type")}' should have no input`);
4160
+ }
4161
+ }
4162
+ function projectColumns(columns, projectionMap) {
4163
+ return projectionMap.length === 0 ? columns : projectionMap.map((index) => itemAt(columns, index, "projected column"));
4164
+ }
4165
+ function rebind(columns, tableIndex) {
4166
+ return columns.map((column, index) => ({
4167
+ dependencies: column.dependencies,
4168
+ binding: bindingKey(tableIndex, index)
4169
+ }));
4170
+ }
4171
+ function dependenciesOf(expressions, input) {
4172
+ return unionOf(...expressions.map((expression) => expressionDependencies(expression, input)));
4173
+ }
4174
+ function sameRelation(left, right) {
4175
+ return sameDependencies(left.rows, right.rows) && left.columns.every((column, index) => sameDependencies(column.dependencies, itemAt(right.columns, index, "column").dependencies));
4176
+ }
4177
+ function sortedIds(ids) {
4178
+ return [...ids].sort((left, right) => left - right);
4179
+ }
4180
+
4181
+ // src/internal/sql-lineage/statement-gate.ts
4182
+ function assertSingleSelect(statements) {
4183
+ const [statement] = statements;
4184
+ if (statements.length !== 1 || statement === undefined) {
4185
+ reject("the transform must be exactly one SELECT statement");
4186
+ }
4187
+ visit(statement, (node) => {
4188
+ if (node.type === "PIVOT") {
4189
+ const unpivot = listField(node, "unpivot_names").length > 0;
4190
+ reject(`${unpivot ? "UNPIVOT" : "PIVOT"} is not supported`);
4191
+ }
4192
+ const unread = readsBeyondSources(node);
4193
+ if (unread)
4194
+ reject(unread);
4195
+ });
4196
+ }
4197
+ function readsOnlySources(statements, sourceTables, builtinFunctions) {
4198
+ const [statement] = statements;
4199
+ if (statements.length !== 1 || statement === undefined)
4200
+ return false;
4201
+ let only = true;
4202
+ visit(statement, (node, ctes) => {
4203
+ if (readsBeyondSources(node))
4204
+ only = false;
4205
+ else if (node.type === "BASE_TABLE" && !readsSourceOrCte(node, sourceTables, ctes))
4206
+ only = false;
4207
+ else if (callsUnknownFunction(node, builtinFunctions))
4208
+ only = false;
4209
+ });
4210
+ return only;
4211
+ }
4212
+ function readsBeyondSources(node) {
4213
+ if (node.type === "SHOW_REF") {
4214
+ return "DESCRIBE, SHOW and SUMMARIZE are not supported";
4215
+ }
4216
+ if (node.type === "TABLE_FUNCTION") {
4217
+ const name = stringField(objectField(node, "function"), "function_name");
4218
+ if (!PURE_TABLE_FUNCTIONS.has(name))
4219
+ return `table function '${name}' is not supported`;
4220
+ }
4221
+ if (node.class === "PARAMETER") {
4222
+ return "query parameters are not supported";
4223
+ }
4224
+ return;
4225
+ }
4226
+ function readsSourceOrCte(table, sourceTables, ctes) {
4227
+ if (qualified(table, "schema_name") || qualified(table, "catalog_name"))
4228
+ return false;
4229
+ const name = stringField(table, "table_name");
4230
+ return sourceTables.has(name) || ctes.has(name.toLowerCase());
4231
+ }
4232
+ function callsUnknownFunction(node, builtinFunctions) {
4233
+ if (node.class !== "FUNCTION" && node.class !== "WINDOW")
4234
+ return false;
4235
+ if (qualified(node, "catalog") || qualified(node, "schema") && node.schema !== "main") {
4236
+ return true;
4237
+ }
4238
+ const name = node.function_name;
4239
+ return typeof name !== "string" || !builtinFunctions.has(name.toLowerCase());
4240
+ }
4241
+ function qualified(node, key) {
4242
+ const value = node[key];
4243
+ return typeof value === "string" && value !== "";
4244
+ }
4245
+ function visit(value, check, ctes = new Set) {
4246
+ if (Array.isArray(value)) {
4247
+ for (const item of value)
4248
+ visit(item, check, ctes);
4249
+ return;
4250
+ }
4251
+ if (!isJsonObject(value))
4252
+ return;
4253
+ const entries = cteEntries(value);
4254
+ const defined = new Set(ctes);
4255
+ for (const { name, query } of entries) {
4256
+ visit(query, check, new Set(defined));
4257
+ defined.add(name);
4258
+ }
4259
+ check(value, defined);
4260
+ const recursive = value.type === "RECURSIVE_CTE_NODE" && typeof value.cte_name === "string" ? new Set([...defined, value.cte_name.toLowerCase()]) : undefined;
4261
+ for (const [key, child] of Object.entries(value)) {
4262
+ if (key === "cte_map" && entries.length > 0)
4263
+ continue;
4264
+ visit(child, check, key === "right" && recursive ? recursive : defined);
4265
+ }
4266
+ }
4267
+ function cteEntries(node) {
4268
+ const map = node.cte_map;
4269
+ if (!isJsonObject(map) || !Array.isArray(map.map))
4270
+ return [];
4271
+ const entries = [];
4272
+ for (const entry of map.map) {
4273
+ if (!isJsonObject(entry) || typeof entry.key !== "string" || !isJsonObject(entry.value)) {
4274
+ return [];
4275
+ }
4276
+ entries.push({ name: entry.key.toLowerCase(), query: entry.value.query });
4277
+ }
4278
+ return entries;
4279
+ }
4280
+
4281
+ // src/internal/sql-lineage/analysis.ts
4282
+ var ISOLATED_INSTANCE_CONFIG = {
4283
+ enable_external_access: "false",
4284
+ autoinstall_known_extensions: "false",
4285
+ autoload_known_extensions: "false"
4286
+ };
4287
+ async function analyzeSqlTransformLineage(input) {
4288
+ const tables = analysisTables(input.sources);
4289
+ const sql = input.template.render(new Map(tables.map((table) => [table.sourceName, table.relationSql])));
4290
+ try {
4291
+ return await withAnalysisConnection(tables, async (connection) => {
4292
+ const statements = await serialize(connection, `json_serialize_sql(${quoteSqlString(sql)}, skip_null := false, skip_empty := false)`, "statements", "DuckDB could not parse the transform");
4293
+ try {
4294
+ return { supported: true, ...await analyzeSql(connection, sql, statements, tables) };
4295
+ } catch (error) {
4296
+ const sourceTables = new Set(tables.map((table) => table.tableName));
4297
+ const functions = await builtinFunctions(connection).catch(() => {
4298
+ return;
4299
+ });
4300
+ return unsupported(error, functions !== undefined && readsOnlySources(statements, sourceTables, functions));
4301
+ }
4302
+ });
4303
+ } catch (error) {
4304
+ return unsupported(error, false);
4305
+ }
4306
+ }
4307
+ var builtins;
4308
+ function builtinFunctions(connection) {
4309
+ if (builtins)
4310
+ return builtins;
4311
+ const read = connection.runAndReadAll("SELECT DISTINCT lower(function_name) FROM duckdb_functions()").then((reader) => new Set(reader.getRows().map(([name]) => String(name))));
4312
+ builtins = read;
4313
+ read.catch(() => {
4314
+ if (builtins === read)
4315
+ builtins = undefined;
4316
+ });
4317
+ return read;
4318
+ }
4319
+ function unsupported(error, readsOnlySources) {
4320
+ return {
4321
+ supported: false,
4322
+ reason: error instanceof Error ? error.message : String(error),
4323
+ readsOnlySources
4324
+ };
4325
+ }
4326
+ function analysisTables(sources) {
4327
+ const token = randomUUID4().replaceAll("-", "");
4328
+ return Object.entries(sources).map(([sourceName, source], index) => {
4329
+ const tableName = `sixb_lineage_${token}_${index}`;
4330
+ return {
4331
+ sourceName,
4332
+ tableName,
4333
+ columns: pickSourceColumns(sourceName, source.dataset, source.columns),
4334
+ relationSql: renderSourceRelation(quoteIdentifier(tableName), source.columns)
4335
+ };
4336
+ });
4337
+ }
4338
+ async function withAnalysisConnection(tables, use) {
4339
+ const instance = await DuckDBInstance2.create(":memory:", ISOLATED_INSTANCE_CONFIG);
4340
+ try {
4341
+ const connection = await instance.connect();
4342
+ try {
4343
+ for (const table of tables) {
4344
+ const columnsSql = table.columns.map((column) => datasetColumnToDuckDbSql(column));
4345
+ await connection.run(`CREATE TABLE ${quoteIdentifier(table.tableName)} (${columnsSql.join(", ")})`);
4346
+ }
4347
+ await connection.run("SET lock_configuration = true");
4348
+ return await use(connection);
4349
+ } finally {
4350
+ connection.closeSync();
4351
+ }
4352
+ } finally {
4353
+ instance.closeSync();
4354
+ }
4355
+ }
4356
+ async function analyzeSql(connection, sql, statements, tables) {
4357
+ assertSingleSelect(statements);
4358
+ const plans = await serialize(connection, `json_serialize_plan(${quoteSqlString(sql)}, skip_null := false, skip_empty := false, optimize := false)`, "plans", "DuckDB could not bind the transform");
4359
+ if (plans.length !== 1) {
4360
+ rejectPlanFormat(`expected one plan, got ${plans.length}`);
4361
+ }
4362
+ const sourceColumns = numberSourceColumns(tables);
4363
+ const lineage = planLineage(plans[0], sourceColumns.ids);
4364
+ const names = await outputColumnNames(connection, sql);
4365
+ if (lineage.columns.length !== names.length) {
4366
+ rejectPlanFormat(`the plan has ${lineage.columns.length} columns, the result ${names.length}`);
4367
+ }
4368
+ const refs = (ids) => ids.map((id) => sourceColumns.refs[id]);
4369
+ return {
4370
+ lineage: {
4371
+ rows: refs(lineage.rows),
4372
+ columns: Object.fromEntries(names.map((name, index) => [name, refs(lineage.columns[index])]))
4373
+ },
4374
+ outputColumns: names
4375
+ };
4376
+ }
4377
+ function numberSourceColumns(tables) {
4378
+ const refs = [];
4379
+ const ids = new Map(tables.map((table) => {
4380
+ const columnIds = table.columns.map((column) => {
4381
+ refs.push({ source: table.sourceName, column: column.name });
4382
+ return [column.name, refs.length - 1];
4383
+ });
4384
+ return [table.tableName, new Map(columnIds)];
4385
+ }));
4386
+ return { ids, refs };
4387
+ }
4388
+ async function outputColumnNames(connection, sql) {
4389
+ const prepared = await connection.prepare(`SELECT * FROM (${sql}) AS sixb_lineage_result`);
4390
+ try {
4391
+ return Array.from({ length: prepared.columnCount }, (_, index) => prepared.columnName(index));
4392
+ } finally {
4393
+ prepared.destroySync();
4394
+ }
4395
+ }
4396
+ async function serialize(connection, serializerSql, listKey, failure) {
4397
+ const reader = await connection.runAndReadAll(`SELECT ${serializerSql}`);
4398
+ const text = reader.getRows()[0]?.[0];
4399
+ if (typeof text !== "string") {
4400
+ rejectPlanFormat("the serializer returned no JSON");
4401
+ }
4402
+ return parseSerializerOutput(text, listKey, failure);
3011
4403
  }
3012
4404
 
3013
4405
  // src/internal/ducklake-sql-executor.ts
@@ -3035,13 +4427,10 @@ class DuckLakeSqlExecutor {
3035
4427
  }
3036
4428
  async* preview(input) {
3037
4429
  this.connections.assertOpen();
4430
+ const template = createDuckLakeSqlTransformTemplate(Object.keys(input.sources), input.sql);
3038
4431
  const rows = await this.connections.withAttachedRuntime(async (runtime) => {
3039
4432
  const sources = await this.resolveSources(runtime, input.sources);
3040
- const sql = renderDuckLakeSqlTransformSql({
3041
- options: this.options,
3042
- sources: sources.relations,
3043
- sql: input.sql
3044
- });
4433
+ const sql = template.render(renderDuckLakeSourceRelations(this.options, sources.relations));
3045
4434
  return runtime.query(buildPreviewSql(sql, previewLimit(input.limit)));
3046
4435
  });
3047
4436
  for (const row of rows) {
@@ -3051,15 +4440,16 @@ class DuckLakeSqlExecutor {
3051
4440
  async execute(input) {
3052
4441
  this.connections.assertOpen();
3053
4442
  assertDatasetWriteMode(input.mode, "SQL transform");
4443
+ const template = createDuckLakeSqlTransformTemplate(Object.keys(input.sources), input.sql);
3054
4444
  return this.writes.withCommitRuntime(async (runtime) => {
3055
4445
  const target = await this.resolveTargetDataset(runtime, input.target);
3056
4446
  const sources = await this.resolveSources(runtime, input.sources);
3057
- const sql = renderDuckLakeSqlTransformSql({
3058
- options: this.options,
3059
- sources: sources.relations,
3060
- sql: input.sql
3061
- });
3062
- const checkedCasts = await this.assertResultSchemaMatchesTarget(runtime, target, sql);
4447
+ const sql = template.render(renderDuckLakeSourceRelations(this.options, sources.relations));
4448
+ const sourceVersions = await this.readSourceVersions(runtime, sources).catch((error) => skipLineage(target, error));
4449
+ const [checkedCasts, stepLineage] = await Promise.all([
4450
+ this.assertResultSchemaMatchesTarget(runtime, target, sql),
4451
+ sourceVersions === undefined ? undefined : analyzeExecutedLineage(template, pinnedSources(sources, sourceVersions), sources.relations, target)
4452
+ ]);
3063
4453
  return this.writes.commitDatasetVersionOnExclusiveRuntime(runtime, {
3064
4454
  dataset: target,
3065
4455
  mode: input.mode,
@@ -3067,6 +4457,7 @@ class DuckLakeSqlExecutor {
3067
4457
  commitMessage: input.commitMessage ?? `execute SQL transform for dataset ${target.id}`,
3068
4458
  producer: input.producer,
3069
4459
  inputs: sources.inputs,
4460
+ lineage: stepLineage === undefined || sourceVersions === undefined ? unanalyzedLineage(target, input.producer) : resolveStepLineage(stepLineage, [...sourceVersions.values()]),
3070
4461
  applyChanges: (runtime, context) => this.applySqlTransform({
3071
4462
  runtime,
3072
4463
  target,
@@ -3078,16 +4469,30 @@ class DuckLakeSqlExecutor {
3078
4469
  });
3079
4470
  });
3080
4471
  }
4472
+ async analyze(input) {
4473
+ for (const source of Object.values(input.sources)) {
4474
+ this.datasets.assertSchema(source.dataset);
4475
+ }
4476
+ const analysis = await analyzeSqlTransformLineage({
4477
+ sources: input.sources,
4478
+ template: createDuckLakeSqlTransformTemplate(Object.keys(input.sources), input.sql)
4479
+ });
4480
+ return analysis.supported ? { supported: true, lineage: analysis.lineage } : { supported: false, reason: analysis.reason };
4481
+ }
3081
4482
  async resolveSources(runtime, sources) {
3082
4483
  const relations = Object.create(null);
4484
+ const tables = Object.create(null);
3083
4485
  const inputs = [];
3084
4486
  for (const [sourceName, source] of Object.entries(sources)) {
3085
4487
  const definition = await this.resolveSourceDataset(runtime, source.dataset);
4488
+ pickSourceColumns(sourceName, definition, source.columns);
3086
4489
  const versionRef = await this.resolveSourceVersion(runtime, definition, source.versionId);
3087
4490
  relations[sourceName] = {
3088
4491
  datasetId: definition.id,
3089
- versionId: versionRef.versionId
4492
+ versionId: versionRef.versionId,
4493
+ ...source.columns === undefined ? {} : { columns: source.columns }
3090
4494
  };
4495
+ tables[sourceName] = { definition, version: versionRef };
3091
4496
  inputs.push({
3092
4497
  datasetId: definition.id,
3093
4498
  versionId: versionRef.versionId
@@ -3095,24 +4500,30 @@ class DuckLakeSqlExecutor {
3095
4500
  }
3096
4501
  return {
3097
4502
  relations: Object.freeze(relations),
4503
+ tables: Object.freeze(tables),
3098
4504
  inputs: Object.freeze(inputs)
3099
4505
  };
3100
4506
  }
3101
4507
  async resolveSourceDataset(runtime, dataset) {
3102
4508
  const definition = await this.datasets.getDatasetOnRuntime(runtime, dataset.id);
3103
4509
  if (!definition) {
3104
- throw new LakeStorageError20(`[SixbDuckLake] Unknown SQL transform source dataset '${dataset.id}'.`);
4510
+ throw new LakeStorageError22(`[SixbDuckLake] Unknown SQL transform source dataset '${dataset.id}'.`);
3105
4511
  }
3106
4512
  return definition;
3107
4513
  }
3108
4514
  async resolveTargetDataset(runtime, dataset) {
3109
4515
  const definition = await this.datasets.getDatasetOnRuntime(runtime, dataset.id);
3110
4516
  if (!definition) {
3111
- throw new LakeStorageError20(`[SixbDuckLake] Unknown SQL transform target dataset '${dataset.id}'.`);
4517
+ throw new LakeStorageError22(`[SixbDuckLake] Unknown SQL transform target dataset '${dataset.id}'.`);
3112
4518
  }
3113
4519
  this.datasets.assertSchema(definition);
3114
4520
  return definition;
3115
4521
  }
4522
+ async readSourceVersions(runtime, sources) {
4523
+ const names = Object.keys(sources.tables);
4524
+ const versions = await this.snapshots.getVersionsForRefs(runtime, names.map((sourceName) => sources.tables[sourceName].version));
4525
+ return new Map(names.map((sourceName, index) => [sourceName, versions[index]]));
4526
+ }
3116
4527
  async resolveSourceVersion(runtime, dataset, versionId) {
3117
4528
  if (versionId !== undefined) {
3118
4529
  const snapshotId = parseVersionId(versionId);
@@ -3158,7 +4569,7 @@ class DuckLakeSqlExecutor {
3158
4569
  return checkedCasts;
3159
4570
  }
3160
4571
  async applySqlTransform(input) {
3161
- const tempTableName = `sixb_sql_transform_${randomUUID4().replaceAll("-", "")}`;
4572
+ const tempTableName = `sixb_sql_transform_${randomUUID5().replaceAll("-", "")}`;
3162
4573
  const tempTable = quoteIdentifier(tempTableName);
3163
4574
  await input.runtime.run(`CREATE TEMP TABLE ${tempTable} AS SELECT * FROM (${input.sql}) AS sixb_sql_transform_result`);
3164
4575
  let result;
@@ -3184,6 +4595,62 @@ class DuckLakeSqlExecutor {
3184
4595
  return result;
3185
4596
  }
3186
4597
  }
4598
+ function pinnedSources(sources, versions) {
4599
+ const pinned = Object.create(null);
4600
+ for (const [sourceName, version] of versions) {
4601
+ pinned[sourceName] = {
4602
+ dataset: { ...sources.tables[sourceName].definition, schema: version.schema },
4603
+ columns: sources.relations[sourceName].columns
4604
+ };
4605
+ }
4606
+ return pinned;
4607
+ }
4608
+ async function analyzeExecutedLineage(template, pinnedSources, relations, target) {
4609
+ let analysis;
4610
+ try {
4611
+ analysis = await analyzeSqlTransformLineage({ sources: pinnedSources, template });
4612
+ } catch (error) {
4613
+ return skipLineage(target, error);
4614
+ }
4615
+ if (!analysis.supported) {
4616
+ return analysis.readsOnlySources ? toDatasetColumnLineage(everySourceColumn(pinnedSources, target), relations) : undefined;
4617
+ }
4618
+ if (!describesTarget(analysis.outputColumns, target))
4619
+ return;
4620
+ return toDatasetColumnLineage(analysis.lineage, relations);
4621
+ }
4622
+ function everySourceColumn(sources, target) {
4623
+ const rows = Object.entries(sources).flatMap(([source, { dataset, columns }]) => (columns ?? dataset.schema.columns.map((column) => column.name)).map((column) => ({
4624
+ source,
4625
+ column
4626
+ })));
4627
+ return { rows, columns: Object.fromEntries(target.schema.columns.map(({ name }) => [name, []])) };
4628
+ }
4629
+ function unanalyzedLineage(target, producer) {
4630
+ return producer?.kind === "pipeline" ? undefined : { unknown: `a SQL transform into '${target.id}' could not be analyzed` };
4631
+ }
4632
+ function skipLineage(target, error) {
4633
+ const message = error instanceof Error ? error.message : String(error);
4634
+ console.warn(`[SixbDuckLake] SQL transform into dataset '${target.id}' records no column lineage: ${message}`);
4635
+ return;
4636
+ }
4637
+ function describesTarget(outputColumns, target) {
4638
+ return outputColumns.length === target.schema.columns.length && target.schema.columns.every((column, index) => outputColumns[index] === column.name);
4639
+ }
4640
+ function toDatasetColumnLineage(lineage, relations) {
4641
+ const toRefs = (refs) => {
4642
+ const unique = new Map;
4643
+ for (const { source, column } of refs) {
4644
+ const datasetId = relations[source].datasetId;
4645
+ unique.set(JSON.stringify([datasetId, column]), { datasetId, column });
4646
+ }
4647
+ return [...unique.values()];
4648
+ };
4649
+ return {
4650
+ rows: toRefs(lineage.rows),
4651
+ columns: Object.fromEntries(Object.entries(lineage.columns).map(([column, refs]) => [column, toRefs(refs)]))
4652
+ };
4653
+ }
3187
4654
  function previewLimit(limit) {
3188
4655
  if (limit === undefined || !Number.isFinite(limit)) {
3189
4656
  return DEFAULT_PREVIEW_LIMIT;
@@ -3229,33 +4696,34 @@ function normalizePreviewValue(value) {
3229
4696
  return value;
3230
4697
  }
3231
4698
  function throwNoCommittedSourceVersion(datasetId) {
3232
- throw new LakeStorageError20(`[SixbDuckLake] No committed version found for SQL transform source dataset '${datasetId}'.`);
4699
+ throw new LakeStorageError22(`[SixbDuckLake] No committed version found for SQL transform source dataset '${datasetId}'.`);
3233
4700
  }
3234
4701
  function throwResultSchemaMismatch(datasetId, detail) {
3235
- throw new LakeStorageError20(`[SixbDuckLake] SQL transform result schema does not match target dataset '${datasetId}': ${detail}.`);
4702
+ throw new LakeStorageError22(`[SixbDuckLake] SQL transform result schema does not match target dataset '${datasetId}': ${detail}.`);
3236
4703
  }
3237
4704
 
3238
4705
  // src/internal/ducklake-write-coordinator.ts
3239
- import { randomUUID as randomUUID7 } from "crypto";
4706
+ import { randomUUID as randomUUID8 } from "crypto";
3240
4707
  import {
3241
4708
  assertUnsequencedDatasetWrite,
3242
4709
  getDatasetPrimaryKeyColumns as getDatasetPrimaryKeyColumns4,
3243
- hasDatasetInputChanges,
3244
- LakeConcurrencyError,
3245
- LakeStorageError as LakeStorageError24,
4710
+ hasDatasetProvenanceChanges,
4711
+ LakeConcurrencyError as LakeConcurrencyError2,
4712
+ LakeStorageError as LakeStorageError26,
4713
+ lineageAtCommit as lineageAtCommit2,
3246
4714
  retryDatasetMergeCommit
3247
4715
  } from "@sixb/core/lake-storage";
3248
4716
 
3249
4717
  // src/internal/dataset-row-merge.ts
3250
4718
  import {
3251
4719
  getDatasetPrimaryKeyColumns as getDatasetPrimaryKeyColumns2,
3252
- LakeStorageError as LakeStorageError21,
4720
+ LakeStorageError as LakeStorageError23,
3253
4721
  reconcileDatasetSequences
3254
4722
  } from "@sixb/core/lake-storage";
3255
4723
  async function applyDatasetMergeFromRelation(input) {
3256
4724
  const primaryKeyColumns = getDatasetPrimaryKeyColumns2(input.dataset);
3257
4725
  if (primaryKeyColumns === null) {
3258
- throw new LakeStorageError21(`[SixbDuckLake] Dataset '${input.dataset.id}' must define a primaryKey before it can be merged.`);
4726
+ throw new LakeStorageError23(`[SixbDuckLake] Dataset '${input.dataset.id}' must define a primaryKey before it can be merged.`);
3259
4727
  }
3260
4728
  const stagingTable = quoteIdentifier(input.stagingTableName);
3261
4729
  const targetTable = qualifiedTableName(input.options, encodeDatasetTableName(input.dataset.id));
@@ -3268,7 +4736,9 @@ async function applyDatasetMergeFromRelation(input) {
3268
4736
  return {
3269
4737
  dataChangeExpected: orderingChanged,
3270
4738
  sourceRowCount,
3271
- resultingRowCount: { kind: "exact", value: previousRowCount }
4739
+ resultingRowCount: { kind: "exact", value: previousRowCount },
4740
+ targetWasEmpty: previousRowCount === 0,
4741
+ changedTableRows: false
3272
4742
  };
3273
4743
  }
3274
4744
  const keyMatchSql = primaryKeyMatchSql(primaryKeyColumns, "target", "source");
@@ -3307,7 +4777,9 @@ async function applyDatasetMergeFromRelation(input) {
3307
4777
  resultingRowCount: {
3308
4778
  kind: "exact",
3309
4779
  value: previousRowCount + effects.inserts - effects.deletes
3310
- }
4780
+ },
4781
+ targetWasEmpty: previousRowCount === 0,
4782
+ changedTableRows: true
3311
4783
  };
3312
4784
  });
3313
4785
  }
@@ -3416,7 +4888,7 @@ function finalChangesSelectSql(input, primaryKeyColumns) {
3416
4888
  async function countMergeEffects(input, targetTable, finalChanges, primaryKeyColumns) {
3417
4889
  const matchColumn = primaryKeyColumns[0];
3418
4890
  if (matchColumn === undefined) {
3419
- throw new LakeStorageError21(`[SixbDuckLake] Dataset '${input.dataset.id}' primaryKey must contain at least one column.`);
4891
+ throw new LakeStorageError23(`[SixbDuckLake] Dataset '${input.dataset.id}' primaryKey must contain at least one column.`);
3420
4892
  }
3421
4893
  const keyMatchSql = primaryKeyMatchSql(primaryKeyColumns, "target", "source");
3422
4894
  const targetMatch = `target.${quoteIdentifier(matchColumn)} IS NOT NULL`;
@@ -3465,12 +4937,12 @@ function sameColumns(left, right) {
3465
4937
  }
3466
4938
 
3467
4939
  // src/internal/ducklake-merge-session.ts
3468
- import { randomUUID as randomUUID5 } from "crypto";
4940
+ import { randomUUID as randomUUID6 } from "crypto";
3469
4941
  import {
3470
4942
  cloneDatasetMergeChange,
3471
4943
  datasetSequenceChange,
3472
4944
  getDatasetMergeChangeValidationError,
3473
- LakeStorageError as LakeStorageError22
4945
+ LakeStorageError as LakeStorageError24
3474
4946
  } from "@sixb/core/lake-storage";
3475
4947
 
3476
4948
  // src/internal/row-appender.ts
@@ -3529,7 +5001,7 @@ function fileRefToDuckDbStruct(fileRef) {
3529
5001
  // src/internal/ducklake-merge-session.ts
3530
5002
  var MERGE_CHANGE_BATCH_SIZE = 1000;
3531
5003
  async function createDuckLakeMergeSession(input) {
3532
- const suffix = randomUUID5().replaceAll("-", "");
5004
+ const suffix = randomUUID6().replaceAll("-", "");
3533
5005
  const stagingTableName = `sixb_merge_${suffix}`;
3534
5006
  const sequenceColumnName = `sixb_merge_sequence_${suffix}`;
3535
5007
  const kindColumnName = `sixb_merge_kind_${suffix}`;
@@ -3571,7 +5043,7 @@ class DuckLakeMergeSession {
3571
5043
  for await (const change of changes) {
3572
5044
  const validationError = getDatasetMergeChangeValidationError(change, this.input.dataset);
3573
5045
  if (validationError) {
3574
- throw new LakeStorageError22(`[SixbDuckLake] ${validationError}`);
5046
+ throw new LakeStorageError24(`[SixbDuckLake] ${validationError}`);
3575
5047
  }
3576
5048
  batch.push({
3577
5049
  sequence: this.nextSequence,
@@ -3634,7 +5106,7 @@ class DuckLakeMergeSession {
3634
5106
  }
3635
5107
  assertOpen() {
3636
5108
  if (this.closed) {
3637
- throw new LakeStorageError22("[SixbDuckLake] Merge session is already closed.");
5109
+ throw new LakeStorageError24("[SixbDuckLake] Merge session is already closed.");
3638
5110
  }
3639
5111
  }
3640
5112
  async cleanup() {
@@ -3649,16 +5121,16 @@ class DuckLakeMergeSession {
3649
5121
  }
3650
5122
 
3651
5123
  // src/internal/ducklake-write-session.ts
3652
- import { randomUUID as randomUUID6 } from "crypto";
5124
+ import { randomUUID as randomUUID7 } from "crypto";
3653
5125
  import { getDatasetRowValidationError } from "@sixb/core";
3654
5126
  import {
3655
5127
  encodeDatasetPrimaryKey,
3656
5128
  getDatasetPrimaryKeyColumns as getDatasetPrimaryKeyColumns3,
3657
- LakeStorageError as LakeStorageError23
5129
+ LakeStorageError as LakeStorageError25
3658
5130
  } from "@sixb/core/lake-storage";
3659
5131
  var WRITE_ROW_BATCH_SIZE = 1000;
3660
5132
  async function createDuckLakeWriteSession(input) {
3661
- const stagingTableName = `sixb_write_${randomUUID6().replaceAll("-", "")}`;
5133
+ const stagingTableName = `sixb_write_${randomUUID7().replaceAll("-", "")}`;
3662
5134
  await input.runtime.run(`CREATE TEMP TABLE ${quoteIdentifier(stagingTableName)} (${datasetSchemaToDuckDbColumnsSql(input.write.dataset.schema)})`);
3663
5135
  return new DuckLakeWriteSession(input.commitWrite, input.runtime, input.write, stagingTableName);
3664
5136
  }
@@ -3685,13 +5157,13 @@ class DuckLakeWriteSession {
3685
5157
  for await (const row of rows) {
3686
5158
  const validationError = getDatasetRowValidationError(row, this.input.dataset);
3687
5159
  if (validationError) {
3688
- throw new LakeStorageError23(`[SixbDuckLake] ${validationError}`);
5160
+ throw new LakeStorageError25(`[SixbDuckLake] ${validationError}`);
3689
5161
  }
3690
5162
  const clonedRow = structuredClone(row);
3691
5163
  if (getDatasetPrimaryKeyColumns3(this.input.dataset) !== null) {
3692
5164
  const primaryKey = encodeDatasetPrimaryKey(this.input.dataset, clonedRow);
3693
5165
  if (this.stagedPrimaryKeys.has(primaryKey) || batchPrimaryKeys.has(primaryKey)) {
3694
- throw new LakeStorageError23(`[SixbDuckLake] Dataset '${this.input.dataset.id}' ${this.input.mode ?? "snapshot"} source contains duplicate primary key.`);
5166
+ throw new LakeStorageError25(`[SixbDuckLake] Dataset '${this.input.dataset.id}' ${this.input.mode ?? "snapshot"} source contains duplicate primary key.`);
3695
5167
  }
3696
5168
  batchPrimaryKeys.add(primaryKey);
3697
5169
  }
@@ -3748,7 +5220,7 @@ class DuckLakeWriteSession {
3748
5220
  }
3749
5221
  assertOpen() {
3750
5222
  if (this.closed) {
3751
- throw new LakeStorageError23("[SixbDuckLake] Write session is already closed.");
5223
+ throw new LakeStorageError25("[SixbDuckLake] Write session is already closed.");
3752
5224
  }
3753
5225
  }
3754
5226
  async cleanup() {
@@ -3782,7 +5254,7 @@ class DuckLakeWriteCoordinator {
3782
5254
  this.connections.assertOpen();
3783
5255
  const definition = await this.datasets.getDataset(input.dataset.id);
3784
5256
  if (!definition) {
3785
- throw new LakeStorageError24(`[SixbDuckLake] Unknown dataset '${input.dataset.id}'.`);
5257
+ throw new LakeStorageError26(`[SixbDuckLake] Unknown dataset '${input.dataset.id}'.`);
3786
5258
  }
3787
5259
  this.datasets.assertSchema(definition);
3788
5260
  assertUnsequencedDatasetWrite(definition);
@@ -3803,15 +5275,15 @@ class DuckLakeWriteCoordinator {
3803
5275
  this.connections.assertOpen();
3804
5276
  const definition = await this.datasets.getDataset(input.dataset.id);
3805
5277
  if (!definition) {
3806
- throw new LakeStorageError24(`[SixbDuckLake] Unknown dataset '${input.dataset.id}'.`);
5278
+ throw new LakeStorageError26(`[SixbDuckLake] Unknown dataset '${input.dataset.id}'.`);
3807
5279
  }
3808
5280
  if (getDatasetPrimaryKeyColumns4(definition) === null) {
3809
- throw new LakeStorageError24(`[SixbDuckLake] Dataset '${definition.id}' must define a primaryKey before it can be merged.`);
5281
+ throw new LakeStorageError26(`[SixbDuckLake] Dataset '${definition.id}' must define a primaryKey before it can be merged.`);
3810
5282
  }
3811
5283
  this.datasets.assertSchema(definition);
3812
5284
  const latestVersion = await this.connections.withAttachedRuntime((runtime) => this.snapshots.getLatestVersionForDefinition(runtime, definition));
3813
5285
  if (input.expectedLatestVersionId !== undefined && latestVersion?.versionId !== input.expectedLatestVersionId) {
3814
- throw new LakeConcurrencyError(`[SixbDuckLake] Optimistic merge start failed for dataset '${definition.id}': expected latest version '${input.expectedLatestVersionId}', found '${latestVersion?.versionId ?? "none"}'.`);
5286
+ throw new LakeConcurrencyError2(`[SixbDuckLake] Optimistic merge start failed for dataset '${definition.id}': expected latest version '${input.expectedLatestVersionId}', found '${latestVersion?.versionId ?? "none"}'.`);
3815
5287
  }
3816
5288
  const runtime = await this.connections.stagingRuntime();
3817
5289
  return createDuckLakeMergeSession({
@@ -3824,7 +5296,7 @@ class DuckLakeWriteCoordinator {
3824
5296
  async commitWrite(input) {
3825
5297
  const definition = await this.datasets.getDataset(input.write.dataset.id);
3826
5298
  if (!definition) {
3827
- throw new LakeStorageError24(`[SixbDuckLake] Unknown dataset '${input.write.dataset.id}'.`);
5299
+ throw new LakeStorageError26(`[SixbDuckLake] Unknown dataset '${input.write.dataset.id}'.`);
3828
5300
  }
3829
5301
  const mode = input.write.mode ?? "snapshot";
3830
5302
  assertDatasetWriteMode(mode, "write");
@@ -3835,6 +5307,7 @@ class DuckLakeWriteCoordinator {
3835
5307
  commitMessage: input.commit?.commitMessage ?? `write dataset ${input.write.dataset.id}`,
3836
5308
  producer: input.write.producer,
3837
5309
  inputs: input.write.inputs,
5310
+ lineage: input.write.lineage,
3838
5311
  applyChanges: (runtime, context) => this.applyStagedRows({
3839
5312
  ...input,
3840
5313
  runtime,
@@ -3845,10 +5318,10 @@ class DuckLakeWriteCoordinator {
3845
5318
  async commitMerge(input) {
3846
5319
  const definition = await this.datasets.getDataset(input.merge.dataset.id);
3847
5320
  if (!definition) {
3848
- throw new LakeStorageError24(`[SixbDuckLake] Unknown dataset '${input.merge.dataset.id}'.`);
5321
+ throw new LakeStorageError26(`[SixbDuckLake] Unknown dataset '${input.merge.dataset.id}'.`);
3849
5322
  }
3850
5323
  if (getDatasetPrimaryKeyColumns4(definition) === null) {
3851
- throw new LakeStorageError24(`[SixbDuckLake] Dataset '${definition.id}' must define a primaryKey before it can be merged.`);
5324
+ throw new LakeStorageError26(`[SixbDuckLake] Dataset '${definition.id}' must define a primaryKey before it can be merged.`);
3852
5325
  }
3853
5326
  return retryDatasetMergeCommit(input.merge, input.commit, (rebase) => this.withCommitRuntime(async (runtime) => {
3854
5327
  input.commit?.signal?.throwIfAborted();
@@ -3859,7 +5332,6 @@ class DuckLakeWriteCoordinator {
3859
5332
  expectedLatestVersionId: baseVersionId,
3860
5333
  allowInitialNoOp: !input.commit?.createInitialVersion,
3861
5334
  signal: input.commit?.signal,
3862
- disableRetries: true,
3863
5335
  optimisticOperation: "merge commit",
3864
5336
  commitMessage: input.commit?.commitMessage ?? `merge dataset ${definition.id}`,
3865
5337
  producer: input.merge.producer,
@@ -3876,7 +5348,7 @@ class DuckLakeWriteCoordinator {
3876
5348
  validatedPrimaryKeyColumns: context.validatedPrimaryKeyColumns
3877
5349
  });
3878
5350
  if (result.sourceRowCount !== input.changesWritten) {
3879
- throw new LakeStorageError24(`[SixbDuckLake] Staged merge for dataset '${definition.id}' accepted ${input.changesWritten} change(s), but DuckLake saw ${result.sourceRowCount} source change(s) at commit time.`);
5351
+ throw new LakeStorageError26(`[SixbDuckLake] Staged merge for dataset '${definition.id}' accepted ${input.changesWritten} change(s), but DuckLake saw ${result.sourceRowCount} source change(s) at commit time.`);
3880
5352
  }
3881
5353
  return result;
3882
5354
  }
@@ -3893,39 +5365,14 @@ class DuckLakeWriteCoordinator {
3893
5365
  }
3894
5366
  async commitDatasetVersionOnExclusiveRuntime(runtime, input) {
3895
5367
  assertUnsequencedDatasetWrite(input.dataset);
3896
- const result = await this.commitVersionOutcomeOnExclusiveRuntime(runtime, {
3897
- ...input,
3898
- disableRetries: input.mode === "append" && getDatasetPrimaryKeyColumns4(input.dataset) !== null
3899
- });
5368
+ const result = await this.commitVersionOutcomeOnExclusiveRuntime(runtime, input);
3900
5369
  if (result.version === null) {
3901
- throw new LakeStorageError24(`[SixbDuckLake] No DuckLake changes were committed for dataset '${input.dataset.id}', and no previous version exists.`);
5370
+ throw new LakeStorageError26(`[SixbDuckLake] No DuckLake changes were committed for dataset '${input.dataset.id}', and no previous version exists.`);
3902
5371
  }
3903
5372
  return { ...result.version, outcome: result.outcome };
3904
5373
  }
3905
5374
  async commitVersionOutcomeOnExclusiveRuntime(runtime, input) {
3906
- const runtimeInput = { ...input, runtime };
3907
- return this.withGuardedRetrySetting(runtimeInput, () => this.commitDatasetVersionUnlocked(runtimeInput));
3908
- }
3909
- async withGuardedRetrySetting(input, run) {
3910
- if (input.expectedLatestVersionId === undefined && !input.disableRetries) {
3911
- return run();
3912
- }
3913
- await input.runtime.run("SET ducklake_max_retry_count = 0");
3914
- let outcome;
3915
- try {
3916
- outcome = { kind: "success", value: await run() };
3917
- } catch (error) {
3918
- outcome = { kind: "error", error };
3919
- }
3920
- try {
3921
- await input.runtime.run("RESET ducklake_max_retry_count");
3922
- } catch {
3923
- this.connections.poisonRuntime();
3924
- }
3925
- if (outcome.kind === "error") {
3926
- throw outcome.error;
3927
- }
3928
- return outcome.value;
5375
+ return rerunWhenCatalogMoves(input.dataset.id, () => this.commitDatasetVersionUnlocked({ ...input, runtime }), input.signal);
3929
5376
  }
3930
5377
  async commitDatasetVersionUnlocked(input) {
3931
5378
  const previousWriteSnapshotId = await this.lastCommittedSnapshotId(input.runtime);
@@ -3940,15 +5387,26 @@ class DuckLakeWriteCoordinator {
3940
5387
  operation: input.optimisticOperation ?? "commit"
3941
5388
  });
3942
5389
  }
5390
+ const lineage = lineageAtCommit2({
5391
+ mode: input.mode,
5392
+ schema: input.dataset.schema,
5393
+ producer: input.producer,
5394
+ written: input.lineage,
5395
+ parent: latestVersion
5396
+ });
3943
5397
  const changeResult = await input.applyChanges(input.runtime, {
3944
5398
  previousRowCount: latestVersion?.rowCount,
3945
5399
  validatedPrimaryKeyColumns: latestVersion?.validatedPrimaryKeyColumns
3946
5400
  });
3947
5401
  input.signal?.throwIfAborted();
3948
- await this.ensureNoOpHasSnapshot(input, changeResult, latestVersion);
3949
- const commitId = randomUUID7();
3950
- await this.setCommitMetadata(input, commitId, commitRowCount(input, changeResult, latestVersion));
3951
- await input.runtime.run("COMMIT");
5402
+ const noOpBase = changeResult.dataChangeExpected ? undefined : latestVersion === undefined ? await this.latestVersionAtReadSnapshot(input) : latestVersion;
5403
+ const createsVersion = noOpBase === undefined || this.noOpCreatesVersion(input, noOpBase, lineage);
5404
+ if (createsVersion && mustClaimTable(input, changeResult, lineage)) {
5405
+ await this.claimTable(input);
5406
+ }
5407
+ const commitId = randomUUID8();
5408
+ await this.setCommitMetadata(input, commitId, commitRowCount(input, changeResult, latestVersion), lineage);
5409
+ await commitVersionTransaction(input.runtime, input.dataset.id, createsVersion && mustCommitOnReadSnapshot(input, changeResult) ? { poisonRuntime: () => this.connections.poisonRuntime() } : undefined);
3952
5410
  committed = true;
3953
5411
  const committedWriteSnapshotId = await this.lastCommittedSnapshotId(input.runtime);
3954
5412
  try {
@@ -3957,7 +5415,8 @@ class DuckLakeWriteCoordinator {
3957
5415
  commitId,
3958
5416
  previousWriteSnapshotId,
3959
5417
  committedWriteSnapshotId,
3960
- changeResult
5418
+ changeResult,
5419
+ noOpBaseVersionId: noOpBase?.versionId ?? null
3961
5420
  });
3962
5421
  } finally {
3963
5422
  await this.connections.detachLocalCatalogAfterCommit(input.runtime);
@@ -3965,9 +5424,6 @@ class DuckLakeWriteCoordinator {
3965
5424
  } catch (error) {
3966
5425
  if (!committed) {
3967
5426
  await this.rollbackTransaction(input.runtime);
3968
- if (error instanceof Error && error.message.startsWith("TransactionContext Error: Failed to commit: Failed to commit DuckLake transaction.") && error.message.includes("Exceeded the maximum retry count of 0") && error.message.includes('duplicate key value violates unique constraint "ducklake_snapshot_pkey"')) {
3969
- throw new LakeConcurrencyError(`[SixbDuckLake] Concurrent catalog commit for dataset '${input.dataset.id}'.`, { cause: error });
3970
- }
3971
5427
  }
3972
5428
  throw error;
3973
5429
  }
@@ -3979,7 +5435,7 @@ class DuckLakeWriteCoordinator {
3979
5435
  if (version) {
3980
5436
  return { outcome: "created", version };
3981
5437
  }
3982
- throw new LakeStorageError24(`[SixbDuckLake] DuckLake committed snapshot '${ownSnapshotId}' for dataset '${input.dataset.id}', but Sixb could not hydrate it as a dataset version.`);
5438
+ throw new LakeStorageError26(`[SixbDuckLake] DuckLake committed snapshot '${ownSnapshotId}' for dataset '${input.dataset.id}', but Sixb could not hydrate it as a dataset version.`);
3983
5439
  }
3984
5440
  if (!input.changeResult.dataChangeExpected) {
3985
5441
  const version = await this.versionForNoOpCommit(input.runtime, input.dataset, input.allowInitialNoOp ?? false);
@@ -3991,15 +5447,18 @@ class DuckLakeWriteCoordinator {
3991
5447
  operation: input.optimisticOperation ?? "commit"
3992
5448
  });
3993
5449
  }
5450
+ if ((version?.versionId ?? null) !== input.noOpBaseVersionId) {
5451
+ throw new CatalogMovedError(`[SixbDuckLake] Dataset '${input.dataset.id}' changed while an unchanged write was compared with it.`);
5452
+ }
3994
5453
  return {
3995
5454
  outcome: "unchanged",
3996
5455
  version
3997
5456
  };
3998
5457
  }
3999
5458
  if (input.committedWriteSnapshotId === input.previousWriteSnapshotId) {
4000
- throw new LakeStorageError24(`[SixbDuckLake] DuckLake commit for dataset '${input.dataset.id}' completed without producing a snapshot for this non-empty write.`);
5459
+ throw new LakeStorageError26(`[SixbDuckLake] DuckLake commit for dataset '${input.dataset.id}' completed without producing a snapshot for this non-empty write.`);
4001
5460
  }
4002
- throw new LakeStorageError24(`[SixbDuckLake] DuckLake commit for dataset '${input.dataset.id}' completed, but Sixb could not find a matching snapshot for this write.`);
5461
+ throw new LakeStorageError26(`[SixbDuckLake] DuckLake commit for dataset '${input.dataset.id}' completed, but Sixb could not find a matching snapshot for this write.`);
4003
5462
  }
4004
5463
  async applyStagedRows(input) {
4005
5464
  const mode = input.write.mode ?? "snapshot";
@@ -4012,11 +5471,11 @@ class DuckLakeWriteCoordinator {
4012
5471
  previousRowCount: input.previousRowCount
4013
5472
  });
4014
5473
  if (result.sourceRowCount !== input.rowsWritten) {
4015
- throw new LakeStorageError24(`[SixbDuckLake] Staged write for dataset '${input.write.dataset.id}' accepted ${input.rowsWritten} row(s), but DuckLake saw ${result.sourceRowCount} source row(s) at commit time.`);
5474
+ throw new LakeStorageError26(`[SixbDuckLake] Staged write for dataset '${input.write.dataset.id}' accepted ${input.rowsWritten} row(s), but DuckLake saw ${result.sourceRowCount} source row(s) at commit time.`);
4016
5475
  }
4017
5476
  return result;
4018
5477
  }
4019
- async setCommitMetadata(input, commitId, rowCount) {
5478
+ async setCommitMetadata(input, commitId, rowCount, lineage) {
4020
5479
  const validatedPrimaryKeyColumns = getDatasetPrimaryKeyColumns4(input.dataset);
4021
5480
  const metadata = {
4022
5481
  kind: "datasetVersion",
@@ -4026,22 +5485,35 @@ class DuckLakeWriteCoordinator {
4026
5485
  producer: input.producer ? structuredClone(input.producer) : undefined,
4027
5486
  inputs: input.inputs ? structuredClone(input.inputs) : undefined,
4028
5487
  ...rowCount.kind === "exact" ? { rowCount: rowCount.value } : {},
4029
- ...validatedPrimaryKeyColumns !== null ? { validatedPrimaryKeyColumns: [...validatedPrimaryKeyColumns] } : {}
5488
+ ...validatedPrimaryKeyColumns !== null ? { validatedPrimaryKeyColumns: [...validatedPrimaryKeyColumns] } : {},
5489
+ ...lineage === undefined ? {} : { lineage: encodeLineage(lineage) }
4030
5490
  };
4031
5491
  await input.runtime.run(`CALL ${quoteIdentifier(duckLakeAlias(this.options))}.set_commit_message(${quoteSqlString("Sixb")}, ${quoteSqlString(input.commitMessage)}, extra_info => ${quoteSqlString(JSON.stringify({ sixb: metadata }))})`);
4032
5492
  }
4033
- async ensureNoOpHasSnapshot(input, changeResult, knownLatestVersion) {
4034
- if (changeResult.dataChangeExpected) {
4035
- return;
4036
- }
4037
- const latestVersion = knownLatestVersion === undefined ? await this.snapshots.getLatestVersionSummaryForDefinition(input.runtime, input.dataset) : knownLatestVersion;
4038
- if (latestVersion ? input.mode === "merge" || !hasDatasetInputChanges(latestVersion.inputs, input.inputs) : input.allowInitialNoOp) {
4039
- return;
4040
- }
5493
+ async claimTable(input) {
4041
5494
  const table = qualifiedTableName(this.options, encodeDatasetTableName(input.dataset.id));
4042
5495
  const descriptionSql = input.dataset.description === undefined ? "NULL" : quoteSqlString(input.dataset.description);
4043
5496
  await input.runtime.run(`COMMENT ON TABLE ${table} IS ${descriptionSql}`);
4044
5497
  }
5498
+ noOpCreatesVersion(input, latestVersion, lineage) {
5499
+ if (!latestVersion) {
5500
+ return !input.allowInitialNoOp;
5501
+ }
5502
+ return input.mode !== "merge" && hasDatasetProvenanceChanges(latestVersion, {
5503
+ datasetId: input.dataset.id,
5504
+ schema: input.dataset.schema,
5505
+ producer: input.producer,
5506
+ inputs: input.inputs,
5507
+ lineage
5508
+ });
5509
+ }
5510
+ async latestVersionAtReadSnapshot(input) {
5511
+ const { snapshotId, latestVersion } = await this.snapshots.getLatestVersionSummaryAtCurrentSnapshot(input.runtime, input.dataset);
5512
+ if (latestVersion && BigInt(parseVersionId(latestVersion.versionId)) > BigInt(snapshotId)) {
5513
+ throw new CatalogMovedError(`[SixbDuckLake] Dataset '${input.dataset.id}' changed while an unchanged write was compared with it.`);
5514
+ }
5515
+ return latestVersion;
5516
+ }
4045
5517
  async versionForNoOpCommit(runtime, definition, allowInitialNoOp) {
4046
5518
  const latestVersion = await this.snapshots.getLatestVersionForDefinition(runtime, definition);
4047
5519
  if (latestVersion) {
@@ -4050,7 +5522,7 @@ class DuckLakeWriteCoordinator {
4050
5522
  if (allowInitialNoOp) {
4051
5523
  return null;
4052
5524
  }
4053
- throw new LakeStorageError24(`[SixbDuckLake] No DuckLake changes were committed for dataset '${definition.id}', and no previous version exists.`);
5525
+ throw new LakeStorageError26(`[SixbDuckLake] No DuckLake changes were committed for dataset '${definition.id}', and no previous version exists.`);
4054
5526
  }
4055
5527
  async rollbackTransaction(runtime) {
4056
5528
  try {
@@ -4058,31 +5530,17 @@ class DuckLakeWriteCoordinator {
4058
5530
  } catch {}
4059
5531
  }
4060
5532
  async beginVersionTransaction(input) {
4061
- if (!isSqliteCatalog(this.options.catalog)) {
5533
+ if (input.mode === "snapshot" && input.expectedLatestVersionId === undefined && !isSqliteCatalog(this.options.catalog)) {
4062
5534
  await input.runtime.run("BEGIN TRANSACTION");
4063
- if (input.expectedLatestVersionId === undefined && input.mode !== "append") {
4064
- return;
4065
- }
4066
- return this.snapshots.getLatestVersionSummaryForDefinition(input.runtime, input.dataset);
4067
- }
4068
- for (let attempt = 0;attempt < 3; attempt++) {
4069
- input.signal?.throwIfAborted();
4070
- const snapshotId = await this.currentSnapshotId(input.runtime);
4071
- const latestVersion = await this.snapshots.getLatestVersionSummaryForDefinition(input.runtime, input.dataset);
4072
- await input.runtime.run("BEGIN TRANSACTION");
4073
- if (await this.currentSnapshotId(input.runtime) === snapshotId) {
4074
- return latestVersion;
4075
- }
4076
- await input.runtime.run("ROLLBACK");
4077
- }
4078
- throw new LakeConcurrencyError(`[SixbDuckLake] Catalog changed during three write preparations for dataset '${input.dataset.id}'. Retry the write.`);
4079
- }
4080
- async currentSnapshotId(runtime) {
4081
- const [row] = await runtime.query(`SELECT id FROM ${quoteIdentifier(duckLakeAlias(this.options))}.current_snapshot()`);
4082
- if (row === undefined) {
4083
- throw new LakeStorageError24("[SixbDuckLake] DuckLake did not return a current snapshot.");
5535
+ return;
4084
5536
  }
4085
- return String(getBigIntLike(row, "id"));
5537
+ return beginTransactionOnLatestVersion({
5538
+ options: this.options,
5539
+ runtime: input.runtime,
5540
+ datasetId: input.dataset.id,
5541
+ signal: input.signal,
5542
+ readLatestVersion: () => this.snapshots.getLatestVersionSummaryAtCurrentSnapshot(input.runtime, input.dataset)
5543
+ });
4086
5544
  }
4087
5545
  async lastCommittedSnapshotId(runtime) {
4088
5546
  const [row] = await runtime.query(`SELECT id FROM ${quoteIdentifier(duckLakeAlias(this.options))}.last_committed_snapshot()`);
@@ -4127,12 +5585,33 @@ class DuckLakeWriteCoordinator {
4127
5585
  return;
4128
5586
  }
4129
5587
  if (input.actualLatestVersionId !== input.expectedLatestVersionId) {
4130
- throw new LakeConcurrencyError(`[SixbDuckLake] Optimistic ${input.operation} failed for dataset '${input.datasetId}': expected latest version '${input.expectedLatestVersionId ?? "none"}', found '${input.actualLatestVersionId ?? "none"}'.`);
5588
+ throw new LakeConcurrencyError2(`[SixbDuckLake] Optimistic ${input.operation} failed for dataset '${input.datasetId}': expected latest version '${input.expectedLatestVersionId ?? "none"}', found '${input.actualLatestVersionId ?? "none"}'.`);
4131
5589
  }
4132
5590
  }
4133
5591
  }
4134
- function isSqliteCatalog(catalog) {
4135
- return catalog.type === "sqlite" || catalog.type === "custom" && catalog.uri.toLowerCase().startsWith("sqlite:");
5592
+ function mustClaimTable(input, changeResult, lineage) {
5593
+ if (!changeResult.dataChangeExpected || changeResult.targetWasEmpty) {
5594
+ return true;
5595
+ }
5596
+ if (input.mode === "snapshot") {
5597
+ return false;
5598
+ }
5599
+ return getDatasetPrimaryKeyColumns4(input.dataset) !== null || carriesDependencies(input.producer, lineage);
5600
+ }
5601
+ function mustCommitOnReadSnapshot(input, changeResult) {
5602
+ if (!changeResult.changedTableRows) {
5603
+ return true;
5604
+ }
5605
+ return input.expectedLatestVersionId !== undefined && input.mode === "append" && getDatasetPrimaryKeyColumns4(input.dataset) === null;
5606
+ }
5607
+ function carriesDependencies(producer, lineage) {
5608
+ if (lineage === undefined) {
5609
+ return producer?.kind === "pipeline";
5610
+ }
5611
+ if ("unknown" in lineage) {
5612
+ return true;
5613
+ }
5614
+ return lineage.rows.length > 0 || Object.values(lineage.columns).some((refs) => refs.length > 0);
4136
5615
  }
4137
5616
  function commitRowCount(input, changeResult, latestVersion) {
4138
5617
  if (input.mode === "append" && input.expectedLatestVersionId === undefined) {
@@ -4148,7 +5627,7 @@ function commitRowCount(input, changeResult, latestVersion) {
4148
5627
  }
4149
5628
  function assertDuckLakeSnapshotId3(snapshotId) {
4150
5629
  if (!/^\d+$/.test(snapshotId)) {
4151
- throw new LakeStorageError24(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`);
5630
+ throw new LakeStorageError26(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`);
4152
5631
  }
4153
5632
  }
4154
5633
  var localCatalogCommitLocks = new Map;
@@ -4179,6 +5658,7 @@ async function withCatalogCommitLock(options, run) {
4179
5658
  // src/ducklake-storage.ts
4180
5659
  class DuckLakeStorage {
4181
5660
  standard = { id: "ducklake", version: "1.0" };
5661
+ recordsLineage = true;
4182
5662
  sql;
4183
5663
  connections;
4184
5664
  datasets;
@@ -4200,15 +5680,15 @@ class DuckLakeStorage {
4200
5680
  };
4201
5681
  this.connections = new DuckLakeConnectionManager(normalizedOptions);
4202
5682
  this.datasets = new DuckLakeDatasetCatalog(normalizedOptions, this.connections);
4203
- this.maintenance = new DuckLakeMaintenance(normalizedOptions, this.connections);
4204
5683
  this.snapshotReader = new DuckLakeSnapshotReader(normalizedOptions, this.connections, this.datasets);
5684
+ this.maintenance = new DuckLakeMaintenance(normalizedOptions, this.connections, this.snapshotReader);
4205
5685
  this.rows = new DuckLakeRowReader(normalizedOptions, this.connections, this.snapshotReader);
4206
5686
  this.changes = new DuckLakeChangeReader(normalizedOptions, this.connections, this.snapshotReader);
4207
5687
  this.writes = new DuckLakeWriteCoordinator(normalizedOptions, this.connections, this.datasets, this.snapshotReader);
4208
5688
  this.sql = new DuckLakeSqlExecutor(normalizedOptions, this.connections, this.datasets, this.snapshotReader, this.writes);
4209
5689
  }
4210
5690
  async createDataset(definition) {
4211
- return this.datasets.createDataset(definition);
5691
+ return this.datasets.createDataset(definition, (runtime, dataset) => this.snapshotReader.getLatestVersionSummaryAtCurrentSnapshot(runtime, dataset));
4212
5692
  }
4213
5693
  async assertDatasetDefinitionsCompatible(definitions) {
4214
5694
  await this.datasets.assertDatasetDefinitionsCompatible(definitions);
@@ -4237,6 +5717,9 @@ class DuckLakeStorage {
4237
5717
  async getVersion(datasetId, versionId) {
4238
5718
  return this.snapshotReader.getVersion(datasetId, versionId);
4239
5719
  }
5720
+ async listLatestVersionsSince(input) {
5721
+ return this.snapshotReader.listLatestVersionsSince(input);
5722
+ }
4240
5723
  readRows(input) {
4241
5724
  return this.rows.readRows(input);
4242
5725
  }
@@ -4254,4 +5737,4 @@ export {
4254
5737
  DuckLakeStorage
4255
5738
  };
4256
5739
 
4257
- //# debugId=35ABBEC5A94F36A964756E2164756E21
5740
+ //# debugId=9F26B0918A26BCC464756E2164756E21