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