@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.
- package/README.md +59 -4
- package/dist/ducklake-storage.d.ts +1 -0
- package/dist/ducklake-storage.d.ts.map +1 -1
- package/dist/index.js +1817 -511
- package/dist/index.js.map +24 -14
- package/dist/internal/dataset-row-commit.d.ts +10 -0
- package/dist/internal/dataset-row-commit.d.ts.map +1 -1
- package/dist/internal/dataset-row-merge.d.ts.map +1 -1
- package/dist/internal/ducklake-dataset-catalog.d.ts +20 -3
- package/dist/internal/ducklake-dataset-catalog.d.ts.map +1 -1
- package/dist/internal/ducklake-dataset-table-ref.d.ts +13 -0
- package/dist/internal/ducklake-dataset-table-ref.d.ts.map +1 -1
- package/dist/internal/ducklake-maintenance.d.ts +21 -1
- package/dist/internal/ducklake-maintenance.d.ts.map +1 -1
- package/dist/internal/ducklake-snapshot-reader.d.ts +63 -16
- package/dist/internal/ducklake-snapshot-reader.d.ts.map +1 -1
- package/dist/internal/ducklake-sql-executor.d.ts +9 -1
- package/dist/internal/ducklake-sql-executor.d.ts.map +1 -1
- package/dist/internal/ducklake-version-transaction.d.ts +54 -0
- package/dist/internal/ducklake-version-transaction.d.ts.map +1 -0
- package/dist/internal/ducklake-write-coordinator.d.ts +30 -3
- package/dist/internal/ducklake-write-coordinator.d.ts.map +1 -1
- package/dist/internal/lineage-encoding.d.ts +8 -0
- package/dist/internal/lineage-encoding.d.ts.map +1 -0
- package/dist/internal/sql-lineage/analysis.d.ts +32 -0
- package/dist/internal/sql-lineage/analysis.d.ts.map +1 -0
- package/dist/internal/sql-lineage/expressions.d.ts +9 -0
- package/dist/internal/sql-lineage/expressions.d.ts.map +1 -0
- package/dist/internal/sql-lineage/plan-json.d.ts +34 -0
- package/dist/internal/sql-lineage/plan-json.d.ts.map +1 -0
- package/dist/internal/sql-lineage/plan-relation.d.ts +28 -0
- package/dist/internal/sql-lineage/plan-relation.d.ts.map +1 -0
- package/dist/internal/sql-lineage/plan-walker.d.ts +18 -0
- package/dist/internal/sql-lineage/plan-walker.d.ts.map +1 -0
- package/dist/internal/sql-lineage/rejection.d.ts +11 -0
- package/dist/internal/sql-lineage/rejection.d.ts.map +1 -0
- package/dist/internal/sql-lineage/statement-gate.d.ts +21 -0
- package/dist/internal/sql-lineage/statement-gate.d.ts.map +1 -0
- package/dist/internal/sql-lineage/table-functions.d.ts +6 -0
- package/dist/internal/sql-lineage/table-functions.d.ts.map +1 -0
- package/dist/internal/sql-transform-relations.d.ts +20 -5
- package/dist/internal/sql-transform-relations.d.ts.map +1 -1
- package/dist/internal/versions.d.ts +10 -1
- package/dist/internal/versions.d.ts.map +1 -1
- package/dist/types.d.ts +10 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/ducklake-storage.ts +9 -2
- package/src/internal/dataset-row-commit.ts +28 -2
- package/src/internal/dataset-row-merge.ts +4 -0
- package/src/internal/ducklake-dataset-catalog.ts +162 -49
- package/src/internal/ducklake-dataset-table-ref.ts +75 -17
- package/src/internal/ducklake-maintenance.ts +110 -48
- package/src/internal/ducklake-snapshot-reader.ts +352 -304
- package/src/internal/ducklake-sql-executor.ts +214 -14
- package/src/internal/ducklake-version-transaction.ts +224 -0
- package/src/internal/ducklake-write-coordinator.ts +226 -148
- package/src/internal/lineage-encoding.ts +136 -0
- package/src/internal/sql-lineage/analysis.ts +257 -0
- package/src/internal/sql-lineage/expressions.ts +208 -0
- package/src/internal/sql-lineage/plan-json.ts +130 -0
- package/src/internal/sql-lineage/plan-relation.ts +59 -0
- package/src/internal/sql-lineage/plan-walker.ts +727 -0
- package/src/internal/sql-lineage/rejection.ts +16 -0
- package/src/internal/sql-lineage/statement-gate.ts +164 -0
- package/src/internal/sql-lineage/table-functions.ts +11 -0
- package/src/internal/sql-transform-relations.ts +90 -40
- package/src/internal/versions.ts +37 -14
- 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
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
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
|
|
461
|
+
tableId,
|
|
462
|
+
...sequenceTableId === undefined ? {} : { sequenceTableId }
|
|
428
463
|
};
|
|
429
464
|
}
|
|
430
465
|
|
|
431
466
|
// src/internal/schema.ts
|
|
432
|
-
import { LakeStorageError as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
618
|
+
import { LakeStorageError as LakeStorageError7 } from "@sixb/core/lake-storage";
|
|
584
619
|
function parseVersionId(versionId) {
|
|
585
620
|
if (!versionId.startsWith("ducklake:")) {
|
|
586
|
-
throw new
|
|
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
|
|
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
|
-
...
|
|
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
|
|
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
|
|
649
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
857
|
+
import { LakeStorageError as LakeStorageError11 } from "@sixb/core/lake-storage";
|
|
812
858
|
|
|
813
859
|
// src/internal/duckdb-reader.ts
|
|
814
|
-
import { LakeStorageError as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1091
|
+
throw new LakeStorageError11("[SixbDuckLake] DuckDB runtime is closed.");
|
|
1046
1092
|
}
|
|
1047
1093
|
}
|
|
1048
1094
|
assertNotClosed() {
|
|
1049
1095
|
if (this.closed) {
|
|
1050
|
-
throw new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
2030
|
-
|
|
2031
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
2127
|
-
AND begin_snapshot <= ${
|
|
2128
|
-
AND (end_snapshot IS NULL OR end_snapshot > ${
|
|
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
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
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,
|
|
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}
|
|
2492
|
+
return `CALL ${procedure}(${aliasSql}, ${dryRunSql}${selection})`;
|
|
2199
2493
|
}
|
|
2200
2494
|
|
|
2201
2495
|
// src/internal/ducklake-row-reader.ts
|
|
2202
|
-
import { LakeStorageError as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2379
|
-
const latestRowByDatasetId = await this.resolveLatestSnapshotRows(runtime,
|
|
2673
|
+
const tablesByDatasetId = await this.resolveDatasetTables(runtime, uniqueIds);
|
|
2674
|
+
const latestRowByDatasetId = await this.resolveLatestSnapshotRows(runtime, tablesByDatasetId);
|
|
2380
2675
|
return uniqueIds.map((datasetId) => {
|
|
2381
|
-
if (!
|
|
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
|
|
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
|
|
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
|
|
2844
|
+
const tablesByDatasetId = new Map;
|
|
2544
2845
|
for (const row of rows) {
|
|
2545
|
-
const
|
|
2846
|
+
const tableName = getString(row, "table_name");
|
|
2847
|
+
const datasetId = datasetIdByTableName.get(tableName);
|
|
2546
2848
|
if (datasetId !== undefined) {
|
|
2547
|
-
|
|
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
|
|
2866
|
+
return tablesByDatasetId;
|
|
2551
2867
|
}
|
|
2552
|
-
async resolveLatestSnapshotRows(runtime,
|
|
2868
|
+
async resolveLatestSnapshotRows(runtime, tablesByDatasetId, scanLimit = CATALOG_SNAPSHOT_SCAN_LIMIT) {
|
|
2553
2869
|
const result = new Map;
|
|
2554
|
-
if (
|
|
2870
|
+
if (tablesByDatasetId.size === 0) {
|
|
2555
2871
|
return result;
|
|
2556
2872
|
}
|
|
2557
|
-
const unresolved = new Map(
|
|
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 <
|
|
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
|
|
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
|
-
|
|
2570
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
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
|
|
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.
|
|
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 ?
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2785
|
-
tableId: input.tableId,
|
|
3068
|
+
table: input.table,
|
|
2786
3069
|
exactSnapshotId: input.exactSnapshotId,
|
|
2787
3070
|
beforeSnapshotId,
|
|
2788
|
-
limit: input.exactSnapshotId === undefined ?
|
|
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
|
-
|
|
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 <
|
|
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
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
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
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
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
|
|
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
|
|
2960
|
-
const
|
|
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
|
|
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
|
|
3241
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
2991
3242
|
import {
|
|
2992
|
-
LakeStorageError as
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3154
|
-
|
|
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
|
-
|
|
3159
|
-
|
|
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
|
-
|
|
3163
|
-
render(sql) {
|
|
3426
|
+
render(relationSql) {
|
|
3164
3427
|
const rendered = sql.replace(RELATION_PLACEHOLDER_PATTERN, (placeholder) => {
|
|
3165
|
-
const
|
|
3166
|
-
|
|
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
|
|
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
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
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
|
|
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
|
|
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_${
|
|
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
|
|
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
|
|
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
|
|
4706
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
3414
4707
|
import {
|
|
3415
4708
|
assertUnsequencedDatasetWrite,
|
|
3416
4709
|
getDatasetPrimaryKeyColumns as getDatasetPrimaryKeyColumns4,
|
|
3417
|
-
|
|
3418
|
-
LakeConcurrencyError,
|
|
3419
|
-
LakeStorageError as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4940
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
3643
4941
|
import {
|
|
3644
4942
|
cloneDatasetMergeChange,
|
|
3645
4943
|
datasetSequenceChange,
|
|
3646
4944
|
getDatasetMergeChangeValidationError,
|
|
3647
|
-
LakeStorageError as
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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_${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5278
|
+
throw new LakeStorageError26(`[SixbDuckLake] Unknown dataset '${input.dataset.id}'.`);
|
|
3981
5279
|
}
|
|
3982
5280
|
if (getDatasetPrimaryKeyColumns4(definition) === null) {
|
|
3983
|
-
throw new
|
|
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
|
|
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
|
|
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
|
|
5321
|
+
throw new LakeStorageError26(`[SixbDuckLake] Unknown dataset '${input.merge.dataset.id}'.`);
|
|
4023
5322
|
}
|
|
4024
5323
|
if (getDatasetPrimaryKeyColumns4(definition) === null) {
|
|
4025
|
-
throw new
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
4123
|
-
const
|
|
4124
|
-
|
|
4125
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
4309
|
-
|
|
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
|
|
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=
|
|
5740
|
+
//# debugId=9F26B0918A26BCC464756E2164756E21
|