@sixb/ducklake 0.1.9 → 0.2.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 +8 -2
- package/dist/index.js +87 -12
- package/dist/index.js.map +7 -7
- 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 +5 -5
- package/src/internal/dataset-row-merge.ts +9 -2
- 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
|
@@ -102,6 +102,10 @@ budget and PostgreSQL metadata pool. Metadata and writes remain available betwee
|
|
|
102
102
|
Each read stays pinned to one immutable snapshot. Maintenance waits until no reads remain; it
|
|
103
103
|
blocks new reads only while running, so continuous traffic can postpone it.
|
|
104
104
|
|
|
105
|
+
`readChanges` compares two versions in DuckDB. When no version between them changed rows
|
|
106
|
+
(`rowsChanged` in the commit metadata, read from at most 32 snapshots), it returns an empty change
|
|
107
|
+
set without reading the table.
|
|
108
|
+
|
|
105
109
|
`maxStreamingReads` limits native streaming readers per provider (default: `4`); extra reads use
|
|
106
110
|
bounded pages on the main connection. It does not limit pipeline inputs. Raise it when memory and
|
|
107
111
|
catalog capacity allow. Finish the iterator, use `for await` with `break`, or pass `signal` to cancel
|
|
@@ -190,8 +194,10 @@ Datasets declaring `sequenceBy` instead compare source revisions: older changes
|
|
|
190
194
|
identical ties are unchanged, and conflicting ties abort the merge. Deletes require
|
|
191
195
|
`change.delete(key, { sequence })`. Source revisions and deletion tombstones are stored in a
|
|
192
196
|
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
|
-
|
|
197
|
+
New tombstone state creates a version even when the visible rows stay the same; such a version
|
|
198
|
+
records `rowsChanged: false`. So does a newer upsert that differs from the stored row only in its
|
|
199
|
+
`sequenceBy` column: the row is kept and only its source revision advances. Sequenced provider
|
|
200
|
+
writes use merge sessions; snapshot syncs convert rows to ordered upserts.
|
|
195
201
|
Snapshot syncs use `commit({ createInitialVersion: true })` to make a first empty result
|
|
196
202
|
addressable. Later no-op commits reuse the current version, including after a concurrent write.
|
|
197
203
|
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 ");
|
|
@@ -2619,6 +2641,7 @@ import { LakeStorageError as LakeStorageError19 } from "@sixb/core/lake-storage"
|
|
|
2619
2641
|
var SNAPSHOT_ROW_BATCH_SIZE = 128;
|
|
2620
2642
|
var FEED_PAGE_SIZE = 512;
|
|
2621
2643
|
var SHORT_READ_BATCH_SIZE = 8;
|
|
2644
|
+
var UNCHANGED_ROWS_WALK_LIMIT = 32;
|
|
2622
2645
|
var CATALOG_SNAPSHOT_SCAN_LIMIT = 512;
|
|
2623
2646
|
|
|
2624
2647
|
class DuckLakeSnapshotReader {
|
|
@@ -2661,6 +2684,33 @@ class DuckLakeSnapshotReader {
|
|
|
2661
2684
|
return this.getVersionForTableRef(runtime, tableRef, snapshotId);
|
|
2662
2685
|
});
|
|
2663
2686
|
}
|
|
2687
|
+
async holdsRowsOf(datasetId, versionId, earlierVersionId) {
|
|
2688
|
+
const snapshotId = parseVersionId(versionId);
|
|
2689
|
+
const earlierSnapshotId = parseVersionId(earlierVersionId);
|
|
2690
|
+
if (snapshotId === earlierSnapshotId) {
|
|
2691
|
+
return true;
|
|
2692
|
+
}
|
|
2693
|
+
if (BigInt(snapshotId) < BigInt(earlierSnapshotId)) {
|
|
2694
|
+
return false;
|
|
2695
|
+
}
|
|
2696
|
+
this.connections.assertOpen();
|
|
2697
|
+
return this.connections.withAttachedRuntime(async (runtime) => {
|
|
2698
|
+
const tableRef = await resolveDatasetTableRef(this.options, runtime, datasetId);
|
|
2699
|
+
if (!tableRef) {
|
|
2700
|
+
return false;
|
|
2701
|
+
}
|
|
2702
|
+
const candidates = await this.querySnapshotCandidates(runtime, {
|
|
2703
|
+
table: tableRef,
|
|
2704
|
+
beforeSnapshotId: String(BigInt(snapshotId) + 1n),
|
|
2705
|
+
fromSnapshotId: earlierSnapshotId,
|
|
2706
|
+
limit: UNCHANGED_ROWS_WALK_LIMIT + 1
|
|
2707
|
+
});
|
|
2708
|
+
if (candidates[0]?.snapshotId !== snapshotId || candidates[candidates.length - 1]?.snapshotId !== earlierSnapshotId) {
|
|
2709
|
+
return false;
|
|
2710
|
+
}
|
|
2711
|
+
return candidates.slice(0, -1).every((candidate) => this.candidateToSnapshotRow(datasetId, tableRef.tableId, candidate)?.rowsChanged === false);
|
|
2712
|
+
});
|
|
2713
|
+
}
|
|
2664
2714
|
async listDatasetCatalogState(datasetIds) {
|
|
2665
2715
|
this.connections.assertOpen();
|
|
2666
2716
|
if (datasetIds.length === 0) {
|
|
@@ -3101,6 +3151,9 @@ class DuckLakeSnapshotReader {
|
|
|
3101
3151
|
if (input.beforeSnapshotId !== undefined) {
|
|
3102
3152
|
assertDuckLakeSnapshotId2(input.beforeSnapshotId);
|
|
3103
3153
|
}
|
|
3154
|
+
if (input.fromSnapshotId !== undefined) {
|
|
3155
|
+
assertDuckLakeSnapshotId2(input.fromSnapshotId);
|
|
3156
|
+
}
|
|
3104
3157
|
const rows = await runtime.query(buildDuckLakeMetadataQuery(this.options, (table) => {
|
|
3105
3158
|
const bounds = [
|
|
3106
3159
|
`changes.snapshot_id >= (
|
|
@@ -3115,6 +3168,9 @@ class DuckLakeSnapshotReader {
|
|
|
3115
3168
|
if (input.beforeSnapshotId !== undefined) {
|
|
3116
3169
|
bounds.push(`changes.snapshot_id < ${input.beforeSnapshotId}`);
|
|
3117
3170
|
}
|
|
3171
|
+
if (input.fromSnapshotId !== undefined) {
|
|
3172
|
+
bounds.push(`changes.snapshot_id >= ${input.fromSnapshotId}`);
|
|
3173
|
+
}
|
|
3118
3174
|
const namesDatasetTable = datasetTableIds(input.table).map((tableId) => `strpos(',' || changes.changes_made || ',', ${quoteSqlString(`:${tableId},`)}) > 0`).join(" OR ");
|
|
3119
3175
|
return `
|
|
3120
3176
|
SELECT
|
|
@@ -3175,6 +3231,8 @@ class DuckLakeSnapshotReader {
|
|
|
3175
3231
|
tableId,
|
|
3176
3232
|
createdAt: candidate.createdAt,
|
|
3177
3233
|
mode: metadata.mode ?? "schema",
|
|
3234
|
+
rowsChanged: metadata.rowsChanged ?? true,
|
|
3235
|
+
lineageChanged: metadata.lineageChanged ?? true,
|
|
3178
3236
|
metadata
|
|
3179
3237
|
};
|
|
3180
3238
|
}
|
|
@@ -3184,6 +3242,8 @@ class DuckLakeSnapshotReader {
|
|
|
3184
3242
|
tableId,
|
|
3185
3243
|
createdAt: candidate.createdAt,
|
|
3186
3244
|
mode: candidate.metadata?.mode ?? (hasDeleteChange ? "snapshot" : "append"),
|
|
3245
|
+
rowsChanged: true,
|
|
3246
|
+
lineageChanged: candidate.metadata?.lineageChanged ?? true,
|
|
3187
3247
|
...candidate.metadata !== undefined ? { metadata: candidate.metadata } : {}
|
|
3188
3248
|
};
|
|
3189
3249
|
}
|
|
@@ -3209,6 +3269,8 @@ function snapshotToVersion(tableRef, row, schema) {
|
|
|
3209
3269
|
producer: row.metadata?.producer,
|
|
3210
3270
|
inputs: row.metadata?.inputs,
|
|
3211
3271
|
...storedLineage(tableRef.datasetId, row),
|
|
3272
|
+
rowsChanged: row.rowsChanged,
|
|
3273
|
+
lineageChanged: row.lineageChanged,
|
|
3212
3274
|
...row.metadata?.rowCount !== undefined ? { rowCount: row.metadata.rowCount } : {}
|
|
3213
3275
|
};
|
|
3214
3276
|
}
|
|
@@ -3306,7 +3368,7 @@ async function applyDatasetRowsFromRelation(input) {
|
|
|
3306
3368
|
changedTableRows
|
|
3307
3369
|
};
|
|
3308
3370
|
}
|
|
3309
|
-
const sourceRowCount = await
|
|
3371
|
+
const sourceRowCount = await countRows2(input.runtime, input.sourceRelationSql);
|
|
3310
3372
|
const knownRowCount = baselineRowCount ?? input.previousRowCount;
|
|
3311
3373
|
const targetWasEmpty = knownRowCount === undefined ? await isEmptyRelation(input.runtime, table) : knownRowCount === 0;
|
|
3312
3374
|
await input.runtime.run(`INSERT INTO ${table} (${columnsSql}) SELECT ${columnsSql} FROM ${input.sourceRelationSql}`);
|
|
@@ -3376,7 +3438,7 @@ async function fingerprintRelation(runtime, relationSql, columnsSql) {
|
|
|
3376
3438
|
function sameFingerprint(left, right) {
|
|
3377
3439
|
return left.rowCount === right.rowCount && left.hashSum === right.hashSum && left.hashXor === right.hashXor;
|
|
3378
3440
|
}
|
|
3379
|
-
async function
|
|
3441
|
+
async function countRows2(runtime, relationSql) {
|
|
3380
3442
|
const [row] = await runtime.query(`SELECT count(*) AS row_count FROM ${relationSql}`);
|
|
3381
3443
|
if (row === undefined) {
|
|
3382
3444
|
return 0;
|
|
@@ -4707,6 +4769,7 @@ import { randomUUID as randomUUID8 } from "crypto";
|
|
|
4707
4769
|
import {
|
|
4708
4770
|
assertUnsequencedDatasetWrite,
|
|
4709
4771
|
getDatasetPrimaryKeyColumns as getDatasetPrimaryKeyColumns4,
|
|
4772
|
+
hasDatasetLineageChanges,
|
|
4710
4773
|
hasDatasetProvenanceChanges,
|
|
4711
4774
|
LakeConcurrencyError as LakeConcurrencyError2,
|
|
4712
4775
|
LakeStorageError as LakeStorageError26,
|
|
@@ -4727,7 +4790,7 @@ async function applyDatasetMergeFromRelation(input) {
|
|
|
4727
4790
|
}
|
|
4728
4791
|
const stagingTable = quoteIdentifier(input.stagingTableName);
|
|
4729
4792
|
const targetTable = qualifiedTableName(input.options, encodeDatasetTableName(input.dataset.id));
|
|
4730
|
-
const sourceRowCount = await
|
|
4793
|
+
const sourceRowCount = await countRows3(input.runtime, stagingTable);
|
|
4731
4794
|
const previousRowCount = await currentBaselineRowCount(input, targetTable, primaryKeyColumns);
|
|
4732
4795
|
return withMaterializedFinalChanges(input, primaryKeyColumns, async (finalChanges, orderingChanged) => {
|
|
4733
4796
|
const effects = await countMergeEffects(input, targetTable, finalChanges, primaryKeyColumns);
|
|
@@ -4827,7 +4890,7 @@ async function applySourceOrdering(input, acceptedTable) {
|
|
|
4827
4890
|
}
|
|
4828
4891
|
async function currentBaselineRowCount(input, targetTable, primaryKeyColumns) {
|
|
4829
4892
|
if (sameColumns(input.validatedPrimaryKeyColumns, primaryKeyColumns)) {
|
|
4830
|
-
return input.previousRowCount ??
|
|
4893
|
+
return input.previousRowCount ?? countRows3(input.runtime, targetTable);
|
|
4831
4894
|
}
|
|
4832
4895
|
return inspectUniqueKeyedRelation({
|
|
4833
4896
|
runtime: input.runtime,
|
|
@@ -4920,9 +4983,10 @@ function primaryKeyMatchSql(primaryKeyColumns, leftAlias, rightAlias) {
|
|
|
4920
4983
|
return primaryKeyColumns.map((column) => `${leftAlias}.${quoteIdentifier(column)} = ${rightAlias}.${quoteIdentifier(column)}`).join(" AND ");
|
|
4921
4984
|
}
|
|
4922
4985
|
function datasetRowDifferenceSql(dataset, leftAlias, rightAlias) {
|
|
4923
|
-
|
|
4986
|
+
const differences = dataset.schema.columns.filter((column) => column.name !== dataset.sequenceBy).map((column) => `${leftAlias}.${quoteIdentifier(column.name)} IS DISTINCT FROM ${rightAlias}.${quoteIdentifier(column.name)}`);
|
|
4987
|
+
return differences.length > 0 ? differences.join(" OR ") : "FALSE";
|
|
4924
4988
|
}
|
|
4925
|
-
async function
|
|
4989
|
+
async function countRows3(runtime, relationSql) {
|
|
4926
4990
|
const [row] = await runtime.query(`SELECT count(*) AS row_count FROM ${relationSql}`);
|
|
4927
4991
|
return row === undefined ? 0 : Number(getBigIntLike(row, "row_count"));
|
|
4928
4992
|
}
|
|
@@ -5399,13 +5463,22 @@ class DuckLakeWriteCoordinator {
|
|
|
5399
5463
|
validatedPrimaryKeyColumns: latestVersion?.validatedPrimaryKeyColumns
|
|
5400
5464
|
});
|
|
5401
5465
|
input.signal?.throwIfAborted();
|
|
5402
|
-
const
|
|
5466
|
+
const parent = latestVersion === undefined ? await this.latestVersionAtReadSnapshot(input) : latestVersion;
|
|
5467
|
+
const noOpBase = changeResult.dataChangeExpected ? undefined : parent;
|
|
5403
5468
|
const createsVersion = noOpBase === undefined || this.noOpCreatesVersion(input, noOpBase, lineage);
|
|
5404
5469
|
if (createsVersion && mustClaimTable(input, changeResult, lineage)) {
|
|
5405
5470
|
await this.claimTable(input);
|
|
5406
5471
|
}
|
|
5407
5472
|
const commitId = randomUUID8();
|
|
5408
|
-
await this.setCommitMetadata(input, commitId, commitRowCount(input, changeResult, latestVersion), lineage
|
|
5473
|
+
await this.setCommitMetadata(input, commitId, commitRowCount(input, changeResult, latestVersion), lineage, {
|
|
5474
|
+
rowsChanged: parent === null || changeResult.changedTableRows,
|
|
5475
|
+
lineageChanged: hasDatasetLineageChanges(parent, {
|
|
5476
|
+
datasetId: input.dataset.id,
|
|
5477
|
+
schema: input.dataset.schema,
|
|
5478
|
+
producer: input.producer,
|
|
5479
|
+
lineage
|
|
5480
|
+
})
|
|
5481
|
+
});
|
|
5409
5482
|
await commitVersionTransaction(input.runtime, input.dataset.id, createsVersion && mustCommitOnReadSnapshot(input, changeResult) ? { poisonRuntime: () => this.connections.poisonRuntime() } : undefined);
|
|
5410
5483
|
committed = true;
|
|
5411
5484
|
const committedWriteSnapshotId = await this.lastCommittedSnapshotId(input.runtime);
|
|
@@ -5475,7 +5548,7 @@ class DuckLakeWriteCoordinator {
|
|
|
5475
5548
|
}
|
|
5476
5549
|
return result;
|
|
5477
5550
|
}
|
|
5478
|
-
async setCommitMetadata(input, commitId, rowCount, lineage) {
|
|
5551
|
+
async setCommitMetadata(input, commitId, rowCount, lineage, changed) {
|
|
5479
5552
|
const validatedPrimaryKeyColumns = getDatasetPrimaryKeyColumns4(input.dataset);
|
|
5480
5553
|
const metadata = {
|
|
5481
5554
|
kind: "datasetVersion",
|
|
@@ -5486,7 +5559,9 @@ class DuckLakeWriteCoordinator {
|
|
|
5486
5559
|
inputs: input.inputs ? structuredClone(input.inputs) : undefined,
|
|
5487
5560
|
...rowCount.kind === "exact" ? { rowCount: rowCount.value } : {},
|
|
5488
5561
|
...validatedPrimaryKeyColumns !== null ? { validatedPrimaryKeyColumns: [...validatedPrimaryKeyColumns] } : {},
|
|
5489
|
-
...lineage === undefined ? {} : { lineage: encodeLineage(lineage) }
|
|
5562
|
+
...lineage === undefined ? {} : { lineage: encodeLineage(lineage) },
|
|
5563
|
+
rowsChanged: changed.rowsChanged,
|
|
5564
|
+
lineageChanged: changed.lineageChanged
|
|
5490
5565
|
};
|
|
5491
5566
|
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
5567
|
}
|
|
@@ -5737,4 +5812,4 @@ export {
|
|
|
5737
5812
|
DuckLakeStorage
|
|
5738
5813
|
};
|
|
5739
5814
|
|
|
5740
|
-
//# debugId=
|
|
5815
|
+
//# debugId=8FEE4C4A10B2BA3064756E2164756E21
|