@sixb/ducklake 0.0.1 → 0.1.1
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 +37 -0
- package/dist/ducklake-storage.d.ts +2 -1
- package/dist/ducklake-storage.d.ts.map +1 -1
- package/dist/index.js +598 -43
- package/dist/index.js.map +15 -13
- 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-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 +187 -0
- 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 +28 -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,6 +1779,9 @@ 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
|
|
1693
1787
|
class DuckLakeRowReader {
|
|
@@ -2002,7 +2096,8 @@ class DuckLakeSnapshotReader {
|
|
|
2002
2096
|
return {
|
|
2003
2097
|
datasetId: tableRef.datasetId,
|
|
2004
2098
|
versionId: toVersionId(row.snapshotId),
|
|
2005
|
-
...row.metadata?.rowCount !== undefined ? { rowCount: row.metadata.rowCount } : {}
|
|
2099
|
+
...row.metadata?.rowCount !== undefined ? { rowCount: row.metadata.rowCount } : {},
|
|
2100
|
+
...row.metadata?.validatedPrimaryKeyColumns !== undefined ? { validatedPrimaryKeyColumns: row.metadata.validatedPrimaryKeyColumns } : {}
|
|
2006
2101
|
};
|
|
2007
2102
|
}
|
|
2008
2103
|
async getVersionForSnapshot(runtime, dataset, snapshotId) {
|
|
@@ -2062,7 +2157,7 @@ class DuckLakeSnapshotReader {
|
|
|
2062
2157
|
if (!row) {
|
|
2063
2158
|
return null;
|
|
2064
2159
|
}
|
|
2065
|
-
if (!options.includeParent || row.mode !== "append" && row.mode !== "schema") {
|
|
2160
|
+
if (!options.includeParent || row.mode !== "append" && row.mode !== "merge" && row.mode !== "schema") {
|
|
2066
2161
|
return row;
|
|
2067
2162
|
}
|
|
2068
2163
|
const [parent] = await this.collectVisibleSnapshotRows(runtime, {
|
|
@@ -2250,7 +2345,7 @@ class DuckLakeSnapshotReader {
|
|
|
2250
2345
|
}
|
|
2251
2346
|
withParentSnapshotIds(snapshots) {
|
|
2252
2347
|
return snapshots.map((snapshot, index) => {
|
|
2253
|
-
const parentSnapshotId = snapshot.mode === "append" || snapshot.mode === "schema" ? snapshots[index + 1]?.snapshotId : undefined;
|
|
2348
|
+
const parentSnapshotId = snapshot.mode === "append" || snapshot.mode === "merge" || snapshot.mode === "schema" ? snapshots[index + 1]?.snapshotId : undefined;
|
|
2254
2349
|
return parentSnapshotId === undefined ? snapshot : { ...snapshot, parentSnapshotId };
|
|
2255
2350
|
});
|
|
2256
2351
|
}
|
|
@@ -2271,7 +2366,10 @@ import {
|
|
|
2271
2366
|
} from "@sixb/core/lake-storage";
|
|
2272
2367
|
|
|
2273
2368
|
// src/internal/dataset-row-commit.ts
|
|
2274
|
-
import {
|
|
2369
|
+
import {
|
|
2370
|
+
getDatasetPrimaryKeyColumns,
|
|
2371
|
+
LakeStorageError as LakeStorageError16
|
|
2372
|
+
} from "@sixb/core/lake-storage";
|
|
2275
2373
|
function assertDatasetWriteMode(mode, operation) {
|
|
2276
2374
|
if (mode !== "snapshot" && mode !== "append") {
|
|
2277
2375
|
throw new LakeStorageError16(`[SixbDuckLake] Invalid ${operation} mode '${String(mode)}'. Expected 'snapshot' or 'append'.`);
|
|
@@ -2281,6 +2379,30 @@ async function applyDatasetRowsFromRelation(input) {
|
|
|
2281
2379
|
const tableName = encodeDatasetTableName(input.dataset.id);
|
|
2282
2380
|
const table = qualifiedTableName(input.options, tableName);
|
|
2283
2381
|
const columnsSql = datasetSchemaColumnNamesSql(input.dataset.schema);
|
|
2382
|
+
const primaryKeyColumns = getDatasetPrimaryKeyColumns(input.dataset);
|
|
2383
|
+
if (primaryKeyColumns !== null) {
|
|
2384
|
+
await inspectUniqueKeyedRelation({
|
|
2385
|
+
runtime: input.runtime,
|
|
2386
|
+
dataset: input.dataset,
|
|
2387
|
+
relationSql: input.sourceRelationSql,
|
|
2388
|
+
context: `${input.mode} source`
|
|
2389
|
+
});
|
|
2390
|
+
if (input.mode === "append") {
|
|
2391
|
+
await inspectUniqueKeyedRelation({
|
|
2392
|
+
runtime: input.runtime,
|
|
2393
|
+
dataset: input.dataset,
|
|
2394
|
+
relationSql: table,
|
|
2395
|
+
context: "current baseline"
|
|
2396
|
+
});
|
|
2397
|
+
await assertNoPrimaryKeyOverlap({
|
|
2398
|
+
runtime: input.runtime,
|
|
2399
|
+
dataset: input.dataset,
|
|
2400
|
+
leftRelationSql: input.sourceRelationSql,
|
|
2401
|
+
rightRelationSql: table,
|
|
2402
|
+
context: "append"
|
|
2403
|
+
});
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2284
2406
|
if (input.mode === "snapshot") {
|
|
2285
2407
|
const source = await fingerprintRelation(input.runtime, input.sourceRelationSql, columnsSql);
|
|
2286
2408
|
const target = await fingerprintRelation(input.runtime, table, columnsSql);
|
|
@@ -2307,6 +2429,44 @@ async function applyDatasetRowsFromRelation(input) {
|
|
|
2307
2429
|
resultingRowCount: input.previousRowCount === undefined ? { kind: "unknown" } : { kind: "exact", value: input.previousRowCount + sourceRowCount }
|
|
2308
2430
|
};
|
|
2309
2431
|
}
|
|
2432
|
+
async function inspectUniqueKeyedRelation(input) {
|
|
2433
|
+
const primaryKeyColumns = getDatasetPrimaryKeyColumns(input.dataset);
|
|
2434
|
+
if (primaryKeyColumns === null) {
|
|
2435
|
+
throw new LakeStorageError16(`[SixbDuckLake] Dataset '${input.dataset.id}' must define a primaryKey before rows can be keyed.`);
|
|
2436
|
+
}
|
|
2437
|
+
const keysSql = primaryKeyColumns.map((column) => quoteIdentifier(column)).join(", ");
|
|
2438
|
+
const [row] = await input.runtime.query(`
|
|
2439
|
+
SELECT
|
|
2440
|
+
count(*) AS row_count,
|
|
2441
|
+
coalesce(max(key_count), 0) AS max_key_count
|
|
2442
|
+
FROM (
|
|
2443
|
+
SELECT count(*) OVER (PARTITION BY ${keysSql}) AS key_count
|
|
2444
|
+
FROM ${input.relationSql}
|
|
2445
|
+
) keyed_rows
|
|
2446
|
+
`);
|
|
2447
|
+
const rowCount = row === undefined ? 0 : Number(getBigIntLike(row, "row_count"));
|
|
2448
|
+
const maxKeyCount = row === undefined ? 0 : Number(getBigIntLike(row, "max_key_count"));
|
|
2449
|
+
if (maxKeyCount > 1) {
|
|
2450
|
+
throw new LakeStorageError16(`[SixbDuckLake] Dataset '${input.dataset.id}' ${input.context} contains duplicate primary key.`);
|
|
2451
|
+
}
|
|
2452
|
+
return rowCount;
|
|
2453
|
+
}
|
|
2454
|
+
async function assertNoPrimaryKeyOverlap(input) {
|
|
2455
|
+
const primaryKeyColumns = getDatasetPrimaryKeyColumns(input.dataset);
|
|
2456
|
+
if (primaryKeyColumns === null) {
|
|
2457
|
+
return;
|
|
2458
|
+
}
|
|
2459
|
+
const matchSql = primaryKeyColumns.map((column) => `left_rows.${quoteIdentifier(column)} = right_rows.${quoteIdentifier(column)}`).join(" AND ");
|
|
2460
|
+
const [collision] = await input.runtime.query(`
|
|
2461
|
+
SELECT 1
|
|
2462
|
+
FROM ${input.leftRelationSql} left_rows
|
|
2463
|
+
JOIN ${input.rightRelationSql} right_rows ON ${matchSql}
|
|
2464
|
+
LIMIT 1
|
|
2465
|
+
`);
|
|
2466
|
+
if (collision !== undefined) {
|
|
2467
|
+
throw new LakeStorageError16(`[SixbDuckLake] Dataset '${input.dataset.id}' ${input.context} contains duplicate primary key.`);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2310
2470
|
async function fingerprintRelation(runtime, relationSql, columnsSql) {
|
|
2311
2471
|
const [row] = await runtime.query(`
|
|
2312
2472
|
SELECT
|
|
@@ -2604,13 +2764,183 @@ function throwResultSchemaMismatch(datasetId, detail) {
|
|
|
2604
2764
|
}
|
|
2605
2765
|
|
|
2606
2766
|
// src/internal/ducklake-write-coordinator.ts
|
|
2607
|
-
import { randomUUID as
|
|
2608
|
-
import { LakeStorageError as
|
|
2767
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
2768
|
+
import { getDatasetPrimaryKeyColumns as getDatasetPrimaryKeyColumns4, LakeStorageError as LakeStorageError22 } from "@sixb/core/lake-storage";
|
|
2609
2769
|
|
|
2610
|
-
// src/internal/
|
|
2770
|
+
// src/internal/dataset-row-merge.ts
|
|
2771
|
+
import { getDatasetPrimaryKeyColumns as getDatasetPrimaryKeyColumns2, LakeStorageError as LakeStorageError19 } from "@sixb/core/lake-storage";
|
|
2772
|
+
async function applyDatasetMergeFromRelation(input) {
|
|
2773
|
+
const primaryKeyColumns = getDatasetPrimaryKeyColumns2(input.dataset);
|
|
2774
|
+
if (primaryKeyColumns === null) {
|
|
2775
|
+
throw new LakeStorageError19(`[SixbDuckLake] Dataset '${input.dataset.id}' must define a primaryKey before it can be merged.`);
|
|
2776
|
+
}
|
|
2777
|
+
const stagingTable = quoteIdentifier(input.stagingTableName);
|
|
2778
|
+
const targetTable = qualifiedTableName(input.options, encodeDatasetTableName(input.dataset.id));
|
|
2779
|
+
const sourceRowCount = await countRows2(input.runtime, stagingTable);
|
|
2780
|
+
const previousRowCount = await currentBaselineRowCount(input, targetTable, primaryKeyColumns);
|
|
2781
|
+
return withMaterializedFinalChanges(input, primaryKeyColumns, async (finalChanges) => {
|
|
2782
|
+
const effects = await countMergeEffects(input, targetTable, finalChanges, primaryKeyColumns);
|
|
2783
|
+
const effectiveChangeCount = effects.inserts + effects.updates + effects.deletes;
|
|
2784
|
+
if (effectiveChangeCount === 0) {
|
|
2785
|
+
return {
|
|
2786
|
+
dataChangeExpected: false,
|
|
2787
|
+
sourceRowCount,
|
|
2788
|
+
resultingRowCount: { kind: "exact", value: previousRowCount }
|
|
2789
|
+
};
|
|
2790
|
+
}
|
|
2791
|
+
const keyMatchSql = primaryKeyMatchSql(primaryKeyColumns, "target", "source");
|
|
2792
|
+
const rowDifferenceSql = datasetRowDifferenceSql(input.dataset, "target", "source");
|
|
2793
|
+
const kindColumn = `source.${quoteIdentifier(input.kindColumnName)}`;
|
|
2794
|
+
await input.runtime.run(`
|
|
2795
|
+
DELETE FROM ${targetTable} AS target
|
|
2796
|
+
USING ${finalChanges} AS source
|
|
2797
|
+
WHERE ${keyMatchSql}
|
|
2798
|
+
AND (
|
|
2799
|
+
${kindColumn} = ${quoteSqlString("delete")}
|
|
2800
|
+
OR (
|
|
2801
|
+
${kindColumn} = ${quoteSqlString("upsert")}
|
|
2802
|
+
AND (${rowDifferenceSql})
|
|
2803
|
+
)
|
|
2804
|
+
)
|
|
2805
|
+
`);
|
|
2806
|
+
const columnsSql = datasetSchemaColumnNamesSql(input.dataset.schema);
|
|
2807
|
+
const selectedColumnsSql = input.dataset.schema.columns.map((column) => `source.${quoteIdentifier(column.name)}`).join(", ");
|
|
2808
|
+
const firstSequenceColumnName = firstSequenceName(input.sequenceColumnName);
|
|
2809
|
+
await input.runtime.run(`
|
|
2810
|
+
INSERT INTO ${targetTable} (${columnsSql})
|
|
2811
|
+
SELECT ${selectedColumnsSql}
|
|
2812
|
+
FROM ${finalChanges} AS source
|
|
2813
|
+
WHERE ${kindColumn} = ${quoteSqlString("upsert")}
|
|
2814
|
+
AND NOT EXISTS (
|
|
2815
|
+
SELECT 1
|
|
2816
|
+
FROM ${targetTable} AS target
|
|
2817
|
+
WHERE ${keyMatchSql}
|
|
2818
|
+
)
|
|
2819
|
+
ORDER BY source.${quoteIdentifier(firstSequenceColumnName)}
|
|
2820
|
+
`);
|
|
2821
|
+
return {
|
|
2822
|
+
dataChangeExpected: true,
|
|
2823
|
+
sourceRowCount,
|
|
2824
|
+
resultingRowCount: {
|
|
2825
|
+
kind: "exact",
|
|
2826
|
+
value: previousRowCount + effects.inserts - effects.deletes
|
|
2827
|
+
}
|
|
2828
|
+
};
|
|
2829
|
+
});
|
|
2830
|
+
}
|
|
2831
|
+
async function currentBaselineRowCount(input, targetTable, primaryKeyColumns) {
|
|
2832
|
+
if (sameColumns(input.validatedPrimaryKeyColumns, primaryKeyColumns)) {
|
|
2833
|
+
return input.previousRowCount ?? countRows2(input.runtime, targetTable);
|
|
2834
|
+
}
|
|
2835
|
+
return inspectUniqueKeyedRelation({
|
|
2836
|
+
runtime: input.runtime,
|
|
2837
|
+
dataset: input.dataset,
|
|
2838
|
+
relationSql: targetTable,
|
|
2839
|
+
context: "current baseline"
|
|
2840
|
+
});
|
|
2841
|
+
}
|
|
2842
|
+
async function withMaterializedFinalChanges(input, primaryKeyColumns, run) {
|
|
2843
|
+
const table = quoteIdentifier(`${input.stagingTableName}_final`);
|
|
2844
|
+
await input.runtime.run(`
|
|
2845
|
+
CREATE TEMP TABLE ${table} AS
|
|
2846
|
+
${finalChangesSelectSql(input, primaryKeyColumns)}
|
|
2847
|
+
`);
|
|
2848
|
+
let outcome;
|
|
2849
|
+
try {
|
|
2850
|
+
outcome = { kind: "success", value: await run(table) };
|
|
2851
|
+
} catch (error) {
|
|
2852
|
+
outcome = { kind: "error", error };
|
|
2853
|
+
}
|
|
2854
|
+
try {
|
|
2855
|
+
await input.runtime.run(`DROP TABLE IF EXISTS ${table}`);
|
|
2856
|
+
} catch (cleanupError) {
|
|
2857
|
+
if (outcome.kind === "success") {
|
|
2858
|
+
throw cleanupError;
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
if (outcome.kind === "error") {
|
|
2862
|
+
throw outcome.error;
|
|
2863
|
+
}
|
|
2864
|
+
return outcome.value;
|
|
2865
|
+
}
|
|
2866
|
+
function finalChangesSelectSql(input, primaryKeyColumns) {
|
|
2867
|
+
const sequenceColumn = quoteIdentifier(input.sequenceColumnName);
|
|
2868
|
+
const keyColumnsSql = primaryKeyColumns.map((column) => quoteIdentifier(column)).join(", ");
|
|
2869
|
+
const rankColumn = quoteIdentifier(rankName(input.sequenceColumnName));
|
|
2870
|
+
const firstSequenceColumn = quoteIdentifier(firstSequenceName(input.sequenceColumnName));
|
|
2871
|
+
return `
|
|
2872
|
+
SELECT * EXCLUDE (${rankColumn})
|
|
2873
|
+
FROM (
|
|
2874
|
+
SELECT
|
|
2875
|
+
*,
|
|
2876
|
+
min(${sequenceColumn}) OVER (PARTITION BY ${keyColumnsSql}) AS ${firstSequenceColumn},
|
|
2877
|
+
row_number() OVER (
|
|
2878
|
+
PARTITION BY ${keyColumnsSql}
|
|
2879
|
+
ORDER BY ${sequenceColumn} DESC
|
|
2880
|
+
) AS ${rankColumn}
|
|
2881
|
+
FROM ${quoteIdentifier(input.stagingTableName)}
|
|
2882
|
+
) ranked_changes
|
|
2883
|
+
WHERE ${rankColumn} = 1
|
|
2884
|
+
`;
|
|
2885
|
+
}
|
|
2886
|
+
async function countMergeEffects(input, targetTable, finalChanges, primaryKeyColumns) {
|
|
2887
|
+
const matchColumn = primaryKeyColumns[0];
|
|
2888
|
+
if (matchColumn === undefined) {
|
|
2889
|
+
throw new LakeStorageError19(`[SixbDuckLake] Dataset '${input.dataset.id}' primaryKey must contain at least one column.`);
|
|
2890
|
+
}
|
|
2891
|
+
const keyMatchSql = primaryKeyMatchSql(primaryKeyColumns, "target", "source");
|
|
2892
|
+
const targetMatch = `target.${quoteIdentifier(matchColumn)} IS NOT NULL`;
|
|
2893
|
+
const kindColumn = `source.${quoteIdentifier(input.kindColumnName)}`;
|
|
2894
|
+
const rowDifferenceSql = datasetRowDifferenceSql(input.dataset, "target", "source");
|
|
2895
|
+
const [row] = await input.runtime.query(`
|
|
2896
|
+
SELECT
|
|
2897
|
+
count(*) FILTER (
|
|
2898
|
+
WHERE ${kindColumn} = ${quoteSqlString("upsert")} AND NOT (${targetMatch})
|
|
2899
|
+
) AS insert_count,
|
|
2900
|
+
count(*) FILTER (
|
|
2901
|
+
WHERE ${kindColumn} = ${quoteSqlString("upsert")}
|
|
2902
|
+
AND ${targetMatch}
|
|
2903
|
+
AND (${rowDifferenceSql})
|
|
2904
|
+
) AS update_count,
|
|
2905
|
+
count(*) FILTER (
|
|
2906
|
+
WHERE ${kindColumn} = ${quoteSqlString("delete")} AND ${targetMatch}
|
|
2907
|
+
) AS delete_count
|
|
2908
|
+
FROM ${finalChanges} AS source
|
|
2909
|
+
LEFT JOIN ${targetTable} AS target ON ${keyMatchSql}
|
|
2910
|
+
`);
|
|
2911
|
+
return {
|
|
2912
|
+
inserts: row === undefined ? 0 : Number(getBigIntLike(row, "insert_count")),
|
|
2913
|
+
updates: row === undefined ? 0 : Number(getBigIntLike(row, "update_count")),
|
|
2914
|
+
deletes: row === undefined ? 0 : Number(getBigIntLike(row, "delete_count"))
|
|
2915
|
+
};
|
|
2916
|
+
}
|
|
2917
|
+
function primaryKeyMatchSql(primaryKeyColumns, leftAlias, rightAlias) {
|
|
2918
|
+
return primaryKeyColumns.map((column) => `${leftAlias}.${quoteIdentifier(column)} = ${rightAlias}.${quoteIdentifier(column)}`).join(" AND ");
|
|
2919
|
+
}
|
|
2920
|
+
function datasetRowDifferenceSql(dataset, leftAlias, rightAlias) {
|
|
2921
|
+
return dataset.schema.columns.map((column) => `${leftAlias}.${quoteIdentifier(column.name)} IS DISTINCT FROM ${rightAlias}.${quoteIdentifier(column.name)}`).join(" OR ");
|
|
2922
|
+
}
|
|
2923
|
+
async function countRows2(runtime, relationSql) {
|
|
2924
|
+
const [row] = await runtime.query(`SELECT count(*) AS row_count FROM ${relationSql}`);
|
|
2925
|
+
return row === undefined ? 0 : Number(getBigIntLike(row, "row_count"));
|
|
2926
|
+
}
|
|
2927
|
+
function rankName(sequenceColumnName) {
|
|
2928
|
+
return `${sequenceColumnName}_rank`;
|
|
2929
|
+
}
|
|
2930
|
+
function firstSequenceName(sequenceColumnName) {
|
|
2931
|
+
return `${sequenceColumnName}_first`;
|
|
2932
|
+
}
|
|
2933
|
+
function sameColumns(left, right) {
|
|
2934
|
+
return left !== undefined && left.length === right.length && left.every((value, index) => value === right[index]);
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
// src/internal/ducklake-merge-session.ts
|
|
2611
2938
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
2612
|
-
import {
|
|
2613
|
-
|
|
2939
|
+
import {
|
|
2940
|
+
cloneDatasetMergeChange,
|
|
2941
|
+
getDatasetMergeChangeValidationError,
|
|
2942
|
+
LakeStorageError as LakeStorageError20
|
|
2943
|
+
} from "@sixb/core/lake-storage";
|
|
2614
2944
|
|
|
2615
2945
|
// src/internal/row-appender.ts
|
|
2616
2946
|
function appendDatasetRow(appender, schema, row) {
|
|
@@ -2665,10 +2995,134 @@ function fileRefToDuckDbStruct(fileRef) {
|
|
|
2665
2995
|
};
|
|
2666
2996
|
}
|
|
2667
2997
|
|
|
2998
|
+
// src/internal/ducklake-merge-session.ts
|
|
2999
|
+
var MERGE_CHANGE_BATCH_SIZE = 1000;
|
|
3000
|
+
async function createDuckLakeMergeSession(input) {
|
|
3001
|
+
const suffix = randomUUID4().replaceAll("-", "");
|
|
3002
|
+
const stagingTableName = `sixb_merge_${suffix}`;
|
|
3003
|
+
const sequenceColumnName = `sixb_merge_sequence_${suffix}`;
|
|
3004
|
+
const kindColumnName = `sixb_merge_kind_${suffix}`;
|
|
3005
|
+
await input.runtime.run(`
|
|
3006
|
+
CREATE TEMP TABLE ${quoteIdentifier(stagingTableName)} (
|
|
3007
|
+
${quoteIdentifier(sequenceColumnName)} UBIGINT NOT NULL,
|
|
3008
|
+
${quoteIdentifier(kindColumnName)} VARCHAR NOT NULL,
|
|
3009
|
+
${datasetSchemaToDuckDbNullableColumnsSql(input.merge.dataset.schema)}
|
|
3010
|
+
)
|
|
3011
|
+
`);
|
|
3012
|
+
return new DuckLakeMergeSession(input.commitMerge, input.runtime, input.merge, input.baseVersionId, stagingTableName, sequenceColumnName, kindColumnName);
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
class DuckLakeMergeSession {
|
|
3016
|
+
commitMerge;
|
|
3017
|
+
runtime;
|
|
3018
|
+
input;
|
|
3019
|
+
baseVersionId;
|
|
3020
|
+
stagingTableName;
|
|
3021
|
+
sequenceColumnName;
|
|
3022
|
+
kindColumnName;
|
|
3023
|
+
closed = false;
|
|
3024
|
+
cleanedUp = false;
|
|
3025
|
+
nextSequence = 0n;
|
|
3026
|
+
changesWritten = 0;
|
|
3027
|
+
constructor(commitMerge, runtime, input, baseVersionId, stagingTableName, sequenceColumnName, kindColumnName) {
|
|
3028
|
+
this.commitMerge = commitMerge;
|
|
3029
|
+
this.runtime = runtime;
|
|
3030
|
+
this.input = input;
|
|
3031
|
+
this.baseVersionId = baseVersionId;
|
|
3032
|
+
this.stagingTableName = stagingTableName;
|
|
3033
|
+
this.sequenceColumnName = sequenceColumnName;
|
|
3034
|
+
this.kindColumnName = kindColumnName;
|
|
3035
|
+
}
|
|
3036
|
+
async writeChanges(changes) {
|
|
3037
|
+
this.assertOpen();
|
|
3038
|
+
let batch = [];
|
|
3039
|
+
for await (const change of changes) {
|
|
3040
|
+
const validationError = getDatasetMergeChangeValidationError(change, this.input.dataset);
|
|
3041
|
+
if (validationError) {
|
|
3042
|
+
throw new LakeStorageError20(`[SixbDuckLake] ${validationError}`);
|
|
3043
|
+
}
|
|
3044
|
+
batch.push({
|
|
3045
|
+
sequence: this.nextSequence,
|
|
3046
|
+
change: cloneDatasetMergeChange(change)
|
|
3047
|
+
});
|
|
3048
|
+
this.nextSequence += 1n;
|
|
3049
|
+
if (batch.length >= MERGE_CHANGE_BATCH_SIZE) {
|
|
3050
|
+
await this.appendBatchToStagingTable(batch);
|
|
3051
|
+
batch = [];
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
if (batch.length > 0) {
|
|
3055
|
+
await this.appendBatchToStagingTable(batch);
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
async appendBatchToStagingTable(batch) {
|
|
3059
|
+
await this.runtime.withAppender(this.stagingTableName, (appender) => {
|
|
3060
|
+
for (const staged of batch) {
|
|
3061
|
+
appender.appendBigInt(staged.sequence);
|
|
3062
|
+
appender.appendVarchar(staged.change.kind);
|
|
3063
|
+
appendDatasetRow(appender, this.input.dataset.schema, staged.change.kind === "upsert" ? staged.change.row : staged.change.key);
|
|
3064
|
+
}
|
|
3065
|
+
});
|
|
3066
|
+
this.changesWritten += batch.length;
|
|
3067
|
+
}
|
|
3068
|
+
async commit(input) {
|
|
3069
|
+
this.assertOpen();
|
|
3070
|
+
this.closed = true;
|
|
3071
|
+
let committed = false;
|
|
3072
|
+
try {
|
|
3073
|
+
const result = await this.commitMerge({
|
|
3074
|
+
merge: this.input,
|
|
3075
|
+
baseVersionId: this.baseVersionId,
|
|
3076
|
+
commit: input,
|
|
3077
|
+
stagingTableName: this.stagingTableName,
|
|
3078
|
+
sequenceColumnName: this.sequenceColumnName,
|
|
3079
|
+
kindColumnName: this.kindColumnName,
|
|
3080
|
+
changesWritten: this.changesWritten
|
|
3081
|
+
});
|
|
3082
|
+
committed = true;
|
|
3083
|
+
await this.cleanup();
|
|
3084
|
+
return result;
|
|
3085
|
+
} catch (error) {
|
|
3086
|
+
if (!committed) {
|
|
3087
|
+
await this.cleanup();
|
|
3088
|
+
}
|
|
3089
|
+
throw error;
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
async abort() {
|
|
3093
|
+
if (this.closed) {
|
|
3094
|
+
return;
|
|
3095
|
+
}
|
|
3096
|
+
this.closed = true;
|
|
3097
|
+
await this.cleanup();
|
|
3098
|
+
}
|
|
3099
|
+
assertOpen() {
|
|
3100
|
+
if (this.closed) {
|
|
3101
|
+
throw new LakeStorageError20("[SixbDuckLake] Merge session is already closed.");
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
async cleanup() {
|
|
3105
|
+
if (this.cleanedUp) {
|
|
3106
|
+
return;
|
|
3107
|
+
}
|
|
3108
|
+
this.cleanedUp = true;
|
|
3109
|
+
try {
|
|
3110
|
+
await this.runtime.run(`DROP TABLE IF EXISTS ${quoteIdentifier(this.stagingTableName)}`);
|
|
3111
|
+
} catch {}
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
|
|
2668
3115
|
// src/internal/ducklake-write-session.ts
|
|
3116
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
3117
|
+
import { getDatasetRowValidationError } from "@sixb/core";
|
|
3118
|
+
import {
|
|
3119
|
+
encodeDatasetPrimaryKey,
|
|
3120
|
+
getDatasetPrimaryKeyColumns as getDatasetPrimaryKeyColumns3,
|
|
3121
|
+
LakeStorageError as LakeStorageError21
|
|
3122
|
+
} from "@sixb/core/lake-storage";
|
|
2669
3123
|
var WRITE_ROW_BATCH_SIZE = 1000;
|
|
2670
3124
|
async function createDuckLakeWriteSession(input) {
|
|
2671
|
-
const stagingTableName = `sixb_write_${
|
|
3125
|
+
const stagingTableName = `sixb_write_${randomUUID5().replaceAll("-", "")}`;
|
|
2672
3126
|
await input.runtime.run(`CREATE TEMP TABLE ${quoteIdentifier(stagingTableName)} (${datasetSchemaToDuckDbColumnsSql(input.write.dataset.schema)})`);
|
|
2673
3127
|
return new DuckLakeWriteSession(input.commitWrite, input.runtime, input.write, stagingTableName);
|
|
2674
3128
|
}
|
|
@@ -2681,6 +3135,7 @@ class DuckLakeWriteSession {
|
|
|
2681
3135
|
closed = false;
|
|
2682
3136
|
cleanedUp = false;
|
|
2683
3137
|
rowsWritten = 0;
|
|
3138
|
+
stagedPrimaryKeys = new Set;
|
|
2684
3139
|
constructor(commitWrite, runtime, input, stagingTableName) {
|
|
2685
3140
|
this.commitWrite = commitWrite;
|
|
2686
3141
|
this.runtime = runtime;
|
|
@@ -2690,28 +3145,41 @@ class DuckLakeWriteSession {
|
|
|
2690
3145
|
async writeRows(rows) {
|
|
2691
3146
|
this.assertOpen();
|
|
2692
3147
|
let batch = [];
|
|
3148
|
+
let batchPrimaryKeys = new Set;
|
|
2693
3149
|
for await (const row of rows) {
|
|
2694
3150
|
const validationError = getDatasetRowValidationError(row, this.input.dataset);
|
|
2695
3151
|
if (validationError) {
|
|
2696
|
-
throw new
|
|
3152
|
+
throw new LakeStorageError21(`[SixbDuckLake] ${validationError}`);
|
|
2697
3153
|
}
|
|
2698
|
-
|
|
3154
|
+
const clonedRow = structuredClone(row);
|
|
3155
|
+
if (getDatasetPrimaryKeyColumns3(this.input.dataset) !== null) {
|
|
3156
|
+
const primaryKey = encodeDatasetPrimaryKey(this.input.dataset, clonedRow);
|
|
3157
|
+
if (this.stagedPrimaryKeys.has(primaryKey) || batchPrimaryKeys.has(primaryKey)) {
|
|
3158
|
+
throw new LakeStorageError21(`[SixbDuckLake] Dataset '${this.input.dataset.id}' ${this.input.mode ?? "snapshot"} source contains duplicate primary key.`);
|
|
3159
|
+
}
|
|
3160
|
+
batchPrimaryKeys.add(primaryKey);
|
|
3161
|
+
}
|
|
3162
|
+
batch.push(clonedRow);
|
|
2699
3163
|
if (batch.length >= WRITE_ROW_BATCH_SIZE) {
|
|
2700
|
-
await this.appendBatchToStagingTable(batch);
|
|
3164
|
+
await this.appendBatchToStagingTable(batch, batchPrimaryKeys);
|
|
2701
3165
|
batch = [];
|
|
3166
|
+
batchPrimaryKeys = new Set;
|
|
2702
3167
|
}
|
|
2703
3168
|
}
|
|
2704
3169
|
if (batch.length > 0) {
|
|
2705
|
-
await this.appendBatchToStagingTable(batch);
|
|
3170
|
+
await this.appendBatchToStagingTable(batch, batchPrimaryKeys);
|
|
2706
3171
|
}
|
|
2707
3172
|
}
|
|
2708
|
-
async appendBatchToStagingTable(batch) {
|
|
3173
|
+
async appendBatchToStagingTable(batch, primaryKeys) {
|
|
2709
3174
|
await this.runtime.withAppender(this.stagingTableName, (appender) => {
|
|
2710
3175
|
for (const row of batch) {
|
|
2711
3176
|
appendDatasetRow(appender, this.input.dataset.schema, row);
|
|
2712
3177
|
}
|
|
2713
3178
|
});
|
|
2714
3179
|
this.rowsWritten += batch.length;
|
|
3180
|
+
for (const primaryKey of primaryKeys) {
|
|
3181
|
+
this.stagedPrimaryKeys.add(primaryKey);
|
|
3182
|
+
}
|
|
2715
3183
|
}
|
|
2716
3184
|
async commit(input) {
|
|
2717
3185
|
this.assertOpen();
|
|
@@ -2743,7 +3211,7 @@ class DuckLakeWriteSession {
|
|
|
2743
3211
|
}
|
|
2744
3212
|
assertOpen() {
|
|
2745
3213
|
if (this.closed) {
|
|
2746
|
-
throw new
|
|
3214
|
+
throw new LakeStorageError21("[SixbDuckLake] Write session is already closed.");
|
|
2747
3215
|
}
|
|
2748
3216
|
}
|
|
2749
3217
|
async cleanup() {
|
|
@@ -2776,7 +3244,7 @@ class DuckLakeWriteCoordinator {
|
|
|
2776
3244
|
this.connections.assertOpen();
|
|
2777
3245
|
const definition = await this.datasets.getDataset(input.dataset.id);
|
|
2778
3246
|
if (!definition) {
|
|
2779
|
-
throw new
|
|
3247
|
+
throw new LakeStorageError22(`[SixbDuckLake] Unknown dataset '${input.dataset.id}'.`);
|
|
2780
3248
|
}
|
|
2781
3249
|
this.datasets.assertSchema(definition);
|
|
2782
3250
|
const mode = input.mode ?? "snapshot";
|
|
@@ -2792,10 +3260,32 @@ class DuckLakeWriteCoordinator {
|
|
|
2792
3260
|
}
|
|
2793
3261
|
});
|
|
2794
3262
|
}
|
|
3263
|
+
async beginMerge(input) {
|
|
3264
|
+
this.connections.assertOpen();
|
|
3265
|
+
const definition = await this.datasets.getDataset(input.dataset.id);
|
|
3266
|
+
if (!definition) {
|
|
3267
|
+
throw new LakeStorageError22(`[SixbDuckLake] Unknown dataset '${input.dataset.id}'.`);
|
|
3268
|
+
}
|
|
3269
|
+
if (getDatasetPrimaryKeyColumns4(definition) === null) {
|
|
3270
|
+
throw new LakeStorageError22(`[SixbDuckLake] Dataset '${definition.id}' must define a primaryKey before it can be merged.`);
|
|
3271
|
+
}
|
|
3272
|
+
this.datasets.assertSchema(definition);
|
|
3273
|
+
const latestVersion = await this.connections.withAttachedRuntime((runtime2) => this.snapshots.getLatestVersionForDefinition(runtime2, definition));
|
|
3274
|
+
if (input.expectedLatestVersionId !== undefined && latestVersion?.versionId !== input.expectedLatestVersionId) {
|
|
3275
|
+
throw new LakeStorageError22(`[SixbDuckLake] Optimistic merge start failed for dataset '${definition.id}': expected latest version '${input.expectedLatestVersionId}', found '${latestVersion?.versionId ?? "none"}'.`);
|
|
3276
|
+
}
|
|
3277
|
+
const runtime = await this.connections.stagingRuntime();
|
|
3278
|
+
return createDuckLakeMergeSession({
|
|
3279
|
+
commitMerge: (options) => this.commitMerge(options),
|
|
3280
|
+
runtime,
|
|
3281
|
+
merge: { ...input, dataset: definition },
|
|
3282
|
+
baseVersionId: latestVersion?.versionId ?? null
|
|
3283
|
+
});
|
|
3284
|
+
}
|
|
2795
3285
|
async commitWrite(input) {
|
|
2796
3286
|
const definition = await this.datasets.getDataset(input.write.dataset.id);
|
|
2797
3287
|
if (!definition) {
|
|
2798
|
-
throw new
|
|
3288
|
+
throw new LakeStorageError22(`[SixbDuckLake] Unknown dataset '${input.write.dataset.id}'.`);
|
|
2799
3289
|
}
|
|
2800
3290
|
const mode = input.write.mode ?? "snapshot";
|
|
2801
3291
|
assertDatasetWriteMode(mode, "write");
|
|
@@ -2813,6 +3303,42 @@ class DuckLakeWriteCoordinator {
|
|
|
2813
3303
|
})
|
|
2814
3304
|
});
|
|
2815
3305
|
}
|
|
3306
|
+
async commitMerge(input) {
|
|
3307
|
+
const definition = await this.datasets.getDataset(input.merge.dataset.id);
|
|
3308
|
+
if (!definition) {
|
|
3309
|
+
throw new LakeStorageError22(`[SixbDuckLake] Unknown dataset '${input.merge.dataset.id}'.`);
|
|
3310
|
+
}
|
|
3311
|
+
if (getDatasetPrimaryKeyColumns4(definition) === null) {
|
|
3312
|
+
throw new LakeStorageError22(`[SixbDuckLake] Dataset '${definition.id}' must define a primaryKey before it can be merged.`);
|
|
3313
|
+
}
|
|
3314
|
+
return this.withCommitRuntime((runtime) => this.commitVersionOutcomeOnExclusiveRuntime(runtime, {
|
|
3315
|
+
dataset: definition,
|
|
3316
|
+
mode: "merge",
|
|
3317
|
+
expectedLatestVersionId: input.baseVersionId,
|
|
3318
|
+
allowInitialNoOp: true,
|
|
3319
|
+
disableRetries: true,
|
|
3320
|
+
optimisticOperation: "merge commit",
|
|
3321
|
+
commitMessage: input.commit?.commitMessage ?? `merge dataset ${definition.id}`,
|
|
3322
|
+
producer: input.merge.producer,
|
|
3323
|
+
inputs: input.merge.inputs,
|
|
3324
|
+
applyChanges: async (commitRuntime, context) => {
|
|
3325
|
+
const result = await applyDatasetMergeFromRelation({
|
|
3326
|
+
options: this.options,
|
|
3327
|
+
runtime: commitRuntime,
|
|
3328
|
+
dataset: definition,
|
|
3329
|
+
stagingTableName: input.stagingTableName,
|
|
3330
|
+
sequenceColumnName: input.sequenceColumnName,
|
|
3331
|
+
kindColumnName: input.kindColumnName,
|
|
3332
|
+
previousRowCount: context.previousRowCount,
|
|
3333
|
+
validatedPrimaryKeyColumns: context.validatedPrimaryKeyColumns
|
|
3334
|
+
});
|
|
3335
|
+
if (result.sourceRowCount !== input.changesWritten) {
|
|
3336
|
+
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.`);
|
|
3337
|
+
}
|
|
3338
|
+
return result;
|
|
3339
|
+
}
|
|
3340
|
+
}));
|
|
3341
|
+
}
|
|
2816
3342
|
async commitDatasetVersion(input) {
|
|
2817
3343
|
return this.withCommitRuntime((runtime) => this.commitDatasetVersionOnExclusiveRuntime(runtime, input));
|
|
2818
3344
|
}
|
|
@@ -2822,11 +3348,21 @@ class DuckLakeWriteCoordinator {
|
|
|
2822
3348
|
});
|
|
2823
3349
|
}
|
|
2824
3350
|
async commitDatasetVersionOnExclusiveRuntime(runtime, input) {
|
|
3351
|
+
const result = await this.commitVersionOutcomeOnExclusiveRuntime(runtime, {
|
|
3352
|
+
...input,
|
|
3353
|
+
disableRetries: input.mode === "append" && getDatasetPrimaryKeyColumns4(input.dataset) !== null
|
|
3354
|
+
});
|
|
3355
|
+
if (result.version === null) {
|
|
3356
|
+
throw new LakeStorageError22(`[SixbDuckLake] No DuckLake changes were committed for dataset '${input.dataset.id}', and no previous version exists.`);
|
|
3357
|
+
}
|
|
3358
|
+
return { ...result.version, outcome: result.outcome };
|
|
3359
|
+
}
|
|
3360
|
+
async commitVersionOutcomeOnExclusiveRuntime(runtime, input) {
|
|
2825
3361
|
const runtimeInput = { ...input, runtime };
|
|
2826
3362
|
return this.withGuardedRetrySetting(runtimeInput, () => this.commitDatasetVersionUnlocked(runtimeInput));
|
|
2827
3363
|
}
|
|
2828
3364
|
async withGuardedRetrySetting(input, run) {
|
|
2829
|
-
if (input.expectedLatestVersionId === undefined) {
|
|
3365
|
+
if (input.expectedLatestVersionId === undefined && !input.disableRetries) {
|
|
2830
3366
|
return run();
|
|
2831
3367
|
}
|
|
2832
3368
|
await input.runtime.run("SET ducklake_max_retry_count = 0");
|
|
@@ -2856,13 +3392,15 @@ class DuckLakeWriteCoordinator {
|
|
|
2856
3392
|
this.assertExpectedLatestVersion({
|
|
2857
3393
|
datasetId: input.dataset.id,
|
|
2858
3394
|
expectedLatestVersionId: input.expectedLatestVersionId,
|
|
2859
|
-
actualLatestVersionId: latestVersion?.versionId ?? null
|
|
3395
|
+
actualLatestVersionId: latestVersion?.versionId ?? null,
|
|
3396
|
+
operation: input.optimisticOperation ?? "commit"
|
|
2860
3397
|
});
|
|
2861
3398
|
}
|
|
2862
3399
|
const changeResult = await input.applyChanges(input.runtime, {
|
|
2863
|
-
previousRowCount: latestVersion?.rowCount
|
|
3400
|
+
previousRowCount: latestVersion?.rowCount,
|
|
3401
|
+
validatedPrimaryKeyColumns: latestVersion?.validatedPrimaryKeyColumns
|
|
2864
3402
|
});
|
|
2865
|
-
const commitId =
|
|
3403
|
+
const commitId = randomUUID6();
|
|
2866
3404
|
await this.setCommitMetadata(input, commitId, commitRowCount(input, changeResult, latestVersion));
|
|
2867
3405
|
await input.runtime.run("COMMIT");
|
|
2868
3406
|
committed = true;
|
|
@@ -2890,20 +3428,29 @@ class DuckLakeWriteCoordinator {
|
|
|
2890
3428
|
if (ownSnapshotId !== null) {
|
|
2891
3429
|
const version = await this.snapshots.getVersionForSnapshot(input.runtime, input.dataset, ownSnapshotId);
|
|
2892
3430
|
if (version) {
|
|
2893
|
-
return {
|
|
3431
|
+
return { outcome: "created", version };
|
|
2894
3432
|
}
|
|
2895
|
-
throw new
|
|
3433
|
+
throw new LakeStorageError22(`[SixbDuckLake] DuckLake committed snapshot '${ownSnapshotId}' for dataset '${input.dataset.id}', but Sixb could not hydrate it as a dataset version.`);
|
|
2896
3434
|
}
|
|
2897
3435
|
if (!input.changeResult.dataChangeExpected) {
|
|
3436
|
+
const version = await this.versionForNoOpCommit(input.runtime, input.dataset, input.allowInitialNoOp ?? false);
|
|
3437
|
+
if (input.expectedLatestVersionId !== undefined) {
|
|
3438
|
+
this.assertExpectedLatestVersion({
|
|
3439
|
+
datasetId: input.dataset.id,
|
|
3440
|
+
expectedLatestVersionId: input.expectedLatestVersionId,
|
|
3441
|
+
actualLatestVersionId: version?.versionId ?? null,
|
|
3442
|
+
operation: input.optimisticOperation ?? "commit"
|
|
3443
|
+
});
|
|
3444
|
+
}
|
|
2898
3445
|
return {
|
|
2899
|
-
|
|
2900
|
-
|
|
3446
|
+
outcome: "unchanged",
|
|
3447
|
+
version
|
|
2901
3448
|
};
|
|
2902
3449
|
}
|
|
2903
3450
|
if (input.committedWriteSnapshotId === input.previousWriteSnapshotId) {
|
|
2904
|
-
throw new
|
|
3451
|
+
throw new LakeStorageError22(`[SixbDuckLake] DuckLake commit for dataset '${input.dataset.id}' completed without producing a snapshot for this non-empty write.`);
|
|
2905
3452
|
}
|
|
2906
|
-
throw new
|
|
3453
|
+
throw new LakeStorageError22(`[SixbDuckLake] DuckLake commit for dataset '${input.dataset.id}' completed, but Sixb could not find a matching snapshot for this write.`);
|
|
2907
3454
|
}
|
|
2908
3455
|
async applyStagedRows(input) {
|
|
2909
3456
|
const mode = input.write.mode ?? "snapshot";
|
|
@@ -2916,11 +3463,12 @@ class DuckLakeWriteCoordinator {
|
|
|
2916
3463
|
previousRowCount: input.previousRowCount
|
|
2917
3464
|
});
|
|
2918
3465
|
if (result.sourceRowCount !== input.rowsWritten) {
|
|
2919
|
-
throw new
|
|
3466
|
+
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
3467
|
}
|
|
2921
3468
|
return result;
|
|
2922
3469
|
}
|
|
2923
3470
|
async setCommitMetadata(input, commitId, rowCount) {
|
|
3471
|
+
const validatedPrimaryKeyColumns = getDatasetPrimaryKeyColumns4(input.dataset);
|
|
2924
3472
|
const metadata = {
|
|
2925
3473
|
kind: "datasetVersion",
|
|
2926
3474
|
datasetId: input.dataset.id,
|
|
@@ -2928,16 +3476,20 @@ class DuckLakeWriteCoordinator {
|
|
|
2928
3476
|
mode: input.mode,
|
|
2929
3477
|
producer: input.producer ? structuredClone(input.producer) : undefined,
|
|
2930
3478
|
inputs: input.inputs ? structuredClone(input.inputs) : undefined,
|
|
2931
|
-
...rowCount.kind === "exact" ? { rowCount: rowCount.value } : {}
|
|
3479
|
+
...rowCount.kind === "exact" ? { rowCount: rowCount.value } : {},
|
|
3480
|
+
...validatedPrimaryKeyColumns !== null ? { validatedPrimaryKeyColumns: [...validatedPrimaryKeyColumns] } : {}
|
|
2932
3481
|
};
|
|
2933
3482
|
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
3483
|
}
|
|
2935
|
-
async versionForNoOpCommit(runtime, definition) {
|
|
3484
|
+
async versionForNoOpCommit(runtime, definition, allowInitialNoOp) {
|
|
2936
3485
|
const latestVersion = await this.snapshots.getLatestVersionForDefinition(runtime, definition);
|
|
2937
3486
|
if (latestVersion) {
|
|
2938
3487
|
return latestVersion;
|
|
2939
3488
|
}
|
|
2940
|
-
|
|
3489
|
+
if (allowInitialNoOp) {
|
|
3490
|
+
return null;
|
|
3491
|
+
}
|
|
3492
|
+
throw new LakeStorageError22(`[SixbDuckLake] No DuckLake changes were committed for dataset '${definition.id}', and no previous version exists.`);
|
|
2941
3493
|
}
|
|
2942
3494
|
async rollbackTransaction(runtime) {
|
|
2943
3495
|
try {
|
|
@@ -2993,7 +3545,7 @@ class DuckLakeWriteCoordinator {
|
|
|
2993
3545
|
return;
|
|
2994
3546
|
}
|
|
2995
3547
|
if (input.actualLatestVersionId !== input.expectedLatestVersionId) {
|
|
2996
|
-
throw new
|
|
3548
|
+
throw new LakeStorageError22(`[SixbDuckLake] Optimistic ${input.operation} failed for dataset '${input.datasetId}': expected latest version '${input.expectedLatestVersionId ?? "none"}', found '${input.actualLatestVersionId ?? "none"}'.`);
|
|
2997
3549
|
}
|
|
2998
3550
|
}
|
|
2999
3551
|
}
|
|
@@ -3011,7 +3563,7 @@ function commitRowCount(input, changeResult, latestVersion) {
|
|
|
3011
3563
|
}
|
|
3012
3564
|
function assertDuckLakeSnapshotId3(snapshotId) {
|
|
3013
3565
|
if (!/^\d+$/.test(snapshotId)) {
|
|
3014
|
-
throw new
|
|
3566
|
+
throw new LakeStorageError22(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`);
|
|
3015
3567
|
}
|
|
3016
3568
|
}
|
|
3017
3569
|
var localCatalogCommitLocks = new Map;
|
|
@@ -3089,6 +3641,9 @@ class DuckLakeStorage {
|
|
|
3089
3641
|
async beginWrite(input) {
|
|
3090
3642
|
return this.writes.beginWrite(input);
|
|
3091
3643
|
}
|
|
3644
|
+
async beginMerge(input) {
|
|
3645
|
+
return this.writes.beginMerge(input);
|
|
3646
|
+
}
|
|
3092
3647
|
async getLatestVersion(datasetId) {
|
|
3093
3648
|
return this.snapshotReader.getLatestVersion(datasetId);
|
|
3094
3649
|
}
|
|
@@ -3109,4 +3664,4 @@ export {
|
|
|
3109
3664
|
DuckLakeStorage
|
|
3110
3665
|
};
|
|
3111
3666
|
|
|
3112
|
-
//# debugId=
|
|
3667
|
+
//# debugId=8AEF2C0D635BEB8A64756E2164756E21
|