@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
|
@@ -10,7 +10,14 @@ import type {
|
|
|
10
10
|
LakeMergeSession,
|
|
11
11
|
LakeWriteSession,
|
|
12
12
|
} from "@sixb/core/lake-storage"
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
assertUnsequencedDatasetWrite,
|
|
15
|
+
getDatasetPrimaryKeyColumns,
|
|
16
|
+
hasDatasetInputChanges,
|
|
17
|
+
LakeConcurrencyError,
|
|
18
|
+
LakeStorageError,
|
|
19
|
+
retryDatasetMergeCommit,
|
|
20
|
+
} from "@sixb/core/lake-storage"
|
|
14
21
|
import type { DuckLakeStorageOptions } from "../types"
|
|
15
22
|
import { localCatalogCoordinationKey } from "./catalog-key"
|
|
16
23
|
import {
|
|
@@ -27,13 +34,20 @@ import type { DuckLakeDatasetCatalog } from "./ducklake-dataset-catalog"
|
|
|
27
34
|
import { createDuckLakeMergeSession, type DuckLakeCommitMergeInput } from "./ducklake-merge-session"
|
|
28
35
|
import type { DuckLakeSnapshotReader, DuckLakeVersionSummary } from "./ducklake-snapshot-reader"
|
|
29
36
|
import { createDuckLakeWriteSession, type DuckLakeCommitWriteInput } from "./ducklake-write-session"
|
|
30
|
-
import {
|
|
37
|
+
import { encodeDatasetTableName } from "./names"
|
|
38
|
+
import {
|
|
39
|
+
duckLakeAlias,
|
|
40
|
+
duckLakeMetadataTableName,
|
|
41
|
+
qualifiedTableName,
|
|
42
|
+
quoteIdentifier,
|
|
43
|
+
quoteSqlString,
|
|
44
|
+
} from "./sql"
|
|
31
45
|
import { parseCommitMetadata, type SixbCommitMetadata } from "./versions"
|
|
32
46
|
|
|
33
47
|
export interface DuckLakeCommitDatasetVersionInput {
|
|
34
48
|
readonly dataset: DatasetDefinition
|
|
35
49
|
readonly mode: DatasetWriteMode
|
|
36
|
-
readonly expectedLatestVersionId?: string
|
|
50
|
+
readonly expectedLatestVersionId?: string | null
|
|
37
51
|
readonly commitMessage: string
|
|
38
52
|
readonly producer?: BeginDatasetWriteInput["producer"]
|
|
39
53
|
readonly inputs?: BeginDatasetWriteInput["inputs"]
|
|
@@ -48,6 +62,7 @@ interface DuckLakeCommitVersionOutcomeInput
|
|
|
48
62
|
readonly mode: DatasetWriteMode | "merge"
|
|
49
63
|
readonly expectedLatestVersionId?: string | null
|
|
50
64
|
readonly allowInitialNoOp?: boolean
|
|
65
|
+
readonly signal?: AbortSignal
|
|
51
66
|
readonly disableRetries?: boolean
|
|
52
67
|
readonly optimisticOperation?: "commit" | "merge commit"
|
|
53
68
|
}
|
|
@@ -86,6 +101,7 @@ export class DuckLakeWriteCoordinator {
|
|
|
86
101
|
}
|
|
87
102
|
|
|
88
103
|
this.datasets.assertSchema(definition)
|
|
104
|
+
assertUnsequencedDatasetWrite(definition)
|
|
89
105
|
const mode = input.mode ?? "snapshot"
|
|
90
106
|
assertDatasetWriteMode(mode, "write")
|
|
91
107
|
|
|
@@ -126,7 +142,7 @@ export class DuckLakeWriteCoordinator {
|
|
|
126
142
|
input.expectedLatestVersionId !== undefined &&
|
|
127
143
|
latestVersion?.versionId !== input.expectedLatestVersionId
|
|
128
144
|
) {
|
|
129
|
-
throw new
|
|
145
|
+
throw new LakeConcurrencyError(
|
|
130
146
|
`[SixbDuckLake] Optimistic merge start failed for dataset '${definition.id}': expected latest version '${input.expectedLatestVersionId}', found '${latestVersion?.versionId ?? "none"}'.`
|
|
131
147
|
)
|
|
132
148
|
}
|
|
@@ -175,35 +191,43 @@ export class DuckLakeWriteCoordinator {
|
|
|
175
191
|
)
|
|
176
192
|
}
|
|
177
193
|
|
|
178
|
-
return
|
|
179
|
-
this.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
194
|
+
return retryDatasetMergeCommit(input.merge, input.commit, (rebase) =>
|
|
195
|
+
this.withCommitRuntime(async (runtime) => {
|
|
196
|
+
input.commit?.signal?.throwIfAborted()
|
|
197
|
+
const baseVersionId = rebase
|
|
198
|
+
? ((await this.snapshots.getLatestVersionForDefinition(runtime, definition))?.versionId ??
|
|
199
|
+
null)
|
|
200
|
+
: input.baseVersionId
|
|
201
|
+
return this.commitVersionOutcomeOnExclusiveRuntime(runtime, {
|
|
202
|
+
dataset: definition,
|
|
203
|
+
mode: "merge",
|
|
204
|
+
expectedLatestVersionId: baseVersionId,
|
|
205
|
+
allowInitialNoOp: !input.commit?.createInitialVersion,
|
|
206
|
+
signal: input.commit?.signal,
|
|
207
|
+
disableRetries: true,
|
|
208
|
+
optimisticOperation: "merge commit",
|
|
209
|
+
commitMessage: input.commit?.commitMessage ?? `merge dataset ${definition.id}`,
|
|
210
|
+
producer: input.merge.producer,
|
|
211
|
+
inputs: input.merge.inputs,
|
|
212
|
+
applyChanges: async (commitRuntime, context) => {
|
|
213
|
+
const result = await applyDatasetMergeFromRelation({
|
|
214
|
+
options: this.options,
|
|
215
|
+
runtime: commitRuntime,
|
|
216
|
+
dataset: definition,
|
|
217
|
+
stagingTableName: input.stagingTableName,
|
|
218
|
+
sequenceColumnName: input.sequenceColumnName,
|
|
219
|
+
kindColumnName: input.kindColumnName,
|
|
220
|
+
previousRowCount: context.previousRowCount,
|
|
221
|
+
validatedPrimaryKeyColumns: context.validatedPrimaryKeyColumns,
|
|
222
|
+
})
|
|
223
|
+
if (result.sourceRowCount !== input.changesWritten) {
|
|
224
|
+
throw new LakeStorageError(
|
|
225
|
+
`[SixbDuckLake] Staged merge for dataset '${definition.id}' accepted ${input.changesWritten} change(s), but DuckLake saw ${result.sourceRowCount} source change(s) at commit time.`
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
return result
|
|
229
|
+
},
|
|
230
|
+
})
|
|
207
231
|
})
|
|
208
232
|
)
|
|
209
233
|
}
|
|
@@ -228,6 +252,7 @@ export class DuckLakeWriteCoordinator {
|
|
|
228
252
|
runtime: DuckDbQueryRuntime,
|
|
229
253
|
input: DuckLakeCommitDatasetVersionInput
|
|
230
254
|
): Promise<DatasetWriteCommitResult> {
|
|
255
|
+
assertUnsequencedDatasetWrite(input.dataset)
|
|
231
256
|
const result = await this.commitVersionOutcomeOnExclusiveRuntime(runtime, {
|
|
232
257
|
...input,
|
|
233
258
|
// DuckLake has no primary-key constraint. Do not let its automatic retry replay a keyed
|
|
@@ -325,6 +350,8 @@ export class DuckLakeWriteCoordinator {
|
|
|
325
350
|
previousRowCount: latestVersion?.rowCount,
|
|
326
351
|
validatedPrimaryKeyColumns: latestVersion?.validatedPrimaryKeyColumns,
|
|
327
352
|
})
|
|
353
|
+
input.signal?.throwIfAborted()
|
|
354
|
+
await this.ensureNoOpHasSnapshot(input, changeResult, latestVersion)
|
|
328
355
|
const commitId = randomUUID()
|
|
329
356
|
await this.setCommitMetadata(
|
|
330
357
|
input,
|
|
@@ -350,6 +377,24 @@ export class DuckLakeWriteCoordinator {
|
|
|
350
377
|
} catch (error) {
|
|
351
378
|
if (!committed) {
|
|
352
379
|
await this.rollbackTransaction(input.runtime)
|
|
380
|
+
// DuckLake's snapshot-id uniqueness constraint is its catalog commit CAS. With native
|
|
381
|
+
// retries disabled, this error proves the transaction lost to another writer. Do not
|
|
382
|
+
// classify generic connection/COMMIT errors: their durable outcome may be ambiguous.
|
|
383
|
+
if (
|
|
384
|
+
error instanceof Error &&
|
|
385
|
+
error.message.startsWith(
|
|
386
|
+
"TransactionContext Error: Failed to commit: Failed to commit DuckLake transaction."
|
|
387
|
+
) &&
|
|
388
|
+
error.message.includes("Exceeded the maximum retry count of 0") &&
|
|
389
|
+
error.message.includes(
|
|
390
|
+
'duplicate key value violates unique constraint "ducklake_snapshot_pkey"'
|
|
391
|
+
)
|
|
392
|
+
) {
|
|
393
|
+
throw new LakeConcurrencyError(
|
|
394
|
+
`[SixbDuckLake] Concurrent catalog commit for dataset '${input.dataset.id}'.`,
|
|
395
|
+
{ cause: error }
|
|
396
|
+
)
|
|
397
|
+
}
|
|
353
398
|
}
|
|
354
399
|
throw error
|
|
355
400
|
}
|
|
@@ -479,6 +524,36 @@ export class DuckLakeWriteCoordinator {
|
|
|
479
524
|
)
|
|
480
525
|
}
|
|
481
526
|
|
|
527
|
+
private async ensureNoOpHasSnapshot(
|
|
528
|
+
input: DuckLakeCommitVersionOutcomeRuntimeInput,
|
|
529
|
+
changeResult: ApplyDatasetRowsResult,
|
|
530
|
+
knownLatestVersion: DuckLakeVersionSummary | null
|
|
531
|
+
): Promise<void> {
|
|
532
|
+
if (changeResult.dataChangeExpected) {
|
|
533
|
+
return
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const latestVersion =
|
|
537
|
+
knownLatestVersion ??
|
|
538
|
+
(await this.snapshots.getLatestVersionSummaryForDefinition(input.runtime, input.dataset))
|
|
539
|
+
if (
|
|
540
|
+
latestVersion
|
|
541
|
+
? input.mode === "merge" || !hasDatasetInputChanges(latestVersion.inputs, input.inputs)
|
|
542
|
+
: input.allowInitialNoOp
|
|
543
|
+
) {
|
|
544
|
+
return
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// DuckLake 1.5.2 does not create a snapshot for a transaction whose data and metadata are
|
|
548
|
+
// unchanged, and set_commit_message alone cannot force one. Reapplying the current table
|
|
549
|
+
// comment is an idempotent metadata write: it leaves the visible DatasetDefinition unchanged
|
|
550
|
+
// while giving initial empty writes and changed input lineage a real DuckLake snapshot.
|
|
551
|
+
const table = qualifiedTableName(this.options, encodeDatasetTableName(input.dataset.id))
|
|
552
|
+
const descriptionSql =
|
|
553
|
+
input.dataset.description === undefined ? "NULL" : quoteSqlString(input.dataset.description)
|
|
554
|
+
await input.runtime.run(`COMMENT ON TABLE ${table} IS ${descriptionSql}`)
|
|
555
|
+
}
|
|
556
|
+
|
|
482
557
|
private async versionForNoOpCommit(
|
|
483
558
|
runtime: DuckDbQueryRuntime,
|
|
484
559
|
definition: DatasetDefinition,
|
|
@@ -599,7 +674,7 @@ export class DuckLakeWriteCoordinator {
|
|
|
599
674
|
}
|
|
600
675
|
|
|
601
676
|
if (input.actualLatestVersionId !== input.expectedLatestVersionId) {
|
|
602
|
-
throw new
|
|
677
|
+
throw new LakeConcurrencyError(
|
|
603
678
|
`[SixbDuckLake] Optimistic ${input.operation} failed for dataset '${input.datasetId}': expected latest version '${input.expectedLatestVersionId ?? "none"}', found '${input.actualLatestVersionId ?? "none"}'.`
|
|
604
679
|
)
|
|
605
680
|
}
|
package/src/internal/names.ts
CHANGED
|
@@ -99,6 +99,11 @@ export function encodeDatasetTableName(datasetId: string): string {
|
|
|
99
99
|
return `${DATASET_TABLE_PREFIX}${encodeDatasetId(datasetId)}`
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/** Current source-ordering state, deliberately outside the dataset table namespace. */
|
|
103
|
+
export function encodeDatasetSequenceTableName(datasetId: string): string {
|
|
104
|
+
return `sixb__seq__${encodeDatasetTableName(datasetId)}`
|
|
105
|
+
}
|
|
106
|
+
|
|
102
107
|
/**
|
|
103
108
|
* Decode a physical DuckLake table name back to a Sixb dataset id.
|
|
104
109
|
*/
|
package/src/internal/versions.ts
CHANGED
|
@@ -113,7 +113,7 @@ function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
|
113
113
|
function isDatasetProducer(value: unknown): value is NonNullable<DatasetVersion["producer"]> {
|
|
114
114
|
return (
|
|
115
115
|
isRecord(value) &&
|
|
116
|
-
(value.kind === "sync" || value.kind === "pipeline") &&
|
|
116
|
+
(value.kind === "sync" || value.kind === "pipeline" || value.kind === "ingest") &&
|
|
117
117
|
optionalString(value.id) &&
|
|
118
118
|
optionalString(value.runId)
|
|
119
119
|
)
|