@sixb/ducklake 0.1.2 → 0.1.4
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 +21 -1
- package/dist/index.js +166 -47
- package/dist/index.js.map +12 -12
- package/dist/internal/dataset-row-merge.d.ts.map +1 -1
- package/dist/internal/ducklake-catalog-schema.d.ts +1 -0
- 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.map +1 -1
- package/dist/internal/ducklake-merge-session.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-sql-executor.d.ts.map +1 -1
- package/dist/internal/ducklake-write-coordinator.d.ts +2 -1
- package/dist/internal/ducklake-write-coordinator.d.ts.map +1 -1
- package/dist/internal/names.d.ts +2 -0
- package/dist/internal/names.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/internal/dataset-row-merge.ts +114 -36
- package/src/internal/ducklake-catalog-schema.ts +1 -0
- package/src/internal/ducklake-current-dataset-definitions.ts +5 -0
- package/src/internal/ducklake-dataset-catalog.ts +19 -7
- package/src/internal/ducklake-merge-session.ts +7 -0
- package/src/internal/ducklake-snapshot-reader.ts +2 -0
- package/src/internal/ducklake-sql-executor.ts +54 -11
- package/src/internal/ducklake-write-coordinator.ts +109 -34
- package/src/internal/names.ts +5 -0
- package/src/internal/versions.ts +1 -1
package/README.md
CHANGED
|
@@ -180,12 +180,28 @@ await merge.writeChanges([
|
|
|
180
180
|
const result = await merge.commit()
|
|
181
181
|
```
|
|
182
182
|
|
|
183
|
-
|
|
183
|
+
For datasets without `sequenceBy`, changes remain ordered across `writeChanges(...)` calls, and the final change for a repeated key
|
|
184
184
|
wins. Upserts are complete rows rather than patches. Commit checks the dataset version captured by
|
|
185
185
|
`beginMerge`, applies effective changes atomically, and creates a version only when visible rows
|
|
186
186
|
change. An initial no-op returns `{ outcome: "unchanged", version: null }`; a later no-op returns the
|
|
187
187
|
existing latest version.
|
|
188
188
|
|
|
189
|
+
Datasets declaring `sequenceBy` instead compare source revisions: older changes are ignored,
|
|
190
|
+
identical ties are unchanged, and conflicting ties abort the merge. Deletes require
|
|
191
|
+
`change.delete(key, { sequence })`. Source revisions and deletion tombstones are stored in a
|
|
192
|
+
companion DuckLake table in the same transaction as visible rows, surviving snapshot expiration.
|
|
193
|
+
New tombstone state creates a version even when the visible row count stays zero. Sequenced
|
|
194
|
+
provider writes use merge sessions; snapshot syncs convert rows to ordered upserts.
|
|
195
|
+
Snapshot syncs use `commit({ createInitialVersion: true })` to make a first empty result
|
|
196
|
+
addressable. Later no-op commits reuse the current version, including after a concurrent write.
|
|
197
|
+
Use `commit({ retryOnConflict: true })` to rebase retained changes, with at most 3 attempts.
|
|
198
|
+
Explicit `expectedLatestVersionId` guards are never relaxed.
|
|
199
|
+
See [source ordering](../../docs/data/datasets.md#source-ordering).
|
|
200
|
+
|
|
201
|
+
Local catalogs serialize commits within one process. PostgreSQL catalogs support concurrent
|
|
202
|
+
writer processes; known snapshot-ID conflicts are retried from the retained staging table.
|
|
203
|
+
Other provider/connection failures propagate without an automatic retry.
|
|
204
|
+
|
|
189
205
|
Snapshot, append, and SQL-transform writes to keyed datasets also enforce unique keys so a later
|
|
190
206
|
merge never starts from an ambiguous baseline. Application authors normally use
|
|
191
207
|
`defineSync(..., { mode: "merge" })`; the sync worker stages those changes through this contract.
|
|
@@ -270,6 +286,10 @@ new rows to the existing target. Omitting a source `versionId` resolves the late
|
|
|
270
286
|
committed version at execution time; providing a `versionId` pins that source for
|
|
271
287
|
repeatable transforms.
|
|
272
288
|
|
|
289
|
+
DuckDB promotes `sum(BIGINT)` results to `HUGEINT`. When the matching target column is a Sixb
|
|
290
|
+
`int64`, the executor applies a checked conversion back to `BIGINT` before comparing or writing
|
|
291
|
+
rows. Values outside the signed 64-bit range fail the transform instead of being truncated.
|
|
292
|
+
|
|
273
293
|
## Definition Updates
|
|
274
294
|
|
|
275
295
|
DuckLake supports schema evolution, and this provider exposes a small safe
|
package/dist/index.js
CHANGED
|
@@ -845,7 +845,11 @@ function incrementLocalCatalogGeneration(options) {
|
|
|
845
845
|
|
|
846
846
|
// src/internal/ducklake-dataset-catalog.ts
|
|
847
847
|
import { randomUUID } from "crypto";
|
|
848
|
-
import {
|
|
848
|
+
import {
|
|
849
|
+
assertDatasetDefinition,
|
|
850
|
+
LakeStorageError as LakeStorageError11,
|
|
851
|
+
planDatasetDefinitionUpdate
|
|
852
|
+
} from "@sixb/core/lake-storage";
|
|
849
853
|
|
|
850
854
|
// src/internal/duckdb-row.ts
|
|
851
855
|
import { LakeStorageError as LakeStorageError5 } from "@sixb/core/lake-storage";
|
|
@@ -1060,6 +1064,7 @@ function getIntegerField(record, key) {
|
|
|
1060
1064
|
|
|
1061
1065
|
// src/internal/ducklake-catalog-schema.ts
|
|
1062
1066
|
var SIXB_PRIMARY_KEY_COMMENT_NAMESPACE = "sixb:primary-key:";
|
|
1067
|
+
var DATASET_SEQUENCE_COLUMN_COMMENT = "sixb.sequenceBy.v1";
|
|
1063
1068
|
var SIXB_PRIMARY_KEY_COMMENT_PREFIX = `${SIXB_PRIMARY_KEY_COMMENT_NAMESPACE}v1:`;
|
|
1064
1069
|
var FILE_REF_STRUCT_CHILDREN = [
|
|
1065
1070
|
{ name: "blobId", type: "string" },
|
|
@@ -1241,6 +1246,9 @@ function decodeDatasetId(encoded) {
|
|
|
1241
1246
|
function encodeDatasetTableName(datasetId) {
|
|
1242
1247
|
return `${DATASET_TABLE_PREFIX}${encodeDatasetId(datasetId)}`;
|
|
1243
1248
|
}
|
|
1249
|
+
function encodeDatasetSequenceTableName(datasetId) {
|
|
1250
|
+
return `sixb__seq__${encodeDatasetTableName(datasetId)}`;
|
|
1251
|
+
}
|
|
1244
1252
|
function decodeDatasetTableName(tableName) {
|
|
1245
1253
|
if (!tableName.startsWith(DATASET_TABLE_PREFIX)) {
|
|
1246
1254
|
return null;
|
|
@@ -1264,12 +1272,14 @@ async function readCurrentDatasetDefinitions(input) {
|
|
|
1264
1272
|
for (const table of tables) {
|
|
1265
1273
|
const columns = columnsByTableName.get(table.tableName) ?? [];
|
|
1266
1274
|
const primaryKey = duckLakeCatalogColumnsToDatasetPrimaryKey(table.tableName, columns);
|
|
1275
|
+
const sequenceBy = columns.find((column) => column.comment === DATASET_SEQUENCE_COLUMN_COMMENT)?.columnName;
|
|
1267
1276
|
const partitionBy = partitionByTableName.get(table.tableName) ?? [];
|
|
1268
1277
|
definitions.set(table.datasetId, {
|
|
1269
1278
|
kind: "dataset",
|
|
1270
1279
|
id: table.datasetId,
|
|
1271
1280
|
schema: duckLakeCatalogColumnsToDatasetSchema(table.tableName, columns),
|
|
1272
1281
|
...primaryKey !== undefined ? { primaryKey } : {},
|
|
1282
|
+
...sequenceBy !== undefined ? { sequenceBy } : {},
|
|
1273
1283
|
...partitionBy.length > 0 ? { partitionBy } : {},
|
|
1274
1284
|
...table.comment !== undefined ? { description: table.comment } : {}
|
|
1275
1285
|
});
|
|
@@ -1428,6 +1438,10 @@ class DuckLakeDatasetCatalog {
|
|
|
1428
1438
|
await runtime.run(`COMMENT ON TABLE ${qualifiedName} IS ${quoteSqlString(definition.description)}`);
|
|
1429
1439
|
}
|
|
1430
1440
|
await this.applyPrimaryKey(runtime, qualifiedName, definition);
|
|
1441
|
+
if (definition.sequenceBy !== undefined) {
|
|
1442
|
+
await runtime.run(`COMMENT ON COLUMN ${qualifiedName}.${quoteIdentifier(definition.sequenceBy)} IS ${quoteSqlString(DATASET_SEQUENCE_COLUMN_COMMENT)}`);
|
|
1443
|
+
await runtime.run(`CREATE TABLE ${qualifiedTableName(this.options, encodeDatasetSequenceTableName(definition.id))} (source_key VARCHAR NOT NULL, sequence VARCHAR NOT NULL, content VARCHAR)`);
|
|
1444
|
+
}
|
|
1431
1445
|
await this.applyPartitionBy(runtime, qualifiedName, definition);
|
|
1432
1446
|
await runtime.run("COMMIT");
|
|
1433
1447
|
committed = true;
|
|
@@ -1512,9 +1526,7 @@ ${details}`);
|
|
|
1512
1526
|
return duckLakeCatalogColumnsToDatasetSchema(tableName, columns);
|
|
1513
1527
|
}
|
|
1514
1528
|
assertSchema(definition) {
|
|
1515
|
-
|
|
1516
|
-
throw new LakeStorageError11(`[SixbDuckLake] Dataset '${definition.id}' requires a schema for DuckLake storage.`);
|
|
1517
|
-
}
|
|
1529
|
+
assertDatasetDefinition(definition, (message) => new LakeStorageError11(`[SixbDuckLake] ${message}`));
|
|
1518
1530
|
}
|
|
1519
1531
|
async applyExistingDatasetUpdate(plan) {
|
|
1520
1532
|
const tableName = encodeDatasetTableName(plan.definition.id);
|
|
@@ -1765,7 +1777,7 @@ function isRecord(value) {
|
|
|
1765
1777
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1766
1778
|
}
|
|
1767
1779
|
function isDatasetProducer(value) {
|
|
1768
|
-
return isRecord(value) && (value.kind === "sync" || value.kind === "pipeline") && optionalString(value.id) && optionalString(value.runId);
|
|
1780
|
+
return isRecord(value) && (value.kind === "sync" || value.kind === "pipeline" || value.kind === "ingest") && optionalString(value.id) && optionalString(value.runId);
|
|
1769
1781
|
}
|
|
1770
1782
|
function isDatasetVersionRefs(value) {
|
|
1771
1783
|
return Array.isArray(value) && value.every((item) => isRecord(item) && typeof item.datasetId === "string" && typeof item.versionId === "string");
|
|
@@ -2126,6 +2138,7 @@ class DuckLakeSnapshotReader {
|
|
|
2126
2138
|
return {
|
|
2127
2139
|
datasetId: tableRef.datasetId,
|
|
2128
2140
|
versionId: toVersionId(row.snapshotId),
|
|
2141
|
+
inputs: row.metadata?.inputs,
|
|
2129
2142
|
...row.metadata?.rowCount !== undefined ? { rowCount: row.metadata.rowCount } : {},
|
|
2130
2143
|
...row.metadata?.validatedPrimaryKeyColumns !== undefined ? { validatedPrimaryKeyColumns: row.metadata.validatedPrimaryKeyColumns } : {}
|
|
2131
2144
|
};
|
|
@@ -2636,7 +2649,7 @@ class DuckLakeSqlExecutor {
|
|
|
2636
2649
|
sources: sources.relations,
|
|
2637
2650
|
sql: input.sql
|
|
2638
2651
|
});
|
|
2639
|
-
await this.assertResultSchemaMatchesTarget(runtime, target, sql);
|
|
2652
|
+
const checkedCasts = await this.assertResultSchemaMatchesTarget(runtime, target, sql);
|
|
2640
2653
|
return this.writes.commitDatasetVersionOnExclusiveRuntime(runtime, {
|
|
2641
2654
|
dataset: target,
|
|
2642
2655
|
mode: input.mode,
|
|
@@ -2649,6 +2662,7 @@ class DuckLakeSqlExecutor {
|
|
|
2649
2662
|
target,
|
|
2650
2663
|
mode: input.mode,
|
|
2651
2664
|
sql,
|
|
2665
|
+
checkedCasts,
|
|
2652
2666
|
previousRowCount: context.previousRowCount
|
|
2653
2667
|
})
|
|
2654
2668
|
});
|
|
@@ -2708,6 +2722,7 @@ class DuckLakeSqlExecutor {
|
|
|
2708
2722
|
const rows = await runtime.query(`DESCRIBE SELECT * FROM (${sql}) AS sixb_sql_transform_result_schema`);
|
|
2709
2723
|
const actualColumns = rows.map((row) => resultColumnFromDescribeRow(row));
|
|
2710
2724
|
const expectedColumns = target.schema.columns;
|
|
2725
|
+
const checkedCasts = [];
|
|
2711
2726
|
if (actualColumns.length !== expectedColumns.length) {
|
|
2712
2727
|
throwResultSchemaMismatch(target.id, `expected ${expectedColumns.length} columns, got ${actualColumns.length}`);
|
|
2713
2728
|
}
|
|
@@ -2717,10 +2732,20 @@ class DuckLakeSqlExecutor {
|
|
|
2717
2732
|
if (actual.name !== expected.name) {
|
|
2718
2733
|
throwResultSchemaMismatch(target.id, `column ${index + 1} should be '${expected.name}', got '${actual.name}'`);
|
|
2719
2734
|
}
|
|
2720
|
-
|
|
2721
|
-
|
|
2735
|
+
const actualDatasetType = findDatasetColumnTypeForDuckDbSql(actual.typeSql);
|
|
2736
|
+
if (actualDatasetType === expected.type) {
|
|
2737
|
+
continue;
|
|
2738
|
+
}
|
|
2739
|
+
if (isCheckedInt64Cast(actual.typeSql, expected.type)) {
|
|
2740
|
+
checkedCasts.push({
|
|
2741
|
+
columnName: expected.name,
|
|
2742
|
+
targetTypeSql: datasetColumnTypeToDuckDbSql(expected.type)
|
|
2743
|
+
});
|
|
2744
|
+
continue;
|
|
2722
2745
|
}
|
|
2746
|
+
throwResultSchemaMismatch(target.id, `column '${expected.name}' should have type '${expected.type}', got '${actualDatasetType ?? actual.typeSql}'`);
|
|
2723
2747
|
}
|
|
2748
|
+
return checkedCasts;
|
|
2724
2749
|
}
|
|
2725
2750
|
async applySqlTransform(input) {
|
|
2726
2751
|
const tempTableName = `sixb_sql_transform_${randomUUID3().replaceAll("-", "")}`;
|
|
@@ -2728,6 +2753,9 @@ class DuckLakeSqlExecutor {
|
|
|
2728
2753
|
await input.runtime.run(`CREATE TEMP TABLE ${tempTable} AS SELECT * FROM (${input.sql}) AS sixb_sql_transform_result`);
|
|
2729
2754
|
let result;
|
|
2730
2755
|
try {
|
|
2756
|
+
for (const checkedCast of input.checkedCasts) {
|
|
2757
|
+
await input.runtime.run(`ALTER TABLE ${tempTable} ALTER COLUMN ${quoteIdentifier(checkedCast.columnName)} SET DATA TYPE ${checkedCast.targetTypeSql}`);
|
|
2758
|
+
}
|
|
2731
2759
|
result = await applyDatasetRowsFromRelation({
|
|
2732
2760
|
options: this.options,
|
|
2733
2761
|
runtime: input.runtime,
|
|
@@ -2757,8 +2785,12 @@ function buildPreviewSql(sql, limit) {
|
|
|
2757
2785
|
}
|
|
2758
2786
|
function resultColumnFromDescribeRow(row) {
|
|
2759
2787
|
const name = getString(row, "column_name");
|
|
2760
|
-
const
|
|
2761
|
-
return { name,
|
|
2788
|
+
const typeSql = getString(row, "column_type");
|
|
2789
|
+
return { name, typeSql };
|
|
2790
|
+
}
|
|
2791
|
+
function isCheckedInt64Cast(actualTypeSql, expectedType) {
|
|
2792
|
+
const normalizedType = actualTypeSql.trim().toUpperCase();
|
|
2793
|
+
return expectedType === "int64" && (normalizedType === "HUGEINT" || normalizedType === "INT128");
|
|
2762
2794
|
}
|
|
2763
2795
|
function normalizePreviewRow(row) {
|
|
2764
2796
|
const normalized = {};
|
|
@@ -2795,10 +2827,21 @@ function throwResultSchemaMismatch(datasetId, detail) {
|
|
|
2795
2827
|
|
|
2796
2828
|
// src/internal/ducklake-write-coordinator.ts
|
|
2797
2829
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
2798
|
-
import {
|
|
2830
|
+
import {
|
|
2831
|
+
assertUnsequencedDatasetWrite,
|
|
2832
|
+
getDatasetPrimaryKeyColumns as getDatasetPrimaryKeyColumns4,
|
|
2833
|
+
hasDatasetInputChanges,
|
|
2834
|
+
LakeConcurrencyError,
|
|
2835
|
+
LakeStorageError as LakeStorageError22,
|
|
2836
|
+
retryDatasetMergeCommit
|
|
2837
|
+
} from "@sixb/core/lake-storage";
|
|
2799
2838
|
|
|
2800
2839
|
// src/internal/dataset-row-merge.ts
|
|
2801
|
-
import {
|
|
2840
|
+
import {
|
|
2841
|
+
getDatasetPrimaryKeyColumns as getDatasetPrimaryKeyColumns2,
|
|
2842
|
+
LakeStorageError as LakeStorageError19,
|
|
2843
|
+
reconcileDatasetSequences
|
|
2844
|
+
} from "@sixb/core/lake-storage";
|
|
2802
2845
|
async function applyDatasetMergeFromRelation(input) {
|
|
2803
2846
|
const primaryKeyColumns = getDatasetPrimaryKeyColumns2(input.dataset);
|
|
2804
2847
|
if (primaryKeyColumns === null) {
|
|
@@ -2808,12 +2851,12 @@ async function applyDatasetMergeFromRelation(input) {
|
|
|
2808
2851
|
const targetTable = qualifiedTableName(input.options, encodeDatasetTableName(input.dataset.id));
|
|
2809
2852
|
const sourceRowCount = await countRows2(input.runtime, stagingTable);
|
|
2810
2853
|
const previousRowCount = await currentBaselineRowCount(input, targetTable, primaryKeyColumns);
|
|
2811
|
-
return withMaterializedFinalChanges(input, primaryKeyColumns, async (finalChanges) => {
|
|
2854
|
+
return withMaterializedFinalChanges(input, primaryKeyColumns, async (finalChanges, orderingChanged) => {
|
|
2812
2855
|
const effects = await countMergeEffects(input, targetTable, finalChanges, primaryKeyColumns);
|
|
2813
2856
|
const effectiveChangeCount = effects.inserts + effects.updates + effects.deletes;
|
|
2814
2857
|
if (effectiveChangeCount === 0) {
|
|
2815
2858
|
return {
|
|
2816
|
-
dataChangeExpected:
|
|
2859
|
+
dataChangeExpected: orderingChanged,
|
|
2817
2860
|
sourceRowCount,
|
|
2818
2861
|
resultingRowCount: { kind: "exact", value: previousRowCount }
|
|
2819
2862
|
};
|
|
@@ -2858,6 +2901,48 @@ async function applyDatasetMergeFromRelation(input) {
|
|
|
2858
2901
|
};
|
|
2859
2902
|
});
|
|
2860
2903
|
}
|
|
2904
|
+
async function applySourceOrdering(input, acceptedTable) {
|
|
2905
|
+
const source = quoteIdentifier(`${input.sequenceColumnName}_source`);
|
|
2906
|
+
const ordinal = quoteIdentifier(input.sequenceColumnName);
|
|
2907
|
+
const staging = quoteIdentifier(input.stagingTableName);
|
|
2908
|
+
const stateTable = qualifiedTableName(input.options, encodeDatasetSequenceTableName(input.dataset.id));
|
|
2909
|
+
await input.runtime.run(`CREATE TEMP TABLE ${acceptedTable} (source_key VARCHAR, ordinal UBIGINT)`);
|
|
2910
|
+
let lastOrdinal = -1n;
|
|
2911
|
+
let changed = false;
|
|
2912
|
+
while (true) {
|
|
2913
|
+
const staged = await input.runtime.query(`SELECT ${ordinal} AS ordinal, ${source} AS source FROM ${staging}
|
|
2914
|
+
WHERE ${ordinal} > ${lastOrdinal} ORDER BY ${ordinal} LIMIT 1000`);
|
|
2915
|
+
if (staged.length === 0)
|
|
2916
|
+
return changed;
|
|
2917
|
+
const changes = staged.map((row) => JSON.parse(getString(row, "source")));
|
|
2918
|
+
lastOrdinal = getBigIntLike(staged[staged.length - 1], "ordinal");
|
|
2919
|
+
const batchKeys = changes.map((change) => quoteSqlString(change.key)).join(",");
|
|
2920
|
+
const current = await input.runtime.query(`SELECT * FROM ${stateTable} WHERE source_key IN (${batchKeys})`);
|
|
2921
|
+
const previous = new Map;
|
|
2922
|
+
for (const row of current) {
|
|
2923
|
+
previous.set(getString(row, "source_key"), {
|
|
2924
|
+
sequence: getString(row, "sequence"),
|
|
2925
|
+
content: getOptionalString(row, "content") ?? null
|
|
2926
|
+
});
|
|
2927
|
+
}
|
|
2928
|
+
const { states, accepted } = reconcileDatasetSequences(input.dataset, previous, changes);
|
|
2929
|
+
if (accepted.size === 0)
|
|
2930
|
+
continue;
|
|
2931
|
+
changed = true;
|
|
2932
|
+
const stateValues = [];
|
|
2933
|
+
const acceptedValues = [];
|
|
2934
|
+
for (const [key, index] of accepted) {
|
|
2935
|
+
const state = states.get(key);
|
|
2936
|
+
stateValues.push(`(${quoteSqlString(key)}, ${quoteSqlString(state.sequence)}, ${state.content === null ? "NULL" : quoteSqlString(state.content)})`);
|
|
2937
|
+
acceptedValues.push(`(${quoteSqlString(key)}, ${getBigIntLike(staged[index], "ordinal")})`);
|
|
2938
|
+
}
|
|
2939
|
+
const keys = [...accepted.keys()].map(quoteSqlString).join(",");
|
|
2940
|
+
await input.runtime.run(`DELETE FROM ${stateTable} WHERE source_key IN (${keys})`);
|
|
2941
|
+
await input.runtime.run(`INSERT INTO ${stateTable} VALUES ${stateValues.join(",")}`);
|
|
2942
|
+
await input.runtime.run(`DELETE FROM ${acceptedTable} WHERE source_key IN (${keys})`);
|
|
2943
|
+
await input.runtime.run(`INSERT INTO ${acceptedTable} VALUES ${acceptedValues.join(",")}`);
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2861
2946
|
async function currentBaselineRowCount(input, targetTable, primaryKeyColumns) {
|
|
2862
2947
|
if (sameColumns(input.validatedPrimaryKeyColumns, primaryKeyColumns)) {
|
|
2863
2948
|
return input.previousRowCount ?? countRows2(input.runtime, targetTable);
|
|
@@ -2871,18 +2956,23 @@ async function currentBaselineRowCount(input, targetTable, primaryKeyColumns) {
|
|
|
2871
2956
|
}
|
|
2872
2957
|
async function withMaterializedFinalChanges(input, primaryKeyColumns, run) {
|
|
2873
2958
|
const table = quoteIdentifier(`${input.stagingTableName}_final`);
|
|
2874
|
-
|
|
2875
|
-
CREATE TEMP TABLE ${table} AS
|
|
2876
|
-
${finalChangesSelectSql(input, primaryKeyColumns)}
|
|
2877
|
-
`);
|
|
2959
|
+
const acceptedTable = quoteIdentifier(`${input.stagingTableName}_accepted`);
|
|
2878
2960
|
let outcome;
|
|
2879
2961
|
try {
|
|
2880
|
-
|
|
2962
|
+
const sequenced = input.dataset.sequenceBy !== undefined;
|
|
2963
|
+
const orderingChanged = sequenced ? await applySourceOrdering(input, acceptedTable) : false;
|
|
2964
|
+
const ordinal = quoteIdentifier(input.sequenceColumnName);
|
|
2965
|
+
const selectSql = sequenced ? `SELECT source.*, source.${ordinal} AS ${quoteIdentifier(firstSequenceName(input.sequenceColumnName))}
|
|
2966
|
+
FROM ${quoteIdentifier(input.stagingTableName)} AS source
|
|
2967
|
+
JOIN ${acceptedTable} AS accepted ON source.${ordinal} = accepted.ordinal` : finalChangesSelectSql(input, primaryKeyColumns);
|
|
2968
|
+
await input.runtime.run(`CREATE TEMP TABLE ${table} AS ${selectSql}`);
|
|
2969
|
+
outcome = { kind: "success", value: await run(table, orderingChanged) };
|
|
2881
2970
|
} catch (error) {
|
|
2882
2971
|
outcome = { kind: "error", error };
|
|
2883
2972
|
}
|
|
2884
2973
|
try {
|
|
2885
2974
|
await input.runtime.run(`DROP TABLE IF EXISTS ${table}`);
|
|
2975
|
+
await input.runtime.run(`DROP TABLE IF EXISTS ${acceptedTable}`);
|
|
2886
2976
|
} catch (cleanupError) {
|
|
2887
2977
|
if (outcome.kind === "success") {
|
|
2888
2978
|
throw cleanupError;
|
|
@@ -2968,6 +3058,7 @@ function sameColumns(left, right) {
|
|
|
2968
3058
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
2969
3059
|
import {
|
|
2970
3060
|
cloneDatasetMergeChange,
|
|
3061
|
+
datasetSequenceChange,
|
|
2971
3062
|
getDatasetMergeChangeValidationError,
|
|
2972
3063
|
LakeStorageError as LakeStorageError20
|
|
2973
3064
|
} from "@sixb/core/lake-storage";
|
|
@@ -3036,6 +3127,7 @@ async function createDuckLakeMergeSession(input) {
|
|
|
3036
3127
|
CREATE TEMP TABLE ${quoteIdentifier(stagingTableName)} (
|
|
3037
3128
|
${quoteIdentifier(sequenceColumnName)} UBIGINT NOT NULL,
|
|
3038
3129
|
${quoteIdentifier(kindColumnName)} VARCHAR NOT NULL,
|
|
3130
|
+
${input.merge.dataset.sequenceBy === undefined ? "" : `${quoteIdentifier(`${sequenceColumnName}_source`)} VARCHAR NOT NULL,`}
|
|
3039
3131
|
${datasetSchemaToDuckDbNullableColumnsSql(input.merge.dataset.schema)}
|
|
3040
3132
|
)
|
|
3041
3133
|
`);
|
|
@@ -3091,6 +3183,9 @@ class DuckLakeMergeSession {
|
|
|
3091
3183
|
for (const staged of batch) {
|
|
3092
3184
|
appender.appendBigInt(staged.sequence);
|
|
3093
3185
|
appender.appendVarchar(staged.change.kind);
|
|
3186
|
+
if (this.input.dataset.sequenceBy !== undefined) {
|
|
3187
|
+
appender.appendVarchar(JSON.stringify(datasetSequenceChange(this.input.dataset, staged.change)));
|
|
3188
|
+
}
|
|
3094
3189
|
appendDatasetRow(appender, this.input.dataset.schema, staged.change.kind === "upsert" ? staged.change.row : staged.change.key);
|
|
3095
3190
|
}
|
|
3096
3191
|
});
|
|
@@ -3279,6 +3374,7 @@ class DuckLakeWriteCoordinator {
|
|
|
3279
3374
|
throw new LakeStorageError22(`[SixbDuckLake] Unknown dataset '${input.dataset.id}'.`);
|
|
3280
3375
|
}
|
|
3281
3376
|
this.datasets.assertSchema(definition);
|
|
3377
|
+
assertUnsequencedDatasetWrite(definition);
|
|
3282
3378
|
const mode = input.mode ?? "snapshot";
|
|
3283
3379
|
assertDatasetWriteMode(mode, "write");
|
|
3284
3380
|
const runtime = await this.connections.stagingRuntime();
|
|
@@ -3304,7 +3400,7 @@ class DuckLakeWriteCoordinator {
|
|
|
3304
3400
|
this.datasets.assertSchema(definition);
|
|
3305
3401
|
const latestVersion = await this.connections.withAttachedRuntime((runtime2) => this.snapshots.getLatestVersionForDefinition(runtime2, definition));
|
|
3306
3402
|
if (input.expectedLatestVersionId !== undefined && latestVersion?.versionId !== input.expectedLatestVersionId) {
|
|
3307
|
-
throw new
|
|
3403
|
+
throw new LakeConcurrencyError(`[SixbDuckLake] Optimistic merge start failed for dataset '${definition.id}': expected latest version '${input.expectedLatestVersionId}', found '${latestVersion?.versionId ?? "none"}'.`);
|
|
3308
3404
|
}
|
|
3309
3405
|
const runtime = await this.connections.stagingRuntime();
|
|
3310
3406
|
return createDuckLakeMergeSession({
|
|
@@ -3343,32 +3439,37 @@ class DuckLakeWriteCoordinator {
|
|
|
3343
3439
|
if (getDatasetPrimaryKeyColumns4(definition) === null) {
|
|
3344
3440
|
throw new LakeStorageError22(`[SixbDuckLake] Dataset '${definition.id}' must define a primaryKey before it can be merged.`);
|
|
3345
3441
|
}
|
|
3346
|
-
return
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3442
|
+
return retryDatasetMergeCommit(input.merge, input.commit, (rebase) => this.withCommitRuntime(async (runtime) => {
|
|
3443
|
+
input.commit?.signal?.throwIfAborted();
|
|
3444
|
+
const baseVersionId = rebase ? (await this.snapshots.getLatestVersionForDefinition(runtime, definition))?.versionId ?? null : input.baseVersionId;
|
|
3445
|
+
return this.commitVersionOutcomeOnExclusiveRuntime(runtime, {
|
|
3446
|
+
dataset: definition,
|
|
3447
|
+
mode: "merge",
|
|
3448
|
+
expectedLatestVersionId: baseVersionId,
|
|
3449
|
+
allowInitialNoOp: !input.commit?.createInitialVersion,
|
|
3450
|
+
signal: input.commit?.signal,
|
|
3451
|
+
disableRetries: true,
|
|
3452
|
+
optimisticOperation: "merge commit",
|
|
3453
|
+
commitMessage: input.commit?.commitMessage ?? `merge dataset ${definition.id}`,
|
|
3454
|
+
producer: input.merge.producer,
|
|
3455
|
+
inputs: input.merge.inputs,
|
|
3456
|
+
applyChanges: async (commitRuntime, context) => {
|
|
3457
|
+
const result = await applyDatasetMergeFromRelation({
|
|
3458
|
+
options: this.options,
|
|
3459
|
+
runtime: commitRuntime,
|
|
3460
|
+
dataset: definition,
|
|
3461
|
+
stagingTableName: input.stagingTableName,
|
|
3462
|
+
sequenceColumnName: input.sequenceColumnName,
|
|
3463
|
+
kindColumnName: input.kindColumnName,
|
|
3464
|
+
previousRowCount: context.previousRowCount,
|
|
3465
|
+
validatedPrimaryKeyColumns: context.validatedPrimaryKeyColumns
|
|
3466
|
+
});
|
|
3467
|
+
if (result.sourceRowCount !== input.changesWritten) {
|
|
3468
|
+
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.`);
|
|
3469
|
+
}
|
|
3470
|
+
return result;
|
|
3369
3471
|
}
|
|
3370
|
-
|
|
3371
|
-
}
|
|
3472
|
+
});
|
|
3372
3473
|
}));
|
|
3373
3474
|
}
|
|
3374
3475
|
async commitDatasetVersion(input) {
|
|
@@ -3380,6 +3481,7 @@ class DuckLakeWriteCoordinator {
|
|
|
3380
3481
|
});
|
|
3381
3482
|
}
|
|
3382
3483
|
async commitDatasetVersionOnExclusiveRuntime(runtime, input) {
|
|
3484
|
+
assertUnsequencedDatasetWrite(input.dataset);
|
|
3383
3485
|
const result = await this.commitVersionOutcomeOnExclusiveRuntime(runtime, {
|
|
3384
3486
|
...input,
|
|
3385
3487
|
disableRetries: input.mode === "append" && getDatasetPrimaryKeyColumns4(input.dataset) !== null
|
|
@@ -3432,6 +3534,8 @@ class DuckLakeWriteCoordinator {
|
|
|
3432
3534
|
previousRowCount: latestVersion?.rowCount,
|
|
3433
3535
|
validatedPrimaryKeyColumns: latestVersion?.validatedPrimaryKeyColumns
|
|
3434
3536
|
});
|
|
3537
|
+
input.signal?.throwIfAborted();
|
|
3538
|
+
await this.ensureNoOpHasSnapshot(input, changeResult, latestVersion);
|
|
3435
3539
|
const commitId = randomUUID6();
|
|
3436
3540
|
await this.setCommitMetadata(input, commitId, commitRowCount(input, changeResult, latestVersion));
|
|
3437
3541
|
await input.runtime.run("COMMIT");
|
|
@@ -3451,6 +3555,9 @@ class DuckLakeWriteCoordinator {
|
|
|
3451
3555
|
} catch (error) {
|
|
3452
3556
|
if (!committed) {
|
|
3453
3557
|
await this.rollbackTransaction(input.runtime);
|
|
3558
|
+
if (error instanceof Error && error.message.startsWith("TransactionContext Error: Failed to commit: Failed to commit DuckLake transaction.") && error.message.includes("Exceeded the maximum retry count of 0") && error.message.includes('duplicate key value violates unique constraint "ducklake_snapshot_pkey"')) {
|
|
3559
|
+
throw new LakeConcurrencyError(`[SixbDuckLake] Concurrent catalog commit for dataset '${input.dataset.id}'.`, { cause: error });
|
|
3560
|
+
}
|
|
3454
3561
|
}
|
|
3455
3562
|
throw error;
|
|
3456
3563
|
}
|
|
@@ -3513,6 +3620,18 @@ class DuckLakeWriteCoordinator {
|
|
|
3513
3620
|
};
|
|
3514
3621
|
await input.runtime.run(`CALL ${quoteIdentifier(duckLakeAlias(this.options))}.set_commit_message(${quoteSqlString("Sixb")}, ${quoteSqlString(input.commitMessage)}, extra_info => ${quoteSqlString(JSON.stringify({ sixb: metadata }))})`);
|
|
3515
3622
|
}
|
|
3623
|
+
async ensureNoOpHasSnapshot(input, changeResult, knownLatestVersion) {
|
|
3624
|
+
if (changeResult.dataChangeExpected) {
|
|
3625
|
+
return;
|
|
3626
|
+
}
|
|
3627
|
+
const latestVersion = knownLatestVersion ?? await this.snapshots.getLatestVersionSummaryForDefinition(input.runtime, input.dataset);
|
|
3628
|
+
if (latestVersion ? input.mode === "merge" || !hasDatasetInputChanges(latestVersion.inputs, input.inputs) : input.allowInitialNoOp) {
|
|
3629
|
+
return;
|
|
3630
|
+
}
|
|
3631
|
+
const table = qualifiedTableName(this.options, encodeDatasetTableName(input.dataset.id));
|
|
3632
|
+
const descriptionSql = input.dataset.description === undefined ? "NULL" : quoteSqlString(input.dataset.description);
|
|
3633
|
+
await input.runtime.run(`COMMENT ON TABLE ${table} IS ${descriptionSql}`);
|
|
3634
|
+
}
|
|
3516
3635
|
async versionForNoOpCommit(runtime, definition, allowInitialNoOp) {
|
|
3517
3636
|
const latestVersion = await this.snapshots.getLatestVersionForDefinition(runtime, definition);
|
|
3518
3637
|
if (latestVersion) {
|
|
@@ -3577,7 +3696,7 @@ class DuckLakeWriteCoordinator {
|
|
|
3577
3696
|
return;
|
|
3578
3697
|
}
|
|
3579
3698
|
if (input.actualLatestVersionId !== input.expectedLatestVersionId) {
|
|
3580
|
-
throw new
|
|
3699
|
+
throw new LakeConcurrencyError(`[SixbDuckLake] Optimistic ${input.operation} failed for dataset '${input.datasetId}': expected latest version '${input.expectedLatestVersionId ?? "none"}', found '${input.actualLatestVersionId ?? "none"}'.`);
|
|
3581
3700
|
}
|
|
3582
3701
|
}
|
|
3583
3702
|
}
|
|
@@ -3696,4 +3815,4 @@ export {
|
|
|
3696
3815
|
DuckLakeStorage
|
|
3697
3816
|
};
|
|
3698
3817
|
|
|
3699
|
-
//# debugId=
|
|
3818
|
+
//# debugId=2A2E6355256FAE5864756E2164756E21
|