@sixb/ducklake 0.2.0 → 0.2.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 +20 -2
- package/dist/index.js +160 -18
- package/dist/index.js.map +10 -9
- package/dist/internal/duckdb-extensions.d.ts +15 -0
- package/dist/internal/duckdb-extensions.d.ts.map +1 -0
- package/dist/internal/duckdb-runtime.d.ts.map +1 -1
- package/dist/internal/ducklake-change-reader.d.ts.map +1 -1
- package/dist/internal/ducklake-snapshot-reader.d.ts +11 -0
- package/dist/internal/ducklake-snapshot-reader.d.ts.map +1 -1
- package/dist/internal/ducklake-write-coordinator.d.ts +7 -6
- package/dist/internal/ducklake-write-coordinator.d.ts.map +1 -1
- package/dist/internal/versions.d.ts +4 -0
- package/dist/internal/versions.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/internal/dataset-row-merge.ts +9 -2
- package/src/internal/duckdb-extensions.ts +102 -0
- package/src/internal/duckdb-runtime.ts +5 -1
- package/src/internal/ducklake-change-reader.ts +30 -0
- package/src/internal/ducklake-snapshot-reader.ts +75 -0
- package/src/internal/ducklake-write-coordinator.ts +27 -13
- package/src/internal/versions.ts +7 -0
package/README.md
CHANGED
|
@@ -65,6 +65,18 @@ const lakeStorage = new DuckLakeStorage({
|
|
|
65
65
|
Use `close()` during service shutdown so DuckDB can close its runtime and release
|
|
66
66
|
catalog connections cleanly.
|
|
67
67
|
|
|
68
|
+
### Extensions
|
|
69
|
+
|
|
70
|
+
On first use, the provider installs DuckLake and the extensions its catalog,
|
|
71
|
+
data path and secrets need. A missing extension is downloaded over HTTPS from
|
|
72
|
+
`https://extensions.duckdb.org` into DuckDB's extension directory
|
|
73
|
+
(`duckdb.config.extension_directory`, `~/.duckdb/extensions` by default), so a
|
|
74
|
+
host that allows only that HTTPS domain, such as a restricted Vercel sandbox,
|
|
75
|
+
can open a lake. When that download fails, the provider logs a warning and
|
|
76
|
+
DuckDB's own `INSTALL` downloads the extension over plain HTTP, as before.
|
|
77
|
+
With `custom_extension_repository` set, DuckDB installs from that repository
|
|
78
|
+
alone.
|
|
79
|
+
|
|
68
80
|
### Maintenance
|
|
69
81
|
|
|
70
82
|
DuckLake keeps historical snapshots and deleted files until maintenance runs.
|
|
@@ -102,6 +114,10 @@ budget and PostgreSQL metadata pool. Metadata and writes remain available betwee
|
|
|
102
114
|
Each read stays pinned to one immutable snapshot. Maintenance waits until no reads remain; it
|
|
103
115
|
blocks new reads only while running, so continuous traffic can postpone it.
|
|
104
116
|
|
|
117
|
+
`readChanges` compares two versions in DuckDB. When no version between them changed rows
|
|
118
|
+
(`rowsChanged` in the commit metadata, read from at most 32 snapshots), it returns an empty change
|
|
119
|
+
set without reading the table.
|
|
120
|
+
|
|
105
121
|
`maxStreamingReads` limits native streaming readers per provider (default: `4`); extra reads use
|
|
106
122
|
bounded pages on the main connection. It does not limit pipeline inputs. Raise it when memory and
|
|
107
123
|
catalog capacity allow. Finish the iterator, use `for await` with `break`, or pass `signal` to cancel
|
|
@@ -190,8 +206,10 @@ Datasets declaring `sequenceBy` instead compare source revisions: older changes
|
|
|
190
206
|
identical ties are unchanged, and conflicting ties abort the merge. Deletes require
|
|
191
207
|
`change.delete(key, { sequence })`. Source revisions and deletion tombstones are stored in a
|
|
192
208
|
companion DuckLake table in the same transaction as visible rows, surviving snapshot expiration.
|
|
193
|
-
New tombstone state creates a version even when the visible
|
|
194
|
-
|
|
209
|
+
New tombstone state creates a version even when the visible rows stay the same; such a version
|
|
210
|
+
records `rowsChanged: false`. So does a newer upsert that differs from the stored row only in its
|
|
211
|
+
`sequenceBy` column: the row is kept and only its source revision advances. Sequenced provider
|
|
212
|
+
writes use merge sessions; snapshot syncs convert rows to ordered upserts.
|
|
195
213
|
Snapshot syncs use `commit({ createInitialVersion: true })` to make a first empty result
|
|
196
214
|
addressable. Later no-op commits reuse the current version, including after a concurrent write.
|
|
197
215
|
Use `commit({ retryOnConflict: true })` to rebase retained changes, with at most 3 attempts.
|
package/dist/index.js
CHANGED
|
@@ -647,6 +647,7 @@ function parseCommitMetadata(value) {
|
|
|
647
647
|
const producer = parseDatasetProducer(parsed.sixb.producer);
|
|
648
648
|
const rowCount = parsed.sixb.rowCount;
|
|
649
649
|
const validatedPrimaryKeyColumns = parsed.sixb.validatedPrimaryKeyColumns;
|
|
650
|
+
const { rowsChanged, lineageChanged } = parsed.sixb;
|
|
650
651
|
return {
|
|
651
652
|
kind: "datasetVersion",
|
|
652
653
|
datasetId: parsed.sixb.datasetId,
|
|
@@ -657,7 +658,9 @@ function parseCommitMetadata(value) {
|
|
|
657
658
|
...typeof commitId === "string" && isRowCount(rowCount) ? { rowCount } : {},
|
|
658
659
|
...typeof commitId === "string" && isValidatedPrimaryKeyColumns(validatedPrimaryKeyColumns) ? { validatedPrimaryKeyColumns } : {},
|
|
659
660
|
...isSchemaChangeMetadata(parsed.sixb.schemaChange) ? { schemaChange: parsed.sixb.schemaChange } : {},
|
|
660
|
-
...parsed.sixb.lineage === undefined ? {} : { lineage: parsed.sixb.lineage }
|
|
661
|
+
...parsed.sixb.lineage === undefined ? {} : { lineage: parsed.sixb.lineage },
|
|
662
|
+
...typeof rowsChanged === "boolean" ? { rowsChanged } : {},
|
|
663
|
+
...typeof lineageChanged === "boolean" ? { lineageChanged } : {}
|
|
661
664
|
};
|
|
662
665
|
}
|
|
663
666
|
function changesTouchTables(changesMade, tableIds) {
|
|
@@ -729,6 +732,7 @@ class DuckLakeChangeReader {
|
|
|
729
732
|
const columns = resolveDatasetChangeColumns(input, before, after);
|
|
730
733
|
if (!columns)
|
|
731
734
|
return null;
|
|
735
|
+
const unchanged = (input.toVersionId === input.fromVersionId || !after.rowsChanged) && await this.snapshots.holdsRowsOf(input.datasetId, input.toVersionId, input.fromVersionId);
|
|
732
736
|
const lease = await this.connections.acquireAttachedRuntime();
|
|
733
737
|
const runtime = lease.runtime;
|
|
734
738
|
const temporary = quoteIdentifier(`sixb_delta_${randomUUID().replaceAll("-", "")}`);
|
|
@@ -741,6 +745,9 @@ class DuckLakeChangeReader {
|
|
|
741
745
|
const table = qualifiedTableName(this.options, ref.tableName);
|
|
742
746
|
const oldRelation = `${table} AT (VERSION => ${parseVersionId(input.fromVersionId)})`;
|
|
743
747
|
const newRelation = `${table} AT (VERSION => ${parseVersionId(input.toVersionId)})`;
|
|
748
|
+
if (unchanged) {
|
|
749
|
+
return noChanges(after.rowCount ?? before.rowCount ?? await countRows(runtime, newRelation));
|
|
750
|
+
}
|
|
744
751
|
const previousCount = await uniqueRowCount(runtime, oldRelation, input.keyColumns);
|
|
745
752
|
const nextCount = await uniqueRowCount(runtime, newRelation, input.keyColumns);
|
|
746
753
|
if (previousCount === null || nextCount === null)
|
|
@@ -806,6 +813,21 @@ class DuckLakeChangeReader {
|
|
|
806
813
|
}
|
|
807
814
|
}
|
|
808
815
|
}
|
|
816
|
+
function noChanges(rowCount) {
|
|
817
|
+
return {
|
|
818
|
+
fromRowCount: rowCount,
|
|
819
|
+
toRowCount: rowCount,
|
|
820
|
+
changeCount: 0,
|
|
821
|
+
changes: async function* () {
|
|
822
|
+
yield* [];
|
|
823
|
+
}(),
|
|
824
|
+
async close() {}
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
async function countRows(runtime, relation) {
|
|
828
|
+
const [row] = await runtime.query(`SELECT count(*) AS total FROM ${relation}`);
|
|
829
|
+
return safeCount(row, "total");
|
|
830
|
+
}
|
|
809
831
|
async function uniqueRowCount(runtime, relation, keys) {
|
|
810
832
|
const names = keys.map(quoteIdentifier);
|
|
811
833
|
const invalid = names.map((name) => `${name} IS NULL OR trim(${name}) = ''`).join(" OR ");
|
|
@@ -849,13 +871,78 @@ function localCatalogCoordinationKey(options) {
|
|
|
849
871
|
}
|
|
850
872
|
|
|
851
873
|
// src/internal/duckdb-runtime.ts
|
|
852
|
-
import { mkdir, mkdtemp, rm } from "fs/promises";
|
|
853
|
-
import { join, resolve as resolve2 } from "path";
|
|
874
|
+
import { mkdir, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
|
|
875
|
+
import { join as join2, resolve as resolve2 } from "path";
|
|
854
876
|
import {
|
|
855
877
|
DuckDBInstance
|
|
856
878
|
} from "@duckdb/node-api";
|
|
857
879
|
import { LakeStorageError as LakeStorageError11 } from "@sixb/core/lake-storage";
|
|
858
880
|
|
|
881
|
+
// src/internal/duckdb-extensions.ts
|
|
882
|
+
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
883
|
+
import { tmpdir } from "os";
|
|
884
|
+
import { join } from "path";
|
|
885
|
+
import { promisify } from "util";
|
|
886
|
+
import { gunzip } from "zlib";
|
|
887
|
+
var extensionsUrl = "https://extensions.duckdb.org";
|
|
888
|
+
var downloadTimeoutMillis = 60000;
|
|
889
|
+
var gunzipAsync = promisify(gunzip);
|
|
890
|
+
async function prefetchDuckDbExtensions(runtime, extensions) {
|
|
891
|
+
await prefetch(runtime, extensions).catch((error) => warnFallback(extensions.join("', '"), error));
|
|
892
|
+
}
|
|
893
|
+
async function prefetch(runtime, extensions) {
|
|
894
|
+
const [target] = await runtime.query(`
|
|
895
|
+
SELECT
|
|
896
|
+
version() AS version,
|
|
897
|
+
(SELECT platform FROM pragma_platform()) AS platform,
|
|
898
|
+
current_setting('custom_extension_repository') AS repository
|
|
899
|
+
`);
|
|
900
|
+
if (target?.repository !== "")
|
|
901
|
+
return;
|
|
902
|
+
const missing = await missingExtensions(runtime, extensions);
|
|
903
|
+
if (missing.length === 0)
|
|
904
|
+
return;
|
|
905
|
+
const directory = await mkdtemp(join(tmpdir(), "sixb-duckdb-extensions-"));
|
|
906
|
+
try {
|
|
907
|
+
const downloads = await Promise.all(missing.map(async (name) => ({
|
|
908
|
+
name,
|
|
909
|
+
file: await download(`${target.version}/${target.platform}/${name}`, directory, name).catch((error) => warnFallback(name, error))
|
|
910
|
+
})));
|
|
911
|
+
for (const { name, file } of downloads) {
|
|
912
|
+
if (file === undefined)
|
|
913
|
+
continue;
|
|
914
|
+
await runtime.run(`INSTALL ${quoteSqlString(file)}`).catch((error) => warnFallback(name, error));
|
|
915
|
+
}
|
|
916
|
+
} finally {
|
|
917
|
+
await rm(directory, { recursive: true, force: true });
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
function warnFallback(name, error) {
|
|
921
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
922
|
+
console.warn(`[SixbDuckLake] Could not install DuckDB extension '${name}' over HTTPS (${reason}); falling back to DuckDB's INSTALL.`);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
async function missingExtensions(runtime, extensions) {
|
|
926
|
+
const requested = `[${quoteSqlStringList(extensions.map((extension) => extension.toLowerCase()))}]`;
|
|
927
|
+
const rows = await runtime.query(`
|
|
928
|
+
SELECT extension_name
|
|
929
|
+
FROM duckdb_extensions()
|
|
930
|
+
WHERE NOT installed
|
|
931
|
+
AND (list_contains(${requested}, extension_name) OR list_has_any(${requested}, aliases))
|
|
932
|
+
`);
|
|
933
|
+
return rows.map((row) => String(row.extension_name));
|
|
934
|
+
}
|
|
935
|
+
async function download(path, directory, name) {
|
|
936
|
+
const response = await fetch(`${extensionsUrl}/${path}.duckdb_extension.gz`, {
|
|
937
|
+
signal: AbortSignal.timeout(downloadTimeoutMillis)
|
|
938
|
+
});
|
|
939
|
+
if (!response.ok)
|
|
940
|
+
throw new Error(`HTTP ${response.status}`);
|
|
941
|
+
const file = join(directory, `${name}.duckdb_extension`);
|
|
942
|
+
await writeFile(file, await gunzipAsync(new Uint8Array(await response.arrayBuffer())));
|
|
943
|
+
return file;
|
|
944
|
+
}
|
|
945
|
+
|
|
859
946
|
// src/internal/duckdb-reader.ts
|
|
860
947
|
import { LakeStorageError as LakeStorageError10 } from "@sixb/core/lake-storage";
|
|
861
948
|
|
|
@@ -1072,7 +1159,7 @@ class NodeDuckDbRuntime {
|
|
|
1072
1159
|
this.connection.closeSync();
|
|
1073
1160
|
this.instance.closeSync();
|
|
1074
1161
|
if (this.temporaryDirectory)
|
|
1075
|
-
await
|
|
1162
|
+
await rm2(this.temporaryDirectory, { recursive: true, force: true });
|
|
1076
1163
|
const failed = results.find((result) => result.status === "rejected");
|
|
1077
1164
|
if (failed?.status === "rejected")
|
|
1078
1165
|
throw failed.reason;
|
|
@@ -1159,7 +1246,7 @@ async function createDuckDbRuntime(options = {}) {
|
|
|
1159
1246
|
if (base !== "") {
|
|
1160
1247
|
const directory = resolve2(base);
|
|
1161
1248
|
await mkdir(directory, { recursive: true });
|
|
1162
|
-
temporaryDirectory = await
|
|
1249
|
+
temporaryDirectory = await mkdtemp2(join2(directory, "sixb-"));
|
|
1163
1250
|
}
|
|
1164
1251
|
let instance;
|
|
1165
1252
|
try {
|
|
@@ -1171,13 +1258,15 @@ async function createDuckDbRuntime(options = {}) {
|
|
|
1171
1258
|
} catch (error) {
|
|
1172
1259
|
instance?.closeSync();
|
|
1173
1260
|
if (temporaryDirectory)
|
|
1174
|
-
await
|
|
1261
|
+
await rm2(temporaryDirectory, { recursive: true, force: true });
|
|
1175
1262
|
throw error;
|
|
1176
1263
|
}
|
|
1177
1264
|
}
|
|
1178
1265
|
async function installDuckLakeExtensions(runtime, options) {
|
|
1266
|
+
const extensions = requiredExtensions(options);
|
|
1267
|
+
await prefetchDuckDbExtensions(runtime, ["ducklake", ...extensions]);
|
|
1179
1268
|
await runtime.run("INSTALL ducklake");
|
|
1180
|
-
for (const extension of
|
|
1269
|
+
for (const extension of extensions) {
|
|
1181
1270
|
await runtime.run(`INSTALL ${quoteIdentifier(extension)}`);
|
|
1182
1271
|
}
|
|
1183
1272
|
}
|
|
@@ -2619,6 +2708,7 @@ import { LakeStorageError as LakeStorageError19 } from "@sixb/core/lake-storage"
|
|
|
2619
2708
|
var SNAPSHOT_ROW_BATCH_SIZE = 128;
|
|
2620
2709
|
var FEED_PAGE_SIZE = 512;
|
|
2621
2710
|
var SHORT_READ_BATCH_SIZE = 8;
|
|
2711
|
+
var UNCHANGED_ROWS_WALK_LIMIT = 32;
|
|
2622
2712
|
var CATALOG_SNAPSHOT_SCAN_LIMIT = 512;
|
|
2623
2713
|
|
|
2624
2714
|
class DuckLakeSnapshotReader {
|
|
@@ -2661,6 +2751,33 @@ class DuckLakeSnapshotReader {
|
|
|
2661
2751
|
return this.getVersionForTableRef(runtime, tableRef, snapshotId);
|
|
2662
2752
|
});
|
|
2663
2753
|
}
|
|
2754
|
+
async holdsRowsOf(datasetId, versionId, earlierVersionId) {
|
|
2755
|
+
const snapshotId = parseVersionId(versionId);
|
|
2756
|
+
const earlierSnapshotId = parseVersionId(earlierVersionId);
|
|
2757
|
+
if (snapshotId === earlierSnapshotId) {
|
|
2758
|
+
return true;
|
|
2759
|
+
}
|
|
2760
|
+
if (BigInt(snapshotId) < BigInt(earlierSnapshotId)) {
|
|
2761
|
+
return false;
|
|
2762
|
+
}
|
|
2763
|
+
this.connections.assertOpen();
|
|
2764
|
+
return this.connections.withAttachedRuntime(async (runtime) => {
|
|
2765
|
+
const tableRef = await resolveDatasetTableRef(this.options, runtime, datasetId);
|
|
2766
|
+
if (!tableRef) {
|
|
2767
|
+
return false;
|
|
2768
|
+
}
|
|
2769
|
+
const candidates = await this.querySnapshotCandidates(runtime, {
|
|
2770
|
+
table: tableRef,
|
|
2771
|
+
beforeSnapshotId: String(BigInt(snapshotId) + 1n),
|
|
2772
|
+
fromSnapshotId: earlierSnapshotId,
|
|
2773
|
+
limit: UNCHANGED_ROWS_WALK_LIMIT + 1
|
|
2774
|
+
});
|
|
2775
|
+
if (candidates[0]?.snapshotId !== snapshotId || candidates[candidates.length - 1]?.snapshotId !== earlierSnapshotId) {
|
|
2776
|
+
return false;
|
|
2777
|
+
}
|
|
2778
|
+
return candidates.slice(0, -1).every((candidate) => this.candidateToSnapshotRow(datasetId, tableRef.tableId, candidate)?.rowsChanged === false);
|
|
2779
|
+
});
|
|
2780
|
+
}
|
|
2664
2781
|
async listDatasetCatalogState(datasetIds) {
|
|
2665
2782
|
this.connections.assertOpen();
|
|
2666
2783
|
if (datasetIds.length === 0) {
|
|
@@ -3101,6 +3218,9 @@ class DuckLakeSnapshotReader {
|
|
|
3101
3218
|
if (input.beforeSnapshotId !== undefined) {
|
|
3102
3219
|
assertDuckLakeSnapshotId2(input.beforeSnapshotId);
|
|
3103
3220
|
}
|
|
3221
|
+
if (input.fromSnapshotId !== undefined) {
|
|
3222
|
+
assertDuckLakeSnapshotId2(input.fromSnapshotId);
|
|
3223
|
+
}
|
|
3104
3224
|
const rows = await runtime.query(buildDuckLakeMetadataQuery(this.options, (table) => {
|
|
3105
3225
|
const bounds = [
|
|
3106
3226
|
`changes.snapshot_id >= (
|
|
@@ -3115,6 +3235,9 @@ class DuckLakeSnapshotReader {
|
|
|
3115
3235
|
if (input.beforeSnapshotId !== undefined) {
|
|
3116
3236
|
bounds.push(`changes.snapshot_id < ${input.beforeSnapshotId}`);
|
|
3117
3237
|
}
|
|
3238
|
+
if (input.fromSnapshotId !== undefined) {
|
|
3239
|
+
bounds.push(`changes.snapshot_id >= ${input.fromSnapshotId}`);
|
|
3240
|
+
}
|
|
3118
3241
|
const namesDatasetTable = datasetTableIds(input.table).map((tableId) => `strpos(',' || changes.changes_made || ',', ${quoteSqlString(`:${tableId},`)}) > 0`).join(" OR ");
|
|
3119
3242
|
return `
|
|
3120
3243
|
SELECT
|
|
@@ -3175,6 +3298,8 @@ class DuckLakeSnapshotReader {
|
|
|
3175
3298
|
tableId,
|
|
3176
3299
|
createdAt: candidate.createdAt,
|
|
3177
3300
|
mode: metadata.mode ?? "schema",
|
|
3301
|
+
rowsChanged: metadata.rowsChanged ?? true,
|
|
3302
|
+
lineageChanged: metadata.lineageChanged ?? true,
|
|
3178
3303
|
metadata
|
|
3179
3304
|
};
|
|
3180
3305
|
}
|
|
@@ -3184,6 +3309,8 @@ class DuckLakeSnapshotReader {
|
|
|
3184
3309
|
tableId,
|
|
3185
3310
|
createdAt: candidate.createdAt,
|
|
3186
3311
|
mode: candidate.metadata?.mode ?? (hasDeleteChange ? "snapshot" : "append"),
|
|
3312
|
+
rowsChanged: true,
|
|
3313
|
+
lineageChanged: candidate.metadata?.lineageChanged ?? true,
|
|
3187
3314
|
...candidate.metadata !== undefined ? { metadata: candidate.metadata } : {}
|
|
3188
3315
|
};
|
|
3189
3316
|
}
|
|
@@ -3209,6 +3336,8 @@ function snapshotToVersion(tableRef, row, schema) {
|
|
|
3209
3336
|
producer: row.metadata?.producer,
|
|
3210
3337
|
inputs: row.metadata?.inputs,
|
|
3211
3338
|
...storedLineage(tableRef.datasetId, row),
|
|
3339
|
+
rowsChanged: row.rowsChanged,
|
|
3340
|
+
lineageChanged: row.lineageChanged,
|
|
3212
3341
|
...row.metadata?.rowCount !== undefined ? { rowCount: row.metadata.rowCount } : {}
|
|
3213
3342
|
};
|
|
3214
3343
|
}
|
|
@@ -3306,7 +3435,7 @@ async function applyDatasetRowsFromRelation(input) {
|
|
|
3306
3435
|
changedTableRows
|
|
3307
3436
|
};
|
|
3308
3437
|
}
|
|
3309
|
-
const sourceRowCount = await
|
|
3438
|
+
const sourceRowCount = await countRows2(input.runtime, input.sourceRelationSql);
|
|
3310
3439
|
const knownRowCount = baselineRowCount ?? input.previousRowCount;
|
|
3311
3440
|
const targetWasEmpty = knownRowCount === undefined ? await isEmptyRelation(input.runtime, table) : knownRowCount === 0;
|
|
3312
3441
|
await input.runtime.run(`INSERT INTO ${table} (${columnsSql}) SELECT ${columnsSql} FROM ${input.sourceRelationSql}`);
|
|
@@ -3376,7 +3505,7 @@ async function fingerprintRelation(runtime, relationSql, columnsSql) {
|
|
|
3376
3505
|
function sameFingerprint(left, right) {
|
|
3377
3506
|
return left.rowCount === right.rowCount && left.hashSum === right.hashSum && left.hashXor === right.hashXor;
|
|
3378
3507
|
}
|
|
3379
|
-
async function
|
|
3508
|
+
async function countRows2(runtime, relationSql) {
|
|
3380
3509
|
const [row] = await runtime.query(`SELECT count(*) AS row_count FROM ${relationSql}`);
|
|
3381
3510
|
if (row === undefined) {
|
|
3382
3511
|
return 0;
|
|
@@ -4707,6 +4836,7 @@ import { randomUUID as randomUUID8 } from "crypto";
|
|
|
4707
4836
|
import {
|
|
4708
4837
|
assertUnsequencedDatasetWrite,
|
|
4709
4838
|
getDatasetPrimaryKeyColumns as getDatasetPrimaryKeyColumns4,
|
|
4839
|
+
hasDatasetLineageChanges,
|
|
4710
4840
|
hasDatasetProvenanceChanges,
|
|
4711
4841
|
LakeConcurrencyError as LakeConcurrencyError2,
|
|
4712
4842
|
LakeStorageError as LakeStorageError26,
|
|
@@ -4727,7 +4857,7 @@ async function applyDatasetMergeFromRelation(input) {
|
|
|
4727
4857
|
}
|
|
4728
4858
|
const stagingTable = quoteIdentifier(input.stagingTableName);
|
|
4729
4859
|
const targetTable = qualifiedTableName(input.options, encodeDatasetTableName(input.dataset.id));
|
|
4730
|
-
const sourceRowCount = await
|
|
4860
|
+
const sourceRowCount = await countRows3(input.runtime, stagingTable);
|
|
4731
4861
|
const previousRowCount = await currentBaselineRowCount(input, targetTable, primaryKeyColumns);
|
|
4732
4862
|
return withMaterializedFinalChanges(input, primaryKeyColumns, async (finalChanges, orderingChanged) => {
|
|
4733
4863
|
const effects = await countMergeEffects(input, targetTable, finalChanges, primaryKeyColumns);
|
|
@@ -4827,7 +4957,7 @@ async function applySourceOrdering(input, acceptedTable) {
|
|
|
4827
4957
|
}
|
|
4828
4958
|
async function currentBaselineRowCount(input, targetTable, primaryKeyColumns) {
|
|
4829
4959
|
if (sameColumns(input.validatedPrimaryKeyColumns, primaryKeyColumns)) {
|
|
4830
|
-
return input.previousRowCount ??
|
|
4960
|
+
return input.previousRowCount ?? countRows3(input.runtime, targetTable);
|
|
4831
4961
|
}
|
|
4832
4962
|
return inspectUniqueKeyedRelation({
|
|
4833
4963
|
runtime: input.runtime,
|
|
@@ -4920,9 +5050,10 @@ function primaryKeyMatchSql(primaryKeyColumns, leftAlias, rightAlias) {
|
|
|
4920
5050
|
return primaryKeyColumns.map((column) => `${leftAlias}.${quoteIdentifier(column)} = ${rightAlias}.${quoteIdentifier(column)}`).join(" AND ");
|
|
4921
5051
|
}
|
|
4922
5052
|
function datasetRowDifferenceSql(dataset, leftAlias, rightAlias) {
|
|
4923
|
-
|
|
5053
|
+
const differences = dataset.schema.columns.filter((column) => column.name !== dataset.sequenceBy).map((column) => `${leftAlias}.${quoteIdentifier(column.name)} IS DISTINCT FROM ${rightAlias}.${quoteIdentifier(column.name)}`);
|
|
5054
|
+
return differences.length > 0 ? differences.join(" OR ") : "FALSE";
|
|
4924
5055
|
}
|
|
4925
|
-
async function
|
|
5056
|
+
async function countRows3(runtime, relationSql) {
|
|
4926
5057
|
const [row] = await runtime.query(`SELECT count(*) AS row_count FROM ${relationSql}`);
|
|
4927
5058
|
return row === undefined ? 0 : Number(getBigIntLike(row, "row_count"));
|
|
4928
5059
|
}
|
|
@@ -5399,13 +5530,22 @@ class DuckLakeWriteCoordinator {
|
|
|
5399
5530
|
validatedPrimaryKeyColumns: latestVersion?.validatedPrimaryKeyColumns
|
|
5400
5531
|
});
|
|
5401
5532
|
input.signal?.throwIfAborted();
|
|
5402
|
-
const
|
|
5533
|
+
const parent = latestVersion === undefined ? await this.latestVersionAtReadSnapshot(input) : latestVersion;
|
|
5534
|
+
const noOpBase = changeResult.dataChangeExpected ? undefined : parent;
|
|
5403
5535
|
const createsVersion = noOpBase === undefined || this.noOpCreatesVersion(input, noOpBase, lineage);
|
|
5404
5536
|
if (createsVersion && mustClaimTable(input, changeResult, lineage)) {
|
|
5405
5537
|
await this.claimTable(input);
|
|
5406
5538
|
}
|
|
5407
5539
|
const commitId = randomUUID8();
|
|
5408
|
-
await this.setCommitMetadata(input, commitId, commitRowCount(input, changeResult, latestVersion), lineage
|
|
5540
|
+
await this.setCommitMetadata(input, commitId, commitRowCount(input, changeResult, latestVersion), lineage, {
|
|
5541
|
+
rowsChanged: parent === null || changeResult.changedTableRows,
|
|
5542
|
+
lineageChanged: hasDatasetLineageChanges(parent, {
|
|
5543
|
+
datasetId: input.dataset.id,
|
|
5544
|
+
schema: input.dataset.schema,
|
|
5545
|
+
producer: input.producer,
|
|
5546
|
+
lineage
|
|
5547
|
+
})
|
|
5548
|
+
});
|
|
5409
5549
|
await commitVersionTransaction(input.runtime, input.dataset.id, createsVersion && mustCommitOnReadSnapshot(input, changeResult) ? { poisonRuntime: () => this.connections.poisonRuntime() } : undefined);
|
|
5410
5550
|
committed = true;
|
|
5411
5551
|
const committedWriteSnapshotId = await this.lastCommittedSnapshotId(input.runtime);
|
|
@@ -5475,7 +5615,7 @@ class DuckLakeWriteCoordinator {
|
|
|
5475
5615
|
}
|
|
5476
5616
|
return result;
|
|
5477
5617
|
}
|
|
5478
|
-
async setCommitMetadata(input, commitId, rowCount, lineage) {
|
|
5618
|
+
async setCommitMetadata(input, commitId, rowCount, lineage, changed) {
|
|
5479
5619
|
const validatedPrimaryKeyColumns = getDatasetPrimaryKeyColumns4(input.dataset);
|
|
5480
5620
|
const metadata = {
|
|
5481
5621
|
kind: "datasetVersion",
|
|
@@ -5486,7 +5626,9 @@ class DuckLakeWriteCoordinator {
|
|
|
5486
5626
|
inputs: input.inputs ? structuredClone(input.inputs) : undefined,
|
|
5487
5627
|
...rowCount.kind === "exact" ? { rowCount: rowCount.value } : {},
|
|
5488
5628
|
...validatedPrimaryKeyColumns !== null ? { validatedPrimaryKeyColumns: [...validatedPrimaryKeyColumns] } : {},
|
|
5489
|
-
...lineage === undefined ? {} : { lineage: encodeLineage(lineage) }
|
|
5629
|
+
...lineage === undefined ? {} : { lineage: encodeLineage(lineage) },
|
|
5630
|
+
rowsChanged: changed.rowsChanged,
|
|
5631
|
+
lineageChanged: changed.lineageChanged
|
|
5490
5632
|
};
|
|
5491
5633
|
await input.runtime.run(`CALL ${quoteIdentifier(duckLakeAlias(this.options))}.set_commit_message(${quoteSqlString("Sixb")}, ${quoteSqlString(input.commitMessage)}, extra_info => ${quoteSqlString(JSON.stringify({ sixb: metadata }))})`);
|
|
5492
5634
|
}
|
|
@@ -5737,4 +5879,4 @@ export {
|
|
|
5737
5879
|
DuckLakeStorage
|
|
5738
5880
|
};
|
|
5739
5881
|
|
|
5740
|
-
//# debugId=
|
|
5882
|
+
//# debugId=11CCDDEF58B2058164756E2164756E21
|