@sixb/ducklake 0.1.0 → 0.1.2
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 +48 -4
- package/dist/ducklake-storage.d.ts +2 -1
- package/dist/ducklake-storage.d.ts.map +1 -1
- package/dist/index.js +637 -50
- package/dist/index.js.map +16 -14
- package/dist/internal/dataset-row-commit.d.ts +6 -0
- package/dist/internal/dataset-row-commit.d.ts.map +1 -1
- package/dist/internal/dataset-row-merge.d.ts +16 -0
- package/dist/internal/dataset-row-merge.d.ts.map +1 -0
- package/dist/internal/ducklake-catalog-schema.d.ts +4 -1
- package/dist/internal/ducklake-catalog-schema.d.ts.map +1 -1
- package/dist/internal/ducklake-current-dataset-definitions.d.ts.map +1 -1
- package/dist/internal/ducklake-dataset-catalog.d.ts +1 -0
- package/dist/internal/ducklake-dataset-catalog.d.ts.map +1 -1
- package/dist/internal/ducklake-merge-session.d.ts +21 -0
- package/dist/internal/ducklake-merge-session.d.ts.map +1 -0
- package/dist/internal/ducklake-row-reader.d.ts.map +1 -1
- package/dist/internal/ducklake-snapshot-reader.d.ts +1 -0
- package/dist/internal/ducklake-snapshot-reader.d.ts.map +1 -1
- package/dist/internal/ducklake-write-coordinator.d.ts +5 -1
- package/dist/internal/ducklake-write-coordinator.d.ts.map +1 -1
- package/dist/internal/ducklake-write-session.d.ts.map +1 -1
- package/dist/internal/schema.d.ts +2 -0
- package/dist/internal/schema.d.ts.map +1 -1
- package/dist/internal/versions.d.ts +1 -0
- package/dist/internal/versions.d.ts.map +1 -1
- package/package.json +7 -4
- package/src/ducklake-storage.ts +6 -0
- package/src/internal/dataset-row-commit.ts +95 -2
- package/src/internal/dataset-row-merge.ts +268 -0
- package/src/internal/ducklake-catalog-schema.ts +90 -1
- package/src/internal/ducklake-current-dataset-definitions.ts +14 -5
- package/src/internal/ducklake-dataset-catalog.ts +43 -9
- package/src/internal/ducklake-merge-session.ts +188 -0
- package/src/internal/ducklake-row-reader.ts +51 -27
- package/src/internal/ducklake-snapshot-reader.ts +9 -2
- package/src/internal/ducklake-write-coordinator.ts +160 -20
- package/src/internal/ducklake-write-session.ts +32 -5
- package/src/internal/schema.ts +7 -0
- package/src/internal/versions.ts +17 -1
package/dist/index.js
CHANGED
|
@@ -989,6 +989,9 @@ function datasetColumnToDuckDbSql(column) {
|
|
|
989
989
|
function datasetSchemaToDuckDbColumnsSql(schema) {
|
|
990
990
|
return schema.columns.map((column) => datasetColumnToDuckDbSql(column)).join(", ");
|
|
991
991
|
}
|
|
992
|
+
function datasetSchemaToDuckDbNullableColumnsSql(schema) {
|
|
993
|
+
return schema.columns.map((column) => `${quoteIdentifier(column.name)} ${datasetColumnTypeToDuckDbSql(column.type)}`).join(", ");
|
|
994
|
+
}
|
|
992
995
|
function datasetSchemaColumnNamesSql(schema) {
|
|
993
996
|
return schema.columns.map((column) => quoteIdentifier(column.name)).join(", ");
|
|
994
997
|
}
|
|
@@ -1056,6 +1059,8 @@ function getIntegerField(record, key) {
|
|
|
1056
1059
|
}
|
|
1057
1060
|
|
|
1058
1061
|
// src/internal/ducklake-catalog-schema.ts
|
|
1062
|
+
var SIXB_PRIMARY_KEY_COMMENT_NAMESPACE = "sixb:primary-key:";
|
|
1063
|
+
var SIXB_PRIMARY_KEY_COMMENT_PREFIX = `${SIXB_PRIMARY_KEY_COMMENT_NAMESPACE}v1:`;
|
|
1059
1064
|
var FILE_REF_STRUCT_CHILDREN = [
|
|
1060
1065
|
{ name: "blobId", type: "string" },
|
|
1061
1066
|
{ name: "digest", type: "string" },
|
|
@@ -1080,6 +1085,58 @@ function duckLakeCatalogColumnsToDatasetSchema(tableName, rows) {
|
|
|
1080
1085
|
columns: topLevelColumns.sort(compareDuckLakeCatalogColumnOrder).map((column) => duckLakeCatalogColumnToDefinition(tableName, column, childrenByParentId))
|
|
1081
1086
|
};
|
|
1082
1087
|
}
|
|
1088
|
+
function duckLakePrimaryKeyColumnComment(ordinal) {
|
|
1089
|
+
return `${SIXB_PRIMARY_KEY_COMMENT_PREFIX}${ordinal}`;
|
|
1090
|
+
}
|
|
1091
|
+
function duckLakeCatalogColumnsToDatasetPrimaryKey(tableName, rows) {
|
|
1092
|
+
const keyedColumns = [];
|
|
1093
|
+
const seenOrdinals = new Set;
|
|
1094
|
+
for (const column of rows) {
|
|
1095
|
+
const comment = column.comment;
|
|
1096
|
+
if (comment === undefined || !comment.startsWith(SIXB_PRIMARY_KEY_COMMENT_NAMESPACE)) {
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
if (column.parentColumnId !== undefined || !comment.startsWith(SIXB_PRIMARY_KEY_COMMENT_PREFIX)) {
|
|
1100
|
+
throwInvalidPrimaryKeyMetadata(tableName, column, comment);
|
|
1101
|
+
}
|
|
1102
|
+
const ordinalText = comment.slice(SIXB_PRIMARY_KEY_COMMENT_PREFIX.length);
|
|
1103
|
+
if (!/^(0|[1-9]\d*)$/.test(ordinalText)) {
|
|
1104
|
+
throwInvalidPrimaryKeyMetadata(tableName, column, comment);
|
|
1105
|
+
}
|
|
1106
|
+
const ordinal = Number(ordinalText);
|
|
1107
|
+
if (!Number.isSafeInteger(ordinal)) {
|
|
1108
|
+
throwInvalidPrimaryKeyMetadata(tableName, column, comment);
|
|
1109
|
+
}
|
|
1110
|
+
if (seenOrdinals.has(ordinal)) {
|
|
1111
|
+
throw new LakeStorageError8(`[SixbDuckLake] Dataset table '${tableName}' has duplicate primary-key ordinal '${ordinal}'.`);
|
|
1112
|
+
}
|
|
1113
|
+
seenOrdinals.add(ordinal);
|
|
1114
|
+
if (duckDbTypeToDatasetColumnType(column.columnType) !== "string") {
|
|
1115
|
+
throw new LakeStorageError8(`[SixbDuckLake] Dataset table '${tableName}' primary-key column '${column.columnName}' must map to type 'string'.`);
|
|
1116
|
+
}
|
|
1117
|
+
if (column.nullsAllowed) {
|
|
1118
|
+
throw new LakeStorageError8(`[SixbDuckLake] Dataset table '${tableName}' primary-key column '${column.columnName}' must not be nullable.`);
|
|
1119
|
+
}
|
|
1120
|
+
keyedColumns.push({ column, ordinal });
|
|
1121
|
+
}
|
|
1122
|
+
if (keyedColumns.length === 0) {
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
keyedColumns.sort((left, right) => left.ordinal - right.ordinal);
|
|
1126
|
+
for (const [expectedOrdinal, keyedColumn] of keyedColumns.entries()) {
|
|
1127
|
+
if (keyedColumn.ordinal !== expectedOrdinal) {
|
|
1128
|
+
throw new LakeStorageError8(`[SixbDuckLake] Dataset table '${tableName}' primary-key ordinals must be contiguous from 0.`);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
const columnNames = keyedColumns.map(({ column }) => column.columnName);
|
|
1132
|
+
if (columnNames.length === 1) {
|
|
1133
|
+
return columnNames[0];
|
|
1134
|
+
}
|
|
1135
|
+
return columnNames;
|
|
1136
|
+
}
|
|
1137
|
+
function throwInvalidPrimaryKeyMetadata(tableName, column, comment) {
|
|
1138
|
+
throw new LakeStorageError8(`[SixbDuckLake] Dataset table '${tableName}' has malformed primary-key metadata '${comment}' on column '${column.columnName}'.`);
|
|
1139
|
+
}
|
|
1083
1140
|
function duckLakeCatalogColumnToDefinition(tableName, column, childrenByParentId) {
|
|
1084
1141
|
const children = [...childrenByParentId.get(column.columnId) ?? []].sort(compareDuckLakeCatalogColumnOrder);
|
|
1085
1142
|
return {
|
|
@@ -1205,11 +1262,14 @@ async function readCurrentDatasetDefinitions(input) {
|
|
|
1205
1262
|
const partitionByTableName = groupPartitionsByTableName(await readCurrentDatasetPartitions(input, tableNames));
|
|
1206
1263
|
const definitions = new Map;
|
|
1207
1264
|
for (const table of tables) {
|
|
1265
|
+
const columns = columnsByTableName.get(table.tableName) ?? [];
|
|
1266
|
+
const primaryKey = duckLakeCatalogColumnsToDatasetPrimaryKey(table.tableName, columns);
|
|
1208
1267
|
const partitionBy = partitionByTableName.get(table.tableName) ?? [];
|
|
1209
1268
|
definitions.set(table.datasetId, {
|
|
1210
1269
|
kind: "dataset",
|
|
1211
1270
|
id: table.datasetId,
|
|
1212
|
-
schema: duckLakeCatalogColumnsToDatasetSchema(table.tableName,
|
|
1271
|
+
schema: duckLakeCatalogColumnsToDatasetSchema(table.tableName, columns),
|
|
1272
|
+
...primaryKey !== undefined ? { primaryKey } : {},
|
|
1213
1273
|
...partitionBy.length > 0 ? { partitionBy } : {},
|
|
1214
1274
|
...table.comment !== undefined ? { description: table.comment } : {}
|
|
1215
1275
|
});
|
|
@@ -1243,6 +1303,7 @@ async function readCurrentDatasetColumns(input, tableNames) {
|
|
|
1243
1303
|
}
|
|
1244
1304
|
const ducklakeTable = duckLakeMetadataTableName(input.options, "ducklake_table");
|
|
1245
1305
|
const ducklakeColumn = duckLakeMetadataTableName(input.options, "ducklake_column");
|
|
1306
|
+
const ducklakeColumnTag = duckLakeMetadataTableName(input.options, "ducklake_column_tag");
|
|
1246
1307
|
const rows = await input.runtime.query(`
|
|
1247
1308
|
SELECT
|
|
1248
1309
|
table_meta.table_name,
|
|
@@ -1251,11 +1312,17 @@ async function readCurrentDatasetColumns(input, tableNames) {
|
|
|
1251
1312
|
column_meta.column_name,
|
|
1252
1313
|
column_meta.column_type,
|
|
1253
1314
|
CAST(column_meta.nulls_allowed AS BOOLEAN) AS nulls_allowed,
|
|
1254
|
-
column_meta.parent_column
|
|
1315
|
+
column_meta.parent_column,
|
|
1316
|
+
column_tag."value" AS comment
|
|
1255
1317
|
FROM ${ducklakeTable} table_meta
|
|
1256
1318
|
JOIN ${ducklakeColumn} column_meta
|
|
1257
1319
|
ON column_meta.table_id = table_meta.table_id
|
|
1258
1320
|
AND column_meta.end_snapshot IS NULL
|
|
1321
|
+
LEFT JOIN ${ducklakeColumnTag} column_tag
|
|
1322
|
+
ON column_tag.table_id = column_meta.table_id
|
|
1323
|
+
AND column_tag.column_id = column_meta.column_id
|
|
1324
|
+
AND column_tag."key" = 'comment'
|
|
1325
|
+
AND column_tag.end_snapshot IS NULL
|
|
1259
1326
|
WHERE table_meta.end_snapshot IS NULL
|
|
1260
1327
|
AND table_meta.table_name IN (${quoteSqlStringList(tableNames)})
|
|
1261
1328
|
ORDER BY table_meta.table_name, column_meta.column_order
|
|
@@ -1267,7 +1334,8 @@ async function readCurrentDatasetColumns(input, tableNames) {
|
|
|
1267
1334
|
columnName: getString(row, "column_name"),
|
|
1268
1335
|
columnType: getString(row, "column_type"),
|
|
1269
1336
|
nullsAllowed: getBoolean(row, "nulls_allowed"),
|
|
1270
|
-
parentColumnId: getOptionalBigIntLike(row, "parent_column")
|
|
1337
|
+
parentColumnId: getOptionalBigIntLike(row, "parent_column"),
|
|
1338
|
+
comment: getOptionalString(row, "comment")
|
|
1271
1339
|
}));
|
|
1272
1340
|
}
|
|
1273
1341
|
async function readCurrentDatasetPartitions(input, tableNames) {
|
|
@@ -1352,11 +1420,23 @@ class DuckLakeDatasetCatalog {
|
|
|
1352
1420
|
const tableName = encodeDatasetTableName(definition.id);
|
|
1353
1421
|
const qualifiedName = qualifiedTableName(this.options, tableName);
|
|
1354
1422
|
await this.connections.withExclusiveAttached(async (runtime) => {
|
|
1355
|
-
await runtime.run(
|
|
1356
|
-
|
|
1357
|
-
|
|
1423
|
+
await runtime.run("BEGIN TRANSACTION");
|
|
1424
|
+
let committed = false;
|
|
1425
|
+
try {
|
|
1426
|
+
await runtime.run(`CREATE TABLE ${qualifiedName} (${datasetSchemaToDuckDbColumnsSql(definition.schema)})`);
|
|
1427
|
+
if (definition.description !== undefined) {
|
|
1428
|
+
await runtime.run(`COMMENT ON TABLE ${qualifiedName} IS ${quoteSqlString(definition.description)}`);
|
|
1429
|
+
}
|
|
1430
|
+
await this.applyPrimaryKey(runtime, qualifiedName, definition);
|
|
1431
|
+
await this.applyPartitionBy(runtime, qualifiedName, definition);
|
|
1432
|
+
await runtime.run("COMMIT");
|
|
1433
|
+
committed = true;
|
|
1434
|
+
} catch (error) {
|
|
1435
|
+
if (!committed) {
|
|
1436
|
+
await this.rollbackTransaction(runtime);
|
|
1437
|
+
}
|
|
1438
|
+
throw error;
|
|
1358
1439
|
}
|
|
1359
|
-
await this.applyPartitionBy(runtime, qualifiedName, definition);
|
|
1360
1440
|
});
|
|
1361
1441
|
this.connections.markLocalCatalogChanged();
|
|
1362
1442
|
return structuredClone(definition);
|
|
@@ -1484,6 +1564,15 @@ ${details}`);
|
|
|
1484
1564
|
throw new LakeStorageError11(`[SixbDuckLake] Dataset '${definition.id}' cannot apply partitionBy because DuckLake rejected ALTER TABLE SET PARTITIONED BY: ${message}`);
|
|
1485
1565
|
}
|
|
1486
1566
|
}
|
|
1567
|
+
async applyPrimaryKey(runtime, qualifiedName, definition) {
|
|
1568
|
+
if (definition.primaryKey === undefined) {
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
const columnNames = typeof definition.primaryKey === "string" ? [definition.primaryKey] : definition.primaryKey;
|
|
1572
|
+
for (const [ordinal, columnName] of columnNames.entries()) {
|
|
1573
|
+
await runtime.run(`COMMENT ON COLUMN ${qualifiedName}.${quoteIdentifier(columnName)} IS ${quoteSqlString(duckLakePrimaryKeyColumnComment(ordinal))}`);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1487
1576
|
async setSchemaEvolutionCommitMetadata(runtime, datasetId, plan) {
|
|
1488
1577
|
await runtime.run(`CALL ${quoteIdentifier(duckLakeAlias(this.options))}.set_commit_message(${quoteSqlString("Sixb")}, ${quoteSqlString(`evolve dataset ${datasetId} schema`)}, extra_info => ${quoteSqlString(JSON.stringify({
|
|
1489
1578
|
sixb: {
|
|
@@ -1640,14 +1729,16 @@ function parseCommitMetadata(value) {
|
|
|
1640
1729
|
const mode = parsed.sixb.mode;
|
|
1641
1730
|
const commitId = parsed.sixb.commitId;
|
|
1642
1731
|
const rowCount = parsed.sixb.rowCount;
|
|
1732
|
+
const validatedPrimaryKeyColumns = parsed.sixb.validatedPrimaryKeyColumns;
|
|
1643
1733
|
return {
|
|
1644
1734
|
kind: "datasetVersion",
|
|
1645
1735
|
datasetId: parsed.sixb.datasetId,
|
|
1646
1736
|
...typeof commitId === "string" ? { commitId } : {},
|
|
1647
|
-
...mode === "snapshot" || mode === "append" || mode === "schema" ? { mode } : {},
|
|
1737
|
+
...mode === "snapshot" || mode === "append" || mode === "merge" || mode === "schema" ? { mode } : {},
|
|
1648
1738
|
...isDatasetProducer(parsed.sixb.producer) ? { producer: parsed.sixb.producer } : {},
|
|
1649
1739
|
...isDatasetVersionRefs(parsed.sixb.inputs) ? { inputs: parsed.sixb.inputs } : {},
|
|
1650
1740
|
...typeof commitId === "string" && isRowCount(rowCount) ? { rowCount } : {},
|
|
1741
|
+
...typeof commitId === "string" && isValidatedPrimaryKeyColumns(validatedPrimaryKeyColumns) ? { validatedPrimaryKeyColumns } : {},
|
|
1651
1742
|
...isSchemaChangeMetadata(parsed.sixb.schemaChange) ? { schemaChange: parsed.sixb.schemaChange } : {}
|
|
1652
1743
|
};
|
|
1653
1744
|
}
|
|
@@ -1688,8 +1779,14 @@ function optionalString(value) {
|
|
|
1688
1779
|
function isRowCount(value) {
|
|
1689
1780
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
1690
1781
|
}
|
|
1782
|
+
function isValidatedPrimaryKeyColumns(value) {
|
|
1783
|
+
return Array.isArray(value) && value.length > 0 && value.every((column) => typeof column === "string" && column.trim().length > 0) && new Set(value).size === value.length;
|
|
1784
|
+
}
|
|
1691
1785
|
|
|
1692
1786
|
// src/internal/ducklake-row-reader.ts
|
|
1787
|
+
var READ_PAGE_ROWS = 5000;
|
|
1788
|
+
var PHYSICAL_ROW_ID_ALIAS = "__sixb_physical_row_id";
|
|
1789
|
+
|
|
1693
1790
|
class DuckLakeRowReader {
|
|
1694
1791
|
options;
|
|
1695
1792
|
connections;
|
|
@@ -1709,16 +1806,43 @@ class DuckLakeRowReader {
|
|
|
1709
1806
|
throw new LakeStorageError14(`[SixbDuckLake] Unknown dataset '${input.datasetId}'.`);
|
|
1710
1807
|
}
|
|
1711
1808
|
const version = await this.resolveVersion(runtime, tableRef, input.versionId);
|
|
1712
|
-
const snapshotId = parseVersionId(version.versionId);
|
|
1713
1809
|
const selectedColumns = this.resolveReadColumns(tableRef.datasetId, version.schema, input.columns);
|
|
1810
|
+
const snapshotId = parseVersionId(version.versionId);
|
|
1714
1811
|
const columnsSql = selectedColumns.map((column) => quoteIdentifier(column.name)).join(", ");
|
|
1715
1812
|
const tableSql = qualifiedTableName(this.options, tableRef.tableName);
|
|
1716
|
-
const
|
|
1717
|
-
const
|
|
1718
|
-
const
|
|
1719
|
-
const
|
|
1720
|
-
const
|
|
1721
|
-
|
|
1813
|
+
const schemaNames = new Set(version.schema.columns.map((column) => column.name.toLowerCase()));
|
|
1814
|
+
const canUsePhysicalRowId = !schemaNames.has("rowid") && !schemaNames.has(PHYSICAL_ROW_ID_ALIAS.toLowerCase());
|
|
1815
|
+
const orderSql = canUsePhysicalRowId ? " ORDER BY rowid" : "";
|
|
1816
|
+
const baseSql = `SELECT ${columnsSql} FROM ${tableSql} AT (VERSION => ${snapshotId})`;
|
|
1817
|
+
const requestedOffset = Math.max(0, Math.trunc(input.offset ?? 0));
|
|
1818
|
+
let remaining = input.limit === undefined ? undefined : Math.max(0, Math.trunc(input.limit));
|
|
1819
|
+
if (remaining === 0)
|
|
1820
|
+
return;
|
|
1821
|
+
let offset = requestedOffset;
|
|
1822
|
+
let physicalCursor = null;
|
|
1823
|
+
while (true) {
|
|
1824
|
+
const pageRows = remaining === undefined ? READ_PAGE_ROWS : Math.min(READ_PAGE_ROWS, remaining);
|
|
1825
|
+
const pageSql = canUsePhysicalRowId ? `SELECT rowid AS ${quoteIdentifier(PHYSICAL_ROW_ID_ALIAS)}, ${columnsSql}
|
|
1826
|
+
FROM ${tableSql} AT (VERSION => ${snapshotId})
|
|
1827
|
+
${physicalCursor === null ? "" : `WHERE rowid > ${physicalCursor}`}
|
|
1828
|
+
ORDER BY rowid LIMIT ${pageRows}
|
|
1829
|
+
${physicalCursor === null && offset > 0 ? `OFFSET ${offset}` : ""}` : `${baseSql}${orderSql} LIMIT ${pageRows} OFFSET ${offset}`;
|
|
1830
|
+
const rows = await runtime.query(pageSql);
|
|
1831
|
+
if (rows.length === 0)
|
|
1832
|
+
return;
|
|
1833
|
+
yield* this.normalizeRows(rows, selectedColumns);
|
|
1834
|
+
offset += rows.length;
|
|
1835
|
+
if (remaining !== undefined) {
|
|
1836
|
+
remaining -= rows.length;
|
|
1837
|
+
if (remaining === 0)
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
if (canUsePhysicalRowId) {
|
|
1841
|
+
physicalCursor = getBigIntLike(rows[rows.length - 1], PHYSICAL_ROW_ID_ALIAS);
|
|
1842
|
+
}
|
|
1843
|
+
if (rows.length < pageRows)
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1722
1846
|
} finally {
|
|
1723
1847
|
await lease.release();
|
|
1724
1848
|
}
|
|
@@ -2002,7 +2126,8 @@ class DuckLakeSnapshotReader {
|
|
|
2002
2126
|
return {
|
|
2003
2127
|
datasetId: tableRef.datasetId,
|
|
2004
2128
|
versionId: toVersionId(row.snapshotId),
|
|
2005
|
-
...row.metadata?.rowCount !== undefined ? { rowCount: row.metadata.rowCount } : {}
|
|
2129
|
+
...row.metadata?.rowCount !== undefined ? { rowCount: row.metadata.rowCount } : {},
|
|
2130
|
+
...row.metadata?.validatedPrimaryKeyColumns !== undefined ? { validatedPrimaryKeyColumns: row.metadata.validatedPrimaryKeyColumns } : {}
|
|
2006
2131
|
};
|
|
2007
2132
|
}
|
|
2008
2133
|
async getVersionForSnapshot(runtime, dataset, snapshotId) {
|
|
@@ -2062,7 +2187,7 @@ class DuckLakeSnapshotReader {
|
|
|
2062
2187
|
if (!row) {
|
|
2063
2188
|
return null;
|
|
2064
2189
|
}
|
|
2065
|
-
if (!options.includeParent || row.mode !== "append" && row.mode !== "schema") {
|
|
2190
|
+
if (!options.includeParent || row.mode !== "append" && row.mode !== "merge" && row.mode !== "schema") {
|
|
2066
2191
|
return row;
|
|
2067
2192
|
}
|
|
2068
2193
|
const [parent] = await this.collectVisibleSnapshotRows(runtime, {
|
|
@@ -2250,7 +2375,7 @@ class DuckLakeSnapshotReader {
|
|
|
2250
2375
|
}
|
|
2251
2376
|
withParentSnapshotIds(snapshots) {
|
|
2252
2377
|
return snapshots.map((snapshot, index) => {
|
|
2253
|
-
const parentSnapshotId = snapshot.mode === "append" || snapshot.mode === "schema" ? snapshots[index + 1]?.snapshotId : undefined;
|
|
2378
|
+
const parentSnapshotId = snapshot.mode === "append" || snapshot.mode === "merge" || snapshot.mode === "schema" ? snapshots[index + 1]?.snapshotId : undefined;
|
|
2254
2379
|
return parentSnapshotId === undefined ? snapshot : { ...snapshot, parentSnapshotId };
|
|
2255
2380
|
});
|
|
2256
2381
|
}
|
|
@@ -2271,7 +2396,10 @@ import {
|
|
|
2271
2396
|
} from "@sixb/core/lake-storage";
|
|
2272
2397
|
|
|
2273
2398
|
// src/internal/dataset-row-commit.ts
|
|
2274
|
-
import {
|
|
2399
|
+
import {
|
|
2400
|
+
getDatasetPrimaryKeyColumns,
|
|
2401
|
+
LakeStorageError as LakeStorageError16
|
|
2402
|
+
} from "@sixb/core/lake-storage";
|
|
2275
2403
|
function assertDatasetWriteMode(mode, operation) {
|
|
2276
2404
|
if (mode !== "snapshot" && mode !== "append") {
|
|
2277
2405
|
throw new LakeStorageError16(`[SixbDuckLake] Invalid ${operation} mode '${String(mode)}'. Expected 'snapshot' or 'append'.`);
|
|
@@ -2281,6 +2409,30 @@ async function applyDatasetRowsFromRelation(input) {
|
|
|
2281
2409
|
const tableName = encodeDatasetTableName(input.dataset.id);
|
|
2282
2410
|
const table = qualifiedTableName(input.options, tableName);
|
|
2283
2411
|
const columnsSql = datasetSchemaColumnNamesSql(input.dataset.schema);
|
|
2412
|
+
const primaryKeyColumns = getDatasetPrimaryKeyColumns(input.dataset);
|
|
2413
|
+
if (primaryKeyColumns !== null) {
|
|
2414
|
+
await inspectUniqueKeyedRelation({
|
|
2415
|
+
runtime: input.runtime,
|
|
2416
|
+
dataset: input.dataset,
|
|
2417
|
+
relationSql: input.sourceRelationSql,
|
|
2418
|
+
context: `${input.mode} source`
|
|
2419
|
+
});
|
|
2420
|
+
if (input.mode === "append") {
|
|
2421
|
+
await inspectUniqueKeyedRelation({
|
|
2422
|
+
runtime: input.runtime,
|
|
2423
|
+
dataset: input.dataset,
|
|
2424
|
+
relationSql: table,
|
|
2425
|
+
context: "current baseline"
|
|
2426
|
+
});
|
|
2427
|
+
await assertNoPrimaryKeyOverlap({
|
|
2428
|
+
runtime: input.runtime,
|
|
2429
|
+
dataset: input.dataset,
|
|
2430
|
+
leftRelationSql: input.sourceRelationSql,
|
|
2431
|
+
rightRelationSql: table,
|
|
2432
|
+
context: "append"
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2284
2436
|
if (input.mode === "snapshot") {
|
|
2285
2437
|
const source = await fingerprintRelation(input.runtime, input.sourceRelationSql, columnsSql);
|
|
2286
2438
|
const target = await fingerprintRelation(input.runtime, table, columnsSql);
|
|
@@ -2307,6 +2459,44 @@ async function applyDatasetRowsFromRelation(input) {
|
|
|
2307
2459
|
resultingRowCount: input.previousRowCount === undefined ? { kind: "unknown" } : { kind: "exact", value: input.previousRowCount + sourceRowCount }
|
|
2308
2460
|
};
|
|
2309
2461
|
}
|
|
2462
|
+
async function inspectUniqueKeyedRelation(input) {
|
|
2463
|
+
const primaryKeyColumns = getDatasetPrimaryKeyColumns(input.dataset);
|
|
2464
|
+
if (primaryKeyColumns === null) {
|
|
2465
|
+
throw new LakeStorageError16(`[SixbDuckLake] Dataset '${input.dataset.id}' must define a primaryKey before rows can be keyed.`);
|
|
2466
|
+
}
|
|
2467
|
+
const keysSql = primaryKeyColumns.map((column) => quoteIdentifier(column)).join(", ");
|
|
2468
|
+
const [row] = await input.runtime.query(`
|
|
2469
|
+
SELECT
|
|
2470
|
+
count(*) AS row_count,
|
|
2471
|
+
coalesce(max(key_count), 0) AS max_key_count
|
|
2472
|
+
FROM (
|
|
2473
|
+
SELECT count(*) OVER (PARTITION BY ${keysSql}) AS key_count
|
|
2474
|
+
FROM ${input.relationSql}
|
|
2475
|
+
) keyed_rows
|
|
2476
|
+
`);
|
|
2477
|
+
const rowCount = row === undefined ? 0 : Number(getBigIntLike(row, "row_count"));
|
|
2478
|
+
const maxKeyCount = row === undefined ? 0 : Number(getBigIntLike(row, "max_key_count"));
|
|
2479
|
+
if (maxKeyCount > 1) {
|
|
2480
|
+
throw new LakeStorageError16(`[SixbDuckLake] Dataset '${input.dataset.id}' ${input.context} contains duplicate primary key.`);
|
|
2481
|
+
}
|
|
2482
|
+
return rowCount;
|
|
2483
|
+
}
|
|
2484
|
+
async function assertNoPrimaryKeyOverlap(input) {
|
|
2485
|
+
const primaryKeyColumns = getDatasetPrimaryKeyColumns(input.dataset);
|
|
2486
|
+
if (primaryKeyColumns === null) {
|
|
2487
|
+
return;
|
|
2488
|
+
}
|
|
2489
|
+
const matchSql = primaryKeyColumns.map((column) => `left_rows.${quoteIdentifier(column)} = right_rows.${quoteIdentifier(column)}`).join(" AND ");
|
|
2490
|
+
const [collision] = await input.runtime.query(`
|
|
2491
|
+
SELECT 1
|
|
2492
|
+
FROM ${input.leftRelationSql} left_rows
|
|
2493
|
+
JOIN ${input.rightRelationSql} right_rows ON ${matchSql}
|
|
2494
|
+
LIMIT 1
|
|
2495
|
+
`);
|
|
2496
|
+
if (collision !== undefined) {
|
|
2497
|
+
throw new LakeStorageError16(`[SixbDuckLake] Dataset '${input.dataset.id}' ${input.context} contains duplicate primary key.`);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2310
2500
|
async function fingerprintRelation(runtime, relationSql, columnsSql) {
|
|
2311
2501
|
const [row] = await runtime.query(`
|
|
2312
2502
|
SELECT
|
|
@@ -2604,13 +2794,183 @@ function throwResultSchemaMismatch(datasetId, detail) {
|
|
|
2604
2794
|
}
|
|
2605
2795
|
|
|
2606
2796
|
// src/internal/ducklake-write-coordinator.ts
|
|
2607
|
-
import { randomUUID as
|
|
2608
|
-
import { LakeStorageError as
|
|
2797
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
2798
|
+
import { getDatasetPrimaryKeyColumns as getDatasetPrimaryKeyColumns4, LakeStorageError as LakeStorageError22 } from "@sixb/core/lake-storage";
|
|
2609
2799
|
|
|
2610
|
-
// src/internal/
|
|
2800
|
+
// src/internal/dataset-row-merge.ts
|
|
2801
|
+
import { getDatasetPrimaryKeyColumns as getDatasetPrimaryKeyColumns2, LakeStorageError as LakeStorageError19 } from "@sixb/core/lake-storage";
|
|
2802
|
+
async function applyDatasetMergeFromRelation(input) {
|
|
2803
|
+
const primaryKeyColumns = getDatasetPrimaryKeyColumns2(input.dataset);
|
|
2804
|
+
if (primaryKeyColumns === null) {
|
|
2805
|
+
throw new LakeStorageError19(`[SixbDuckLake] Dataset '${input.dataset.id}' must define a primaryKey before it can be merged.`);
|
|
2806
|
+
}
|
|
2807
|
+
const stagingTable = quoteIdentifier(input.stagingTableName);
|
|
2808
|
+
const targetTable = qualifiedTableName(input.options, encodeDatasetTableName(input.dataset.id));
|
|
2809
|
+
const sourceRowCount = await countRows2(input.runtime, stagingTable);
|
|
2810
|
+
const previousRowCount = await currentBaselineRowCount(input, targetTable, primaryKeyColumns);
|
|
2811
|
+
return withMaterializedFinalChanges(input, primaryKeyColumns, async (finalChanges) => {
|
|
2812
|
+
const effects = await countMergeEffects(input, targetTable, finalChanges, primaryKeyColumns);
|
|
2813
|
+
const effectiveChangeCount = effects.inserts + effects.updates + effects.deletes;
|
|
2814
|
+
if (effectiveChangeCount === 0) {
|
|
2815
|
+
return {
|
|
2816
|
+
dataChangeExpected: false,
|
|
2817
|
+
sourceRowCount,
|
|
2818
|
+
resultingRowCount: { kind: "exact", value: previousRowCount }
|
|
2819
|
+
};
|
|
2820
|
+
}
|
|
2821
|
+
const keyMatchSql = primaryKeyMatchSql(primaryKeyColumns, "target", "source");
|
|
2822
|
+
const rowDifferenceSql = datasetRowDifferenceSql(input.dataset, "target", "source");
|
|
2823
|
+
const kindColumn = `source.${quoteIdentifier(input.kindColumnName)}`;
|
|
2824
|
+
await input.runtime.run(`
|
|
2825
|
+
DELETE FROM ${targetTable} AS target
|
|
2826
|
+
USING ${finalChanges} AS source
|
|
2827
|
+
WHERE ${keyMatchSql}
|
|
2828
|
+
AND (
|
|
2829
|
+
${kindColumn} = ${quoteSqlString("delete")}
|
|
2830
|
+
OR (
|
|
2831
|
+
${kindColumn} = ${quoteSqlString("upsert")}
|
|
2832
|
+
AND (${rowDifferenceSql})
|
|
2833
|
+
)
|
|
2834
|
+
)
|
|
2835
|
+
`);
|
|
2836
|
+
const columnsSql = datasetSchemaColumnNamesSql(input.dataset.schema);
|
|
2837
|
+
const selectedColumnsSql = input.dataset.schema.columns.map((column) => `source.${quoteIdentifier(column.name)}`).join(", ");
|
|
2838
|
+
const firstSequenceColumnName = firstSequenceName(input.sequenceColumnName);
|
|
2839
|
+
await input.runtime.run(`
|
|
2840
|
+
INSERT INTO ${targetTable} (${columnsSql})
|
|
2841
|
+
SELECT ${selectedColumnsSql}
|
|
2842
|
+
FROM ${finalChanges} AS source
|
|
2843
|
+
WHERE ${kindColumn} = ${quoteSqlString("upsert")}
|
|
2844
|
+
AND NOT EXISTS (
|
|
2845
|
+
SELECT 1
|
|
2846
|
+
FROM ${targetTable} AS target
|
|
2847
|
+
WHERE ${keyMatchSql}
|
|
2848
|
+
)
|
|
2849
|
+
ORDER BY source.${quoteIdentifier(firstSequenceColumnName)}
|
|
2850
|
+
`);
|
|
2851
|
+
return {
|
|
2852
|
+
dataChangeExpected: true,
|
|
2853
|
+
sourceRowCount,
|
|
2854
|
+
resultingRowCount: {
|
|
2855
|
+
kind: "exact",
|
|
2856
|
+
value: previousRowCount + effects.inserts - effects.deletes
|
|
2857
|
+
}
|
|
2858
|
+
};
|
|
2859
|
+
});
|
|
2860
|
+
}
|
|
2861
|
+
async function currentBaselineRowCount(input, targetTable, primaryKeyColumns) {
|
|
2862
|
+
if (sameColumns(input.validatedPrimaryKeyColumns, primaryKeyColumns)) {
|
|
2863
|
+
return input.previousRowCount ?? countRows2(input.runtime, targetTable);
|
|
2864
|
+
}
|
|
2865
|
+
return inspectUniqueKeyedRelation({
|
|
2866
|
+
runtime: input.runtime,
|
|
2867
|
+
dataset: input.dataset,
|
|
2868
|
+
relationSql: targetTable,
|
|
2869
|
+
context: "current baseline"
|
|
2870
|
+
});
|
|
2871
|
+
}
|
|
2872
|
+
async function withMaterializedFinalChanges(input, primaryKeyColumns, run) {
|
|
2873
|
+
const table = quoteIdentifier(`${input.stagingTableName}_final`);
|
|
2874
|
+
await input.runtime.run(`
|
|
2875
|
+
CREATE TEMP TABLE ${table} AS
|
|
2876
|
+
${finalChangesSelectSql(input, primaryKeyColumns)}
|
|
2877
|
+
`);
|
|
2878
|
+
let outcome;
|
|
2879
|
+
try {
|
|
2880
|
+
outcome = { kind: "success", value: await run(table) };
|
|
2881
|
+
} catch (error) {
|
|
2882
|
+
outcome = { kind: "error", error };
|
|
2883
|
+
}
|
|
2884
|
+
try {
|
|
2885
|
+
await input.runtime.run(`DROP TABLE IF EXISTS ${table}`);
|
|
2886
|
+
} catch (cleanupError) {
|
|
2887
|
+
if (outcome.kind === "success") {
|
|
2888
|
+
throw cleanupError;
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
if (outcome.kind === "error") {
|
|
2892
|
+
throw outcome.error;
|
|
2893
|
+
}
|
|
2894
|
+
return outcome.value;
|
|
2895
|
+
}
|
|
2896
|
+
function finalChangesSelectSql(input, primaryKeyColumns) {
|
|
2897
|
+
const sequenceColumn = quoteIdentifier(input.sequenceColumnName);
|
|
2898
|
+
const keyColumnsSql = primaryKeyColumns.map((column) => quoteIdentifier(column)).join(", ");
|
|
2899
|
+
const rankColumn = quoteIdentifier(rankName(input.sequenceColumnName));
|
|
2900
|
+
const firstSequenceColumn = quoteIdentifier(firstSequenceName(input.sequenceColumnName));
|
|
2901
|
+
return `
|
|
2902
|
+
SELECT * EXCLUDE (${rankColumn})
|
|
2903
|
+
FROM (
|
|
2904
|
+
SELECT
|
|
2905
|
+
*,
|
|
2906
|
+
min(${sequenceColumn}) OVER (PARTITION BY ${keyColumnsSql}) AS ${firstSequenceColumn},
|
|
2907
|
+
row_number() OVER (
|
|
2908
|
+
PARTITION BY ${keyColumnsSql}
|
|
2909
|
+
ORDER BY ${sequenceColumn} DESC
|
|
2910
|
+
) AS ${rankColumn}
|
|
2911
|
+
FROM ${quoteIdentifier(input.stagingTableName)}
|
|
2912
|
+
) ranked_changes
|
|
2913
|
+
WHERE ${rankColumn} = 1
|
|
2914
|
+
`;
|
|
2915
|
+
}
|
|
2916
|
+
async function countMergeEffects(input, targetTable, finalChanges, primaryKeyColumns) {
|
|
2917
|
+
const matchColumn = primaryKeyColumns[0];
|
|
2918
|
+
if (matchColumn === undefined) {
|
|
2919
|
+
throw new LakeStorageError19(`[SixbDuckLake] Dataset '${input.dataset.id}' primaryKey must contain at least one column.`);
|
|
2920
|
+
}
|
|
2921
|
+
const keyMatchSql = primaryKeyMatchSql(primaryKeyColumns, "target", "source");
|
|
2922
|
+
const targetMatch = `target.${quoteIdentifier(matchColumn)} IS NOT NULL`;
|
|
2923
|
+
const kindColumn = `source.${quoteIdentifier(input.kindColumnName)}`;
|
|
2924
|
+
const rowDifferenceSql = datasetRowDifferenceSql(input.dataset, "target", "source");
|
|
2925
|
+
const [row] = await input.runtime.query(`
|
|
2926
|
+
SELECT
|
|
2927
|
+
count(*) FILTER (
|
|
2928
|
+
WHERE ${kindColumn} = ${quoteSqlString("upsert")} AND NOT (${targetMatch})
|
|
2929
|
+
) AS insert_count,
|
|
2930
|
+
count(*) FILTER (
|
|
2931
|
+
WHERE ${kindColumn} = ${quoteSqlString("upsert")}
|
|
2932
|
+
AND ${targetMatch}
|
|
2933
|
+
AND (${rowDifferenceSql})
|
|
2934
|
+
) AS update_count,
|
|
2935
|
+
count(*) FILTER (
|
|
2936
|
+
WHERE ${kindColumn} = ${quoteSqlString("delete")} AND ${targetMatch}
|
|
2937
|
+
) AS delete_count
|
|
2938
|
+
FROM ${finalChanges} AS source
|
|
2939
|
+
LEFT JOIN ${targetTable} AS target ON ${keyMatchSql}
|
|
2940
|
+
`);
|
|
2941
|
+
return {
|
|
2942
|
+
inserts: row === undefined ? 0 : Number(getBigIntLike(row, "insert_count")),
|
|
2943
|
+
updates: row === undefined ? 0 : Number(getBigIntLike(row, "update_count")),
|
|
2944
|
+
deletes: row === undefined ? 0 : Number(getBigIntLike(row, "delete_count"))
|
|
2945
|
+
};
|
|
2946
|
+
}
|
|
2947
|
+
function primaryKeyMatchSql(primaryKeyColumns, leftAlias, rightAlias) {
|
|
2948
|
+
return primaryKeyColumns.map((column) => `${leftAlias}.${quoteIdentifier(column)} = ${rightAlias}.${quoteIdentifier(column)}`).join(" AND ");
|
|
2949
|
+
}
|
|
2950
|
+
function datasetRowDifferenceSql(dataset, leftAlias, rightAlias) {
|
|
2951
|
+
return dataset.schema.columns.map((column) => `${leftAlias}.${quoteIdentifier(column.name)} IS DISTINCT FROM ${rightAlias}.${quoteIdentifier(column.name)}`).join(" OR ");
|
|
2952
|
+
}
|
|
2953
|
+
async function countRows2(runtime, relationSql) {
|
|
2954
|
+
const [row] = await runtime.query(`SELECT count(*) AS row_count FROM ${relationSql}`);
|
|
2955
|
+
return row === undefined ? 0 : Number(getBigIntLike(row, "row_count"));
|
|
2956
|
+
}
|
|
2957
|
+
function rankName(sequenceColumnName) {
|
|
2958
|
+
return `${sequenceColumnName}_rank`;
|
|
2959
|
+
}
|
|
2960
|
+
function firstSequenceName(sequenceColumnName) {
|
|
2961
|
+
return `${sequenceColumnName}_first`;
|
|
2962
|
+
}
|
|
2963
|
+
function sameColumns(left, right) {
|
|
2964
|
+
return left !== undefined && left.length === right.length && left.every((value, index) => value === right[index]);
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
// src/internal/ducklake-merge-session.ts
|
|
2611
2968
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
2612
|
-
import {
|
|
2613
|
-
|
|
2969
|
+
import {
|
|
2970
|
+
cloneDatasetMergeChange,
|
|
2971
|
+
getDatasetMergeChangeValidationError,
|
|
2972
|
+
LakeStorageError as LakeStorageError20
|
|
2973
|
+
} from "@sixb/core/lake-storage";
|
|
2614
2974
|
|
|
2615
2975
|
// src/internal/row-appender.ts
|
|
2616
2976
|
function appendDatasetRow(appender, schema, row) {
|
|
@@ -2665,10 +3025,135 @@ function fileRefToDuckDbStruct(fileRef) {
|
|
|
2665
3025
|
};
|
|
2666
3026
|
}
|
|
2667
3027
|
|
|
3028
|
+
// src/internal/ducklake-merge-session.ts
|
|
3029
|
+
var MERGE_CHANGE_BATCH_SIZE = 1000;
|
|
3030
|
+
async function createDuckLakeMergeSession(input) {
|
|
3031
|
+
const suffix = randomUUID4().replaceAll("-", "");
|
|
3032
|
+
const stagingTableName = `sixb_merge_${suffix}`;
|
|
3033
|
+
const sequenceColumnName = `sixb_merge_sequence_${suffix}`;
|
|
3034
|
+
const kindColumnName = `sixb_merge_kind_${suffix}`;
|
|
3035
|
+
await input.runtime.run(`
|
|
3036
|
+
CREATE TEMP TABLE ${quoteIdentifier(stagingTableName)} (
|
|
3037
|
+
${quoteIdentifier(sequenceColumnName)} UBIGINT NOT NULL,
|
|
3038
|
+
${quoteIdentifier(kindColumnName)} VARCHAR NOT NULL,
|
|
3039
|
+
${datasetSchemaToDuckDbNullableColumnsSql(input.merge.dataset.schema)}
|
|
3040
|
+
)
|
|
3041
|
+
`);
|
|
3042
|
+
return new DuckLakeMergeSession(input.commitMerge, input.runtime, input.merge, input.baseVersionId, stagingTableName, sequenceColumnName, kindColumnName);
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
class DuckLakeMergeSession {
|
|
3046
|
+
commitMerge;
|
|
3047
|
+
runtime;
|
|
3048
|
+
input;
|
|
3049
|
+
baseVersionId;
|
|
3050
|
+
stagingTableName;
|
|
3051
|
+
sequenceColumnName;
|
|
3052
|
+
kindColumnName;
|
|
3053
|
+
closed = false;
|
|
3054
|
+
cleanedUp = false;
|
|
3055
|
+
nextSequence = 0n;
|
|
3056
|
+
changesWritten = 0;
|
|
3057
|
+
constructor(commitMerge, runtime, input, baseVersionId, stagingTableName, sequenceColumnName, kindColumnName) {
|
|
3058
|
+
this.commitMerge = commitMerge;
|
|
3059
|
+
this.runtime = runtime;
|
|
3060
|
+
this.input = input;
|
|
3061
|
+
this.baseVersionId = baseVersionId;
|
|
3062
|
+
this.stagingTableName = stagingTableName;
|
|
3063
|
+
this.sequenceColumnName = sequenceColumnName;
|
|
3064
|
+
this.kindColumnName = kindColumnName;
|
|
3065
|
+
}
|
|
3066
|
+
async writeChanges(changes) {
|
|
3067
|
+
this.assertOpen();
|
|
3068
|
+
let batch = [];
|
|
3069
|
+
for await (const change of changes) {
|
|
3070
|
+
const validationError = getDatasetMergeChangeValidationError(change, this.input.dataset);
|
|
3071
|
+
if (validationError) {
|
|
3072
|
+
throw new LakeStorageError20(`[SixbDuckLake] ${validationError}`);
|
|
3073
|
+
}
|
|
3074
|
+
batch.push({
|
|
3075
|
+
sequence: this.nextSequence,
|
|
3076
|
+
change: cloneDatasetMergeChange(change)
|
|
3077
|
+
});
|
|
3078
|
+
this.nextSequence += 1n;
|
|
3079
|
+
if (batch.length >= MERGE_CHANGE_BATCH_SIZE) {
|
|
3080
|
+
await this.appendBatchToStagingTable(batch);
|
|
3081
|
+
batch = [];
|
|
3082
|
+
await Bun.sleep(0);
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
if (batch.length > 0) {
|
|
3086
|
+
await this.appendBatchToStagingTable(batch);
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
async appendBatchToStagingTable(batch) {
|
|
3090
|
+
await this.runtime.withAppender(this.stagingTableName, (appender) => {
|
|
3091
|
+
for (const staged of batch) {
|
|
3092
|
+
appender.appendBigInt(staged.sequence);
|
|
3093
|
+
appender.appendVarchar(staged.change.kind);
|
|
3094
|
+
appendDatasetRow(appender, this.input.dataset.schema, staged.change.kind === "upsert" ? staged.change.row : staged.change.key);
|
|
3095
|
+
}
|
|
3096
|
+
});
|
|
3097
|
+
this.changesWritten += batch.length;
|
|
3098
|
+
}
|
|
3099
|
+
async commit(input) {
|
|
3100
|
+
this.assertOpen();
|
|
3101
|
+
this.closed = true;
|
|
3102
|
+
let committed = false;
|
|
3103
|
+
try {
|
|
3104
|
+
const result = await this.commitMerge({
|
|
3105
|
+
merge: this.input,
|
|
3106
|
+
baseVersionId: this.baseVersionId,
|
|
3107
|
+
commit: input,
|
|
3108
|
+
stagingTableName: this.stagingTableName,
|
|
3109
|
+
sequenceColumnName: this.sequenceColumnName,
|
|
3110
|
+
kindColumnName: this.kindColumnName,
|
|
3111
|
+
changesWritten: this.changesWritten
|
|
3112
|
+
});
|
|
3113
|
+
committed = true;
|
|
3114
|
+
await this.cleanup();
|
|
3115
|
+
return result;
|
|
3116
|
+
} catch (error) {
|
|
3117
|
+
if (!committed) {
|
|
3118
|
+
await this.cleanup();
|
|
3119
|
+
}
|
|
3120
|
+
throw error;
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
async abort() {
|
|
3124
|
+
if (this.closed) {
|
|
3125
|
+
return;
|
|
3126
|
+
}
|
|
3127
|
+
this.closed = true;
|
|
3128
|
+
await this.cleanup();
|
|
3129
|
+
}
|
|
3130
|
+
assertOpen() {
|
|
3131
|
+
if (this.closed) {
|
|
3132
|
+
throw new LakeStorageError20("[SixbDuckLake] Merge session is already closed.");
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
async cleanup() {
|
|
3136
|
+
if (this.cleanedUp) {
|
|
3137
|
+
return;
|
|
3138
|
+
}
|
|
3139
|
+
this.cleanedUp = true;
|
|
3140
|
+
try {
|
|
3141
|
+
await this.runtime.run(`DROP TABLE IF EXISTS ${quoteIdentifier(this.stagingTableName)}`);
|
|
3142
|
+
} catch {}
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
|
|
2668
3146
|
// src/internal/ducklake-write-session.ts
|
|
3147
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
3148
|
+
import { getDatasetRowValidationError } from "@sixb/core";
|
|
3149
|
+
import {
|
|
3150
|
+
encodeDatasetPrimaryKey,
|
|
3151
|
+
getDatasetPrimaryKeyColumns as getDatasetPrimaryKeyColumns3,
|
|
3152
|
+
LakeStorageError as LakeStorageError21
|
|
3153
|
+
} from "@sixb/core/lake-storage";
|
|
2669
3154
|
var WRITE_ROW_BATCH_SIZE = 1000;
|
|
2670
3155
|
async function createDuckLakeWriteSession(input) {
|
|
2671
|
-
const stagingTableName = `sixb_write_${
|
|
3156
|
+
const stagingTableName = `sixb_write_${randomUUID5().replaceAll("-", "")}`;
|
|
2672
3157
|
await input.runtime.run(`CREATE TEMP TABLE ${quoteIdentifier(stagingTableName)} (${datasetSchemaToDuckDbColumnsSql(input.write.dataset.schema)})`);
|
|
2673
3158
|
return new DuckLakeWriteSession(input.commitWrite, input.runtime, input.write, stagingTableName);
|
|
2674
3159
|
}
|
|
@@ -2681,6 +3166,7 @@ class DuckLakeWriteSession {
|
|
|
2681
3166
|
closed = false;
|
|
2682
3167
|
cleanedUp = false;
|
|
2683
3168
|
rowsWritten = 0;
|
|
3169
|
+
stagedPrimaryKeys = new Set;
|
|
2684
3170
|
constructor(commitWrite, runtime, input, stagingTableName) {
|
|
2685
3171
|
this.commitWrite = commitWrite;
|
|
2686
3172
|
this.runtime = runtime;
|
|
@@ -2690,28 +3176,42 @@ class DuckLakeWriteSession {
|
|
|
2690
3176
|
async writeRows(rows) {
|
|
2691
3177
|
this.assertOpen();
|
|
2692
3178
|
let batch = [];
|
|
3179
|
+
let batchPrimaryKeys = new Set;
|
|
2693
3180
|
for await (const row of rows) {
|
|
2694
3181
|
const validationError = getDatasetRowValidationError(row, this.input.dataset);
|
|
2695
3182
|
if (validationError) {
|
|
2696
|
-
throw new
|
|
3183
|
+
throw new LakeStorageError21(`[SixbDuckLake] ${validationError}`);
|
|
3184
|
+
}
|
|
3185
|
+
const clonedRow = structuredClone(row);
|
|
3186
|
+
if (getDatasetPrimaryKeyColumns3(this.input.dataset) !== null) {
|
|
3187
|
+
const primaryKey = encodeDatasetPrimaryKey(this.input.dataset, clonedRow);
|
|
3188
|
+
if (this.stagedPrimaryKeys.has(primaryKey) || batchPrimaryKeys.has(primaryKey)) {
|
|
3189
|
+
throw new LakeStorageError21(`[SixbDuckLake] Dataset '${this.input.dataset.id}' ${this.input.mode ?? "snapshot"} source contains duplicate primary key.`);
|
|
3190
|
+
}
|
|
3191
|
+
batchPrimaryKeys.add(primaryKey);
|
|
2697
3192
|
}
|
|
2698
|
-
batch.push(
|
|
3193
|
+
batch.push(clonedRow);
|
|
2699
3194
|
if (batch.length >= WRITE_ROW_BATCH_SIZE) {
|
|
2700
|
-
await this.appendBatchToStagingTable(batch);
|
|
3195
|
+
await this.appendBatchToStagingTable(batch, batchPrimaryKeys);
|
|
2701
3196
|
batch = [];
|
|
3197
|
+
batchPrimaryKeys = new Set;
|
|
3198
|
+
await Bun.sleep(0);
|
|
2702
3199
|
}
|
|
2703
3200
|
}
|
|
2704
3201
|
if (batch.length > 0) {
|
|
2705
|
-
await this.appendBatchToStagingTable(batch);
|
|
3202
|
+
await this.appendBatchToStagingTable(batch, batchPrimaryKeys);
|
|
2706
3203
|
}
|
|
2707
3204
|
}
|
|
2708
|
-
async appendBatchToStagingTable(batch) {
|
|
3205
|
+
async appendBatchToStagingTable(batch, primaryKeys) {
|
|
2709
3206
|
await this.runtime.withAppender(this.stagingTableName, (appender) => {
|
|
2710
3207
|
for (const row of batch) {
|
|
2711
3208
|
appendDatasetRow(appender, this.input.dataset.schema, row);
|
|
2712
3209
|
}
|
|
2713
3210
|
});
|
|
2714
3211
|
this.rowsWritten += batch.length;
|
|
3212
|
+
for (const primaryKey of primaryKeys) {
|
|
3213
|
+
this.stagedPrimaryKeys.add(primaryKey);
|
|
3214
|
+
}
|
|
2715
3215
|
}
|
|
2716
3216
|
async commit(input) {
|
|
2717
3217
|
this.assertOpen();
|
|
@@ -2743,7 +3243,7 @@ class DuckLakeWriteSession {
|
|
|
2743
3243
|
}
|
|
2744
3244
|
assertOpen() {
|
|
2745
3245
|
if (this.closed) {
|
|
2746
|
-
throw new
|
|
3246
|
+
throw new LakeStorageError21("[SixbDuckLake] Write session is already closed.");
|
|
2747
3247
|
}
|
|
2748
3248
|
}
|
|
2749
3249
|
async cleanup() {
|
|
@@ -2776,7 +3276,7 @@ class DuckLakeWriteCoordinator {
|
|
|
2776
3276
|
this.connections.assertOpen();
|
|
2777
3277
|
const definition = await this.datasets.getDataset(input.dataset.id);
|
|
2778
3278
|
if (!definition) {
|
|
2779
|
-
throw new
|
|
3279
|
+
throw new LakeStorageError22(`[SixbDuckLake] Unknown dataset '${input.dataset.id}'.`);
|
|
2780
3280
|
}
|
|
2781
3281
|
this.datasets.assertSchema(definition);
|
|
2782
3282
|
const mode = input.mode ?? "snapshot";
|
|
@@ -2792,10 +3292,32 @@ class DuckLakeWriteCoordinator {
|
|
|
2792
3292
|
}
|
|
2793
3293
|
});
|
|
2794
3294
|
}
|
|
3295
|
+
async beginMerge(input) {
|
|
3296
|
+
this.connections.assertOpen();
|
|
3297
|
+
const definition = await this.datasets.getDataset(input.dataset.id);
|
|
3298
|
+
if (!definition) {
|
|
3299
|
+
throw new LakeStorageError22(`[SixbDuckLake] Unknown dataset '${input.dataset.id}'.`);
|
|
3300
|
+
}
|
|
3301
|
+
if (getDatasetPrimaryKeyColumns4(definition) === null) {
|
|
3302
|
+
throw new LakeStorageError22(`[SixbDuckLake] Dataset '${definition.id}' must define a primaryKey before it can be merged.`);
|
|
3303
|
+
}
|
|
3304
|
+
this.datasets.assertSchema(definition);
|
|
3305
|
+
const latestVersion = await this.connections.withAttachedRuntime((runtime2) => this.snapshots.getLatestVersionForDefinition(runtime2, definition));
|
|
3306
|
+
if (input.expectedLatestVersionId !== undefined && latestVersion?.versionId !== input.expectedLatestVersionId) {
|
|
3307
|
+
throw new LakeStorageError22(`[SixbDuckLake] Optimistic merge start failed for dataset '${definition.id}': expected latest version '${input.expectedLatestVersionId}', found '${latestVersion?.versionId ?? "none"}'.`);
|
|
3308
|
+
}
|
|
3309
|
+
const runtime = await this.connections.stagingRuntime();
|
|
3310
|
+
return createDuckLakeMergeSession({
|
|
3311
|
+
commitMerge: (options) => this.commitMerge(options),
|
|
3312
|
+
runtime,
|
|
3313
|
+
merge: { ...input, dataset: definition },
|
|
3314
|
+
baseVersionId: latestVersion?.versionId ?? null
|
|
3315
|
+
});
|
|
3316
|
+
}
|
|
2795
3317
|
async commitWrite(input) {
|
|
2796
3318
|
const definition = await this.datasets.getDataset(input.write.dataset.id);
|
|
2797
3319
|
if (!definition) {
|
|
2798
|
-
throw new
|
|
3320
|
+
throw new LakeStorageError22(`[SixbDuckLake] Unknown dataset '${input.write.dataset.id}'.`);
|
|
2799
3321
|
}
|
|
2800
3322
|
const mode = input.write.mode ?? "snapshot";
|
|
2801
3323
|
assertDatasetWriteMode(mode, "write");
|
|
@@ -2813,6 +3335,42 @@ class DuckLakeWriteCoordinator {
|
|
|
2813
3335
|
})
|
|
2814
3336
|
});
|
|
2815
3337
|
}
|
|
3338
|
+
async commitMerge(input) {
|
|
3339
|
+
const definition = await this.datasets.getDataset(input.merge.dataset.id);
|
|
3340
|
+
if (!definition) {
|
|
3341
|
+
throw new LakeStorageError22(`[SixbDuckLake] Unknown dataset '${input.merge.dataset.id}'.`);
|
|
3342
|
+
}
|
|
3343
|
+
if (getDatasetPrimaryKeyColumns4(definition) === null) {
|
|
3344
|
+
throw new LakeStorageError22(`[SixbDuckLake] Dataset '${definition.id}' must define a primaryKey before it can be merged.`);
|
|
3345
|
+
}
|
|
3346
|
+
return this.withCommitRuntime((runtime) => this.commitVersionOutcomeOnExclusiveRuntime(runtime, {
|
|
3347
|
+
dataset: definition,
|
|
3348
|
+
mode: "merge",
|
|
3349
|
+
expectedLatestVersionId: input.baseVersionId,
|
|
3350
|
+
allowInitialNoOp: true,
|
|
3351
|
+
disableRetries: true,
|
|
3352
|
+
optimisticOperation: "merge commit",
|
|
3353
|
+
commitMessage: input.commit?.commitMessage ?? `merge dataset ${definition.id}`,
|
|
3354
|
+
producer: input.merge.producer,
|
|
3355
|
+
inputs: input.merge.inputs,
|
|
3356
|
+
applyChanges: async (commitRuntime, context) => {
|
|
3357
|
+
const result = await applyDatasetMergeFromRelation({
|
|
3358
|
+
options: this.options,
|
|
3359
|
+
runtime: commitRuntime,
|
|
3360
|
+
dataset: definition,
|
|
3361
|
+
stagingTableName: input.stagingTableName,
|
|
3362
|
+
sequenceColumnName: input.sequenceColumnName,
|
|
3363
|
+
kindColumnName: input.kindColumnName,
|
|
3364
|
+
previousRowCount: context.previousRowCount,
|
|
3365
|
+
validatedPrimaryKeyColumns: context.validatedPrimaryKeyColumns
|
|
3366
|
+
});
|
|
3367
|
+
if (result.sourceRowCount !== input.changesWritten) {
|
|
3368
|
+
throw new LakeStorageError22(`[SixbDuckLake] Staged merge for dataset '${definition.id}' accepted ${input.changesWritten} change(s), but DuckLake saw ${result.sourceRowCount} source change(s) at commit time.`);
|
|
3369
|
+
}
|
|
3370
|
+
return result;
|
|
3371
|
+
}
|
|
3372
|
+
}));
|
|
3373
|
+
}
|
|
2816
3374
|
async commitDatasetVersion(input) {
|
|
2817
3375
|
return this.withCommitRuntime((runtime) => this.commitDatasetVersionOnExclusiveRuntime(runtime, input));
|
|
2818
3376
|
}
|
|
@@ -2822,11 +3380,21 @@ class DuckLakeWriteCoordinator {
|
|
|
2822
3380
|
});
|
|
2823
3381
|
}
|
|
2824
3382
|
async commitDatasetVersionOnExclusiveRuntime(runtime, input) {
|
|
3383
|
+
const result = await this.commitVersionOutcomeOnExclusiveRuntime(runtime, {
|
|
3384
|
+
...input,
|
|
3385
|
+
disableRetries: input.mode === "append" && getDatasetPrimaryKeyColumns4(input.dataset) !== null
|
|
3386
|
+
});
|
|
3387
|
+
if (result.version === null) {
|
|
3388
|
+
throw new LakeStorageError22(`[SixbDuckLake] No DuckLake changes were committed for dataset '${input.dataset.id}', and no previous version exists.`);
|
|
3389
|
+
}
|
|
3390
|
+
return { ...result.version, outcome: result.outcome };
|
|
3391
|
+
}
|
|
3392
|
+
async commitVersionOutcomeOnExclusiveRuntime(runtime, input) {
|
|
2825
3393
|
const runtimeInput = { ...input, runtime };
|
|
2826
3394
|
return this.withGuardedRetrySetting(runtimeInput, () => this.commitDatasetVersionUnlocked(runtimeInput));
|
|
2827
3395
|
}
|
|
2828
3396
|
async withGuardedRetrySetting(input, run) {
|
|
2829
|
-
if (input.expectedLatestVersionId === undefined) {
|
|
3397
|
+
if (input.expectedLatestVersionId === undefined && !input.disableRetries) {
|
|
2830
3398
|
return run();
|
|
2831
3399
|
}
|
|
2832
3400
|
await input.runtime.run("SET ducklake_max_retry_count = 0");
|
|
@@ -2856,13 +3424,15 @@ class DuckLakeWriteCoordinator {
|
|
|
2856
3424
|
this.assertExpectedLatestVersion({
|
|
2857
3425
|
datasetId: input.dataset.id,
|
|
2858
3426
|
expectedLatestVersionId: input.expectedLatestVersionId,
|
|
2859
|
-
actualLatestVersionId: latestVersion?.versionId ?? null
|
|
3427
|
+
actualLatestVersionId: latestVersion?.versionId ?? null,
|
|
3428
|
+
operation: input.optimisticOperation ?? "commit"
|
|
2860
3429
|
});
|
|
2861
3430
|
}
|
|
2862
3431
|
const changeResult = await input.applyChanges(input.runtime, {
|
|
2863
|
-
previousRowCount: latestVersion?.rowCount
|
|
3432
|
+
previousRowCount: latestVersion?.rowCount,
|
|
3433
|
+
validatedPrimaryKeyColumns: latestVersion?.validatedPrimaryKeyColumns
|
|
2864
3434
|
});
|
|
2865
|
-
const commitId =
|
|
3435
|
+
const commitId = randomUUID6();
|
|
2866
3436
|
await this.setCommitMetadata(input, commitId, commitRowCount(input, changeResult, latestVersion));
|
|
2867
3437
|
await input.runtime.run("COMMIT");
|
|
2868
3438
|
committed = true;
|
|
@@ -2890,20 +3460,29 @@ class DuckLakeWriteCoordinator {
|
|
|
2890
3460
|
if (ownSnapshotId !== null) {
|
|
2891
3461
|
const version = await this.snapshots.getVersionForSnapshot(input.runtime, input.dataset, ownSnapshotId);
|
|
2892
3462
|
if (version) {
|
|
2893
|
-
return {
|
|
3463
|
+
return { outcome: "created", version };
|
|
2894
3464
|
}
|
|
2895
|
-
throw new
|
|
3465
|
+
throw new LakeStorageError22(`[SixbDuckLake] DuckLake committed snapshot '${ownSnapshotId}' for dataset '${input.dataset.id}', but Sixb could not hydrate it as a dataset version.`);
|
|
2896
3466
|
}
|
|
2897
3467
|
if (!input.changeResult.dataChangeExpected) {
|
|
3468
|
+
const version = await this.versionForNoOpCommit(input.runtime, input.dataset, input.allowInitialNoOp ?? false);
|
|
3469
|
+
if (input.expectedLatestVersionId !== undefined) {
|
|
3470
|
+
this.assertExpectedLatestVersion({
|
|
3471
|
+
datasetId: input.dataset.id,
|
|
3472
|
+
expectedLatestVersionId: input.expectedLatestVersionId,
|
|
3473
|
+
actualLatestVersionId: version?.versionId ?? null,
|
|
3474
|
+
operation: input.optimisticOperation ?? "commit"
|
|
3475
|
+
});
|
|
3476
|
+
}
|
|
2898
3477
|
return {
|
|
2899
|
-
|
|
2900
|
-
|
|
3478
|
+
outcome: "unchanged",
|
|
3479
|
+
version
|
|
2901
3480
|
};
|
|
2902
3481
|
}
|
|
2903
3482
|
if (input.committedWriteSnapshotId === input.previousWriteSnapshotId) {
|
|
2904
|
-
throw new
|
|
3483
|
+
throw new LakeStorageError22(`[SixbDuckLake] DuckLake commit for dataset '${input.dataset.id}' completed without producing a snapshot for this non-empty write.`);
|
|
2905
3484
|
}
|
|
2906
|
-
throw new
|
|
3485
|
+
throw new LakeStorageError22(`[SixbDuckLake] DuckLake commit for dataset '${input.dataset.id}' completed, but Sixb could not find a matching snapshot for this write.`);
|
|
2907
3486
|
}
|
|
2908
3487
|
async applyStagedRows(input) {
|
|
2909
3488
|
const mode = input.write.mode ?? "snapshot";
|
|
@@ -2916,11 +3495,12 @@ class DuckLakeWriteCoordinator {
|
|
|
2916
3495
|
previousRowCount: input.previousRowCount
|
|
2917
3496
|
});
|
|
2918
3497
|
if (result.sourceRowCount !== input.rowsWritten) {
|
|
2919
|
-
throw new
|
|
3498
|
+
throw new LakeStorageError22(`[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.`);
|
|
2920
3499
|
}
|
|
2921
3500
|
return result;
|
|
2922
3501
|
}
|
|
2923
3502
|
async setCommitMetadata(input, commitId, rowCount) {
|
|
3503
|
+
const validatedPrimaryKeyColumns = getDatasetPrimaryKeyColumns4(input.dataset);
|
|
2924
3504
|
const metadata = {
|
|
2925
3505
|
kind: "datasetVersion",
|
|
2926
3506
|
datasetId: input.dataset.id,
|
|
@@ -2928,16 +3508,20 @@ class DuckLakeWriteCoordinator {
|
|
|
2928
3508
|
mode: input.mode,
|
|
2929
3509
|
producer: input.producer ? structuredClone(input.producer) : undefined,
|
|
2930
3510
|
inputs: input.inputs ? structuredClone(input.inputs) : undefined,
|
|
2931
|
-
...rowCount.kind === "exact" ? { rowCount: rowCount.value } : {}
|
|
3511
|
+
...rowCount.kind === "exact" ? { rowCount: rowCount.value } : {},
|
|
3512
|
+
...validatedPrimaryKeyColumns !== null ? { validatedPrimaryKeyColumns: [...validatedPrimaryKeyColumns] } : {}
|
|
2932
3513
|
};
|
|
2933
3514
|
await input.runtime.run(`CALL ${quoteIdentifier(duckLakeAlias(this.options))}.set_commit_message(${quoteSqlString("Sixb")}, ${quoteSqlString(input.commitMessage)}, extra_info => ${quoteSqlString(JSON.stringify({ sixb: metadata }))})`);
|
|
2934
3515
|
}
|
|
2935
|
-
async versionForNoOpCommit(runtime, definition) {
|
|
3516
|
+
async versionForNoOpCommit(runtime, definition, allowInitialNoOp) {
|
|
2936
3517
|
const latestVersion = await this.snapshots.getLatestVersionForDefinition(runtime, definition);
|
|
2937
3518
|
if (latestVersion) {
|
|
2938
3519
|
return latestVersion;
|
|
2939
3520
|
}
|
|
2940
|
-
|
|
3521
|
+
if (allowInitialNoOp) {
|
|
3522
|
+
return null;
|
|
3523
|
+
}
|
|
3524
|
+
throw new LakeStorageError22(`[SixbDuckLake] No DuckLake changes were committed for dataset '${definition.id}', and no previous version exists.`);
|
|
2941
3525
|
}
|
|
2942
3526
|
async rollbackTransaction(runtime) {
|
|
2943
3527
|
try {
|
|
@@ -2993,7 +3577,7 @@ class DuckLakeWriteCoordinator {
|
|
|
2993
3577
|
return;
|
|
2994
3578
|
}
|
|
2995
3579
|
if (input.actualLatestVersionId !== input.expectedLatestVersionId) {
|
|
2996
|
-
throw new
|
|
3580
|
+
throw new LakeStorageError22(`[SixbDuckLake] Optimistic ${input.operation} failed for dataset '${input.datasetId}': expected latest version '${input.expectedLatestVersionId ?? "none"}', found '${input.actualLatestVersionId ?? "none"}'.`);
|
|
2997
3581
|
}
|
|
2998
3582
|
}
|
|
2999
3583
|
}
|
|
@@ -3011,7 +3595,7 @@ function commitRowCount(input, changeResult, latestVersion) {
|
|
|
3011
3595
|
}
|
|
3012
3596
|
function assertDuckLakeSnapshotId3(snapshotId) {
|
|
3013
3597
|
if (!/^\d+$/.test(snapshotId)) {
|
|
3014
|
-
throw new
|
|
3598
|
+
throw new LakeStorageError22(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`);
|
|
3015
3599
|
}
|
|
3016
3600
|
}
|
|
3017
3601
|
var localCatalogCommitLocks = new Map;
|
|
@@ -3089,6 +3673,9 @@ class DuckLakeStorage {
|
|
|
3089
3673
|
async beginWrite(input) {
|
|
3090
3674
|
return this.writes.beginWrite(input);
|
|
3091
3675
|
}
|
|
3676
|
+
async beginMerge(input) {
|
|
3677
|
+
return this.writes.beginMerge(input);
|
|
3678
|
+
}
|
|
3092
3679
|
async getLatestVersion(datasetId) {
|
|
3093
3680
|
return this.snapshotReader.getLatestVersion(datasetId);
|
|
3094
3681
|
}
|
|
@@ -3109,4 +3696,4 @@ export {
|
|
|
3109
3696
|
DuckLakeStorage
|
|
3110
3697
|
};
|
|
3111
3698
|
|
|
3112
|
-
//# debugId=
|
|
3699
|
+
//# debugId=36FC46708546A1DD64756E2164756E21
|