@sixb/ducklake 0.1.3 → 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 +17 -1
- package/dist/index.js +133 -46
- package/dist/index.js.map +11 -11
- 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-write-coordinator.d.ts +2 -2
- 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-write-coordinator.ts +79 -38
- 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 {
|
|
@@ -40,7 +47,7 @@ import { parseCommitMetadata, type SixbCommitMetadata } from "./versions"
|
|
|
40
47
|
export interface DuckLakeCommitDatasetVersionInput {
|
|
41
48
|
readonly dataset: DatasetDefinition
|
|
42
49
|
readonly mode: DatasetWriteMode
|
|
43
|
-
readonly expectedLatestVersionId?: string
|
|
50
|
+
readonly expectedLatestVersionId?: string | null
|
|
44
51
|
readonly commitMessage: string
|
|
45
52
|
readonly producer?: BeginDatasetWriteInput["producer"]
|
|
46
53
|
readonly inputs?: BeginDatasetWriteInput["inputs"]
|
|
@@ -55,6 +62,7 @@ interface DuckLakeCommitVersionOutcomeInput
|
|
|
55
62
|
readonly mode: DatasetWriteMode | "merge"
|
|
56
63
|
readonly expectedLatestVersionId?: string | null
|
|
57
64
|
readonly allowInitialNoOp?: boolean
|
|
65
|
+
readonly signal?: AbortSignal
|
|
58
66
|
readonly disableRetries?: boolean
|
|
59
67
|
readonly optimisticOperation?: "commit" | "merge commit"
|
|
60
68
|
}
|
|
@@ -93,6 +101,7 @@ export class DuckLakeWriteCoordinator {
|
|
|
93
101
|
}
|
|
94
102
|
|
|
95
103
|
this.datasets.assertSchema(definition)
|
|
104
|
+
assertUnsequencedDatasetWrite(definition)
|
|
96
105
|
const mode = input.mode ?? "snapshot"
|
|
97
106
|
assertDatasetWriteMode(mode, "write")
|
|
98
107
|
|
|
@@ -133,7 +142,7 @@ export class DuckLakeWriteCoordinator {
|
|
|
133
142
|
input.expectedLatestVersionId !== undefined &&
|
|
134
143
|
latestVersion?.versionId !== input.expectedLatestVersionId
|
|
135
144
|
) {
|
|
136
|
-
throw new
|
|
145
|
+
throw new LakeConcurrencyError(
|
|
137
146
|
`[SixbDuckLake] Optimistic merge start failed for dataset '${definition.id}': expected latest version '${input.expectedLatestVersionId}', found '${latestVersion?.versionId ?? "none"}'.`
|
|
138
147
|
)
|
|
139
148
|
}
|
|
@@ -182,35 +191,43 @@ export class DuckLakeWriteCoordinator {
|
|
|
182
191
|
)
|
|
183
192
|
}
|
|
184
193
|
|
|
185
|
-
return
|
|
186
|
-
this.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
+
})
|
|
214
231
|
})
|
|
215
232
|
)
|
|
216
233
|
}
|
|
@@ -235,6 +252,7 @@ export class DuckLakeWriteCoordinator {
|
|
|
235
252
|
runtime: DuckDbQueryRuntime,
|
|
236
253
|
input: DuckLakeCommitDatasetVersionInput
|
|
237
254
|
): Promise<DatasetWriteCommitResult> {
|
|
255
|
+
assertUnsequencedDatasetWrite(input.dataset)
|
|
238
256
|
const result = await this.commitVersionOutcomeOnExclusiveRuntime(runtime, {
|
|
239
257
|
...input,
|
|
240
258
|
// DuckLake has no primary-key constraint. Do not let its automatic retry replay a keyed
|
|
@@ -332,7 +350,8 @@ export class DuckLakeWriteCoordinator {
|
|
|
332
350
|
previousRowCount: latestVersion?.rowCount,
|
|
333
351
|
validatedPrimaryKeyColumns: latestVersion?.validatedPrimaryKeyColumns,
|
|
334
352
|
})
|
|
335
|
-
|
|
353
|
+
input.signal?.throwIfAborted()
|
|
354
|
+
await this.ensureNoOpHasSnapshot(input, changeResult, latestVersion)
|
|
336
355
|
const commitId = randomUUID()
|
|
337
356
|
await this.setCommitMetadata(
|
|
338
357
|
input,
|
|
@@ -358,6 +377,24 @@ export class DuckLakeWriteCoordinator {
|
|
|
358
377
|
} catch (error) {
|
|
359
378
|
if (!committed) {
|
|
360
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
|
+
}
|
|
361
398
|
}
|
|
362
399
|
throw error
|
|
363
400
|
}
|
|
@@ -487,26 +524,30 @@ export class DuckLakeWriteCoordinator {
|
|
|
487
524
|
)
|
|
488
525
|
}
|
|
489
526
|
|
|
490
|
-
private async
|
|
527
|
+
private async ensureNoOpHasSnapshot(
|
|
491
528
|
input: DuckLakeCommitVersionOutcomeRuntimeInput,
|
|
492
529
|
changeResult: ApplyDatasetRowsResult,
|
|
493
530
|
knownLatestVersion: DuckLakeVersionSummary | null
|
|
494
531
|
): Promise<void> {
|
|
495
|
-
if (changeResult.dataChangeExpected
|
|
532
|
+
if (changeResult.dataChangeExpected) {
|
|
496
533
|
return
|
|
497
534
|
}
|
|
498
535
|
|
|
499
536
|
const latestVersion =
|
|
500
537
|
knownLatestVersion ??
|
|
501
538
|
(await this.snapshots.getLatestVersionSummaryForDefinition(input.runtime, input.dataset))
|
|
502
|
-
if (
|
|
539
|
+
if (
|
|
540
|
+
latestVersion
|
|
541
|
+
? input.mode === "merge" || !hasDatasetInputChanges(latestVersion.inputs, input.inputs)
|
|
542
|
+
: input.allowInitialNoOp
|
|
543
|
+
) {
|
|
503
544
|
return
|
|
504
545
|
}
|
|
505
546
|
|
|
506
547
|
// DuckLake 1.5.2 does not create a snapshot for a transaction whose data and metadata are
|
|
507
548
|
// unchanged, and set_commit_message alone cannot force one. Reapplying the current table
|
|
508
549
|
// comment is an idempotent metadata write: it leaves the visible DatasetDefinition unchanged
|
|
509
|
-
// while giving
|
|
550
|
+
// while giving initial empty writes and changed input lineage a real DuckLake snapshot.
|
|
510
551
|
const table = qualifiedTableName(this.options, encodeDatasetTableName(input.dataset.id))
|
|
511
552
|
const descriptionSql =
|
|
512
553
|
input.dataset.description === undefined ? "NULL" : quoteSqlString(input.dataset.description)
|
|
@@ -633,7 +674,7 @@ export class DuckLakeWriteCoordinator {
|
|
|
633
674
|
}
|
|
634
675
|
|
|
635
676
|
if (input.actualLatestVersionId !== input.expectedLatestVersionId) {
|
|
636
|
-
throw new
|
|
677
|
+
throw new LakeConcurrencyError(
|
|
637
678
|
`[SixbDuckLake] Optimistic ${input.operation} failed for dataset '${input.datasetId}': expected latest version '${input.expectedLatestVersionId ?? "none"}', found '${input.actualLatestVersionId ?? "none"}'.`
|
|
638
679
|
)
|
|
639
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
|
)
|