@sixb/ducklake 0.1.6 → 0.1.7

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.
@@ -18,7 +18,7 @@ import {
18
18
  LakeStorageError,
19
19
  retryDatasetMergeCommit,
20
20
  } from "@sixb/core/lake-storage"
21
- import type { DuckLakeStorageOptions } from "../types"
21
+ import type { DuckLakeCatalogOptions, DuckLakeStorageOptions } from "../types"
22
22
  import { localCatalogCoordinationKey } from "./catalog-key"
23
23
  import {
24
24
  type ApplyDatasetRowsResult,
@@ -330,13 +330,12 @@ export class DuckLakeWriteCoordinator {
330
330
  // which snapshot belongs to this transaction.
331
331
  const previousWriteSnapshotId = await this.lastCommittedSnapshotId(input.runtime)
332
332
 
333
- await input.runtime.run("BEGIN TRANSACTION")
334
333
  let committed = false
335
334
  try {
336
335
  // Guarded Sixb commits need compare-and-swap semantics. DuckLake can
337
336
  // automatically retry non-conflicting commits, so guarded transactions
338
- // disable retries and re-check the dataset head after BEGIN.
339
- const latestVersion = await this.latestVersionForCommit(input)
337
+ // disable retries and validate a head belonging to the native write snapshot.
338
+ const latestVersion = await this.beginVersionTransaction(input)
340
339
  if (input.expectedLatestVersionId !== undefined) {
341
340
  this.assertExpectedLatestVersion({
342
341
  datasetId: input.dataset.id,
@@ -527,15 +526,16 @@ export class DuckLakeWriteCoordinator {
527
526
  private async ensureNoOpHasSnapshot(
528
527
  input: DuckLakeCommitVersionOutcomeRuntimeInput,
529
528
  changeResult: ApplyDatasetRowsResult,
530
- knownLatestVersion: DuckLakeVersionSummary | null
529
+ knownLatestVersion: DuckLakeVersionSummary | null | undefined
531
530
  ): Promise<void> {
532
531
  if (changeResult.dataChangeExpected) {
533
532
  return
534
533
  }
535
534
 
536
535
  const latestVersion =
537
- knownLatestVersion ??
538
- (await this.snapshots.getLatestVersionSummaryForDefinition(input.runtime, input.dataset))
536
+ knownLatestVersion === undefined
537
+ ? await this.snapshots.getLatestVersionSummaryForDefinition(input.runtime, input.dataset)
538
+ : knownLatestVersion
539
539
  if (
540
540
  latestVersion
541
541
  ? input.mode === "merge" || !hasDatasetInputChanges(latestVersion.inputs, input.inputs)
@@ -582,14 +582,49 @@ export class DuckLakeWriteCoordinator {
582
582
  }
583
583
  }
584
584
 
585
- private async latestVersionForCommit(
585
+ private async beginVersionTransaction(
586
586
  input: DuckLakeCommitVersionOutcomeRuntimeInput
587
- ): Promise<DuckLakeVersionSummary | null> {
588
- if (input.expectedLatestVersionId === undefined && input.mode !== "append") {
589
- return null
587
+ ): Promise<DuckLakeVersionSummary | null | undefined> {
588
+ if (!isSqliteCatalog(this.options.catalog)) {
589
+ await input.runtime.run("BEGIN TRANSACTION")
590
+ if (input.expectedLatestVersionId === undefined && input.mode !== "append") {
591
+ return undefined
592
+ }
593
+ return this.snapshots.getLatestVersionSummaryForDefinition(input.runtime, input.dataset)
590
594
  }
591
595
 
592
- return this.snapshots.getLatestVersionSummaryForDefinition(input.runtime, input.dataset)
596
+ // Direct metadata reads enlist a second SQLite transaction on the outer DuckDB connection.
597
+ // Its read lock blocks DuckLake's own metadata connection at COMMIT. Read in autocommit,
598
+ // then pin the native DuckLake snapshot and verify nothing changed in between. The summary
599
+ // is now valid for this write transaction, including null (no previous version).
600
+ // Only preparation can repeat: staged mutations and COMMIT are never replayed here.
601
+ for (let attempt = 0; attempt < 3; attempt++) {
602
+ input.signal?.throwIfAborted()
603
+ const snapshotId = await this.currentSnapshotId(input.runtime)
604
+ const latestVersion = await this.snapshots.getLatestVersionSummaryForDefinition(
605
+ input.runtime,
606
+ input.dataset
607
+ )
608
+ await input.runtime.run("BEGIN TRANSACTION")
609
+ if ((await this.currentSnapshotId(input.runtime)) === snapshotId) {
610
+ return latestVersion
611
+ }
612
+ await input.runtime.run("ROLLBACK")
613
+ }
614
+
615
+ throw new LakeConcurrencyError(
616
+ `[SixbDuckLake] Catalog changed during three write preparations for dataset '${input.dataset.id}'. Retry the write.`
617
+ )
618
+ }
619
+
620
+ private async currentSnapshotId(runtime: DuckDbQueryRuntime): Promise<string> {
621
+ const [row] = await runtime.query(
622
+ `SELECT id FROM ${quoteIdentifier(duckLakeAlias(this.options))}.current_snapshot()`
623
+ )
624
+ if (row === undefined) {
625
+ throw new LakeStorageError("[SixbDuckLake] DuckLake did not return a current snapshot.")
626
+ }
627
+ return String(getBigIntLike(row, "id"))
593
628
  }
594
629
 
595
630
  private async lastCommittedSnapshotId(runtime: DuckDbQueryRuntime): Promise<string | null> {
@@ -681,15 +716,21 @@ export class DuckLakeWriteCoordinator {
681
716
  }
682
717
  }
683
718
 
719
+ function isSqliteCatalog(catalog: DuckLakeCatalogOptions): boolean {
720
+ return (
721
+ catalog.type === "sqlite" ||
722
+ (catalog.type === "custom" && catalog.uri.toLowerCase().startsWith("sqlite:"))
723
+ )
724
+ }
725
+
684
726
  function commitRowCount(
685
727
  input: Pick<DuckLakeCommitVersionOutcomeInput, "expectedLatestVersionId" | "mode">,
686
728
  changeResult: ApplyDatasetRowsResult,
687
- latestVersion: DuckLakeVersionSummary | null
729
+ latestVersion: DuckLakeVersionSummary | null | undefined
688
730
  ): CommitRowCount {
689
731
  // Unguarded append commits can be retried by DuckLake after this transaction
690
732
  // read the previous latest version. In that case previousRowCount +
691
- // sourceRowCount can underreport the committed snapshot, so omit metadata
692
- // and let hydration count the exact committed snapshot instead.
733
+ // sourceRowCount can underreport the committed snapshot, so leave the count unknown.
693
734
  if (input.mode === "append" && input.expectedLatestVersionId === undefined) {
694
735
  return { kind: "unknown" }
695
736
  }
@@ -183,6 +183,7 @@ class DuckLakeWriteSession implements LakeWriteSession {
183
183
  }
184
184
 
185
185
  this.cleanedUp = true
186
+ this.stagedPrimaryKeys.clear()
186
187
  await this.dropStagingTable()
187
188
  }
188
189
 
package/src/types.ts CHANGED
@@ -151,6 +151,8 @@ export interface DuckLakeStorageOptions {
151
151
  readonly alias?: string
152
152
  readonly duckdb?: DuckDbRuntimeOptions
153
153
  readonly postgresPool?: DuckLakePostgresPoolOptions
154
+ /** Maximum native streaming readers per provider; extra reads use pages. Default: 4. */
155
+ readonly maxStreamingReads?: number
154
156
  readonly secrets?: readonly DuckDbSecretOptions[]
155
157
  /** Runs after DuckLake/catalog extensions load and before ATTACH. */
156
158
  readonly setupSql?: readonly string[]