@sixb/ducklake 0.1.8 → 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 (69) hide show
  1. package/README.md +59 -4
  2. package/dist/ducklake-storage.d.ts +1 -0
  3. package/dist/ducklake-storage.d.ts.map +1 -1
  4. package/dist/index.js +1817 -511
  5. package/dist/index.js.map +24 -14
  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 +63 -16
  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/versions.d.ts +10 -1
  44. package/dist/internal/versions.d.ts.map +1 -1
  45. package/dist/types.d.ts +10 -1
  46. package/dist/types.d.ts.map +1 -1
  47. package/package.json +4 -4
  48. package/src/ducklake-storage.ts +9 -2
  49. package/src/internal/dataset-row-commit.ts +28 -2
  50. package/src/internal/dataset-row-merge.ts +4 -0
  51. package/src/internal/ducklake-dataset-catalog.ts +162 -49
  52. package/src/internal/ducklake-dataset-table-ref.ts +75 -17
  53. package/src/internal/ducklake-maintenance.ts +110 -48
  54. package/src/internal/ducklake-snapshot-reader.ts +352 -304
  55. package/src/internal/ducklake-sql-executor.ts +214 -14
  56. package/src/internal/ducklake-version-transaction.ts +224 -0
  57. package/src/internal/ducklake-write-coordinator.ts +226 -148
  58. package/src/internal/lineage-encoding.ts +136 -0
  59. package/src/internal/sql-lineage/analysis.ts +257 -0
  60. package/src/internal/sql-lineage/expressions.ts +208 -0
  61. package/src/internal/sql-lineage/plan-json.ts +130 -0
  62. package/src/internal/sql-lineage/plan-relation.ts +59 -0
  63. package/src/internal/sql-lineage/plan-walker.ts +727 -0
  64. package/src/internal/sql-lineage/rejection.ts +16 -0
  65. package/src/internal/sql-lineage/statement-gate.ts +164 -0
  66. package/src/internal/sql-lineage/table-functions.ts +11 -0
  67. package/src/internal/sql-transform-relations.ts +90 -40
  68. package/src/internal/versions.ts +37 -14
  69. 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__";
@@ -412,24 +415,56 @@ function pathScheme(path) {
412
415
 
413
416
  // src/internal/ducklake-dataset-table-ref.ts
414
417
  async function resolveDatasetTableRef(options, runtime, datasetId) {
415
- const tableName = encodeDatasetTableName(datasetId);
416
- const ducklakeTable = duckLakeMetadataTableName(options, "ducklake_table");
417
- const [row] = await runtime.query(`
418
- SELECT table_id
419
- FROM ${ducklakeTable}
420
- WHERE table_name = ${quoteSqlString(tableName)}
421
- AND end_snapshot IS NULL
422
- 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
423
425
  `);
424
- 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 {
425
459
  datasetId,
426
460
  tableName,
427
- tableId: getBigIntLike(row, "table_id")
461
+ tableId,
462
+ ...sequenceTableId === undefined ? {} : { sequenceTableId }
428
463
  };
429
464
  }
430
465
 
431
466
  // src/internal/schema.ts
432
- import { LakeStorageError as LakeStorageError5 } from "@sixb/core/lake-storage";
467
+ import { LakeStorageError as LakeStorageError6 } from "@sixb/core/lake-storage";
433
468
 
434
469
  // src/internal/duckdb-column-types.ts
435
470
  function normalizeDuckDbTypeSql(typeSql) {
@@ -471,23 +506,23 @@ function findDatasetColumnTypeForDuckDbSql(typeSql) {
471
506
 
472
507
  // src/internal/duckdb-decimal.ts
473
508
  import { normalizeDecimalValue } from "@sixb/core";
474
- import { LakeStorageError as LakeStorageError4 } from "@sixb/core/lake-storage";
509
+ import { LakeStorageError as LakeStorageError5 } from "@sixb/core/lake-storage";
475
510
  var decimalType = DUCKDB_COLUMN_TYPES.decimal;
476
511
  var maxIntegerDigits = decimalType.precision - decimalType.scale;
477
512
  function normalizeDuckDbDecimalValue(value, columnName) {
478
513
  if (typeof value !== "string") {
479
- 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.`);
480
515
  }
481
516
  let normalized;
482
517
  try {
483
518
  normalized = normalizeDecimalValue(value);
484
519
  } catch {
485
- 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.`);
486
521
  }
487
522
  const unsigned = normalized.startsWith("-") ? normalized.slice(1) : normalized;
488
523
  const [integer, fraction = ""] = unsigned.split(".");
489
524
  if (integer.length > maxIntegerDigits || fraction.length > decimalType.scale) {
490
- 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.`);
491
526
  }
492
527
  return normalized;
493
528
  }
@@ -501,7 +536,7 @@ function duckDbTypeToDatasetColumnType(typeSql) {
501
536
  if (type) {
502
537
  return type;
503
538
  }
504
- 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.`);
505
540
  }
506
541
  function datasetColumnToDuckDbSql(column) {
507
542
  const nullableSql = column.nullable ? "" : " NOT NULL";
@@ -539,7 +574,7 @@ function normalizeReadValue(value, column) {
539
574
  }
540
575
  function normalizeFileRef(value) {
541
576
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
542
- throw new LakeStorageError5("[SixbDuckLake] DuckDB fileRef value must be a struct.");
577
+ throw new LakeStorageError6("[SixbDuckLake] DuckDB fileRef value must be a struct.");
543
578
  }
544
579
  const record = value;
545
580
  return {
@@ -554,7 +589,7 @@ function normalizeFileRef(value) {
554
589
  function getStringField(record, key) {
555
590
  const value = record[key];
556
591
  if (typeof value !== "string") {
557
- 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.`);
558
593
  }
559
594
  return value;
560
595
  }
@@ -564,7 +599,7 @@ function getOptionalStringField(record, key) {
564
599
  return;
565
600
  }
566
601
  if (typeof value !== "string") {
567
- 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.`);
568
603
  }
569
604
  return value;
570
605
  }
@@ -576,18 +611,18 @@ function getIntegerField(record, key) {
576
611
  if (typeof value === "bigint") {
577
612
  return Number(value);
578
613
  }
579
- 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.`);
580
615
  }
581
616
 
582
617
  // src/internal/versions.ts
583
- import { LakeStorageError as LakeStorageError6 } from "@sixb/core/lake-storage";
618
+ import { LakeStorageError as LakeStorageError7 } from "@sixb/core/lake-storage";
584
619
  function parseVersionId(versionId) {
585
620
  if (!versionId.startsWith("ducklake:")) {
586
- throw new LakeStorageError6(`[SixbDuckLake] Invalid DuckLake version id '${versionId}'.`);
621
+ throw new LakeStorageError7(`[SixbDuckLake] Invalid DuckLake version id '${versionId}'.`);
587
622
  }
588
623
  const snapshotId = versionId.slice("ducklake:".length);
589
624
  if (!/^\d+$/.test(snapshotId)) {
590
- throw new LakeStorageError6(`[SixbDuckLake] Invalid DuckLake version id '${versionId}'.`);
625
+ throw new LakeStorageError7(`[SixbDuckLake] Invalid DuckLake version id '${versionId}'.`);
591
626
  }
592
627
  return snapshotId;
593
628
  }
@@ -609,6 +644,7 @@ function parseCommitMetadata(value) {
609
644
  }
610
645
  const mode = parsed.sixb.mode;
611
646
  const commitId = parsed.sixb.commitId;
647
+ const producer = parseDatasetProducer(parsed.sixb.producer);
612
648
  const rowCount = parsed.sixb.rowCount;
613
649
  const validatedPrimaryKeyColumns = parsed.sixb.validatedPrimaryKeyColumns;
614
650
  return {
@@ -616,14 +652,19 @@ function parseCommitMetadata(value) {
616
652
  datasetId: parsed.sixb.datasetId,
617
653
  ...typeof commitId === "string" ? { commitId } : {},
618
654
  ...mode === "snapshot" || mode === "append" || mode === "merge" || mode === "schema" ? { mode } : {},
619
- ...isDatasetProducer(parsed.sixb.producer) ? { producer: parsed.sixb.producer } : {},
655
+ ...producer === undefined ? {} : { producer },
620
656
  ...isDatasetVersionRefs(parsed.sixb.inputs) ? { inputs: parsed.sixb.inputs } : {},
621
657
  ...typeof commitId === "string" && isRowCount(rowCount) ? { rowCount } : {},
622
658
  ...typeof commitId === "string" && isValidatedPrimaryKeyColumns(validatedPrimaryKeyColumns) ? { validatedPrimaryKeyColumns } : {},
623
- ...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 }
624
661
  };
625
662
  }
626
- 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) {
627
668
  const tableIdText = tableId.toString();
628
669
  let hasDataChange = false;
629
670
  let hasDeleteChange = false;
@@ -645,8 +686,16 @@ function parseInlineDataChange(changesMade, tableId) {
645
686
  function isRecord(value) {
646
687
  return typeof value === "object" && value !== null && !Array.isArray(value);
647
688
  }
648
- function isDatasetProducer(value) {
649
- 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
+ };
650
699
  }
651
700
  function isDatasetVersionRefs(value) {
652
701
  return Array.isArray(value) && value.every((item) => isRecord(item) && typeof item.datasetId === "string" && typeof item.versionId === "string");
@@ -654,9 +703,6 @@ function isDatasetVersionRefs(value) {
654
703
  function isSchemaChangeMetadata(value) {
655
704
  return isRecord(value) && (value.addColumns === undefined || Array.isArray(value.addColumns) && value.addColumns.every((columnName) => typeof columnName === "string"));
656
705
  }
657
- function optionalString(value) {
658
- return value === undefined || typeof value === "string";
659
- }
660
706
  function isRowCount(value) {
661
707
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
662
708
  }
@@ -729,11 +775,11 @@ class DuckLakeChangeReader {
729
775
  while (delivered < changeCount) {
730
776
  input.signal?.throwIfAborted();
731
777
  if (closePromise)
732
- throw new LakeStorageError7("[SixbDuckLake] Change reader is closed.");
778
+ throw new LakeStorageError8("[SixbDuckLake] Change reader is closed.");
733
779
  const page = await runtime.query(`SELECT rowid AS position, * FROM ${temporary}
734
780
  WHERE rowid > ${cursor} ORDER BY rowid LIMIT ${PAGE_ROWS}`);
735
781
  if (page.length === 0)
736
- throw new LakeStorageError7("[SixbDuckLake] Change read ended before its declared count.");
782
+ throw new LakeStorageError8("[SixbDuckLake] Change read ended before its declared count.");
737
783
  for (const row of page) {
738
784
  input.signal?.throwIfAborted();
739
785
  delivered++;
@@ -772,7 +818,7 @@ async function uniqueRowCount(runtime, relation, keys) {
772
818
  function safeCount(row, name) {
773
819
  const count = Number(getBigIntLike(row, name));
774
820
  if (!Number.isSafeInteger(count) || count < 0)
775
- throw new LakeStorageError7("[SixbDuckLake] Change count exceeds the supported integer range.");
821
+ throw new LakeStorageError8("[SixbDuckLake] Change count exceeds the supported integer range.");
776
822
  return count;
777
823
  }
778
824
  function rowImage(row, prefix, columns) {
@@ -783,7 +829,7 @@ function rowImage(row, prefix, columns) {
783
829
  }
784
830
 
785
831
  // src/internal/ducklake-connection-manager.ts
786
- import { LakeStorageError as LakeStorageError11 } from "@sixb/core/lake-storage";
832
+ import { LakeStorageError as LakeStorageError12 } from "@sixb/core/lake-storage";
787
833
 
788
834
  // src/internal/catalog-key.ts
789
835
  import { resolve } from "path";
@@ -808,10 +854,10 @@ import { join, resolve as resolve2 } from "path";
808
854
  import {
809
855
  DuckDBInstance
810
856
  } from "@duckdb/node-api";
811
- import { LakeStorageError as LakeStorageError10 } from "@sixb/core/lake-storage";
857
+ import { LakeStorageError as LakeStorageError11 } from "@sixb/core/lake-storage";
812
858
 
813
859
  // src/internal/duckdb-reader.ts
814
- import { LakeStorageError as LakeStorageError9 } from "@sixb/core/lake-storage";
860
+ import { LakeStorageError as LakeStorageError10 } from "@sixb/core/lake-storage";
815
861
 
816
862
  // src/internal/duckdb-value-converter.ts
817
863
  import {
@@ -819,14 +865,14 @@ import {
819
865
  DuckDBTypeId,
820
866
  JSDuckDBValueConverter
821
867
  } from "@duckdb/node-api";
822
- import { LakeStorageError as LakeStorageError8 } from "@sixb/core/lake-storage";
868
+ import { LakeStorageError as LakeStorageError9 } from "@sixb/core/lake-storage";
823
869
  var sixbDuckDbValueConverter = (value, type, converter) => {
824
870
  if (value === null) {
825
871
  return null;
826
872
  }
827
873
  if (type.typeId === DuckDBTypeId.DECIMAL) {
828
874
  if (!(value instanceof DuckDBDecimalValue)) {
829
- throw new LakeStorageError8("[SixbDuckLake] DuckDB returned an invalid DECIMAL value.");
875
+ throw new LakeStorageError9("[SixbDuckLake] DuckDB returned an invalid DECIMAL value.");
830
876
  }
831
877
  return value.toString();
832
878
  }
@@ -868,7 +914,7 @@ class NodeDuckDbReader {
868
914
  }
869
915
  async* rows() {
870
916
  if (this.consumed)
871
- throw new LakeStorageError9("[SixbDuckLake] Reader can only be consumed once.");
917
+ throw new LakeStorageError10("[SixbDuckLake] Reader can only be consumed once.");
872
918
  this.consumed = true;
873
919
  try {
874
920
  this.assertOpen();
@@ -931,7 +977,7 @@ class NodeDuckDbReader {
931
977
  assertOpen() {
932
978
  this.options.signal?.throwIfAborted();
933
979
  if (this.closePromise)
934
- throw new LakeStorageError9("[SixbDuckLake] Reader is closed.");
980
+ throw new LakeStorageError10("[SixbDuckLake] Reader is closed.");
935
981
  }
936
982
  }
937
983
 
@@ -1042,12 +1088,12 @@ class NodeDuckDbRuntime {
1042
1088
  }
1043
1089
  assertAcceptingOperations() {
1044
1090
  if (this.closing || this.closed) {
1045
- throw new LakeStorageError10("[SixbDuckLake] DuckDB runtime is closed.");
1091
+ throw new LakeStorageError11("[SixbDuckLake] DuckDB runtime is closed.");
1046
1092
  }
1047
1093
  }
1048
1094
  assertNotClosed() {
1049
1095
  if (this.closed) {
1050
- throw new LakeStorageError10("[SixbDuckLake] DuckDB runtime is closed.");
1096
+ throw new LakeStorageError11("[SixbDuckLake] DuckDB runtime is closed.");
1051
1097
  }
1052
1098
  }
1053
1099
  }
@@ -1239,7 +1285,7 @@ class DuckLakeConnectionManager {
1239
1285
  this.observedLocalCatalogGeneration = currentLocalCatalogGeneration(options);
1240
1286
  this.maxStreamingReads = options.maxStreamingReads ?? 4;
1241
1287
  if (!Number.isSafeInteger(this.maxStreamingReads) || this.maxStreamingReads < 1) {
1242
- throw new LakeStorageError11("[SixbDuckLake] maxStreamingReads must be a positive integer.");
1288
+ throw new LakeStorageError12("[SixbDuckLake] maxStreamingReads must be a positive integer.");
1243
1289
  }
1244
1290
  }
1245
1291
  async acquireRead(signal) {
@@ -1293,7 +1339,7 @@ class DuckLakeConnectionManager {
1293
1339
  }
1294
1340
  assertOpen() {
1295
1341
  if (this.closed) {
1296
- throw new LakeStorageError11("[SixbDuckLake] DuckLakeStorage is closed.");
1342
+ throw new LakeStorageError12("[SixbDuckLake] DuckLakeStorage is closed.");
1297
1343
  }
1298
1344
  }
1299
1345
  async runtime() {
@@ -1481,7 +1527,7 @@ class DuckLakeConnectionManager {
1481
1527
  }
1482
1528
  async finishClose() {
1483
1529
  this.closed = true;
1484
- this.shutdown.abort(new LakeStorageError11("[SixbDuckLake] DuckLakeStorage is closed."));
1530
+ this.shutdown.abort(new LakeStorageError12("[SixbDuckLake] DuckLakeStorage is closed."));
1485
1531
  const runtimePromise = this.runtimePromise;
1486
1532
  this.runtimePromise = undefined;
1487
1533
  await this.closeRuntimePromise(runtimePromise, {
@@ -1643,12 +1689,13 @@ function incrementLocalCatalogGeneration(options) {
1643
1689
  import { randomUUID as randomUUID2 } from "crypto";
1644
1690
  import {
1645
1691
  assertDatasetDefinition,
1646
- LakeStorageError as LakeStorageError14,
1692
+ LakeStorageError as LakeStorageError16,
1693
+ lineageAtCommit,
1647
1694
  planDatasetDefinitionUpdate
1648
1695
  } from "@sixb/core/lake-storage";
1649
1696
 
1650
1697
  // src/internal/ducklake-catalog-schema.ts
1651
- import { LakeStorageError as LakeStorageError12 } from "@sixb/core/lake-storage";
1698
+ import { LakeStorageError as LakeStorageError13 } from "@sixb/core/lake-storage";
1652
1699
  var SIXB_PRIMARY_KEY_COMMENT_NAMESPACE = "sixb:primary-key:";
1653
1700
  var DATASET_SEQUENCE_COLUMN_COMMENT = "sixb.sequenceBy.v1";
1654
1701
  var SIXB_PRIMARY_KEY_COMMENT_PREFIX = `${SIXB_PRIMARY_KEY_COMMENT_NAMESPACE}v1:`;
@@ -1699,14 +1746,14 @@ function duckLakeCatalogColumnsToDatasetPrimaryKey(tableName, rows) {
1699
1746
  throwInvalidPrimaryKeyMetadata(tableName, column, comment);
1700
1747
  }
1701
1748
  if (seenOrdinals.has(ordinal)) {
1702
- 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}'.`);
1703
1750
  }
1704
1751
  seenOrdinals.add(ordinal);
1705
1752
  if (duckDbTypeToDatasetColumnType(column.columnType) !== "string") {
1706
- 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'.`);
1707
1754
  }
1708
1755
  if (column.nullsAllowed) {
1709
- 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.`);
1710
1757
  }
1711
1758
  keyedColumns.push({ column, ordinal });
1712
1759
  }
@@ -1716,7 +1763,7 @@ function duckLakeCatalogColumnsToDatasetPrimaryKey(tableName, rows) {
1716
1763
  keyedColumns.sort((left, right) => left.ordinal - right.ordinal);
1717
1764
  for (const [expectedOrdinal, keyedColumn] of keyedColumns.entries()) {
1718
1765
  if (keyedColumn.ordinal !== expectedOrdinal) {
1719
- 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.`);
1720
1767
  }
1721
1768
  }
1722
1769
  const columnNames = keyedColumns.map(({ column }) => column.columnName);
@@ -1726,7 +1773,7 @@ function duckLakeCatalogColumnsToDatasetPrimaryKey(tableName, rows) {
1726
1773
  return columnNames;
1727
1774
  }
1728
1775
  function throwInvalidPrimaryKeyMetadata(tableName, column, comment) {
1729
- 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}'.`);
1730
1777
  }
1731
1778
  function duckLakeCatalogColumnToDefinition(tableName, column, childrenByParentId) {
1732
1779
  const children = [...childrenByParentId.get(column.columnId) ?? []].sort(compareDuckLakeCatalogColumnOrder);
@@ -1743,7 +1790,7 @@ function duckLakeCatalogColumnTypeToDatasetType(tableName, column, children) {
1743
1790
  if (isFileRefStruct(children)) {
1744
1791
  return "fileRef";
1745
1792
  }
1746
- 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.`);
1747
1794
  }
1748
1795
  function compareDuckLakeCatalogColumnOrder(left, right) {
1749
1796
  return Number(left.columnOrder - right.columnOrder);
@@ -1765,7 +1812,7 @@ function formatDuckLakeCatalogColumnType(column, children) {
1765
1812
  }
1766
1813
 
1767
1814
  // src/internal/ducklake-current-dataset-definitions.ts
1768
- import { LakeStorageError as LakeStorageError13 } from "@sixb/core/lake-storage";
1815
+ import { LakeStorageError as LakeStorageError14 } from "@sixb/core/lake-storage";
1769
1816
  async function readCurrentDatasetDefinitions(input) {
1770
1817
  if (input.datasetIds?.length === 0) {
1771
1818
  return new Map;
@@ -1892,7 +1939,7 @@ async function readCurrentDatasetPartitions(input, tableNames) {
1892
1939
  transform: getString(row, "transform")
1893
1940
  };
1894
1941
  if (partition.transform !== "identity") {
1895
- 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}'.`);
1896
1943
  }
1897
1944
  return partition;
1898
1945
  });
@@ -1916,6 +1963,188 @@ function groupPartitionsByTableName(rows) {
1916
1963
  return result;
1917
1964
  }
1918
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
+
1919
2148
  // src/internal/ducklake-dataset-catalog.ts
1920
2149
  class DuckLakeDatasetCatalog {
1921
2150
  options;
@@ -1924,13 +2153,22 @@ class DuckLakeDatasetCatalog {
1924
2153
  this.options = options;
1925
2154
  this.connections = connections;
1926
2155
  }
1927
- async createDataset(definition) {
2156
+ async createDataset(definition, readLatestVersion) {
1928
2157
  this.assertSchema(definition);
1929
2158
  const existing = await this.getDataset(definition.id);
1930
2159
  if (existing) {
1931
- 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
+ });
1932
2171
  if (plan.changed) {
1933
- await this.applyExistingDatasetUpdate(plan);
1934
2172
  this.connections.markLocalCatalogChanged();
1935
2173
  await this.connections.resetRuntime();
1936
2174
  }
@@ -1994,7 +2232,7 @@ class DuckLakeDatasetCatalog {
1994
2232
  if (failures.length > 0) {
1995
2233
  const details = failures.join(`
1996
2234
  `);
1997
- 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).
1998
2236
  ${details}`);
1999
2237
  }
2000
2238
  }
@@ -2026,32 +2264,50 @@ ${details}`);
2026
2264
  datasetIds: options?.datasetIds
2027
2265
  });
2028
2266
  }
2029
- async getDatasetSchemaAtSnapshot(runtime, datasetId, tableName, tableId, snapshotId) {
2030
- assertDuckLakeSnapshotId(snapshotId);
2031
- const columns = await this.readDatasetColumnsAtSnapshot(runtime, tableName, tableId, snapshotId);
2032
- if (columns.length === 0) {
2033
- 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 [];
2034
2270
  }
2035
- return duckLakeCatalogColumnsToDatasetSchema(tableName, columns);
2271
+ for (const request of requests) {
2272
+ assertDuckLakeSnapshotId(request.snapshotId);
2273
+ }
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
+ });
2036
2284
  }
2037
2285
  assertSchema(definition) {
2038
- assertDatasetDefinition(definition, (message) => new LakeStorageError14(`[SixbDuckLake] ${message}`));
2286
+ assertDatasetDefinition(definition, (message) => new LakeStorageError16(`[SixbDuckLake] ${message}`));
2039
2287
  }
2040
- async applyExistingDatasetUpdate(plan) {
2288
+ async applyExistingDatasetUpdate(plan, readLatestVersion) {
2041
2289
  const tableName = encodeDatasetTableName(plan.definition.id);
2042
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
+ }
2043
2296
  await this.connections.withExclusiveAttached(async (runtime) => {
2044
- if (plan.schema.kind === "none") {
2045
- await this.applyMetadataDelta(runtime, qualifiedName, plan);
2046
- return;
2047
- }
2048
- await runtime.run("BEGIN TRANSACTION");
2049
2297
  let committed = false;
2050
2298
  try {
2051
- 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);
2052
2306
  await this.applyMetadataDelta(runtime, qualifiedName, plan);
2053
- await this.setSchemaEvolutionCommitMetadata(runtime, plan.definition.id, plan.schema);
2054
- 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
+ });
2055
2311
  committed = true;
2056
2312
  } catch (error) {
2057
2313
  if (!committed) {
@@ -2082,7 +2338,7 @@ ${details}`);
2082
2338
  await runtime.run(`ALTER TABLE ${qualifiedName} SET PARTITIONED BY (${definition.partitionBy.map((columnName) => quoteIdentifier(columnName)).join(", ")})`);
2083
2339
  } catch (error) {
2084
2340
  const message = error instanceof Error ? error.message : String(error);
2085
- 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}`);
2086
2342
  }
2087
2343
  }
2088
2344
  async applyPrimaryKey(runtime, qualifiedName, definition) {
@@ -2094,7 +2350,7 @@ ${details}`);
2094
2350
  await runtime.run(`COMMENT ON COLUMN ${qualifiedName}.${quoteIdentifier(columnName)} IS ${quoteSqlString(duckLakePrimaryKeyColumnComment(ordinal))}`);
2095
2351
  }
2096
2352
  }
2097
- async setSchemaEvolutionCommitMetadata(runtime, datasetId, plan) {
2353
+ async setSchemaEvolutionCommitMetadata(runtime, datasetId, plan, lineage) {
2098
2354
  await runtime.run(`CALL ${quoteIdentifier(duckLakeAlias(this.options))}.set_commit_message(${quoteSqlString("Sixb")}, ${quoteSqlString(`evolve dataset ${datasetId} schema`)}, extra_info => ${quoteSqlString(JSON.stringify({
2099
2355
  sixb: {
2100
2356
  kind: "datasetVersion",
@@ -2103,7 +2359,8 @@ ${details}`);
2103
2359
  mode: "schema",
2104
2360
  schemaChange: {
2105
2361
  addColumns: plan.columns.map((column) => column.name)
2106
- }
2362
+ },
2363
+ ...lineage === undefined ? {} : { lineage: encodeLineage(lineage) }
2107
2364
  }
2108
2365
  }))})`);
2109
2366
  }
@@ -2112,63 +2369,71 @@ ${details}`);
2112
2369
  await runtime.run("ROLLBACK");
2113
2370
  } catch {}
2114
2371
  }
2115
- async readDatasetColumnsAtSnapshot(runtime, tableName, tableId, snapshotId) {
2372
+ async readDatasetColumnsBetweenSnapshots(runtime, tables, oldestSnapshot, newestSnapshot) {
2373
+ const tableIds = new Set(tables.map((table) => table.tableId));
2116
2374
  const ducklakeColumn = duckLakeMetadataTableName(this.options, "ducklake_column");
2117
2375
  const rows = await runtime.query(`
2118
2376
  SELECT
2377
+ table_id,
2119
2378
  column_id,
2120
2379
  column_order,
2121
2380
  column_name,
2122
2381
  column_type,
2123
2382
  CAST(nulls_allowed AS BOOLEAN) AS nulls_allowed,
2124
- parent_column
2383
+ parent_column,
2384
+ begin_snapshot,
2385
+ end_snapshot
2125
2386
  FROM ${ducklakeColumn}
2126
- WHERE table_id = ${tableId}
2127
- AND begin_snapshot <= ${snapshotId}
2128
- AND (end_snapshot IS NULL OR end_snapshot > ${snapshotId})
2129
- 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
2130
2391
  `);
2131
2392
  return rows.map((row) => ({
2132
- tableName,
2133
- columnId: getBigIntLike(row, "column_id"),
2134
- columnOrder: getBigIntLike(row, "column_order"),
2135
- columnName: getString(row, "column_name"),
2136
- columnType: getString(row, "column_type"),
2137
- nullsAllowed: getBoolean(row, "nulls_allowed"),
2138
- 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
+ }
2139
2404
  }));
2140
2405
  }
2141
2406
  }
2142
2407
  function assertDuckLakeSnapshotId(snapshotId) {
2143
2408
  if (!/^\d+$/.test(snapshotId)) {
2144
- throw new LakeStorageError14(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`);
2409
+ throw new LakeStorageError16(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`);
2145
2410
  }
2146
2411
  }
2147
2412
 
2148
2413
  // src/internal/ducklake-maintenance.ts
2149
- import { LakeStorageError as LakeStorageError15 } from "@sixb/core/lake-storage";
2414
+ import { LakeStorageError as LakeStorageError17 } from "@sixb/core/lake-storage";
2150
2415
  var DEFAULT_RETENTION = "7 days";
2151
2416
 
2152
2417
  class DuckLakeMaintenance {
2153
2418
  options;
2154
2419
  connections;
2155
- constructor(options, connections) {
2420
+ snapshots;
2421
+ constructor(options, connections, snapshots) {
2156
2422
  this.options = options;
2157
2423
  this.connections = connections;
2424
+ this.snapshots = snapshots;
2158
2425
  }
2159
2426
  async runMaintenance(options = {}) {
2160
2427
  this.connections.assertOpen();
2161
2428
  const expireOlderThan = options.expireOlderThan ?? DEFAULT_RETENTION;
2162
2429
  const deleteOlderThan = options.deleteOlderThan ?? expireOlderThan;
2163
2430
  const dryRun = options.dryRun ?? false;
2431
+ const protect = (options.protect ?? []).map(protectedSnapshotId);
2164
2432
  if (this.options.readOnly && !dryRun) {
2165
- 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.");
2166
2434
  }
2167
2435
  return this.connections.withMaintenanceRuntime(async (runtime) => {
2168
- const counts = await runMaintenanceProcedures(runtime, this.options, {
2169
- expireOlderThan,
2170
- deleteOlderThan
2171
- }, dryRun);
2436
+ const counts = await this.runMaintenanceProcedures(runtime, { expireOlderThan, deleteOlderThan }, protect, dryRun);
2172
2437
  if (!dryRun) {
2173
2438
  this.connections.markLocalCatalogChanged();
2174
2439
  }
@@ -2180,26 +2445,55 @@ class DuckLakeMaintenance {
2180
2445
  };
2181
2446
  });
2182
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
+ }
2183
2479
  }
2184
- async function runMaintenanceProcedures(runtime, options, retention, dryRun) {
2185
- const aliasSql = quoteSqlString(duckLakeAlias(options));
2186
- const snapshots = await runtime.query(maintenanceProcedureSql("ducklake_expire_snapshots", aliasSql, retention.expireOlderThan, dryRun));
2187
- const oldFiles = await runtime.query(maintenanceProcedureSql("ducklake_cleanup_old_files", aliasSql, retention.deleteOlderThan, dryRun));
2188
- const orphanedFiles = await runtime.query(maintenanceProcedureSql("ducklake_delete_orphaned_files", aliasSql, retention.deleteOlderThan, dryRun));
2189
- return {
2190
- snapshots: snapshots.length,
2191
- oldFiles: oldFiles.length,
2192
- orphanedFiles: orphanedFiles.length
2193
- };
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
+ }
2486
+ }
2487
+ function olderThanSql(olderThan) {
2488
+ return `now() - INTERVAL ${quoteSqlString(olderThan)}`;
2194
2489
  }
2195
- function maintenanceProcedureSql(procedure, aliasSql, olderThan, dryRun) {
2196
- const olderThanSql = quoteSqlString(olderThan);
2490
+ function maintenanceProcedureSql(procedure, aliasSql, selection, dryRun) {
2197
2491
  const dryRunSql = dryRun ? "dry_run => true, " : "";
2198
- return `CALL ${procedure}(${aliasSql}, ${dryRunSql}` + `older_than => now() - INTERVAL ${olderThanSql})`;
2492
+ return `CALL ${procedure}(${aliasSql}, ${dryRunSql}${selection})`;
2199
2493
  }
2200
2494
 
2201
2495
  // src/internal/ducklake-row-reader.ts
2202
- import { LakeStorageError as LakeStorageError16 } from "@sixb/core/lake-storage";
2496
+ import { LakeStorageError as LakeStorageError18 } from "@sixb/core/lake-storage";
2203
2497
  var READ_PAGE_ROWS = 5000;
2204
2498
  var PHYSICAL_ROW_ID_ALIAS = "__sixb_physical_row_id";
2205
2499
 
@@ -2220,7 +2514,7 @@ class DuckLakeRowReader {
2220
2514
  read.signal.throwIfAborted();
2221
2515
  const tableRef = await resolveDatasetTableRef(this.options, runtime, input.datasetId);
2222
2516
  if (!tableRef) {
2223
- throw new LakeStorageError16(`[SixbDuckLake] Unknown dataset '${input.datasetId}'.`);
2517
+ throw new LakeStorageError18(`[SixbDuckLake] Unknown dataset '${input.datasetId}'.`);
2224
2518
  }
2225
2519
  const version = await this.resolveVersion(runtime, tableRef, input.versionId);
2226
2520
  const columns = this.resolveReadColumns(tableRef.datasetId, version.schema, input.columns);
@@ -2305,13 +2599,13 @@ class DuckLakeRowReader {
2305
2599
  return columns.map((columnName) => {
2306
2600
  const column = columnsByName.get(columnName);
2307
2601
  if (!column) {
2308
- 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.`);
2309
2603
  }
2310
2604
  return column;
2311
2605
  });
2312
2606
  }
2313
2607
  throwNoCommittedVersion(datasetId) {
2314
- throw new LakeStorageError16(`[SixbDuckLake] No committed version found for dataset '${datasetId}'.`);
2608
+ throw new LakeStorageError18(`[SixbDuckLake] No committed version found for dataset '${datasetId}'.`);
2315
2609
  }
2316
2610
  }
2317
2611
  function pageSql(read, limit, offset, cursor) {
@@ -2321,9 +2615,10 @@ function pageSql(read, limit, offset, cursor) {
2321
2615
  }
2322
2616
 
2323
2617
  // src/internal/ducklake-snapshot-reader.ts
2324
- import { LakeStorageError as LakeStorageError17 } from "@sixb/core/lake-storage";
2618
+ import { LakeStorageError as LakeStorageError19 } from "@sixb/core/lake-storage";
2325
2619
  var SNAPSHOT_ROW_BATCH_SIZE = 128;
2326
2620
  var FEED_PAGE_SIZE = 512;
2621
+ var SHORT_READ_BATCH_SIZE = 8;
2327
2622
  var CATALOG_SNAPSHOT_SCAN_LIMIT = 512;
2328
2623
 
2329
2624
  class DuckLakeSnapshotReader {
@@ -2375,10 +2670,10 @@ class DuckLakeSnapshotReader {
2375
2670
  }
2376
2671
  async collectDatasetCatalogState(runtime, datasetIds) {
2377
2672
  const uniqueIds = [...new Set(datasetIds)];
2378
- const tableIdsByDatasetId = await this.resolveDatasetTableIds(runtime, uniqueIds);
2379
- const latestRowByDatasetId = await this.resolveLatestSnapshotRows(runtime, tableIdsByDatasetId);
2673
+ const tablesByDatasetId = await this.resolveDatasetTables(runtime, uniqueIds);
2674
+ const latestRowByDatasetId = await this.resolveLatestSnapshotRows(runtime, tablesByDatasetId);
2380
2675
  return uniqueIds.map((datasetId) => {
2381
- if (!tableIdsByDatasetId.has(datasetId)) {
2676
+ if (!tablesByDatasetId.has(datasetId)) {
2382
2677
  return { datasetId, materialized: false, latestVersion: null };
2383
2678
  }
2384
2679
  const row = latestRowByDatasetId.get(datasetId);
@@ -2417,7 +2712,7 @@ class DuckLakeSnapshotReader {
2417
2712
  async queryHeadSnapshotId(runtime) {
2418
2713
  const [row] = await runtime.query(buildDuckLakeMetadataQuery(this.options, (table) => `SELECT max(snapshot_id) AS snapshot_id FROM ${table("ducklake_snapshot")}`));
2419
2714
  if (row?.snapshot_id === null || row?.snapshot_id === undefined) {
2420
- throw new LakeStorageError17("[SixbDuckLake] DuckLake catalog has no snapshots.");
2715
+ throw new LakeStorageError19("[SixbDuckLake] DuckLake catalog has no snapshots.");
2421
2716
  }
2422
2717
  return String(getBigIntLike(row, "snapshot_id"));
2423
2718
  }
@@ -2474,10 +2769,8 @@ class DuckLakeSnapshotReader {
2474
2769
  FROM ${table("ducklake_snapshot_changes")}
2475
2770
  WHERE snapshot_id >= ${first} AND snapshot_id <= ${last}
2476
2771
  `));
2477
- const fileFlags = await this.queryFileChangeFlags(runtime, snapshots.map((snapshot) => snapshot.snapshotId), tables.map((table) => table.tableId));
2478
2772
  const changesBySnapshotId = new Map(changeRows.map((row) => [String(getBigIntLike(row, "snapshot_id")), row]));
2479
2773
  const tablesByTableId = new Map(tables.map((table) => [table.tableId.toString(), table]));
2480
- const tablesByDatasetId = new Map(tables.map((table) => [table.datasetId, table]));
2481
2774
  for (const snapshot of snapshots) {
2482
2775
  const changes = changesBySnapshotId.get(snapshot.snapshotId);
2483
2776
  if (changes === undefined) {
@@ -2491,24 +2784,14 @@ class DuckLakeSnapshotReader {
2491
2784
  if (table)
2492
2785
  touched.add(table);
2493
2786
  }
2494
- for (const table of tables) {
2495
- if (fileFlags.has(fileChangeKey(snapshot.snapshotId, table.tableId)))
2496
- touched.add(table);
2497
- }
2498
- const tagged = metadata === undefined ? undefined : tablesByDatasetId.get(metadata.datasetId);
2499
- if (tagged)
2500
- touched.add(tagged);
2501
2787
  for (const table of touched) {
2502
2788
  if (BigInt(snapshot.snapshotId) < table.startSnapshotId) {
2503
2789
  continue;
2504
2790
  }
2505
- const flags = fileFlags.get(fileChangeKey(snapshot.snapshotId, table.tableId));
2506
2791
  const row = this.candidateToSnapshotRow(table.datasetId, table.tableId, {
2507
2792
  snapshotId: snapshot.snapshotId,
2508
2793
  createdAt: snapshot.createdAt,
2509
2794
  changesMade,
2510
- hasFileChange: flags?.hasFileChange ?? false,
2511
- hasFileDeleteChange: flags?.hasFileDeleteChange ?? false,
2512
2795
  ...metadata !== undefined ? { metadata } : {}
2513
2796
  });
2514
2797
  if (row && row.mode !== "schema") {
@@ -2518,6 +2801,22 @@ class DuckLakeSnapshotReader {
2518
2801
  }
2519
2802
  return latest;
2520
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
+ }
2521
2820
  snapshotRowToSummary(datasetId, row) {
2522
2821
  return {
2523
2822
  datasetId,
@@ -2527,54 +2826,69 @@ class DuckLakeSnapshotReader {
2527
2826
  ...row.metadata?.rowCount !== undefined ? { rowCount: row.metadata.rowCount } : {}
2528
2827
  };
2529
2828
  }
2530
- async resolveDatasetTableIds(runtime, datasetIds) {
2829
+ async resolveDatasetTables(runtime, datasetIds) {
2531
2830
  const datasetIdByTableName = new Map;
2831
+ const datasetIdBySequenceTableName = new Map;
2532
2832
  for (const datasetId of datasetIds) {
2533
2833
  datasetIdByTableName.set(encodeDatasetTableName(datasetId), datasetId);
2834
+ datasetIdBySequenceTableName.set(encodeDatasetSequenceTableName(datasetId), datasetId);
2534
2835
  }
2535
2836
  const ducklakeTable = duckLakeMetadataTableName(this.options, "ducklake_table");
2536
- const tableNameList = [...datasetIdByTableName.keys()].map((name) => quoteSqlString(name)).join(", ");
2837
+ const tableNameList = [...datasetIdByTableName.keys(), ...datasetIdBySequenceTableName.keys()].map((name) => quoteSqlString(name)).join(", ");
2537
2838
  const rows = await runtime.query(`
2538
2839
  SELECT table_id, table_name
2539
2840
  FROM ${ducklakeTable}
2540
2841
  WHERE table_name IN (${tableNameList})
2541
2842
  AND end_snapshot IS NULL
2542
2843
  `);
2543
- const tableIdsByDatasetId = new Map;
2844
+ const tablesByDatasetId = new Map;
2544
2845
  for (const row of rows) {
2545
- const datasetId = datasetIdByTableName.get(getString(row, "table_name"));
2846
+ const tableName = getString(row, "table_name");
2847
+ const datasetId = datasetIdByTableName.get(tableName);
2546
2848
  if (datasetId !== undefined) {
2547
- 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
+ });
2548
2864
  }
2549
2865
  }
2550
- return tableIdsByDatasetId;
2866
+ return tablesByDatasetId;
2551
2867
  }
2552
- async resolveLatestSnapshotRows(runtime, tableIdsByDatasetId) {
2868
+ async resolveLatestSnapshotRows(runtime, tablesByDatasetId, scanLimit = CATALOG_SNAPSHOT_SCAN_LIMIT) {
2553
2869
  const result = new Map;
2554
- if (tableIdsByDatasetId.size === 0) {
2870
+ if (tablesByDatasetId.size === 0) {
2555
2871
  return result;
2556
2872
  }
2557
- const unresolved = new Map(tableIdsByDatasetId);
2558
- const tableIds = [...new Set(tableIdsByDatasetId.values())];
2873
+ const unresolved = new Map(tablesByDatasetId);
2559
2874
  let beforeSnapshotId;
2560
2875
  let scanned = 0;
2561
- while (unresolved.size > 0 && scanned < CATALOG_SNAPSHOT_SCAN_LIMIT) {
2876
+ while (unresolved.size > 0 && scanned < scanLimit) {
2562
2877
  const candidates = await this.queryCatalogSnapshotBatch(runtime, beforeSnapshotId, SNAPSHOT_ROW_BATCH_SIZE);
2563
2878
  if (candidates.length === 0) {
2564
2879
  break;
2565
2880
  }
2566
- 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));
2567
2883
  for (const candidate of candidates) {
2568
2884
  scanned += 1;
2569
- for (const [datasetId, tableId] of [...unresolved]) {
2570
- const flags = fileFlags.get(fileChangeKey(candidate.snapshotId, tableId));
2571
- 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, {
2572
2888
  snapshotId: candidate.snapshotId,
2573
2889
  createdAt: candidate.createdAt,
2574
2890
  changesMade: candidate.changesMade,
2575
- hasFileChange: flags?.hasFileChange ?? false,
2576
- hasFileDeleteChange: flags?.hasFileDeleteChange ?? false,
2577
- ...candidate.metadata !== undefined ? { metadata: candidate.metadata } : {}
2891
+ ...candidateMetadata !== undefined ? { metadata: candidateMetadata } : {}
2578
2892
  });
2579
2893
  if (row) {
2580
2894
  result.set(datasetId, row);
@@ -2604,67 +2918,22 @@ class DuckLakeSnapshotReader {
2604
2918
  SELECT
2605
2919
  snapshot.snapshot_id,
2606
2920
  snapshot.snapshot_time,
2607
- changes.changes_made,
2608
- changes.commit_extra_info
2921
+ changes.changes_made
2609
2922
  FROM ${ducklakeSnapshot} snapshot
2610
2923
  JOIN ${ducklakeSnapshotChanges} changes ON changes.snapshot_id = snapshot.snapshot_id
2611
2924
  ${whereSql}
2612
2925
  ORDER BY snapshot.snapshot_id DESC
2613
2926
  LIMIT ${Math.max(0, Math.trunc(limit))}
2614
2927
  `);
2615
- return rows.map((row) => {
2616
- const metadata = parseCommitMetadata(getOptionalString(row, "commit_extra_info"));
2617
- return {
2618
- snapshotId: String(getBigIntLike(row, "snapshot_id")),
2619
- createdAt: getDate(row, "snapshot_time"),
2620
- changesMade: getString(row, "changes_made"),
2621
- ...metadata !== undefined ? { metadata } : {}
2622
- };
2623
- });
2624
- }
2625
- async queryFileChangeFlags(runtime, snapshotIds, tableIds) {
2626
- const flags = new Map;
2627
- if (snapshotIds.length === 0 || tableIds.length === 0) {
2628
- return flags;
2629
- }
2630
- for (const snapshotId of snapshotIds) {
2631
- assertDuckLakeSnapshotId2(snapshotId);
2632
- }
2633
- const snapshotIdList = snapshotIds.join(", ");
2634
- const tableIdList = tableIds.map((tableId) => tableId.toString()).join(", ");
2635
- const ducklakeDataFile = duckLakeMetadataTableName(this.options, "ducklake_data_file");
2636
- const ducklakeDeleteFile = duckLakeMetadataTableName(this.options, "ducklake_delete_file");
2637
- const rows = await runtime.query(`
2638
- WITH file_changes AS (
2639
- SELECT begin_snapshot AS snapshot_id, table_id, false AS is_delete
2640
- FROM ${ducklakeDataFile}
2641
- WHERE table_id IN (${tableIdList}) AND begin_snapshot IN (${snapshotIdList})
2642
- UNION ALL
2643
- SELECT end_snapshot, table_id, true
2644
- FROM ${ducklakeDataFile}
2645
- WHERE table_id IN (${tableIdList}) AND end_snapshot IN (${snapshotIdList})
2646
- UNION ALL
2647
- SELECT begin_snapshot, table_id, true
2648
- FROM ${ducklakeDeleteFile}
2649
- WHERE table_id IN (${tableIdList}) AND begin_snapshot IN (${snapshotIdList})
2650
- UNION ALL
2651
- SELECT end_snapshot, table_id, true
2652
- FROM ${ducklakeDeleteFile}
2653
- WHERE table_id IN (${tableIdList}) AND end_snapshot IN (${snapshotIdList})
2654
- )
2655
- SELECT snapshot_id, table_id, bool_or(is_delete) AS has_delete
2656
- FROM file_changes
2657
- GROUP BY snapshot_id, table_id
2658
- `);
2659
- for (const row of rows) {
2660
- const key = fileChangeKey(String(getBigIntLike(row, "snapshot_id")), getBigIntLike(row, "table_id"));
2661
- flags.set(key, { hasFileChange: true, hasFileDeleteChange: getBoolean(row, "has_delete") });
2662
- }
2663
- 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
+ }));
2664
2933
  }
2665
2934
  assertNoMetadataConflict(snapshotId, datasetId, metadata) {
2666
2935
  if (metadata !== undefined && metadata.datasetId !== datasetId) {
2667
- 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}'.`);
2668
2937
  }
2669
2938
  }
2670
2939
  async getLatestVersionForDefinition(runtime, dataset) {
@@ -2672,22 +2941,39 @@ class DuckLakeSnapshotReader {
2672
2941
  return tableRef ? this.getLatestVersionForTableRef(runtime, tableRef) : null;
2673
2942
  }
2674
2943
  async getLatestVersionRefForDefinition(runtime, dataset) {
2675
- const summary = await this.getLatestVersionSummaryForDefinition(runtime, dataset);
2676
- return summary ? { datasetId: summary.datasetId, versionId: summary.versionId } : null;
2677
- }
2678
- async getLatestVersionSummaryForDefinition(runtime, dataset) {
2679
2944
  const tableRef = await resolveDatasetTableRef(this.options, runtime, dataset.id);
2680
2945
  if (!tableRef) {
2681
2946
  return null;
2682
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) {
2683
2963
  const row = await this.getLatestSnapshotRowForTableRef(runtime, tableRef);
2684
2964
  if (!row) {
2685
2965
  return null;
2686
2966
  }
2967
+ const [schema] = await this.datasets.getDatasetSchemasAtSnapshots(runtime, [
2968
+ { table: tableRef, snapshotId: row.snapshotId }
2969
+ ]);
2687
2970
  return {
2688
2971
  datasetId: tableRef.datasetId,
2689
2972
  versionId: toVersionId(row.snapshotId),
2973
+ schema,
2974
+ producer: row.metadata?.producer,
2690
2975
  inputs: row.metadata?.inputs,
2976
+ ...storedLineage(tableRef.datasetId, row),
2691
2977
  ...row.metadata?.rowCount !== undefined ? { rowCount: row.metadata.rowCount } : {},
2692
2978
  ...row.metadata?.validatedPrimaryKeyColumns !== undefined ? { validatedPrimaryKeyColumns: row.metadata.validatedPrimaryKeyColumns } : {}
2693
2979
  };
@@ -2701,7 +2987,7 @@ class DuckLakeSnapshotReader {
2701
2987
  const match = await this.getSnapshotRowForTableRef(runtime, tableRef, snapshotId, {
2702
2988
  includeParent: true
2703
2989
  });
2704
- return match ? this.snapshotToVersion(runtime, tableRef, match) : null;
2990
+ return match ? (await this.snapshotsToVersions(runtime, tableRef, [match]))[0] : null;
2705
2991
  }
2706
2992
  async getVersionRefForSnapshot(runtime, dataset, snapshotId) {
2707
2993
  assertDuckLakeSnapshotId2(snapshotId);
@@ -2712,7 +2998,11 @@ class DuckLakeSnapshotReader {
2712
2998
  const row = await this.getSnapshotRowForTableRef(runtime, tableRef, snapshotId, {
2713
2999
  includeParent: false
2714
3000
  });
2715
- 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]));
2716
3006
  }
2717
3007
  async listVersionsForDefinition(runtime, dataset, limit) {
2718
3008
  const tableRef = await resolveDatasetTableRef(this.options, runtime, dataset.id);
@@ -2720,11 +3010,7 @@ class DuckLakeSnapshotReader {
2720
3010
  }
2721
3011
  async listVersionsForTableRef(runtime, tableRef, limit) {
2722
3012
  const limitedRows = await this.getSnapshotRowsForTableRef(runtime, tableRef, limit);
2723
- const versions = [];
2724
- for (const row of limitedRows) {
2725
- versions.push(await this.snapshotToVersion(runtime, tableRef, row));
2726
- }
2727
- return versions;
3013
+ return this.snapshotsToVersions(runtime, tableRef, limitedRows);
2728
3014
  }
2729
3015
  async getLatestVersionForTableRef(runtime, tableRef) {
2730
3016
  const [latest] = await this.listVersionsForTableRef(runtime, tableRef, 1);
@@ -2732,8 +3018,7 @@ class DuckLakeSnapshotReader {
2732
3018
  }
2733
3019
  async getLatestSnapshotRowForTableRef(runtime, tableRef) {
2734
3020
  const [row] = await this.collectVisibleSnapshotRows(runtime, {
2735
- datasetId: tableRef.datasetId,
2736
- tableId: tableRef.tableId,
3021
+ table: tableRef,
2737
3022
  visibleRowLimit: 1
2738
3023
  });
2739
3024
  return row ?? null;
@@ -2741,8 +3026,7 @@ class DuckLakeSnapshotReader {
2741
3026
  async getSnapshotRowForTableRef(runtime, tableRef, snapshotId, options) {
2742
3027
  assertDuckLakeSnapshotId2(snapshotId);
2743
3028
  const [row] = await this.collectVisibleSnapshotRows(runtime, {
2744
- datasetId: tableRef.datasetId,
2745
- tableId: tableRef.tableId,
3029
+ table: tableRef,
2746
3030
  exactSnapshotId: snapshotId,
2747
3031
  visibleRowLimit: 1
2748
3032
  });
@@ -2753,8 +3037,7 @@ class DuckLakeSnapshotReader {
2753
3037
  return row;
2754
3038
  }
2755
3039
  const [parent] = await this.collectVisibleSnapshotRows(runtime, {
2756
- datasetId: tableRef.datasetId,
2757
- tableId: tableRef.tableId,
3040
+ table: tableRef,
2758
3041
  beforeSnapshotId: snapshotId,
2759
3042
  visibleRowLimit: 1
2760
3043
  });
@@ -2766,8 +3049,7 @@ class DuckLakeSnapshotReader {
2766
3049
  return [];
2767
3050
  }
2768
3051
  const rows = await this.collectVisibleSnapshotRows(runtime, {
2769
- datasetId: tableRef.datasetId,
2770
- tableId: tableRef.tableId,
3052
+ table: tableRef,
2771
3053
  visibleRowLimit: visibleLimit === undefined ? undefined : visibleLimit + 1
2772
3054
  });
2773
3055
  const rowsWithParents = this.withParentSnapshotIds(rows);
@@ -2779,22 +3061,21 @@ class DuckLakeSnapshotReader {
2779
3061
  }
2780
3062
  const snapshots = [];
2781
3063
  let beforeSnapshotId = input.beforeSnapshotId;
3064
+ let shortRead = input.exactSnapshotId !== undefined || input.visibleRowLimit !== undefined && input.visibleRowLimit <= 2;
2782
3065
  while (input.visibleRowLimit === undefined || snapshots.length < input.visibleRowLimit) {
3066
+ const batchSize = shortRead ? SHORT_READ_BATCH_SIZE : SNAPSHOT_ROW_BATCH_SIZE;
2783
3067
  const candidates = await this.querySnapshotCandidates(runtime, {
2784
- datasetId: input.datasetId,
2785
- tableId: input.tableId,
3068
+ table: input.table,
2786
3069
  exactSnapshotId: input.exactSnapshotId,
2787
3070
  beforeSnapshotId,
2788
- limit: input.exactSnapshotId === undefined ? SNAPSHOT_ROW_BATCH_SIZE : 1
3071
+ limit: input.exactSnapshotId === undefined ? batchSize : 1
2789
3072
  });
3073
+ shortRead = false;
2790
3074
  if (candidates.length === 0) {
2791
3075
  break;
2792
3076
  }
2793
3077
  for (const candidate of candidates) {
2794
- if ("missing" in candidate) {
2795
- continue;
2796
- }
2797
- const snapshot = this.candidateToSnapshotRow(input.datasetId, input.tableId, candidate);
3078
+ const snapshot = this.candidateToSnapshotRow(input.table.datasetId, input.table.tableId, candidate);
2798
3079
  if (snapshot) {
2799
3080
  snapshots.push(snapshot);
2800
3081
  }
@@ -2802,7 +3083,7 @@ class DuckLakeSnapshotReader {
2802
3083
  break;
2803
3084
  }
2804
3085
  }
2805
- if (input.exactSnapshotId !== undefined || candidates.length < SNAPSHOT_ROW_BATCH_SIZE) {
3086
+ if (input.exactSnapshotId !== undefined || candidates.length < batchSize) {
2806
3087
  break;
2807
3088
  }
2808
3089
  beforeSnapshotId = candidates[candidates.length - 1]?.snapshotId;
@@ -2810,7 +3091,6 @@ class DuckLakeSnapshotReader {
2810
3091
  return snapshots;
2811
3092
  }
2812
3093
  async querySnapshotCandidates(runtime, input) {
2813
- const tableId = input.tableId.toString();
2814
3094
  const limit = Math.max(0, Math.trunc(input.limit));
2815
3095
  if (limit === 0) {
2816
3096
  return [];
@@ -2822,118 +3102,69 @@ class DuckLakeSnapshotReader {
2822
3102
  assertDuckLakeSnapshotId2(input.beforeSnapshotId);
2823
3103
  }
2824
3104
  const rows = await runtime.query(buildDuckLakeMetadataQuery(this.options, (table) => {
2825
- const tableStart = `(SELECT min(begin_snapshot) FROM ${table("ducklake_table")} WHERE table_id = ${tableId})`;
2826
- const candidateIds = input.exactSnapshotId !== undefined ? `SELECT CAST(${input.exactSnapshotId} AS BIGINT) AS snapshot_id
2827
- WHERE ${input.exactSnapshotId} >= ${tableStart}` : `SELECT DISTINCT snapshot_id
2828
- FROM (
2829
- SELECT begin_snapshot AS snapshot_id
2830
- FROM ${table("ducklake_data_file")} WHERE table_id = ${tableId}
2831
- UNION ALL
2832
- SELECT end_snapshot
2833
- FROM ${table("ducklake_data_file")}
2834
- WHERE table_id = ${tableId} AND end_snapshot IS NOT NULL
2835
- UNION ALL
2836
- SELECT begin_snapshot
2837
- FROM ${table("ducklake_delete_file")} WHERE table_id = ${tableId}
2838
- UNION ALL
2839
- SELECT end_snapshot
2840
- FROM ${table("ducklake_delete_file")}
2841
- WHERE table_id = ${tableId} AND end_snapshot IS NOT NULL
2842
- UNION ALL
2843
- SELECT snapshot_id
2844
- FROM ${table("ducklake_snapshot_changes")}
2845
- WHERE strpos(',' || changes_made || ',', ${quoteSqlString(`:${tableId},`)}) > 0
2846
- OR strpos(
2847
- commit_extra_info,
2848
- ${quoteSqlString(`"datasetId":${JSON.stringify(input.datasetId)}`)}
2849
- ) > 0
2850
- ) touched
2851
- WHERE snapshot_id >= ${tableStart}
2852
- ${input.beforeSnapshotId === undefined ? "" : `AND snapshot_id < ${input.beforeSnapshotId}`}
2853
- ORDER BY snapshot_id DESC
2854
- LIMIT ${limit}`;
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 ");
2855
3119
  return `
2856
- WITH candidate_ids AS (
2857
- ${candidateIds}
2858
- ),
2859
- candidate_snapshots AS (
2860
- SELECT
2861
- snapshot.snapshot_id,
2862
- snapshot.snapshot_time,
2863
- changes.changes_made,
2864
- changes.commit_extra_info
2865
- FROM ${table("ducklake_snapshot")} snapshot
2866
- JOIN ${table("ducklake_snapshot_changes")} changes
2867
- ON changes.snapshot_id = snapshot.snapshot_id
2868
- WHERE snapshot.snapshot_id IN (SELECT snapshot_id FROM candidate_ids)
2869
- ),
2870
- file_changes AS (
2871
- -- File metadata tells whether this table changed in each candidate snapshot.
2872
- SELECT begin_snapshot AS snapshot_id, false AS is_delete_change
2873
- FROM ${table("ducklake_data_file")}
2874
- WHERE table_id = ${tableId}
2875
- AND begin_snapshot IN (SELECT snapshot_id FROM candidate_ids)
2876
- UNION ALL
2877
- SELECT end_snapshot AS snapshot_id, true AS is_delete_change
2878
- FROM ${table("ducklake_data_file")}
2879
- WHERE table_id = ${tableId}
2880
- AND end_snapshot IN (SELECT snapshot_id FROM candidate_ids)
2881
- UNION ALL
2882
- SELECT begin_snapshot AS snapshot_id, true AS is_delete_change
2883
- FROM ${table("ducklake_delete_file")}
2884
- WHERE table_id = ${tableId}
2885
- AND begin_snapshot IN (SELECT snapshot_id FROM candidate_ids)
2886
- UNION ALL
2887
- SELECT end_snapshot AS snapshot_id, true AS is_delete_change
2888
- FROM ${table("ducklake_delete_file")}
2889
- WHERE table_id = ${tableId}
2890
- AND end_snapshot IN (SELECT snapshot_id FROM candidate_ids)
2891
- ),
2892
- file_changes_by_snapshot AS (
2893
- -- Collapse file-level changes into one row per snapshot.
2894
- SELECT
2895
- snapshot_id,
2896
- count(*) > 0 AS has_file_change,
2897
- count(*) FILTER (WHERE is_delete_change) > 0 AS has_file_delete_change
2898
- FROM file_changes
2899
- GROUP BY snapshot_id
2900
- )
2901
3120
  SELECT
2902
- -- Keep metadata-only candidates; Sixb filters them with commit_extra_info.
2903
- candidate_ids.snapshot_id,
2904
- candidate.snapshot_time,
2905
- candidate.changes_made,
2906
- candidate.commit_extra_info,
2907
- coalesce(file_changes.has_file_change, false) AS has_file_change,
2908
- coalesce(file_changes.has_file_delete_change, false) AS has_file_delete_change
2909
- FROM candidate_ids
2910
- LEFT JOIN candidate_snapshots candidate
2911
- ON candidate.snapshot_id = candidate_ids.snapshot_id
2912
- LEFT JOIN file_changes_by_snapshot file_changes
2913
- ON file_changes.snapshot_id = candidate_ids.snapshot_id
2914
- ORDER BY candidate_ids.snapshot_id DESC
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}
2915
3132
  `;
2916
3133
  }));
2917
3134
  return rows.map((row) => {
2918
- const snapshotId = String(getBigIntLike(row, "snapshot_id"));
2919
- if (row.snapshot_time === null || row.snapshot_time === undefined) {
2920
- return { snapshotId, missing: true };
2921
- }
2922
3135
  const metadata = parseCommitMetadata(row.commit_extra_info);
2923
3136
  return {
2924
- snapshotId,
3137
+ snapshotId: String(getBigIntLike(row, "snapshot_id")),
2925
3138
  createdAt: getDate(row, "snapshot_time"),
2926
3139
  changesMade: getString(row, "changes_made"),
2927
- hasFileChange: getBoolean(row, "has_file_change"),
2928
- hasFileDeleteChange: getBoolean(row, "has_file_delete_change"),
2929
3140
  ...metadata !== undefined ? { metadata } : {}
2930
3141
  };
2931
3142
  });
2932
3143
  }
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
+ }
2933
3166
  candidateToSnapshotRow(datasetId, tableId, candidate) {
2934
- const inlineChange = parseInlineDataChange(candidate.changesMade, tableId);
2935
- const hasDataChange = inlineChange.hasDataChange || candidate.hasFileChange;
2936
- const hasDeleteChange = inlineChange.hasDeleteChange || candidate.hasFileDeleteChange;
3167
+ const { hasDataChange, hasDeleteChange } = parseTableDataChange(candidate.changesMade, tableId);
2937
3168
  if (!hasDataChange) {
2938
3169
  const metadata = candidate.metadata;
2939
3170
  if (!metadata || metadata.datasetId !== datasetId) {
@@ -2956,19 +3187,9 @@ class DuckLakeSnapshotReader {
2956
3187
  ...candidate.metadata !== undefined ? { metadata: candidate.metadata } : {}
2957
3188
  };
2958
3189
  }
2959
- async snapshotToVersion(runtime, tableRef, row) {
2960
- const schemaAtSnapshot = await this.datasets.getDatasetSchemaAtSnapshot(runtime, tableRef.datasetId, tableRef.tableName, tableRef.tableId, row.snapshotId);
2961
- return {
2962
- datasetId: tableRef.datasetId,
2963
- versionId: toVersionId(row.snapshotId),
2964
- parentVersionId: row.parentSnapshotId === undefined ? undefined : toVersionId(row.parentSnapshotId),
2965
- mode: row.mode,
2966
- createdAt: row.createdAt,
2967
- schema: schemaAtSnapshot,
2968
- producer: row.metadata?.producer,
2969
- inputs: row.metadata?.inputs,
2970
- ...row.metadata?.rowCount !== undefined ? { rowCount: row.metadata.rowCount } : {}
2971
- };
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]));
2972
3193
  }
2973
3194
  withParentSnapshotIds(snapshots) {
2974
3195
  return snapshots.map((snapshot, index) => {
@@ -2977,29 +3198,60 @@ class DuckLakeSnapshotReader {
2977
3198
  });
2978
3199
  }
2979
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
+ }
2980
3234
  function assertDuckLakeSnapshotId2(snapshotId) {
2981
3235
  if (!/^\d+$/.test(snapshotId)) {
2982
- throw new LakeStorageError17(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`);
3236
+ throw new LakeStorageError19(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`);
2983
3237
  }
2984
3238
  }
2985
- function fileChangeKey(snapshotId, tableId) {
2986
- return `${snapshotId}|${tableId}`;
2987
- }
2988
3239
 
2989
3240
  // src/internal/ducklake-sql-executor.ts
2990
- import { randomUUID as randomUUID4 } from "crypto";
3241
+ import { randomUUID as randomUUID5 } from "crypto";
2991
3242
  import {
2992
- LakeStorageError as LakeStorageError20
3243
+ LakeStorageError as LakeStorageError22,
3244
+ resolveStepLineage
2993
3245
  } from "@sixb/core/lake-storage";
2994
3246
 
2995
3247
  // src/internal/dataset-row-commit.ts
2996
3248
  import {
2997
3249
  getDatasetPrimaryKeyColumns,
2998
- LakeStorageError as LakeStorageError18
3250
+ LakeStorageError as LakeStorageError20
2999
3251
  } from "@sixb/core/lake-storage";
3000
3252
  function assertDatasetWriteMode(mode, operation) {
3001
3253
  if (mode !== "snapshot" && mode !== "append") {
3002
- 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'.`);
3003
3255
  }
3004
3256
  }
3005
3257
  async function applyDatasetRowsFromRelation(input) {
@@ -3007,6 +3259,7 @@ async function applyDatasetRowsFromRelation(input) {
3007
3259
  const table = qualifiedTableName(input.options, tableName);
3008
3260
  const columnsSql = datasetSchemaColumnNamesSql(input.dataset.schema);
3009
3261
  const primaryKeyColumns = getDatasetPrimaryKeyColumns(input.dataset);
3262
+ let baselineRowCount;
3010
3263
  if (primaryKeyColumns !== null) {
3011
3264
  await inspectUniqueKeyedRelation({
3012
3265
  runtime: input.runtime,
@@ -3015,7 +3268,7 @@ async function applyDatasetRowsFromRelation(input) {
3015
3268
  context: `${input.mode} source`
3016
3269
  });
3017
3270
  if (input.mode === "append") {
3018
- await inspectUniqueKeyedRelation({
3271
+ baselineRowCount = await inspectUniqueKeyedRelation({
3019
3272
  runtime: input.runtime,
3020
3273
  dataset: input.dataset,
3021
3274
  relationSql: table,
@@ -3037,21 +3290,30 @@ async function applyDatasetRowsFromRelation(input) {
3037
3290
  return {
3038
3291
  dataChangeExpected: false,
3039
3292
  sourceRowCount: source.rowCount,
3040
- resultingRowCount: { kind: "exact", value: target.rowCount }
3293
+ resultingRowCount: { kind: "exact", value: target.rowCount },
3294
+ targetWasEmpty: target.rowCount === 0,
3295
+ changedTableRows: false
3041
3296
  };
3042
3297
  }
3043
3298
  await input.runtime.run(`DELETE FROM ${table}`);
3044
3299
  await input.runtime.run(`INSERT INTO ${table} (${columnsSql}) SELECT ${columnsSql} FROM ${input.sourceRelationSql}`);
3300
+ const changedTableRows = source.rowCount > 0 || target.rowCount > 0;
3045
3301
  return {
3046
- dataChangeExpected: source.rowCount > 0 || target.rowCount > 0,
3302
+ dataChangeExpected: changedTableRows,
3047
3303
  sourceRowCount: source.rowCount,
3048
- resultingRowCount: { kind: "exact", value: source.rowCount }
3304
+ resultingRowCount: { kind: "exact", value: source.rowCount },
3305
+ targetWasEmpty: target.rowCount === 0,
3306
+ changedTableRows
3049
3307
  };
3050
3308
  }
3051
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;
3052
3312
  await input.runtime.run(`INSERT INTO ${table} (${columnsSql}) SELECT ${columnsSql} FROM ${input.sourceRelationSql}`);
3053
3313
  return {
3054
3314
  dataChangeExpected: sourceRowCount > 0,
3315
+ targetWasEmpty,
3316
+ changedTableRows: sourceRowCount > 0,
3055
3317
  sourceRowCount,
3056
3318
  resultingRowCount: input.previousRowCount === undefined ? { kind: "unknown" } : { kind: "exact", value: input.previousRowCount + sourceRowCount }
3057
3319
  };
@@ -3059,7 +3321,7 @@ async function applyDatasetRowsFromRelation(input) {
3059
3321
  async function inspectUniqueKeyedRelation(input) {
3060
3322
  const primaryKeyColumns = getDatasetPrimaryKeyColumns(input.dataset);
3061
3323
  if (primaryKeyColumns === null) {
3062
- 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.`);
3063
3325
  }
3064
3326
  const keysSql = primaryKeyColumns.map((column) => quoteIdentifier(column)).join(", ");
3065
3327
  const [row] = await input.runtime.query(`
@@ -3074,7 +3336,7 @@ async function inspectUniqueKeyedRelation(input) {
3074
3336
  const rowCount = row === undefined ? 0 : Number(getBigIntLike(row, "row_count"));
3075
3337
  const maxKeyCount = row === undefined ? 0 : Number(getBigIntLike(row, "max_key_count"));
3076
3338
  if (maxKeyCount > 1) {
3077
- 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.`);
3078
3340
  }
3079
3341
  return rowCount;
3080
3342
  }
@@ -3091,7 +3353,7 @@ async function assertNoPrimaryKeyOverlap(input) {
3091
3353
  LIMIT 1
3092
3354
  `);
3093
3355
  if (collision !== undefined) {
3094
- 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.`);
3095
3357
  }
3096
3358
  }
3097
3359
  async function fingerprintRelation(runtime, relationSql, columnsSql) {
@@ -3121,11 +3383,19 @@ async function countRows(runtime, relationSql) {
3121
3383
  }
3122
3384
  return Number(getBigIntLike(row, "row_count"));
3123
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";
3124
3394
 
3125
3395
  // src/internal/sql-transform-relations.ts
3126
3396
  import { randomUUID as randomUUID3 } from "crypto";
3127
3397
  import {
3128
- LakeStorageError as LakeStorageError19
3398
+ LakeStorageError as LakeStorageError21
3129
3399
  } from "@sixb/core/lake-storage";
3130
3400
  var RELATION_PLACEHOLDER_PREFIX = "__sixb_sql_transform_relation_";
3131
3401
  var RELATION_PLACEHOLDER_PATTERN = /__sixb_sql_transform_relation_[0-9a-f]{32}_[0-9]+__/g;
@@ -3139,34 +3409,28 @@ class DuckLakeSqlTransformRelation {
3139
3409
  return this.placeholder;
3140
3410
  }
3141
3411
  }
3142
- function renderDuckLakeSqlTransformSql(input) {
3143
- const context = createDuckLakeSqlTransformRelationContext(input);
3144
- const sql = input.sql(context.relations);
3145
- if (typeof sql !== "string") {
3146
- throw new LakeStorageError19("[SixbDuckLake] SQL transform body must return a SQL string.");
3147
- }
3148
- return context.render(sql);
3149
- }
3150
- function createDuckLakeSqlTransformRelationContext(input) {
3412
+ function createDuckLakeSqlTransformTemplate(sourceNames, body) {
3151
3413
  const token = randomUUID3().replaceAll("-", "");
3152
3414
  const relations = Object.create(null);
3153
- const replacements = new Map;
3154
- let index = 0;
3155
- for (const [sourceName, source] of Object.entries(input.sources)) {
3415
+ const sourceNamesByPlaceholder = new Map;
3416
+ sourceNames.forEach((sourceName, index) => {
3156
3417
  const placeholder = `${RELATION_PLACEHOLDER_PREFIX}${token}_${index}__`;
3157
3418
  relations[sourceName] = Object.freeze(new DuckLakeSqlTransformRelation(placeholder));
3158
- replacements.set(placeholder, renderDuckLakeSourceRelation(input.options, source));
3159
- 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.");
3160
3424
  }
3161
3425
  return {
3162
- relations: Object.freeze(relations),
3163
- render(sql) {
3426
+ render(relationSql) {
3164
3427
  const rendered = sql.replace(RELATION_PLACEHOLDER_PATTERN, (placeholder) => {
3165
- const replacement = replacements.get(placeholder);
3166
- if (replacement === undefined) {
3428
+ const sourceName = sourceNamesByPlaceholder.get(placeholder);
3429
+ const relation = sourceName === undefined ? undefined : relationSql.get(sourceName);
3430
+ if (relation === undefined) {
3167
3431
  throwUnresolvedPlaceholder();
3168
3432
  }
3169
- return replacement;
3433
+ return relation;
3170
3434
  });
3171
3435
  if (rendered.includes(RELATION_PLACEHOLDER_PREFIX)) {
3172
3436
  throwUnresolvedPlaceholder();
@@ -3175,13 +3439,967 @@ function createDuckLakeSqlTransformRelationContext(input) {
3175
3439
  }
3176
3440
  };
3177
3441
  }
3178
- function renderDuckLakeSourceRelation(options, source) {
3179
- const snapshotId = parseVersionId(source.versionId);
3180
- const tableName = encodeDatasetTableName(source.datasetId);
3181
- 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
+ });
3182
3476
  }
3183
3477
  function throwUnresolvedPlaceholder() {
3184
- 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);
3185
4403
  }
3186
4404
 
3187
4405
  // src/internal/ducklake-sql-executor.ts
@@ -3209,13 +4427,10 @@ class DuckLakeSqlExecutor {
3209
4427
  }
3210
4428
  async* preview(input) {
3211
4429
  this.connections.assertOpen();
4430
+ const template = createDuckLakeSqlTransformTemplate(Object.keys(input.sources), input.sql);
3212
4431
  const rows = await this.connections.withAttachedRuntime(async (runtime) => {
3213
4432
  const sources = await this.resolveSources(runtime, input.sources);
3214
- const sql = renderDuckLakeSqlTransformSql({
3215
- options: this.options,
3216
- sources: sources.relations,
3217
- sql: input.sql
3218
- });
4433
+ const sql = template.render(renderDuckLakeSourceRelations(this.options, sources.relations));
3219
4434
  return runtime.query(buildPreviewSql(sql, previewLimit(input.limit)));
3220
4435
  });
3221
4436
  for (const row of rows) {
@@ -3225,15 +4440,16 @@ class DuckLakeSqlExecutor {
3225
4440
  async execute(input) {
3226
4441
  this.connections.assertOpen();
3227
4442
  assertDatasetWriteMode(input.mode, "SQL transform");
4443
+ const template = createDuckLakeSqlTransformTemplate(Object.keys(input.sources), input.sql);
3228
4444
  return this.writes.withCommitRuntime(async (runtime) => {
3229
4445
  const target = await this.resolveTargetDataset(runtime, input.target);
3230
4446
  const sources = await this.resolveSources(runtime, input.sources);
3231
- const sql = renderDuckLakeSqlTransformSql({
3232
- options: this.options,
3233
- sources: sources.relations,
3234
- sql: input.sql
3235
- });
3236
- 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
+ ]);
3237
4453
  return this.writes.commitDatasetVersionOnExclusiveRuntime(runtime, {
3238
4454
  dataset: target,
3239
4455
  mode: input.mode,
@@ -3241,6 +4457,7 @@ class DuckLakeSqlExecutor {
3241
4457
  commitMessage: input.commitMessage ?? `execute SQL transform for dataset ${target.id}`,
3242
4458
  producer: input.producer,
3243
4459
  inputs: sources.inputs,
4460
+ lineage: stepLineage === undefined || sourceVersions === undefined ? unanalyzedLineage(target, input.producer) : resolveStepLineage(stepLineage, [...sourceVersions.values()]),
3244
4461
  applyChanges: (runtime, context) => this.applySqlTransform({
3245
4462
  runtime,
3246
4463
  target,
@@ -3252,16 +4469,30 @@ class DuckLakeSqlExecutor {
3252
4469
  });
3253
4470
  });
3254
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
+ }
3255
4482
  async resolveSources(runtime, sources) {
3256
4483
  const relations = Object.create(null);
4484
+ const tables = Object.create(null);
3257
4485
  const inputs = [];
3258
4486
  for (const [sourceName, source] of Object.entries(sources)) {
3259
4487
  const definition = await this.resolveSourceDataset(runtime, source.dataset);
4488
+ pickSourceColumns(sourceName, definition, source.columns);
3260
4489
  const versionRef = await this.resolveSourceVersion(runtime, definition, source.versionId);
3261
4490
  relations[sourceName] = {
3262
4491
  datasetId: definition.id,
3263
- versionId: versionRef.versionId
4492
+ versionId: versionRef.versionId,
4493
+ ...source.columns === undefined ? {} : { columns: source.columns }
3264
4494
  };
4495
+ tables[sourceName] = { definition, version: versionRef };
3265
4496
  inputs.push({
3266
4497
  datasetId: definition.id,
3267
4498
  versionId: versionRef.versionId
@@ -3269,24 +4500,30 @@ class DuckLakeSqlExecutor {
3269
4500
  }
3270
4501
  return {
3271
4502
  relations: Object.freeze(relations),
4503
+ tables: Object.freeze(tables),
3272
4504
  inputs: Object.freeze(inputs)
3273
4505
  };
3274
4506
  }
3275
4507
  async resolveSourceDataset(runtime, dataset) {
3276
4508
  const definition = await this.datasets.getDatasetOnRuntime(runtime, dataset.id);
3277
4509
  if (!definition) {
3278
- throw new LakeStorageError20(`[SixbDuckLake] Unknown SQL transform source dataset '${dataset.id}'.`);
4510
+ throw new LakeStorageError22(`[SixbDuckLake] Unknown SQL transform source dataset '${dataset.id}'.`);
3279
4511
  }
3280
4512
  return definition;
3281
4513
  }
3282
4514
  async resolveTargetDataset(runtime, dataset) {
3283
4515
  const definition = await this.datasets.getDatasetOnRuntime(runtime, dataset.id);
3284
4516
  if (!definition) {
3285
- throw new LakeStorageError20(`[SixbDuckLake] Unknown SQL transform target dataset '${dataset.id}'.`);
4517
+ throw new LakeStorageError22(`[SixbDuckLake] Unknown SQL transform target dataset '${dataset.id}'.`);
3286
4518
  }
3287
4519
  this.datasets.assertSchema(definition);
3288
4520
  return definition;
3289
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
+ }
3290
4527
  async resolveSourceVersion(runtime, dataset, versionId) {
3291
4528
  if (versionId !== undefined) {
3292
4529
  const snapshotId = parseVersionId(versionId);
@@ -3332,7 +4569,7 @@ class DuckLakeSqlExecutor {
3332
4569
  return checkedCasts;
3333
4570
  }
3334
4571
  async applySqlTransform(input) {
3335
- const tempTableName = `sixb_sql_transform_${randomUUID4().replaceAll("-", "")}`;
4572
+ const tempTableName = `sixb_sql_transform_${randomUUID5().replaceAll("-", "")}`;
3336
4573
  const tempTable = quoteIdentifier(tempTableName);
3337
4574
  await input.runtime.run(`CREATE TEMP TABLE ${tempTable} AS SELECT * FROM (${input.sql}) AS sixb_sql_transform_result`);
3338
4575
  let result;
@@ -3358,6 +4595,62 @@ class DuckLakeSqlExecutor {
3358
4595
  return result;
3359
4596
  }
3360
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
+ }
3361
4654
  function previewLimit(limit) {
3362
4655
  if (limit === undefined || !Number.isFinite(limit)) {
3363
4656
  return DEFAULT_PREVIEW_LIMIT;
@@ -3403,33 +4696,34 @@ function normalizePreviewValue(value) {
3403
4696
  return value;
3404
4697
  }
3405
4698
  function throwNoCommittedSourceVersion(datasetId) {
3406
- 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}'.`);
3407
4700
  }
3408
4701
  function throwResultSchemaMismatch(datasetId, detail) {
3409
- 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}.`);
3410
4703
  }
3411
4704
 
3412
4705
  // src/internal/ducklake-write-coordinator.ts
3413
- import { randomUUID as randomUUID7 } from "crypto";
4706
+ import { randomUUID as randomUUID8 } from "crypto";
3414
4707
  import {
3415
4708
  assertUnsequencedDatasetWrite,
3416
4709
  getDatasetPrimaryKeyColumns as getDatasetPrimaryKeyColumns4,
3417
- hasDatasetInputChanges,
3418
- LakeConcurrencyError,
3419
- LakeStorageError as LakeStorageError24,
4710
+ hasDatasetProvenanceChanges,
4711
+ LakeConcurrencyError as LakeConcurrencyError2,
4712
+ LakeStorageError as LakeStorageError26,
4713
+ lineageAtCommit as lineageAtCommit2,
3420
4714
  retryDatasetMergeCommit
3421
4715
  } from "@sixb/core/lake-storage";
3422
4716
 
3423
4717
  // src/internal/dataset-row-merge.ts
3424
4718
  import {
3425
4719
  getDatasetPrimaryKeyColumns as getDatasetPrimaryKeyColumns2,
3426
- LakeStorageError as LakeStorageError21,
4720
+ LakeStorageError as LakeStorageError23,
3427
4721
  reconcileDatasetSequences
3428
4722
  } from "@sixb/core/lake-storage";
3429
4723
  async function applyDatasetMergeFromRelation(input) {
3430
4724
  const primaryKeyColumns = getDatasetPrimaryKeyColumns2(input.dataset);
3431
4725
  if (primaryKeyColumns === null) {
3432
- 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.`);
3433
4727
  }
3434
4728
  const stagingTable = quoteIdentifier(input.stagingTableName);
3435
4729
  const targetTable = qualifiedTableName(input.options, encodeDatasetTableName(input.dataset.id));
@@ -3442,7 +4736,9 @@ async function applyDatasetMergeFromRelation(input) {
3442
4736
  return {
3443
4737
  dataChangeExpected: orderingChanged,
3444
4738
  sourceRowCount,
3445
- resultingRowCount: { kind: "exact", value: previousRowCount }
4739
+ resultingRowCount: { kind: "exact", value: previousRowCount },
4740
+ targetWasEmpty: previousRowCount === 0,
4741
+ changedTableRows: false
3446
4742
  };
3447
4743
  }
3448
4744
  const keyMatchSql = primaryKeyMatchSql(primaryKeyColumns, "target", "source");
@@ -3481,7 +4777,9 @@ async function applyDatasetMergeFromRelation(input) {
3481
4777
  resultingRowCount: {
3482
4778
  kind: "exact",
3483
4779
  value: previousRowCount + effects.inserts - effects.deletes
3484
- }
4780
+ },
4781
+ targetWasEmpty: previousRowCount === 0,
4782
+ changedTableRows: true
3485
4783
  };
3486
4784
  });
3487
4785
  }
@@ -3590,7 +4888,7 @@ function finalChangesSelectSql(input, primaryKeyColumns) {
3590
4888
  async function countMergeEffects(input, targetTable, finalChanges, primaryKeyColumns) {
3591
4889
  const matchColumn = primaryKeyColumns[0];
3592
4890
  if (matchColumn === undefined) {
3593
- 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.`);
3594
4892
  }
3595
4893
  const keyMatchSql = primaryKeyMatchSql(primaryKeyColumns, "target", "source");
3596
4894
  const targetMatch = `target.${quoteIdentifier(matchColumn)} IS NOT NULL`;
@@ -3639,12 +4937,12 @@ function sameColumns(left, right) {
3639
4937
  }
3640
4938
 
3641
4939
  // src/internal/ducklake-merge-session.ts
3642
- import { randomUUID as randomUUID5 } from "crypto";
4940
+ import { randomUUID as randomUUID6 } from "crypto";
3643
4941
  import {
3644
4942
  cloneDatasetMergeChange,
3645
4943
  datasetSequenceChange,
3646
4944
  getDatasetMergeChangeValidationError,
3647
- LakeStorageError as LakeStorageError22
4945
+ LakeStorageError as LakeStorageError24
3648
4946
  } from "@sixb/core/lake-storage";
3649
4947
 
3650
4948
  // src/internal/row-appender.ts
@@ -3703,7 +5001,7 @@ function fileRefToDuckDbStruct(fileRef) {
3703
5001
  // src/internal/ducklake-merge-session.ts
3704
5002
  var MERGE_CHANGE_BATCH_SIZE = 1000;
3705
5003
  async function createDuckLakeMergeSession(input) {
3706
- const suffix = randomUUID5().replaceAll("-", "");
5004
+ const suffix = randomUUID6().replaceAll("-", "");
3707
5005
  const stagingTableName = `sixb_merge_${suffix}`;
3708
5006
  const sequenceColumnName = `sixb_merge_sequence_${suffix}`;
3709
5007
  const kindColumnName = `sixb_merge_kind_${suffix}`;
@@ -3745,7 +5043,7 @@ class DuckLakeMergeSession {
3745
5043
  for await (const change of changes) {
3746
5044
  const validationError = getDatasetMergeChangeValidationError(change, this.input.dataset);
3747
5045
  if (validationError) {
3748
- throw new LakeStorageError22(`[SixbDuckLake] ${validationError}`);
5046
+ throw new LakeStorageError24(`[SixbDuckLake] ${validationError}`);
3749
5047
  }
3750
5048
  batch.push({
3751
5049
  sequence: this.nextSequence,
@@ -3808,7 +5106,7 @@ class DuckLakeMergeSession {
3808
5106
  }
3809
5107
  assertOpen() {
3810
5108
  if (this.closed) {
3811
- throw new LakeStorageError22("[SixbDuckLake] Merge session is already closed.");
5109
+ throw new LakeStorageError24("[SixbDuckLake] Merge session is already closed.");
3812
5110
  }
3813
5111
  }
3814
5112
  async cleanup() {
@@ -3823,16 +5121,16 @@ class DuckLakeMergeSession {
3823
5121
  }
3824
5122
 
3825
5123
  // src/internal/ducklake-write-session.ts
3826
- import { randomUUID as randomUUID6 } from "crypto";
5124
+ import { randomUUID as randomUUID7 } from "crypto";
3827
5125
  import { getDatasetRowValidationError } from "@sixb/core";
3828
5126
  import {
3829
5127
  encodeDatasetPrimaryKey,
3830
5128
  getDatasetPrimaryKeyColumns as getDatasetPrimaryKeyColumns3,
3831
- LakeStorageError as LakeStorageError23
5129
+ LakeStorageError as LakeStorageError25
3832
5130
  } from "@sixb/core/lake-storage";
3833
5131
  var WRITE_ROW_BATCH_SIZE = 1000;
3834
5132
  async function createDuckLakeWriteSession(input) {
3835
- const stagingTableName = `sixb_write_${randomUUID6().replaceAll("-", "")}`;
5133
+ const stagingTableName = `sixb_write_${randomUUID7().replaceAll("-", "")}`;
3836
5134
  await input.runtime.run(`CREATE TEMP TABLE ${quoteIdentifier(stagingTableName)} (${datasetSchemaToDuckDbColumnsSql(input.write.dataset.schema)})`);
3837
5135
  return new DuckLakeWriteSession(input.commitWrite, input.runtime, input.write, stagingTableName);
3838
5136
  }
@@ -3859,13 +5157,13 @@ class DuckLakeWriteSession {
3859
5157
  for await (const row of rows) {
3860
5158
  const validationError = getDatasetRowValidationError(row, this.input.dataset);
3861
5159
  if (validationError) {
3862
- throw new LakeStorageError23(`[SixbDuckLake] ${validationError}`);
5160
+ throw new LakeStorageError25(`[SixbDuckLake] ${validationError}`);
3863
5161
  }
3864
5162
  const clonedRow = structuredClone(row);
3865
5163
  if (getDatasetPrimaryKeyColumns3(this.input.dataset) !== null) {
3866
5164
  const primaryKey = encodeDatasetPrimaryKey(this.input.dataset, clonedRow);
3867
5165
  if (this.stagedPrimaryKeys.has(primaryKey) || batchPrimaryKeys.has(primaryKey)) {
3868
- 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.`);
3869
5167
  }
3870
5168
  batchPrimaryKeys.add(primaryKey);
3871
5169
  }
@@ -3922,7 +5220,7 @@ class DuckLakeWriteSession {
3922
5220
  }
3923
5221
  assertOpen() {
3924
5222
  if (this.closed) {
3925
- throw new LakeStorageError23("[SixbDuckLake] Write session is already closed.");
5223
+ throw new LakeStorageError25("[SixbDuckLake] Write session is already closed.");
3926
5224
  }
3927
5225
  }
3928
5226
  async cleanup() {
@@ -3956,7 +5254,7 @@ class DuckLakeWriteCoordinator {
3956
5254
  this.connections.assertOpen();
3957
5255
  const definition = await this.datasets.getDataset(input.dataset.id);
3958
5256
  if (!definition) {
3959
- throw new LakeStorageError24(`[SixbDuckLake] Unknown dataset '${input.dataset.id}'.`);
5257
+ throw new LakeStorageError26(`[SixbDuckLake] Unknown dataset '${input.dataset.id}'.`);
3960
5258
  }
3961
5259
  this.datasets.assertSchema(definition);
3962
5260
  assertUnsequencedDatasetWrite(definition);
@@ -3977,15 +5275,15 @@ class DuckLakeWriteCoordinator {
3977
5275
  this.connections.assertOpen();
3978
5276
  const definition = await this.datasets.getDataset(input.dataset.id);
3979
5277
  if (!definition) {
3980
- throw new LakeStorageError24(`[SixbDuckLake] Unknown dataset '${input.dataset.id}'.`);
5278
+ throw new LakeStorageError26(`[SixbDuckLake] Unknown dataset '${input.dataset.id}'.`);
3981
5279
  }
3982
5280
  if (getDatasetPrimaryKeyColumns4(definition) === null) {
3983
- 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.`);
3984
5282
  }
3985
5283
  this.datasets.assertSchema(definition);
3986
5284
  const latestVersion = await this.connections.withAttachedRuntime((runtime) => this.snapshots.getLatestVersionForDefinition(runtime, definition));
3987
5285
  if (input.expectedLatestVersionId !== undefined && latestVersion?.versionId !== input.expectedLatestVersionId) {
3988
- 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"}'.`);
3989
5287
  }
3990
5288
  const runtime = await this.connections.stagingRuntime();
3991
5289
  return createDuckLakeMergeSession({
@@ -3998,7 +5296,7 @@ class DuckLakeWriteCoordinator {
3998
5296
  async commitWrite(input) {
3999
5297
  const definition = await this.datasets.getDataset(input.write.dataset.id);
4000
5298
  if (!definition) {
4001
- throw new LakeStorageError24(`[SixbDuckLake] Unknown dataset '${input.write.dataset.id}'.`);
5299
+ throw new LakeStorageError26(`[SixbDuckLake] Unknown dataset '${input.write.dataset.id}'.`);
4002
5300
  }
4003
5301
  const mode = input.write.mode ?? "snapshot";
4004
5302
  assertDatasetWriteMode(mode, "write");
@@ -4009,6 +5307,7 @@ class DuckLakeWriteCoordinator {
4009
5307
  commitMessage: input.commit?.commitMessage ?? `write dataset ${input.write.dataset.id}`,
4010
5308
  producer: input.write.producer,
4011
5309
  inputs: input.write.inputs,
5310
+ lineage: input.write.lineage,
4012
5311
  applyChanges: (runtime, context) => this.applyStagedRows({
4013
5312
  ...input,
4014
5313
  runtime,
@@ -4019,10 +5318,10 @@ class DuckLakeWriteCoordinator {
4019
5318
  async commitMerge(input) {
4020
5319
  const definition = await this.datasets.getDataset(input.merge.dataset.id);
4021
5320
  if (!definition) {
4022
- throw new LakeStorageError24(`[SixbDuckLake] Unknown dataset '${input.merge.dataset.id}'.`);
5321
+ throw new LakeStorageError26(`[SixbDuckLake] Unknown dataset '${input.merge.dataset.id}'.`);
4023
5322
  }
4024
5323
  if (getDatasetPrimaryKeyColumns4(definition) === null) {
4025
- 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.`);
4026
5325
  }
4027
5326
  return retryDatasetMergeCommit(input.merge, input.commit, (rebase) => this.withCommitRuntime(async (runtime) => {
4028
5327
  input.commit?.signal?.throwIfAborted();
@@ -4033,7 +5332,6 @@ class DuckLakeWriteCoordinator {
4033
5332
  expectedLatestVersionId: baseVersionId,
4034
5333
  allowInitialNoOp: !input.commit?.createInitialVersion,
4035
5334
  signal: input.commit?.signal,
4036
- disableRetries: true,
4037
5335
  optimisticOperation: "merge commit",
4038
5336
  commitMessage: input.commit?.commitMessage ?? `merge dataset ${definition.id}`,
4039
5337
  producer: input.merge.producer,
@@ -4050,7 +5348,7 @@ class DuckLakeWriteCoordinator {
4050
5348
  validatedPrimaryKeyColumns: context.validatedPrimaryKeyColumns
4051
5349
  });
4052
5350
  if (result.sourceRowCount !== input.changesWritten) {
4053
- 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.`);
4054
5352
  }
4055
5353
  return result;
4056
5354
  }
@@ -4067,39 +5365,14 @@ class DuckLakeWriteCoordinator {
4067
5365
  }
4068
5366
  async commitDatasetVersionOnExclusiveRuntime(runtime, input) {
4069
5367
  assertUnsequencedDatasetWrite(input.dataset);
4070
- const result = await this.commitVersionOutcomeOnExclusiveRuntime(runtime, {
4071
- ...input,
4072
- disableRetries: input.mode === "append" && getDatasetPrimaryKeyColumns4(input.dataset) !== null
4073
- });
5368
+ const result = await this.commitVersionOutcomeOnExclusiveRuntime(runtime, input);
4074
5369
  if (result.version === null) {
4075
- 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.`);
4076
5371
  }
4077
5372
  return { ...result.version, outcome: result.outcome };
4078
5373
  }
4079
5374
  async commitVersionOutcomeOnExclusiveRuntime(runtime, input) {
4080
- const runtimeInput = { ...input, runtime };
4081
- return this.withGuardedRetrySetting(runtimeInput, () => this.commitDatasetVersionUnlocked(runtimeInput));
4082
- }
4083
- async withGuardedRetrySetting(input, run) {
4084
- if (input.expectedLatestVersionId === undefined && !input.disableRetries) {
4085
- return run();
4086
- }
4087
- await input.runtime.run("SET ducklake_max_retry_count = 0");
4088
- let outcome;
4089
- try {
4090
- outcome = { kind: "success", value: await run() };
4091
- } catch (error) {
4092
- outcome = { kind: "error", error };
4093
- }
4094
- try {
4095
- await input.runtime.run("RESET ducklake_max_retry_count");
4096
- } catch {
4097
- this.connections.poisonRuntime();
4098
- }
4099
- if (outcome.kind === "error") {
4100
- throw outcome.error;
4101
- }
4102
- return outcome.value;
5375
+ return rerunWhenCatalogMoves(input.dataset.id, () => this.commitDatasetVersionUnlocked({ ...input, runtime }), input.signal);
4103
5376
  }
4104
5377
  async commitDatasetVersionUnlocked(input) {
4105
5378
  const previousWriteSnapshotId = await this.lastCommittedSnapshotId(input.runtime);
@@ -4114,15 +5387,26 @@ class DuckLakeWriteCoordinator {
4114
5387
  operation: input.optimisticOperation ?? "commit"
4115
5388
  });
4116
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
+ });
4117
5397
  const changeResult = await input.applyChanges(input.runtime, {
4118
5398
  previousRowCount: latestVersion?.rowCount,
4119
5399
  validatedPrimaryKeyColumns: latestVersion?.validatedPrimaryKeyColumns
4120
5400
  });
4121
5401
  input.signal?.throwIfAborted();
4122
- await this.ensureNoOpHasSnapshot(input, changeResult, latestVersion);
4123
- const commitId = randomUUID7();
4124
- await this.setCommitMetadata(input, commitId, commitRowCount(input, changeResult, latestVersion));
4125
- 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);
4126
5410
  committed = true;
4127
5411
  const committedWriteSnapshotId = await this.lastCommittedSnapshotId(input.runtime);
4128
5412
  try {
@@ -4131,7 +5415,8 @@ class DuckLakeWriteCoordinator {
4131
5415
  commitId,
4132
5416
  previousWriteSnapshotId,
4133
5417
  committedWriteSnapshotId,
4134
- changeResult
5418
+ changeResult,
5419
+ noOpBaseVersionId: noOpBase?.versionId ?? null
4135
5420
  });
4136
5421
  } finally {
4137
5422
  await this.connections.detachLocalCatalogAfterCommit(input.runtime);
@@ -4139,9 +5424,6 @@ class DuckLakeWriteCoordinator {
4139
5424
  } catch (error) {
4140
5425
  if (!committed) {
4141
5426
  await this.rollbackTransaction(input.runtime);
4142
- 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"')) {
4143
- throw new LakeConcurrencyError(`[SixbDuckLake] Concurrent catalog commit for dataset '${input.dataset.id}'.`, { cause: error });
4144
- }
4145
5427
  }
4146
5428
  throw error;
4147
5429
  }
@@ -4153,7 +5435,7 @@ class DuckLakeWriteCoordinator {
4153
5435
  if (version) {
4154
5436
  return { outcome: "created", version };
4155
5437
  }
4156
- 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.`);
4157
5439
  }
4158
5440
  if (!input.changeResult.dataChangeExpected) {
4159
5441
  const version = await this.versionForNoOpCommit(input.runtime, input.dataset, input.allowInitialNoOp ?? false);
@@ -4165,15 +5447,18 @@ class DuckLakeWriteCoordinator {
4165
5447
  operation: input.optimisticOperation ?? "commit"
4166
5448
  });
4167
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
+ }
4168
5453
  return {
4169
5454
  outcome: "unchanged",
4170
5455
  version
4171
5456
  };
4172
5457
  }
4173
5458
  if (input.committedWriteSnapshotId === input.previousWriteSnapshotId) {
4174
- 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.`);
4175
5460
  }
4176
- 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.`);
4177
5462
  }
4178
5463
  async applyStagedRows(input) {
4179
5464
  const mode = input.write.mode ?? "snapshot";
@@ -4186,11 +5471,11 @@ class DuckLakeWriteCoordinator {
4186
5471
  previousRowCount: input.previousRowCount
4187
5472
  });
4188
5473
  if (result.sourceRowCount !== input.rowsWritten) {
4189
- 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.`);
4190
5475
  }
4191
5476
  return result;
4192
5477
  }
4193
- async setCommitMetadata(input, commitId, rowCount) {
5478
+ async setCommitMetadata(input, commitId, rowCount, lineage) {
4194
5479
  const validatedPrimaryKeyColumns = getDatasetPrimaryKeyColumns4(input.dataset);
4195
5480
  const metadata = {
4196
5481
  kind: "datasetVersion",
@@ -4200,22 +5485,35 @@ class DuckLakeWriteCoordinator {
4200
5485
  producer: input.producer ? structuredClone(input.producer) : undefined,
4201
5486
  inputs: input.inputs ? structuredClone(input.inputs) : undefined,
4202
5487
  ...rowCount.kind === "exact" ? { rowCount: rowCount.value } : {},
4203
- ...validatedPrimaryKeyColumns !== null ? { validatedPrimaryKeyColumns: [...validatedPrimaryKeyColumns] } : {}
5488
+ ...validatedPrimaryKeyColumns !== null ? { validatedPrimaryKeyColumns: [...validatedPrimaryKeyColumns] } : {},
5489
+ ...lineage === undefined ? {} : { lineage: encodeLineage(lineage) }
4204
5490
  };
4205
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 }))})`);
4206
5492
  }
4207
- async ensureNoOpHasSnapshot(input, changeResult, knownLatestVersion) {
4208
- if (changeResult.dataChangeExpected) {
4209
- return;
4210
- }
4211
- const latestVersion = knownLatestVersion === undefined ? await this.snapshots.getLatestVersionSummaryForDefinition(input.runtime, input.dataset) : knownLatestVersion;
4212
- if (latestVersion ? input.mode === "merge" || !hasDatasetInputChanges(latestVersion.inputs, input.inputs) : input.allowInitialNoOp) {
4213
- return;
4214
- }
5493
+ async claimTable(input) {
4215
5494
  const table = qualifiedTableName(this.options, encodeDatasetTableName(input.dataset.id));
4216
5495
  const descriptionSql = input.dataset.description === undefined ? "NULL" : quoteSqlString(input.dataset.description);
4217
5496
  await input.runtime.run(`COMMENT ON TABLE ${table} IS ${descriptionSql}`);
4218
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
+ }
4219
5517
  async versionForNoOpCommit(runtime, definition, allowInitialNoOp) {
4220
5518
  const latestVersion = await this.snapshots.getLatestVersionForDefinition(runtime, definition);
4221
5519
  if (latestVersion) {
@@ -4224,7 +5522,7 @@ class DuckLakeWriteCoordinator {
4224
5522
  if (allowInitialNoOp) {
4225
5523
  return null;
4226
5524
  }
4227
- 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.`);
4228
5526
  }
4229
5527
  async rollbackTransaction(runtime) {
4230
5528
  try {
@@ -4232,31 +5530,17 @@ class DuckLakeWriteCoordinator {
4232
5530
  } catch {}
4233
5531
  }
4234
5532
  async beginVersionTransaction(input) {
4235
- if (!isSqliteCatalog(this.options.catalog)) {
5533
+ if (input.mode === "snapshot" && input.expectedLatestVersionId === undefined && !isSqliteCatalog(this.options.catalog)) {
4236
5534
  await input.runtime.run("BEGIN TRANSACTION");
4237
- if (input.expectedLatestVersionId === undefined && input.mode !== "append") {
4238
- return;
4239
- }
4240
- return this.snapshots.getLatestVersionSummaryForDefinition(input.runtime, input.dataset);
4241
- }
4242
- for (let attempt = 0;attempt < 3; attempt++) {
4243
- input.signal?.throwIfAborted();
4244
- const snapshotId = await this.currentSnapshotId(input.runtime);
4245
- const latestVersion = await this.snapshots.getLatestVersionSummaryForDefinition(input.runtime, input.dataset);
4246
- await input.runtime.run("BEGIN TRANSACTION");
4247
- if (await this.currentSnapshotId(input.runtime) === snapshotId) {
4248
- return latestVersion;
4249
- }
4250
- await input.runtime.run("ROLLBACK");
4251
- }
4252
- throw new LakeConcurrencyError(`[SixbDuckLake] Catalog changed during three write preparations for dataset '${input.dataset.id}'. Retry the write.`);
4253
- }
4254
- async currentSnapshotId(runtime) {
4255
- const [row] = await runtime.query(`SELECT id FROM ${quoteIdentifier(duckLakeAlias(this.options))}.current_snapshot()`);
4256
- if (row === undefined) {
4257
- throw new LakeStorageError24("[SixbDuckLake] DuckLake did not return a current snapshot.");
5535
+ return;
4258
5536
  }
4259
- 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
+ });
4260
5544
  }
4261
5545
  async lastCommittedSnapshotId(runtime) {
4262
5546
  const [row] = await runtime.query(`SELECT id FROM ${quoteIdentifier(duckLakeAlias(this.options))}.last_committed_snapshot()`);
@@ -4301,12 +5585,33 @@ class DuckLakeWriteCoordinator {
4301
5585
  return;
4302
5586
  }
4303
5587
  if (input.actualLatestVersionId !== input.expectedLatestVersionId) {
4304
- 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"}'.`);
4305
5589
  }
4306
5590
  }
4307
5591
  }
4308
- function isSqliteCatalog(catalog) {
4309
- 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);
4310
5615
  }
4311
5616
  function commitRowCount(input, changeResult, latestVersion) {
4312
5617
  if (input.mode === "append" && input.expectedLatestVersionId === undefined) {
@@ -4322,7 +5627,7 @@ function commitRowCount(input, changeResult, latestVersion) {
4322
5627
  }
4323
5628
  function assertDuckLakeSnapshotId3(snapshotId) {
4324
5629
  if (!/^\d+$/.test(snapshotId)) {
4325
- throw new LakeStorageError24(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`);
5630
+ throw new LakeStorageError26(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`);
4326
5631
  }
4327
5632
  }
4328
5633
  var localCatalogCommitLocks = new Map;
@@ -4353,6 +5658,7 @@ async function withCatalogCommitLock(options, run) {
4353
5658
  // src/ducklake-storage.ts
4354
5659
  class DuckLakeStorage {
4355
5660
  standard = { id: "ducklake", version: "1.0" };
5661
+ recordsLineage = true;
4356
5662
  sql;
4357
5663
  connections;
4358
5664
  datasets;
@@ -4374,15 +5680,15 @@ class DuckLakeStorage {
4374
5680
  };
4375
5681
  this.connections = new DuckLakeConnectionManager(normalizedOptions);
4376
5682
  this.datasets = new DuckLakeDatasetCatalog(normalizedOptions, this.connections);
4377
- this.maintenance = new DuckLakeMaintenance(normalizedOptions, this.connections);
4378
5683
  this.snapshotReader = new DuckLakeSnapshotReader(normalizedOptions, this.connections, this.datasets);
5684
+ this.maintenance = new DuckLakeMaintenance(normalizedOptions, this.connections, this.snapshotReader);
4379
5685
  this.rows = new DuckLakeRowReader(normalizedOptions, this.connections, this.snapshotReader);
4380
5686
  this.changes = new DuckLakeChangeReader(normalizedOptions, this.connections, this.snapshotReader);
4381
5687
  this.writes = new DuckLakeWriteCoordinator(normalizedOptions, this.connections, this.datasets, this.snapshotReader);
4382
5688
  this.sql = new DuckLakeSqlExecutor(normalizedOptions, this.connections, this.datasets, this.snapshotReader, this.writes);
4383
5689
  }
4384
5690
  async createDataset(definition) {
4385
- return this.datasets.createDataset(definition);
5691
+ return this.datasets.createDataset(definition, (runtime, dataset) => this.snapshotReader.getLatestVersionSummaryAtCurrentSnapshot(runtime, dataset));
4386
5692
  }
4387
5693
  async assertDatasetDefinitionsCompatible(definitions) {
4388
5694
  await this.datasets.assertDatasetDefinitionsCompatible(definitions);
@@ -4431,4 +5737,4 @@ export {
4431
5737
  DuckLakeStorage
4432
5738
  };
4433
5739
 
4434
- //# debugId=99E61447F4C74C9E64756E2164756E21
5740
+ //# debugId=9F26B0918A26BCC464756E2164756E21