@sixb/ducklake 0.1.5 → 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.
Files changed (34) hide show
  1. package/README.md +24 -26
  2. package/dist/ducklake-storage.d.ts +3 -1
  3. package/dist/ducklake-storage.d.ts.map +1 -1
  4. package/dist/index.js +1100 -661
  5. package/dist/index.js.map +22 -19
  6. package/dist/internal/duckdb-reader.d.ts +28 -0
  7. package/dist/internal/duckdb-reader.d.ts.map +1 -0
  8. package/dist/internal/duckdb-runtime.d.ts +5 -3
  9. package/dist/internal/duckdb-runtime.d.ts.map +1 -1
  10. package/dist/internal/ducklake-change-reader.d.ts +13 -0
  11. package/dist/internal/ducklake-change-reader.d.ts.map +1 -0
  12. package/dist/internal/ducklake-connection-manager.d.ts +19 -3
  13. package/dist/internal/ducklake-connection-manager.d.ts.map +1 -1
  14. package/dist/internal/ducklake-read-gate.d.ts +11 -0
  15. package/dist/internal/ducklake-read-gate.d.ts.map +1 -0
  16. package/dist/internal/ducklake-row-reader.d.ts.map +1 -1
  17. package/dist/internal/ducklake-snapshot-reader.d.ts.map +1 -1
  18. package/dist/internal/ducklake-write-coordinator.d.ts +2 -1
  19. package/dist/internal/ducklake-write-coordinator.d.ts.map +1 -1
  20. package/dist/types.d.ts +2 -0
  21. package/dist/types.d.ts.map +1 -1
  22. package/package.json +4 -4
  23. package/src/ducklake-storage.ts +13 -0
  24. package/src/internal/duckdb-reader.ts +129 -0
  25. package/src/internal/duckdb-runtime.ts +75 -51
  26. package/src/internal/ducklake-change-reader.ts +157 -0
  27. package/src/internal/ducklake-connection-manager.ts +117 -33
  28. package/src/internal/ducklake-maintenance.ts +1 -1
  29. package/src/internal/ducklake-read-gate.ts +58 -0
  30. package/src/internal/ducklake-row-reader.ts +84 -63
  31. package/src/internal/ducklake-snapshot-reader.ts +7 -2
  32. package/src/internal/ducklake-write-coordinator.ts +56 -15
  33. package/src/internal/ducklake-write-session.ts +1 -0
  34. package/src/types.ts +2 -0
package/dist/index.js CHANGED
@@ -1,52 +1,145 @@
1
1
  // @bun
2
- // src/internal/ducklake-connection-manager.ts
3
- import { LakeStorageError as LakeStorageError4 } from "@sixb/core/lake-storage";
2
+ // src/internal/ducklake-change-reader.ts
3
+ import { randomUUID } from "crypto";
4
+ import {
5
+ LakeStorageError as LakeStorageError7,
6
+ resolveDatasetChangeColumns
7
+ } from "@sixb/core/lake-storage";
4
8
 
5
- // src/internal/catalog-key.ts
6
- import { resolve } from "path";
7
- function localCatalogCoordinationKey(options) {
8
- switch (options.catalog.type) {
9
- case "duckdb":
10
- case "sqlite":
11
- if (options.catalog.path === ":memory:") {
12
- return;
13
- }
14
- return `${options.catalog.type}:${resolve(options.catalog.path)}`;
15
- case "custom":
16
- return `custom:${options.catalog.uri}`;
17
- case "postgres":
18
- return;
9
+ // src/internal/duckdb-row.ts
10
+ import { LakeStorageError } from "@sixb/core/lake-storage";
11
+ function getString(row, key) {
12
+ const value = row[key];
13
+ if (typeof value !== "string") {
14
+ throw new LakeStorageError(`[SixbDuckLake] Expected DuckDB column '${key}' to be a string, got ${typeof value}.`);
19
15
  }
16
+ return value;
17
+ }
18
+ function getOptionalString(row, key) {
19
+ const value = row[key];
20
+ if (value === null || value === undefined) {
21
+ return;
22
+ }
23
+ if (typeof value !== "string") {
24
+ throw new LakeStorageError(`[SixbDuckLake] Expected DuckDB column '${key}' to be a string, got ${typeof value}.`);
25
+ }
26
+ return value;
27
+ }
28
+ function getBoolean(row, key) {
29
+ const value = row[key];
30
+ if (typeof value !== "boolean") {
31
+ throw new LakeStorageError(`[SixbDuckLake] Expected DuckDB column '${key}' to be a boolean, got ${typeof value}.`);
32
+ }
33
+ return value;
34
+ }
35
+ function getBigIntLike(row, key) {
36
+ const value = row[key];
37
+ if (typeof value === "bigint") {
38
+ return value;
39
+ }
40
+ if (typeof value === "number" && Number.isInteger(value)) {
41
+ return BigInt(value);
42
+ }
43
+ if (typeof value === "string" && /^-?\d+$/.test(value)) {
44
+ return BigInt(value);
45
+ }
46
+ throw new LakeStorageError(`[SixbDuckLake] Expected DuckDB column '${key}' to be an integer, got ${typeof value}.`);
47
+ }
48
+ function getOptionalBigIntLike(row, key) {
49
+ const value = row[key];
50
+ if (value === null || value === undefined) {
51
+ return;
52
+ }
53
+ return getBigIntLike(row, key);
54
+ }
55
+ function getDate(row, key) {
56
+ const value = row[key];
57
+ if (value instanceof Date) {
58
+ return value;
59
+ }
60
+ if (typeof value === "string") {
61
+ return new Date(value);
62
+ }
63
+ throw new LakeStorageError(`[SixbDuckLake] Expected DuckDB column '${key}' to be a date, got ${typeof value}.`);
20
64
  }
21
65
 
22
- // src/internal/duckdb-runtime.ts
23
- import {
24
- DuckDBInstance
25
- } from "@duckdb/node-api";
26
- import { LakeStorageError as LakeStorageError3 } from "@sixb/core/lake-storage";
27
-
28
- // src/internal/duckdb-value-converter.ts
29
- import {
30
- DuckDBDecimalValue,
31
- DuckDBTypeId,
32
- JSDuckDBValueConverter
33
- } from "@duckdb/node-api";
34
- import { LakeStorageError } from "@sixb/core/lake-storage";
35
- var sixbDuckDbValueConverter = (value, type, converter) => {
36
- if (value === null) {
66
+ // src/internal/names.ts
67
+ import { LakeStorageError as LakeStorageError2 } from "@sixb/core/lake-storage";
68
+ var DATASET_TABLE_PREFIX = "sixb__ds__";
69
+ var textEncoder = new TextEncoder;
70
+ var textDecoder = new TextDecoder("utf-8", { fatal: true });
71
+ function isInlineByte(byte) {
72
+ return byte >= 97 && byte <= 122 || byte >= 48 && byte <= 57;
73
+ }
74
+ function isHexPair(value) {
75
+ return /^[0-9a-f]{2}$/.test(value);
76
+ }
77
+ function encodeDatasetId(datasetId) {
78
+ if (datasetId.trim().length === 0) {
79
+ throw new LakeStorageError2("[SixbDuckLake] Dataset id must not be empty.");
80
+ }
81
+ let encoded = "";
82
+ for (const byte of textEncoder.encode(datasetId)) {
83
+ if (isInlineByte(byte)) {
84
+ encoded += String.fromCharCode(byte);
85
+ } else if (byte === 46) {
86
+ encoded += "__";
87
+ } else {
88
+ encoded += `_${byte.toString(16).padStart(2, "0")}`;
89
+ }
90
+ }
91
+ return encoded;
92
+ }
93
+ function decodeDatasetId(encoded) {
94
+ if (encoded.length === 0) {
37
95
  return null;
38
96
  }
39
- if (type.typeId === DuckDBTypeId.DECIMAL) {
40
- if (!(value instanceof DuckDBDecimalValue)) {
41
- throw new LakeStorageError("[SixbDuckLake] DuckDB returned an invalid DECIMAL value.");
97
+ const bytes = [];
98
+ let index = 0;
99
+ while (index < encoded.length) {
100
+ const char = encoded[index];
101
+ const byte = char.charCodeAt(0);
102
+ if (isInlineByte(byte)) {
103
+ bytes.push(byte);
104
+ index += 1;
105
+ continue;
42
106
  }
43
- return value.toString();
107
+ if (char !== "_") {
108
+ return null;
109
+ }
110
+ if (encoded[index + 1] === "_") {
111
+ bytes.push(46);
112
+ index += 2;
113
+ continue;
114
+ }
115
+ const hex = encoded.slice(index + 1, index + 3);
116
+ if (!isHexPair(hex)) {
117
+ return null;
118
+ }
119
+ bytes.push(Number.parseInt(hex, 16));
120
+ index += 3;
44
121
  }
45
- return JSDuckDBValueConverter(value, type, converter);
46
- };
122
+ try {
123
+ return textDecoder.decode(new Uint8Array(bytes));
124
+ } catch {
125
+ return null;
126
+ }
127
+ }
128
+ function encodeDatasetTableName(datasetId) {
129
+ return `${DATASET_TABLE_PREFIX}${encodeDatasetId(datasetId)}`;
130
+ }
131
+ function encodeDatasetSequenceTableName(datasetId) {
132
+ return `sixb__seq__${encodeDatasetTableName(datasetId)}`;
133
+ }
134
+ function decodeDatasetTableName(tableName) {
135
+ if (!tableName.startsWith(DATASET_TABLE_PREFIX)) {
136
+ return null;
137
+ }
138
+ return decodeDatasetId(tableName.slice(DATASET_TABLE_PREFIX.length));
139
+ }
47
140
 
48
141
  // src/internal/sql.ts
49
- import { LakeStorageError as LakeStorageError2 } from "@sixb/core/lake-storage";
142
+ import { LakeStorageError as LakeStorageError3 } from "@sixb/core/lake-storage";
50
143
  var DEFAULT_ALIAS = "sixb_lake";
51
144
  function duckLakeAlias(options) {
52
145
  return options.alias ?? DEFAULT_ALIAS;
@@ -70,7 +163,7 @@ function duckLakeMetadataTableName(options, tableName) {
70
163
  }
71
164
  function quoteIdentifier(identifier) {
72
165
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
73
- throw new LakeStorageError2(`[SixbDuckLake] Invalid SQL identifier '${identifier}'.`);
166
+ throw new LakeStorageError3(`[SixbDuckLake] Invalid SQL identifier '${identifier}'.`);
74
167
  }
75
168
  return `"${identifier}"`;
76
169
  }
@@ -279,7 +372,7 @@ function optionalIdentifierParameter(name, value) {
279
372
  return [];
280
373
  }
281
374
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
282
- throw new LakeStorageError2(`[SixbDuckLake] Invalid SQL identifier '${value}'.`);
375
+ throw new LakeStorageError3(`[SixbDuckLake] Invalid SQL identifier '${value}'.`);
283
376
  }
284
377
  return [`${name} ${value}`];
285
378
  }
@@ -288,11 +381,11 @@ function appendPostgresConnectionParameter(parameters, names, name, value) {
288
381
  return;
289
382
  }
290
383
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
291
- throw new LakeStorageError2(`[SixbDuckLake] Invalid PostgreSQL catalog parameter '${name}'.`);
384
+ throw new LakeStorageError3(`[SixbDuckLake] Invalid PostgreSQL catalog parameter '${name}'.`);
292
385
  }
293
386
  const normalizedName = name.toLowerCase();
294
387
  if (names.has(normalizedName)) {
295
- throw new LakeStorageError2(`[SixbDuckLake] Duplicate PostgreSQL catalog parameter '${name}'.`);
388
+ throw new LakeStorageError3(`[SixbDuckLake] Duplicate PostgreSQL catalog parameter '${name}'.`);
296
389
  }
297
390
  names.add(normalizedName);
298
391
  parameters.push(`${name}=${quotePostgresConnectionValue(String(value))}`);
@@ -305,57 +398,589 @@ function pathScheme(path) {
305
398
  return match ? match[1].toLowerCase() : null;
306
399
  }
307
400
 
308
- // src/internal/duckdb-runtime.ts
309
- class NodeDuckDbRuntime {
310
- instance;
311
- connection;
312
- closing = false;
313
- closed = false;
314
- operations = Promise.resolve();
315
- constructor(instance, connection) {
316
- this.instance = instance;
317
- this.connection = connection;
401
+ // src/internal/ducklake-dataset-table-ref.ts
402
+ async function resolveDatasetTableRef(options, runtime, datasetId) {
403
+ const tableName = encodeDatasetTableName(datasetId);
404
+ const ducklakeTable = duckLakeMetadataTableName(options, "ducklake_table");
405
+ const [row] = await runtime.query(`
406
+ SELECT table_id
407
+ FROM ${ducklakeTable}
408
+ WHERE table_name = ${quoteSqlString(tableName)}
409
+ AND end_snapshot IS NULL
410
+ LIMIT 1
411
+ `);
412
+ return row === undefined ? null : {
413
+ datasetId,
414
+ tableName,
415
+ tableId: getBigIntLike(row, "table_id")
416
+ };
417
+ }
418
+
419
+ // src/internal/schema.ts
420
+ import { LakeStorageError as LakeStorageError5 } from "@sixb/core/lake-storage";
421
+
422
+ // src/internal/duckdb-column-types.ts
423
+ function normalizeDuckDbTypeSql(typeSql) {
424
+ return typeSql.trim().toUpperCase().replace(/\s+/g, " ").replace(/\s*([(),])\s*/g, "$1");
425
+ }
426
+ function defineDuckDbColumnType(sql, aliases = []) {
427
+ const acceptedTypes = new Set([sql, ...aliases].map(normalizeDuckDbTypeSql));
428
+ return {
429
+ sql,
430
+ matches: (typeSql) => acceptedTypes.has(normalizeDuckDbTypeSql(typeSql))
431
+ };
432
+ }
433
+ function defineDuckDbDecimalType(precision, scale) {
434
+ return {
435
+ ...defineDuckDbColumnType(`DECIMAL(${precision}, ${scale})`),
436
+ precision,
437
+ scale
438
+ };
439
+ }
440
+ var DUCKDB_COLUMN_TYPES = {
441
+ string: defineDuckDbColumnType("VARCHAR"),
442
+ boolean: defineDuckDbColumnType("BOOLEAN"),
443
+ int64: defineDuckDbColumnType("BIGINT", ["INT64"]),
444
+ float64: defineDuckDbColumnType("DOUBLE", ["FLOAT64"]),
445
+ decimal: defineDuckDbDecimalType(38, 9),
446
+ date: defineDuckDbColumnType("DATE"),
447
+ timestamp: defineDuckDbColumnType("TIMESTAMPTZ", ["TIMESTAMP WITH TIME ZONE"]),
448
+ json: defineDuckDbColumnType("JSON"),
449
+ fileRef: defineDuckDbColumnType("STRUCT(blobId VARCHAR, digest VARCHAR, sizeBytes BIGINT, fileName VARCHAR, mediaType VARCHAR, logicalPath VARCHAR)")
450
+ };
451
+ function findDatasetColumnTypeForDuckDbSql(typeSql) {
452
+ for (const type of Object.keys(DUCKDB_COLUMN_TYPES)) {
453
+ if (DUCKDB_COLUMN_TYPES[type].matches(typeSql)) {
454
+ return type;
455
+ }
318
456
  }
319
- async run(sql, values) {
320
- await this.enqueue(async () => {
321
- await this.connection.run(sql, values === undefined ? undefined : [...values]);
322
- });
457
+ return;
458
+ }
459
+
460
+ // src/internal/duckdb-decimal.ts
461
+ import { normalizeDecimalValue } from "@sixb/core";
462
+ import { LakeStorageError as LakeStorageError4 } from "@sixb/core/lake-storage";
463
+ var decimalType = DUCKDB_COLUMN_TYPES.decimal;
464
+ var maxIntegerDigits = decimalType.precision - decimalType.scale;
465
+ function normalizeDuckDbDecimalValue(value, columnName) {
466
+ if (typeof value !== "string") {
467
+ throw new LakeStorageError4(`[SixbDuckLake] Decimal column '${columnName}' must be an exact decimal string.`);
323
468
  }
324
- async runStatements(statements) {
325
- await this.enqueue(async () => {
326
- for (const sql of statements) {
327
- await this.connection.run(sql);
328
- }
329
- });
469
+ let normalized;
470
+ try {
471
+ normalized = normalizeDecimalValue(value);
472
+ } catch {
473
+ throw new LakeStorageError4(`[SixbDuckLake] Decimal column '${columnName}' must be an exact decimal string.`);
330
474
  }
331
- async query(sql, values) {
332
- return this.enqueue(async () => {
333
- const reader = await this.connection.runAndReadAll(sql, values === undefined ? undefined : [...values]);
334
- return reader.convertRowObjects(sixbDuckDbValueConverter);
335
- });
475
+ const unsigned = normalized.startsWith("-") ? normalized.slice(1) : normalized;
476
+ const [integer, fraction = ""] = unsigned.split(".");
477
+ if (integer.length > maxIntegerDigits || fraction.length > decimalType.scale) {
478
+ throw new LakeStorageError4(`[SixbDuckLake] Decimal column '${columnName}' cannot be represented exactly as ${decimalType.sql}; expected at most ${maxIntegerDigits} integer digits and ${decimalType.scale} fractional digits.`);
336
479
  }
337
- async* streamRows(sql, values) {
338
- this.assertAcceptingOperations();
339
- let releaseOperation;
340
- const operationFinished = new Promise((resolve) => {
341
- releaseOperation = resolve;
342
- });
343
- const waitForTurn = this.operations.then(() => {
344
- this.assertNotClosed();
345
- });
346
- this.operations = waitForTurn.then(() => operationFinished).then(() => {}, () => {});
480
+ return normalized;
481
+ }
482
+
483
+ // src/internal/schema.ts
484
+ function datasetColumnTypeToDuckDbSql(type) {
485
+ return DUCKDB_COLUMN_TYPES[type].sql;
486
+ }
487
+ function duckDbTypeToDatasetColumnType(typeSql) {
488
+ const type = findDatasetColumnTypeForDuckDbSql(typeSql);
489
+ if (type) {
490
+ return type;
491
+ }
492
+ throw new LakeStorageError5(`[SixbDuckLake] DuckDB column type '${typeSql}' cannot be mapped to a Sixb dataset column type.`);
493
+ }
494
+ function datasetColumnToDuckDbSql(column) {
495
+ const nullableSql = column.nullable ? "" : " NOT NULL";
496
+ return `${quoteIdentifier(column.name)} ${datasetColumnTypeToDuckDbSql(column.type)}${nullableSql}`;
497
+ }
498
+ function datasetSchemaToDuckDbColumnsSql(schema) {
499
+ return schema.columns.map((column) => datasetColumnToDuckDbSql(column)).join(", ");
500
+ }
501
+ function datasetSchemaToDuckDbNullableColumnsSql(schema) {
502
+ return schema.columns.map((column) => `${quoteIdentifier(column.name)} ${datasetColumnTypeToDuckDbSql(column.type)}`).join(", ");
503
+ }
504
+ function datasetSchemaColumnNamesSql(schema) {
505
+ return schema.columns.map((column) => quoteIdentifier(column.name)).join(", ");
506
+ }
507
+ function normalizeReadValue(value, column) {
508
+ if (value === null || value === undefined) {
509
+ return null;
510
+ }
511
+ switch (column.type) {
512
+ case "int64":
513
+ return typeof value === "bigint" ? value.toString() : value;
514
+ case "decimal":
515
+ return normalizeDuckDbDecimalValue(value, column.name);
516
+ case "date":
517
+ return value instanceof Date ? value.toISOString().slice(0, 10) : String(value);
518
+ case "timestamp":
519
+ return value instanceof Date ? value.toISOString() : String(value);
520
+ case "json":
521
+ return typeof value === "string" ? JSON.parse(value) : value;
522
+ case "fileRef":
523
+ return normalizeFileRef(value);
524
+ default:
525
+ return value;
526
+ }
527
+ }
528
+ function normalizeFileRef(value) {
529
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
530
+ throw new LakeStorageError5("[SixbDuckLake] DuckDB fileRef value must be a struct.");
531
+ }
532
+ const record = value;
533
+ return {
534
+ blobId: getStringField(record, "blobId"),
535
+ digest: getStringField(record, "digest"),
536
+ sizeBytes: getIntegerField(record, "sizeBytes"),
537
+ fileName: getOptionalStringField(record, "fileName"),
538
+ mediaType: getOptionalStringField(record, "mediaType"),
539
+ logicalPath: getOptionalStringField(record, "logicalPath")
540
+ };
541
+ }
542
+ function getStringField(record, key) {
543
+ const value = record[key];
544
+ if (typeof value !== "string") {
545
+ throw new LakeStorageError5(`[SixbDuckLake] DuckDB fileRef field '${key}' must be a string.`);
546
+ }
547
+ return value;
548
+ }
549
+ function getOptionalStringField(record, key) {
550
+ const value = record[key];
551
+ if (value === null || value === undefined) {
552
+ return;
553
+ }
554
+ if (typeof value !== "string") {
555
+ throw new LakeStorageError5(`[SixbDuckLake] DuckDB fileRef field '${key}' must be a string.`);
556
+ }
557
+ return value;
558
+ }
559
+ function getIntegerField(record, key) {
560
+ const value = record[key];
561
+ if (typeof value === "number" && Number.isInteger(value)) {
562
+ return value;
563
+ }
564
+ if (typeof value === "bigint") {
565
+ return Number(value);
566
+ }
567
+ throw new LakeStorageError5(`[SixbDuckLake] DuckDB fileRef field '${key}' must be an integer.`);
568
+ }
569
+
570
+ // src/internal/versions.ts
571
+ import { LakeStorageError as LakeStorageError6 } from "@sixb/core/lake-storage";
572
+ function parseVersionId(versionId) {
573
+ if (!versionId.startsWith("ducklake:")) {
574
+ throw new LakeStorageError6(`[SixbDuckLake] Invalid DuckLake version id '${versionId}'.`);
575
+ }
576
+ const snapshotId = versionId.slice("ducklake:".length);
577
+ if (!/^\d+$/.test(snapshotId)) {
578
+ throw new LakeStorageError6(`[SixbDuckLake] Invalid DuckLake version id '${versionId}'.`);
579
+ }
580
+ return snapshotId;
581
+ }
582
+ function toVersionId(snapshotId) {
583
+ return `ducklake:${snapshotId}`;
584
+ }
585
+ function parseCommitMetadata(value) {
586
+ if (typeof value !== "string") {
587
+ return;
588
+ }
589
+ let parsed;
590
+ try {
591
+ parsed = JSON.parse(value);
592
+ } catch {
593
+ return;
594
+ }
595
+ if (!isRecord(parsed) || !isRecord(parsed.sixb) || parsed.sixb.kind !== "datasetVersion" || typeof parsed.sixb.datasetId !== "string") {
596
+ return;
597
+ }
598
+ const mode = parsed.sixb.mode;
599
+ const commitId = parsed.sixb.commitId;
600
+ const rowCount = parsed.sixb.rowCount;
601
+ const validatedPrimaryKeyColumns = parsed.sixb.validatedPrimaryKeyColumns;
602
+ return {
603
+ kind: "datasetVersion",
604
+ datasetId: parsed.sixb.datasetId,
605
+ ...typeof commitId === "string" ? { commitId } : {},
606
+ ...mode === "snapshot" || mode === "append" || mode === "merge" || mode === "schema" ? { mode } : {},
607
+ ...isDatasetProducer(parsed.sixb.producer) ? { producer: parsed.sixb.producer } : {},
608
+ ...isDatasetVersionRefs(parsed.sixb.inputs) ? { inputs: parsed.sixb.inputs } : {},
609
+ ...typeof commitId === "string" && isRowCount(rowCount) ? { rowCount } : {},
610
+ ...typeof commitId === "string" && isValidatedPrimaryKeyColumns(validatedPrimaryKeyColumns) ? { validatedPrimaryKeyColumns } : {},
611
+ ...isSchemaChangeMetadata(parsed.sixb.schemaChange) ? { schemaChange: parsed.sixb.schemaChange } : {}
612
+ };
613
+ }
614
+ function parseInlineDataChange(changesMade, tableId) {
615
+ const tableIdText = tableId.toString();
616
+ let hasDataChange = false;
617
+ let hasDeleteChange = false;
618
+ for (const change of changesMade.split(",")) {
619
+ const [kind, changedTableId] = change.split(":");
620
+ if (changedTableId !== tableIdText) {
621
+ continue;
622
+ }
623
+ const normalizedKind = kind.toLowerCase();
624
+ if (normalizedKind.includes("insert") || normalizedKind.includes("delete")) {
625
+ hasDataChange = true;
626
+ }
627
+ if (normalizedKind.includes("delete")) {
628
+ hasDeleteChange = true;
629
+ }
630
+ }
631
+ return { hasDataChange, hasDeleteChange };
632
+ }
633
+ function isRecord(value) {
634
+ return typeof value === "object" && value !== null && !Array.isArray(value);
635
+ }
636
+ function isDatasetProducer(value) {
637
+ return isRecord(value) && (value.kind === "sync" || value.kind === "pipeline" || value.kind === "ingest") && optionalString(value.id) && optionalString(value.runId);
638
+ }
639
+ function isDatasetVersionRefs(value) {
640
+ return Array.isArray(value) && value.every((item) => isRecord(item) && typeof item.datasetId === "string" && typeof item.versionId === "string");
641
+ }
642
+ function isSchemaChangeMetadata(value) {
643
+ return isRecord(value) && (value.addColumns === undefined || Array.isArray(value.addColumns) && value.addColumns.every((columnName) => typeof columnName === "string"));
644
+ }
645
+ function optionalString(value) {
646
+ return value === undefined || typeof value === "string";
647
+ }
648
+ function isRowCount(value) {
649
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
650
+ }
651
+ function isValidatedPrimaryKeyColumns(value) {
652
+ return Array.isArray(value) && value.length > 0 && value.every((column) => typeof column === "string" && column.trim().length > 0) && new Set(value).size === value.length;
653
+ }
654
+
655
+ // src/internal/ducklake-change-reader.ts
656
+ var PAGE_ROWS = 5000;
657
+
658
+ class DuckLakeChangeReader {
659
+ options;
660
+ connections;
661
+ snapshots;
662
+ constructor(options, connections, snapshots) {
663
+ this.options = options;
664
+ this.connections = connections;
665
+ this.snapshots = snapshots;
666
+ }
667
+ async readChanges(input) {
668
+ input.signal?.throwIfAborted();
669
+ const before = await this.snapshots.getVersion(input.datasetId, input.fromVersionId);
670
+ const after = await this.snapshots.getVersion(input.datasetId, input.toVersionId);
671
+ const columns = resolveDatasetChangeColumns(input, before, after);
672
+ if (!columns)
673
+ return null;
674
+ const lease = await this.connections.acquireAttachedRuntime();
675
+ const runtime = lease.runtime;
676
+ const temporary = quoteIdentifier(`sixb_delta_${randomUUID().replaceAll("-", "")}`);
677
+ let retained = false;
678
+ let created = false;
679
+ try {
680
+ const ref = await resolveDatasetTableRef(this.options, runtime, input.datasetId);
681
+ if (!ref)
682
+ return null;
683
+ const table = qualifiedTableName(this.options, ref.tableName);
684
+ const oldRelation = `${table} AT (VERSION => ${parseVersionId(input.fromVersionId)})`;
685
+ const newRelation = `${table} AT (VERSION => ${parseVersionId(input.toVersionId)})`;
686
+ const previousCount = await uniqueRowCount(runtime, oldRelation, input.keyColumns);
687
+ const nextCount = await uniqueRowCount(runtime, newRelation, input.keyColumns);
688
+ if (previousCount === null || nextCount === null)
689
+ return null;
690
+ input.signal?.throwIfAborted();
691
+ const key = quoteIdentifier(input.keyColumns[0]);
692
+ const values = columns.flatMap((column, index) => [
693
+ `b.${quoteIdentifier(column.name)} AS b${index}`,
694
+ `a.${quoteIdentifier(column.name)} AS a${index}`
695
+ ]);
696
+ const join = input.keyColumns.map((name) => `b.${quoteIdentifier(name)} = a.${quoteIdentifier(name)}`).join(" AND ");
697
+ const changed = columns.map((column) => `b.${quoteIdentifier(column.name)} IS DISTINCT FROM a.${quoteIdentifier(column.name)}`).join(" OR ");
698
+ await runtime.run(`CREATE TEMP TABLE ${temporary} AS
699
+ SELECT b.${key} IS NOT NULL AS before_present, a.${key} IS NOT NULL AS after_present,
700
+ ${values.join(", ")}
701
+ FROM (SELECT * FROM ${oldRelation}) b
702
+ FULL OUTER JOIN (SELECT * FROM ${newRelation}) a ON ${join}
703
+ WHERE b.${key} IS NULL OR a.${key} IS NULL OR ${changed}`);
704
+ created = true;
705
+ const [count] = await runtime.query(`SELECT count(*) AS count FROM ${temporary}`);
706
+ const changeCount = safeCount(count, "count");
707
+ input.signal?.throwIfAborted();
708
+ let closePromise;
709
+ const close = () => {
710
+ closePromise ??= runtime.run(`DROP TABLE IF EXISTS ${temporary}`);
711
+ return closePromise;
712
+ };
713
+ const changes = async function* () {
714
+ try {
715
+ let cursor = -1n;
716
+ let delivered = 0;
717
+ while (delivered < changeCount) {
718
+ input.signal?.throwIfAborted();
719
+ if (closePromise)
720
+ throw new LakeStorageError7("[SixbDuckLake] Change reader is closed.");
721
+ const page = await runtime.query(`SELECT rowid AS position, * FROM ${temporary}
722
+ WHERE rowid > ${cursor} ORDER BY rowid LIMIT ${PAGE_ROWS}`);
723
+ if (page.length === 0)
724
+ throw new LakeStorageError7("[SixbDuckLake] Change read ended before its declared count.");
725
+ for (const row of page) {
726
+ input.signal?.throwIfAborted();
727
+ delivered++;
728
+ yield {
729
+ before: row.before_present === true ? rowImage(row, "b", columns) : null,
730
+ after: row.after_present === true ? rowImage(row, "a", columns) : null
731
+ };
732
+ }
733
+ cursor = getBigIntLike(page[page.length - 1], "position");
734
+ }
735
+ } finally {
736
+ await close();
737
+ }
738
+ }();
739
+ retained = true;
740
+ return { fromRowCount: previousCount, toRowCount: nextCount, changeCount, changes, close };
741
+ } finally {
742
+ try {
743
+ if (!retained && created)
744
+ await runtime.run(`DROP TABLE IF EXISTS ${temporary}`);
745
+ } finally {
746
+ await lease.release();
747
+ }
748
+ }
749
+ }
750
+ }
751
+ async function uniqueRowCount(runtime, relation, keys) {
752
+ const names = keys.map(quoteIdentifier);
753
+ const invalid = names.map((name) => `${name} IS NULL OR trim(${name}) = ''`).join(" OR ");
754
+ const [row] = await runtime.query(`SELECT count(*) AS total,
755
+ count(DISTINCT (${names.join(", ")})) AS unique_keys,
756
+ count(*) FILTER (WHERE ${invalid}) AS invalid_keys FROM ${relation}`);
757
+ const count = safeCount(row, "total");
758
+ return safeCount(row, "unique_keys") === count && safeCount(row, "invalid_keys") === 0 ? count : null;
759
+ }
760
+ function safeCount(row, name) {
761
+ const count = Number(getBigIntLike(row, name));
762
+ if (!Number.isSafeInteger(count) || count < 0)
763
+ throw new LakeStorageError7("[SixbDuckLake] Change count exceeds the supported integer range.");
764
+ return count;
765
+ }
766
+ function rowImage(row, prefix, columns) {
767
+ return Object.fromEntries(columns.map((column, index) => [
768
+ column.name,
769
+ normalizeReadValue(row[`${prefix}${index}`], column)
770
+ ]));
771
+ }
772
+
773
+ // src/internal/ducklake-connection-manager.ts
774
+ import { LakeStorageError as LakeStorageError11 } from "@sixb/core/lake-storage";
775
+
776
+ // src/internal/catalog-key.ts
777
+ import { resolve } from "path";
778
+ function localCatalogCoordinationKey(options) {
779
+ switch (options.catalog.type) {
780
+ case "duckdb":
781
+ case "sqlite":
782
+ if (options.catalog.path === ":memory:") {
783
+ return;
784
+ }
785
+ return `${options.catalog.type}:${resolve(options.catalog.path)}`;
786
+ case "custom":
787
+ return `custom:${options.catalog.uri}`;
788
+ case "postgres":
789
+ return;
790
+ }
791
+ }
792
+
793
+ // src/internal/duckdb-runtime.ts
794
+ import { mkdir, mkdtemp, rm } from "fs/promises";
795
+ import { join, resolve as resolve2 } from "path";
796
+ import {
797
+ DuckDBInstance
798
+ } from "@duckdb/node-api";
799
+ import { LakeStorageError as LakeStorageError10 } from "@sixb/core/lake-storage";
800
+
801
+ // src/internal/duckdb-reader.ts
802
+ import { LakeStorageError as LakeStorageError9 } from "@sixb/core/lake-storage";
803
+
804
+ // src/internal/duckdb-value-converter.ts
805
+ import {
806
+ DuckDBDecimalValue,
807
+ DuckDBTypeId,
808
+ JSDuckDBValueConverter
809
+ } from "@duckdb/node-api";
810
+ import { LakeStorageError as LakeStorageError8 } from "@sixb/core/lake-storage";
811
+ var sixbDuckDbValueConverter = (value, type, converter) => {
812
+ if (value === null) {
813
+ return null;
814
+ }
815
+ if (type.typeId === DuckDBTypeId.DECIMAL) {
816
+ if (!(value instanceof DuckDBDecimalValue)) {
817
+ throw new LakeStorageError8("[SixbDuckLake] DuckDB returned an invalid DECIMAL value.");
818
+ }
819
+ return value.toString();
820
+ }
821
+ return JSDuckDBValueConverter(value, type, converter);
822
+ };
823
+
824
+ // src/internal/duckdb-reader.ts
825
+ class NodeDuckDbReader {
826
+ connection;
827
+ options;
828
+ resultPromise;
829
+ pending;
830
+ closePromise;
831
+ finished = false;
832
+ consumed = false;
833
+ onAbort = () => {
834
+ this.close().catch(() => {});
835
+ };
836
+ constructor(connection, options) {
837
+ this.connection = connection;
838
+ this.options = options;
839
+ }
840
+ async start(sql) {
841
+ this.options.signal?.throwIfAborted();
842
+ this.options.signal?.addEventListener("abort", this.onAbort, { once: true });
843
+ await this.track(this.connection.run("SET preserve_insertion_order = true"));
844
+ this.assertOpen();
845
+ const prepared = await this.track(this.connection.prepare(sql));
347
846
  try {
348
- await waitForTurn;
349
- const result = await this.connection.stream(sql, values === undefined ? undefined : [...values]);
350
- for await (const batch of result.yieldConvertedRowObjects(sixbDuckDbValueConverter)) {
351
- for (const row of batch) {
847
+ this.assertOpen();
848
+ this.resultPromise = this.track(prepared.startStream().getResult()).finally(() => {
849
+ prepared.destroySync();
850
+ });
851
+ this.resultPromise.catch(() => {});
852
+ } catch (error) {
853
+ prepared.destroySync();
854
+ throw error;
855
+ }
856
+ }
857
+ async* rows() {
858
+ if (this.consumed)
859
+ throw new LakeStorageError9("[SixbDuckLake] Reader can only be consumed once.");
860
+ this.consumed = true;
861
+ try {
862
+ this.assertOpen();
863
+ const result = await this.resultPromise;
864
+ this.assertOpen();
865
+ const batches = result.yieldConvertedRowObjects(sixbDuckDbValueConverter)[Symbol.asyncIterator]();
866
+ while (true) {
867
+ this.assertOpen();
868
+ const batch = await this.track(batches.next());
869
+ this.assertOpen();
870
+ if (batch.done) {
871
+ this.finished = true;
872
+ return;
873
+ }
874
+ for (const row of batch.value) {
875
+ this.assertOpen();
352
876
  yield row;
353
877
  }
354
878
  }
355
879
  } finally {
356
- releaseOperation?.();
880
+ await this.close();
357
881
  }
358
882
  }
883
+ close() {
884
+ if (!this.closePromise) {
885
+ this.options.signal?.removeEventListener("abort", this.onAbort);
886
+ if (this.pending)
887
+ this.connection.interrupt();
888
+ this.closePromise = this.finish();
889
+ }
890
+ return this.closePromise;
891
+ }
892
+ async finish() {
893
+ try {
894
+ await this.pending?.catch(() => {});
895
+ if (!this.finished) {
896
+ await this.connection.run("SELECT 1");
897
+ }
898
+ } finally {
899
+ this.resultPromise = undefined;
900
+ try {
901
+ this.connection.closeSync();
902
+ } finally {
903
+ await this.options.onClose?.();
904
+ }
905
+ }
906
+ }
907
+ async track(operation) {
908
+ this.pending = operation;
909
+ try {
910
+ return await operation;
911
+ } catch (error) {
912
+ this.assertOpen();
913
+ throw error;
914
+ } finally {
915
+ if (this.pending === operation)
916
+ this.pending = undefined;
917
+ }
918
+ }
919
+ assertOpen() {
920
+ this.options.signal?.throwIfAborted();
921
+ if (this.closePromise)
922
+ throw new LakeStorageError9("[SixbDuckLake] Reader is closed.");
923
+ }
924
+ }
925
+
926
+ // src/internal/duckdb-runtime.ts
927
+ class NodeDuckDbRuntime {
928
+ instance;
929
+ connection;
930
+ temporaryDirectory;
931
+ closing = false;
932
+ closed = false;
933
+ operations = Promise.resolve();
934
+ readers = new Set;
935
+ closePromise;
936
+ constructor(instance, connection, temporaryDirectory) {
937
+ this.instance = instance;
938
+ this.connection = connection;
939
+ this.temporaryDirectory = temporaryDirectory;
940
+ }
941
+ async openReader(sql, options = {}) {
942
+ return this.enqueue(async () => {
943
+ this.assertAcceptingOperations();
944
+ const connection = await this.instance.connect();
945
+ if (this.closing) {
946
+ connection.closeSync();
947
+ this.assertAcceptingOperations();
948
+ }
949
+ const reader = new NodeDuckDbReader(connection, {
950
+ ...options,
951
+ onClose: async () => {
952
+ this.readers.delete(reader);
953
+ await options.onClose?.();
954
+ }
955
+ });
956
+ this.readers.add(reader);
957
+ try {
958
+ await reader.start(sql);
959
+ return reader;
960
+ } catch (error) {
961
+ await reader.close();
962
+ throw error;
963
+ }
964
+ });
965
+ }
966
+ async run(sql, values) {
967
+ await this.enqueue(async () => {
968
+ await this.connection.run(sql, values === undefined ? undefined : [...values]);
969
+ });
970
+ }
971
+ async runStatements(statements) {
972
+ await this.enqueue(async () => {
973
+ for (const sql of statements) {
974
+ await this.connection.run(sql);
975
+ }
976
+ });
977
+ }
978
+ async query(sql, values) {
979
+ return this.enqueue(async () => {
980
+ const reader = await this.connection.runAndReadAll(sql, values === undefined ? undefined : [...values]);
981
+ return reader.convertRowObjects(sixbDuckDbValueConverter);
982
+ });
983
+ }
359
984
  async withAppender(tableName, useAppender) {
360
985
  return this.enqueue(async () => {
361
986
  const appender = await this.connection.createAppender(tableName);
@@ -371,15 +996,28 @@ class NodeDuckDbRuntime {
371
996
  return useRuntime(new NodeDuckDbExclusiveRuntime(this.connection));
372
997
  });
373
998
  }
374
- async close() {
375
- if (this.closed) {
376
- return;
377
- }
999
+ close() {
1000
+ this.closePromise ??= this.finish();
1001
+ return this.closePromise;
1002
+ }
1003
+ async stopReaders() {
1004
+ const results = await Promise.allSettled([...this.readers].map((reader) => reader.close()));
1005
+ const failed = results.find((result) => result.status === "rejected");
1006
+ if (failed?.status === "rejected")
1007
+ throw failed.reason;
1008
+ }
1009
+ async finish() {
378
1010
  this.closing = true;
1011
+ const results = await Promise.allSettled([...this.readers].map((reader) => reader.close()));
379
1012
  await this.operations.catch(() => {});
380
1013
  this.closed = true;
381
1014
  this.connection.closeSync();
382
1015
  this.instance.closeSync();
1016
+ if (this.temporaryDirectory)
1017
+ await rm(this.temporaryDirectory, { recursive: true, force: true });
1018
+ const failed = results.find((result) => result.status === "rejected");
1019
+ if (failed?.status === "rejected")
1020
+ throw failed.reason;
383
1021
  }
384
1022
  enqueue(operation) {
385
1023
  this.assertAcceptingOperations();
@@ -392,12 +1030,12 @@ class NodeDuckDbRuntime {
392
1030
  }
393
1031
  assertAcceptingOperations() {
394
1032
  if (this.closing || this.closed) {
395
- throw new LakeStorageError3("[SixbDuckLake] DuckDB runtime is closed.");
1033
+ throw new LakeStorageError10("[SixbDuckLake] DuckDB runtime is closed.");
396
1034
  }
397
1035
  }
398
1036
  assertNotClosed() {
399
1037
  if (this.closed) {
400
- throw new LakeStorageError3("[SixbDuckLake] DuckDB runtime is closed.");
1038
+ throw new LakeStorageError10("[SixbDuckLake] DuckDB runtime is closed.");
401
1039
  }
402
1040
  }
403
1041
  }
@@ -457,8 +1095,27 @@ class NodeDuckDbAppenderAdapter {
457
1095
  }
458
1096
  }
459
1097
  async function createDuckDbRuntime(options = {}) {
460
- const instance = await DuckDBInstance.create(options.path ?? ":memory:", options.config ?? {});
461
- return new NodeDuckDbRuntime(instance, await instance.connect());
1098
+ const path = options.path ?? ":memory:";
1099
+ const base = options.config?.temp_directory ?? (path === ":memory:" ? ".tmp" : `${path}.tmp`);
1100
+ let temporaryDirectory;
1101
+ if (base !== "") {
1102
+ const directory = resolve2(base);
1103
+ await mkdir(directory, { recursive: true });
1104
+ temporaryDirectory = await mkdtemp(join(directory, "sixb-"));
1105
+ }
1106
+ let instance;
1107
+ try {
1108
+ instance = await DuckDBInstance.create(path, {
1109
+ ...options.config,
1110
+ ...temporaryDirectory ? { temp_directory: temporaryDirectory } : {}
1111
+ });
1112
+ return new NodeDuckDbRuntime(instance, await instance.connect(), temporaryDirectory);
1113
+ } catch (error) {
1114
+ instance?.closeSync();
1115
+ if (temporaryDirectory)
1116
+ await rm(temporaryDirectory, { recursive: true, force: true });
1117
+ throw error;
1118
+ }
462
1119
  }
463
1120
  async function installDuckLakeExtensions(runtime, options) {
464
1121
  await runtime.run("INSTALL ducklake");
@@ -480,14 +1137,70 @@ async function setupDuckLake(runtime, options, setupOptions = {}) {
480
1137
  if (options.catalog.type === "postgres") {
481
1138
  await runtime.runStatements(buildSetPostgresPoolSql(options));
482
1139
  }
483
- for (const secret of options.secrets ?? []) {
484
- await runtime.run(buildCreateSecretSql(secret));
1140
+ for (const secret of options.secrets ?? []) {
1141
+ await runtime.run(buildCreateSecretSql(secret));
1142
+ }
1143
+ if (setupOptions.attach ?? true) {
1144
+ await runtime.run(buildAttachSql(options));
1145
+ if (options.catalog.type === "postgres") {
1146
+ await runtime.run(buildConfigurePostgresMetadataPoolSql(options));
1147
+ }
1148
+ }
1149
+ }
1150
+
1151
+ // src/internal/ducklake-read-gate.ts
1152
+ class DuckLakeReadGate {
1153
+ active = 0;
1154
+ maintenance = false;
1155
+ waiters = new Set;
1156
+ async acquire(signal) {
1157
+ while (true) {
1158
+ signal.throwIfAborted();
1159
+ if (!this.maintenance) {
1160
+ this.active++;
1161
+ let released = false;
1162
+ return () => {
1163
+ if (released)
1164
+ return;
1165
+ released = true;
1166
+ this.active--;
1167
+ this.wake();
1168
+ };
1169
+ }
1170
+ await this.wait(signal);
1171
+ }
1172
+ }
1173
+ async withMaintenance(signal, run) {
1174
+ while (this.active > 0 || this.maintenance)
1175
+ await this.wait(signal);
1176
+ signal.throwIfAborted();
1177
+ this.maintenance = true;
1178
+ try {
1179
+ return await run();
1180
+ } finally {
1181
+ this.maintenance = false;
1182
+ this.wake();
1183
+ }
1184
+ }
1185
+ wait(signal) {
1186
+ return new Promise((resolve, reject) => {
1187
+ const wake = () => {
1188
+ signal.removeEventListener("abort", abort);
1189
+ this.waiters.delete(wake);
1190
+ resolve();
1191
+ };
1192
+ const abort = () => {
1193
+ this.waiters.delete(wake);
1194
+ reject(signal.reason);
1195
+ };
1196
+ signal.throwIfAborted();
1197
+ this.waiters.add(wake);
1198
+ signal.addEventListener("abort", abort, { once: true });
1199
+ });
485
1200
  }
486
- if (setupOptions.attach ?? true) {
487
- await runtime.run(buildAttachSql(options));
488
- if (options.catalog.type === "postgres") {
489
- await runtime.run(buildConfigurePostgresMetadataPoolSql(options));
490
- }
1201
+ wake() {
1202
+ for (const wake of this.waiters)
1203
+ wake();
491
1204
  }
492
1205
  }
493
1206
 
@@ -498,18 +1211,77 @@ class DuckLakeConnectionManager {
498
1211
  installExtensionsPromise;
499
1212
  attachPromise;
500
1213
  refreshPromise;
501
- localAttachmentLock = Promise.resolve();
1214
+ attachmentLock = Promise.resolve();
502
1215
  observedLocalCatalogGeneration;
503
1216
  attached = false;
504
1217
  closed = false;
1218
+ closePromise;
505
1219
  runtimePoisoned = false;
1220
+ pinnedReaders = 0;
1221
+ refreshRequested = false;
1222
+ shutdown = new AbortController;
1223
+ reads = new DuckLakeReadGate;
1224
+ maxStreamingReads;
506
1225
  constructor(options) {
507
1226
  this.options = options;
508
1227
  this.observedLocalCatalogGeneration = currentLocalCatalogGeneration(options);
1228
+ this.maxStreamingReads = options.maxStreamingReads ?? 4;
1229
+ if (!Number.isSafeInteger(this.maxStreamingReads) || this.maxStreamingReads < 1) {
1230
+ throw new LakeStorageError11("[SixbDuckLake] maxStreamingReads must be a positive integer.");
1231
+ }
1232
+ }
1233
+ async acquireRead(signal) {
1234
+ this.assertOpen();
1235
+ const combined = signal ? AbortSignal.any([signal, this.shutdown.signal]) : this.shutdown.signal;
1236
+ return { signal: combined, release: await this.reads.acquire(combined) };
1237
+ }
1238
+ async nativeReaderLimit(runtime) {
1239
+ if (this.options.catalog.type === "duckdb")
1240
+ return this.maxStreamingReads;
1241
+ if (this.options.catalog.type !== "postgres")
1242
+ return 0;
1243
+ const [setting] = await runtime.query("SELECT current_setting('pg_pool_max_connections') AS value");
1244
+ const budget = Number(setting?.value);
1245
+ return Number.isFinite(budget) ? Math.min(this.maxStreamingReads, Math.max(0, budget - 4)) : 0;
1246
+ }
1247
+ async openReader(runtime, sql, signal, releaseRead) {
1248
+ const limit = await this.nativeReaderLimit(runtime);
1249
+ signal.throwIfAborted();
1250
+ if (this.pinnedReaders >= limit)
1251
+ return null;
1252
+ this.pinnedReaders++;
1253
+ let starting = true;
1254
+ let released = false;
1255
+ const onClose = async () => {
1256
+ if (released)
1257
+ return;
1258
+ released = true;
1259
+ this.pinnedReaders--;
1260
+ if (signal.aborted || this.closed || this.runtimePoisoned)
1261
+ releaseRead();
1262
+ if (starting || this.closed || this.runtimePoisoned)
1263
+ return;
1264
+ await this.withAttachmentLock(async () => {
1265
+ if (this.pinnedReaders === 0 && this.refreshRequested)
1266
+ await this.resetRuntimeUnlocked();
1267
+ await this.detachDuckDbCatalogAfterLease();
1268
+ });
1269
+ };
1270
+ try {
1271
+ return await runtime.openReader(sql, { signal, onClose });
1272
+ } catch (error) {
1273
+ await onClose();
1274
+ throw error;
1275
+ } finally {
1276
+ starting = false;
1277
+ }
1278
+ }
1279
+ async withMaintenanceRuntime(run) {
1280
+ return this.reads.withMaintenance(this.shutdown.signal, () => this.withExclusiveAttached(run));
509
1281
  }
510
1282
  assertOpen() {
511
1283
  if (this.closed) {
512
- throw new LakeStorageError4("[SixbDuckLake] DuckLakeStorage is closed.");
1284
+ throw new LakeStorageError11("[SixbDuckLake] DuckLakeStorage is closed.");
513
1285
  }
514
1286
  }
515
1287
  async runtime() {
@@ -540,7 +1312,7 @@ class DuckLakeConnectionManager {
540
1312
  }
541
1313
  async acquireAttachedRuntime() {
542
1314
  this.assertOpen();
543
- const releaseLock = await this.acquireLocalAttachmentLock();
1315
+ const releaseLock = await this.acquireAttachmentLock();
544
1316
  let released = false;
545
1317
  const release = async () => {
546
1318
  if (released) {
@@ -549,8 +1321,8 @@ class DuckLakeConnectionManager {
549
1321
  released = true;
550
1322
  try {
551
1323
  if (this.runtimePoisoned) {
552
- this.runtimePoisoned = false;
553
1324
  await this.discardRuntimeUnlocked();
1325
+ this.runtimePoisoned = false;
554
1326
  } else {
555
1327
  await this.detachDuckDbCatalogAfterLease();
556
1328
  }
@@ -599,7 +1371,7 @@ class DuckLakeConnectionManager {
599
1371
  }
600
1372
  }
601
1373
  async resetRuntime() {
602
- const releaseLock = await this.acquireLocalAttachmentLock();
1374
+ const releaseLock = await this.acquireAttachmentLock();
603
1375
  try {
604
1376
  await this.resetRuntimeUnlocked();
605
1377
  } finally {
@@ -607,6 +1379,11 @@ class DuckLakeConnectionManager {
607
1379
  }
608
1380
  }
609
1381
  async resetRuntimeUnlocked() {
1382
+ if (this.pinnedReaders > 0) {
1383
+ this.refreshRequested = true;
1384
+ return;
1385
+ }
1386
+ this.refreshRequested = false;
610
1387
  const runtimePromise = this.runtimePromise;
611
1388
  if (runtimePromise === undefined) {
612
1389
  return;
@@ -647,7 +1424,7 @@ class DuckLakeConnectionManager {
647
1424
  }
648
1425
  if (this.refreshPromise === undefined) {
649
1426
  let refreshPromise;
650
- refreshPromise = this.withLocalAttachmentLock(() => this.refreshForExternalChangesUnlocked()).finally(() => {
1427
+ refreshPromise = this.withAttachmentLock(() => this.refreshForExternalChangesUnlocked()).finally(() => {
651
1428
  if (this.refreshPromise === refreshPromise) {
652
1429
  this.refreshPromise = undefined;
653
1430
  }
@@ -667,8 +1444,10 @@ class DuckLakeConnectionManager {
667
1444
  return;
668
1445
  }
669
1446
  try {
670
- await runtime.run(`DETACH ${quoteIdentifier(duckLakeAlias(this.options))}`);
671
- this.attached = false;
1447
+ if (this.pinnedReaders === 0) {
1448
+ await runtime.run(`DETACH ${quoteIdentifier(duckLakeAlias(this.options))}`);
1449
+ this.attached = false;
1450
+ }
672
1451
  } catch {} finally {
673
1452
  this.markLocalCatalogChanged();
674
1453
  }
@@ -684,11 +1463,13 @@ class DuckLakeConnectionManager {
684
1463
  needsLocalCatalogRefresh() {
685
1464
  return this.observedLocalCatalogGeneration !== currentLocalCatalogGeneration(this.options);
686
1465
  }
687
- async close() {
688
- if (this.closed) {
689
- return;
690
- }
1466
+ close() {
1467
+ this.closePromise ??= this.finishClose();
1468
+ return this.closePromise;
1469
+ }
1470
+ async finishClose() {
691
1471
  this.closed = true;
1472
+ this.shutdown.abort(new LakeStorageError11("[SixbDuckLake] DuckLakeStorage is closed."));
692
1473
  const runtimePromise = this.runtimePromise;
693
1474
  this.runtimePromise = undefined;
694
1475
  await this.closeRuntimePromise(runtimePromise, {
@@ -747,13 +1528,13 @@ class DuckLakeConnectionManager {
747
1528
  ]);
748
1529
  }
749
1530
  async detachRuntime() {
750
- await this.withLocalAttachmentLock(() => this.detachRuntimeUnlocked());
1531
+ await this.withAttachmentLock(() => this.detachRuntimeUnlocked());
751
1532
  }
752
1533
  poisonRuntime() {
753
1534
  this.runtimePoisoned = true;
754
1535
  }
755
1536
  async detachDuckDbCatalogAfterLease() {
756
- if (this.options.catalog.type !== "duckdb" || !this.attached) {
1537
+ if (this.options.catalog.type !== "duckdb" || !this.attached || this.pinnedReaders > 0) {
757
1538
  return;
758
1539
  }
759
1540
  try {
@@ -769,6 +1550,8 @@ class DuckLakeConnectionManager {
769
1550
  await this.closeRuntimePromise(runtimePromise, { detach: false }).catch(() => {});
770
1551
  }
771
1552
  async detachRuntimeUnlocked() {
1553
+ if (this.pinnedReaders > 0)
1554
+ return;
772
1555
  if (this.attachPromise !== undefined) {
773
1556
  await this.attachPromise;
774
1557
  }
@@ -779,24 +1562,21 @@ class DuckLakeConnectionManager {
779
1562
  await runtime.run(`DETACH ${quoteIdentifier(duckLakeAlias(this.options))}`);
780
1563
  this.attached = false;
781
1564
  }
782
- async withLocalAttachmentLock(run) {
783
- const releaseLock = await this.acquireLocalAttachmentLock();
1565
+ async withAttachmentLock(run) {
1566
+ const releaseLock = await this.acquireAttachmentLock();
784
1567
  try {
785
1568
  return await run();
786
1569
  } finally {
787
1570
  releaseLock();
788
1571
  }
789
1572
  }
790
- async acquireLocalAttachmentLock() {
791
- if (this.options.catalog.type === "postgres") {
792
- return () => {};
793
- }
794
- const previous = this.localAttachmentLock.catch(() => {});
1573
+ async acquireAttachmentLock() {
1574
+ const previous = this.attachmentLock.catch(() => {});
795
1575
  let release;
796
1576
  const current = new Promise((resolve) => {
797
1577
  release = resolve;
798
1578
  });
799
- this.localAttachmentLock = previous.then(() => current);
1579
+ this.attachmentLock = previous.then(() => current);
800
1580
  await previous;
801
1581
  let released = false;
802
1582
  return () => {
@@ -820,249 +1600,43 @@ class DuckLakeConnectionManager {
820
1600
  await this.closeRuntime(runtime, options);
821
1601
  }
822
1602
  async closeRuntime(runtime, options) {
823
- if (options.detach) {
824
- try {
825
- await runtime.run(`DETACH ${quoteIdentifier(duckLakeAlias(this.options))}`);
826
- } catch {}
827
- }
828
- await runtime.close();
829
- }
830
- }
831
- var localCatalogGenerations = new Map;
832
- function currentLocalCatalogGeneration(options) {
833
- const key = localCatalogCoordinationKey(options);
834
- return key === undefined ? 0 : localCatalogGenerations.get(key) ?? 0;
835
- }
836
- function incrementLocalCatalogGeneration(options) {
837
- const key = localCatalogCoordinationKey(options);
838
- if (key === undefined) {
839
- return 0;
840
- }
841
- const nextGeneration = (localCatalogGenerations.get(key) ?? 0) + 1;
842
- localCatalogGenerations.set(key, nextGeneration);
843
- return nextGeneration;
844
- }
845
-
846
- // src/internal/ducklake-dataset-catalog.ts
847
- import { randomUUID } from "crypto";
848
- import {
849
- assertDatasetDefinition,
850
- LakeStorageError as LakeStorageError11,
851
- planDatasetDefinitionUpdate
852
- } from "@sixb/core/lake-storage";
853
-
854
- // src/internal/duckdb-row.ts
855
- import { LakeStorageError as LakeStorageError5 } from "@sixb/core/lake-storage";
856
- function getString(row, key) {
857
- const value = row[key];
858
- if (typeof value !== "string") {
859
- throw new LakeStorageError5(`[SixbDuckLake] Expected DuckDB column '${key}' to be a string, got ${typeof value}.`);
860
- }
861
- return value;
862
- }
863
- function getOptionalString(row, key) {
864
- const value = row[key];
865
- if (value === null || value === undefined) {
866
- return;
867
- }
868
- if (typeof value !== "string") {
869
- throw new LakeStorageError5(`[SixbDuckLake] Expected DuckDB column '${key}' to be a string, got ${typeof value}.`);
870
- }
871
- return value;
872
- }
873
- function getBoolean(row, key) {
874
- const value = row[key];
875
- if (typeof value !== "boolean") {
876
- throw new LakeStorageError5(`[SixbDuckLake] Expected DuckDB column '${key}' to be a boolean, got ${typeof value}.`);
877
- }
878
- return value;
879
- }
880
- function getBigIntLike(row, key) {
881
- const value = row[key];
882
- if (typeof value === "bigint") {
883
- return value;
884
- }
885
- if (typeof value === "number" && Number.isInteger(value)) {
886
- return BigInt(value);
887
- }
888
- if (typeof value === "string" && /^-?\d+$/.test(value)) {
889
- return BigInt(value);
890
- }
891
- throw new LakeStorageError5(`[SixbDuckLake] Expected DuckDB column '${key}' to be an integer, got ${typeof value}.`);
892
- }
893
- function getOptionalBigIntLike(row, key) {
894
- const value = row[key];
895
- if (value === null || value === undefined) {
896
- return;
897
- }
898
- return getBigIntLike(row, key);
899
- }
900
- function getDate(row, key) {
901
- const value = row[key];
902
- if (value instanceof Date) {
903
- return value;
904
- }
905
- if (typeof value === "string") {
906
- return new Date(value);
907
- }
908
- throw new LakeStorageError5(`[SixbDuckLake] Expected DuckDB column '${key}' to be a date, got ${typeof value}.`);
909
- }
910
-
911
- // src/internal/ducklake-catalog-schema.ts
912
- import { LakeStorageError as LakeStorageError8 } from "@sixb/core/lake-storage";
913
-
914
- // src/internal/schema.ts
915
- import { LakeStorageError as LakeStorageError7 } from "@sixb/core/lake-storage";
916
-
917
- // src/internal/duckdb-column-types.ts
918
- function normalizeDuckDbTypeSql(typeSql) {
919
- return typeSql.trim().toUpperCase().replace(/\s+/g, " ").replace(/\s*([(),])\s*/g, "$1");
920
- }
921
- function defineDuckDbColumnType(sql, aliases = []) {
922
- const acceptedTypes = new Set([sql, ...aliases].map(normalizeDuckDbTypeSql));
923
- return {
924
- sql,
925
- matches: (typeSql) => acceptedTypes.has(normalizeDuckDbTypeSql(typeSql))
926
- };
927
- }
928
- function defineDuckDbDecimalType(precision, scale) {
929
- return {
930
- ...defineDuckDbColumnType(`DECIMAL(${precision}, ${scale})`),
931
- precision,
932
- scale
933
- };
934
- }
935
- var DUCKDB_COLUMN_TYPES = {
936
- string: defineDuckDbColumnType("VARCHAR"),
937
- boolean: defineDuckDbColumnType("BOOLEAN"),
938
- int64: defineDuckDbColumnType("BIGINT", ["INT64"]),
939
- float64: defineDuckDbColumnType("DOUBLE", ["FLOAT64"]),
940
- decimal: defineDuckDbDecimalType(38, 9),
941
- date: defineDuckDbColumnType("DATE"),
942
- timestamp: defineDuckDbColumnType("TIMESTAMPTZ", ["TIMESTAMP WITH TIME ZONE"]),
943
- json: defineDuckDbColumnType("JSON"),
944
- fileRef: defineDuckDbColumnType("STRUCT(blobId VARCHAR, digest VARCHAR, sizeBytes BIGINT, fileName VARCHAR, mediaType VARCHAR, logicalPath VARCHAR)")
945
- };
946
- function findDatasetColumnTypeForDuckDbSql(typeSql) {
947
- for (const type of Object.keys(DUCKDB_COLUMN_TYPES)) {
948
- if (DUCKDB_COLUMN_TYPES[type].matches(typeSql)) {
949
- return type;
1603
+ try {
1604
+ await runtime.stopReaders();
1605
+ if (options.detach) {
1606
+ try {
1607
+ await runtime.run(`DETACH ${quoteIdentifier(duckLakeAlias(this.options))}`);
1608
+ } catch {}
1609
+ }
1610
+ } finally {
1611
+ await runtime.close();
950
1612
  }
951
1613
  }
952
- return;
953
- }
954
-
955
- // src/internal/duckdb-decimal.ts
956
- import { normalizeDecimalValue } from "@sixb/core";
957
- import { LakeStorageError as LakeStorageError6 } from "@sixb/core/lake-storage";
958
- var decimalType = DUCKDB_COLUMN_TYPES.decimal;
959
- var maxIntegerDigits = decimalType.precision - decimalType.scale;
960
- function normalizeDuckDbDecimalValue(value, columnName) {
961
- if (typeof value !== "string") {
962
- throw new LakeStorageError6(`[SixbDuckLake] Decimal column '${columnName}' must be an exact decimal string.`);
963
- }
964
- let normalized;
965
- try {
966
- normalized = normalizeDecimalValue(value);
967
- } catch {
968
- throw new LakeStorageError6(`[SixbDuckLake] Decimal column '${columnName}' must be an exact decimal string.`);
969
- }
970
- const unsigned = normalized.startsWith("-") ? normalized.slice(1) : normalized;
971
- const [integer, fraction = ""] = unsigned.split(".");
972
- if (integer.length > maxIntegerDigits || fraction.length > decimalType.scale) {
973
- throw new LakeStorageError6(`[SixbDuckLake] Decimal column '${columnName}' cannot be represented exactly as ${decimalType.sql}; expected at most ${maxIntegerDigits} integer digits and ${decimalType.scale} fractional digits.`);
974
- }
975
- return normalized;
976
- }
977
-
978
- // src/internal/schema.ts
979
- function datasetColumnTypeToDuckDbSql(type) {
980
- return DUCKDB_COLUMN_TYPES[type].sql;
981
- }
982
- function duckDbTypeToDatasetColumnType(typeSql) {
983
- const type = findDatasetColumnTypeForDuckDbSql(typeSql);
984
- if (type) {
985
- return type;
986
- }
987
- throw new LakeStorageError7(`[SixbDuckLake] DuckDB column type '${typeSql}' cannot be mapped to a Sixb dataset column type.`);
988
- }
989
- function datasetColumnToDuckDbSql(column) {
990
- const nullableSql = column.nullable ? "" : " NOT NULL";
991
- return `${quoteIdentifier(column.name)} ${datasetColumnTypeToDuckDbSql(column.type)}${nullableSql}`;
992
- }
993
- function datasetSchemaToDuckDbColumnsSql(schema) {
994
- return schema.columns.map((column) => datasetColumnToDuckDbSql(column)).join(", ");
995
- }
996
- function datasetSchemaToDuckDbNullableColumnsSql(schema) {
997
- return schema.columns.map((column) => `${quoteIdentifier(column.name)} ${datasetColumnTypeToDuckDbSql(column.type)}`).join(", ");
998
- }
999
- function datasetSchemaColumnNamesSql(schema) {
1000
- return schema.columns.map((column) => quoteIdentifier(column.name)).join(", ");
1001
- }
1002
- function normalizeReadValue(value, column) {
1003
- if (value === null || value === undefined) {
1004
- return null;
1005
- }
1006
- switch (column.type) {
1007
- case "int64":
1008
- return typeof value === "bigint" ? value.toString() : value;
1009
- case "decimal":
1010
- return normalizeDuckDbDecimalValue(value, column.name);
1011
- case "date":
1012
- return value instanceof Date ? value.toISOString().slice(0, 10) : String(value);
1013
- case "timestamp":
1014
- return value instanceof Date ? value.toISOString() : String(value);
1015
- case "json":
1016
- return typeof value === "string" ? JSON.parse(value) : value;
1017
- case "fileRef":
1018
- return normalizeFileRef(value);
1019
- default:
1020
- return value;
1021
- }
1022
- }
1023
- function normalizeFileRef(value) {
1024
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
1025
- throw new LakeStorageError7("[SixbDuckLake] DuckDB fileRef value must be a struct.");
1026
- }
1027
- const record = value;
1028
- return {
1029
- blobId: getStringField(record, "blobId"),
1030
- digest: getStringField(record, "digest"),
1031
- sizeBytes: getIntegerField(record, "sizeBytes"),
1032
- fileName: getOptionalStringField(record, "fileName"),
1033
- mediaType: getOptionalStringField(record, "mediaType"),
1034
- logicalPath: getOptionalStringField(record, "logicalPath")
1035
- };
1036
- }
1037
- function getStringField(record, key) {
1038
- const value = record[key];
1039
- if (typeof value !== "string") {
1040
- throw new LakeStorageError7(`[SixbDuckLake] DuckDB fileRef field '${key}' must be a string.`);
1041
- }
1042
- return value;
1043
- }
1044
- function getOptionalStringField(record, key) {
1045
- const value = record[key];
1046
- if (value === null || value === undefined) {
1047
- return;
1048
- }
1049
- if (typeof value !== "string") {
1050
- throw new LakeStorageError7(`[SixbDuckLake] DuckDB fileRef field '${key}' must be a string.`);
1051
- }
1052
- return value;
1053
1614
  }
1054
- function getIntegerField(record, key) {
1055
- const value = record[key];
1056
- if (typeof value === "number" && Number.isInteger(value)) {
1057
- return value;
1058
- }
1059
- if (typeof value === "bigint") {
1060
- return Number(value);
1615
+ var localCatalogGenerations = new Map;
1616
+ function currentLocalCatalogGeneration(options) {
1617
+ const key = localCatalogCoordinationKey(options);
1618
+ return key === undefined ? 0 : localCatalogGenerations.get(key) ?? 0;
1619
+ }
1620
+ function incrementLocalCatalogGeneration(options) {
1621
+ const key = localCatalogCoordinationKey(options);
1622
+ if (key === undefined) {
1623
+ return 0;
1061
1624
  }
1062
- throw new LakeStorageError7(`[SixbDuckLake] DuckDB fileRef field '${key}' must be an integer.`);
1625
+ const nextGeneration = (localCatalogGenerations.get(key) ?? 0) + 1;
1626
+ localCatalogGenerations.set(key, nextGeneration);
1627
+ return nextGeneration;
1063
1628
  }
1064
1629
 
1630
+ // src/internal/ducklake-dataset-catalog.ts
1631
+ import { randomUUID as randomUUID2 } from "crypto";
1632
+ import {
1633
+ assertDatasetDefinition,
1634
+ LakeStorageError as LakeStorageError14,
1635
+ planDatasetDefinitionUpdate
1636
+ } from "@sixb/core/lake-storage";
1637
+
1065
1638
  // src/internal/ducklake-catalog-schema.ts
1639
+ import { LakeStorageError as LakeStorageError12 } from "@sixb/core/lake-storage";
1066
1640
  var SIXB_PRIMARY_KEY_COMMENT_NAMESPACE = "sixb:primary-key:";
1067
1641
  var DATASET_SEQUENCE_COLUMN_COMMENT = "sixb.sequenceBy.v1";
1068
1642
  var SIXB_PRIMARY_KEY_COMMENT_PREFIX = `${SIXB_PRIMARY_KEY_COMMENT_NAMESPACE}v1:`;
@@ -1113,14 +1687,14 @@ function duckLakeCatalogColumnsToDatasetPrimaryKey(tableName, rows) {
1113
1687
  throwInvalidPrimaryKeyMetadata(tableName, column, comment);
1114
1688
  }
1115
1689
  if (seenOrdinals.has(ordinal)) {
1116
- throw new LakeStorageError8(`[SixbDuckLake] Dataset table '${tableName}' has duplicate primary-key ordinal '${ordinal}'.`);
1690
+ throw new LakeStorageError12(`[SixbDuckLake] Dataset table '${tableName}' has duplicate primary-key ordinal '${ordinal}'.`);
1117
1691
  }
1118
1692
  seenOrdinals.add(ordinal);
1119
1693
  if (duckDbTypeToDatasetColumnType(column.columnType) !== "string") {
1120
- throw new LakeStorageError8(`[SixbDuckLake] Dataset table '${tableName}' primary-key column '${column.columnName}' must map to type 'string'.`);
1694
+ throw new LakeStorageError12(`[SixbDuckLake] Dataset table '${tableName}' primary-key column '${column.columnName}' must map to type 'string'.`);
1121
1695
  }
1122
1696
  if (column.nullsAllowed) {
1123
- throw new LakeStorageError8(`[SixbDuckLake] Dataset table '${tableName}' primary-key column '${column.columnName}' must not be nullable.`);
1697
+ throw new LakeStorageError12(`[SixbDuckLake] Dataset table '${tableName}' primary-key column '${column.columnName}' must not be nullable.`);
1124
1698
  }
1125
1699
  keyedColumns.push({ column, ordinal });
1126
1700
  }
@@ -1130,7 +1704,7 @@ function duckLakeCatalogColumnsToDatasetPrimaryKey(tableName, rows) {
1130
1704
  keyedColumns.sort((left, right) => left.ordinal - right.ordinal);
1131
1705
  for (const [expectedOrdinal, keyedColumn] of keyedColumns.entries()) {
1132
1706
  if (keyedColumn.ordinal !== expectedOrdinal) {
1133
- throw new LakeStorageError8(`[SixbDuckLake] Dataset table '${tableName}' primary-key ordinals must be contiguous from 0.`);
1707
+ throw new LakeStorageError12(`[SixbDuckLake] Dataset table '${tableName}' primary-key ordinals must be contiguous from 0.`);
1134
1708
  }
1135
1709
  }
1136
1710
  const columnNames = keyedColumns.map(({ column }) => column.columnName);
@@ -1140,7 +1714,7 @@ function duckLakeCatalogColumnsToDatasetPrimaryKey(tableName, rows) {
1140
1714
  return columnNames;
1141
1715
  }
1142
1716
  function throwInvalidPrimaryKeyMetadata(tableName, column, comment) {
1143
- throw new LakeStorageError8(`[SixbDuckLake] Dataset table '${tableName}' has malformed primary-key metadata '${comment}' on column '${column.columnName}'.`);
1717
+ throw new LakeStorageError12(`[SixbDuckLake] Dataset table '${tableName}' has malformed primary-key metadata '${comment}' on column '${column.columnName}'.`);
1144
1718
  }
1145
1719
  function duckLakeCatalogColumnToDefinition(tableName, column, childrenByParentId) {
1146
1720
  const children = [...childrenByParentId.get(column.columnId) ?? []].sort(compareDuckLakeCatalogColumnOrder);
@@ -1157,7 +1731,7 @@ function duckLakeCatalogColumnTypeToDatasetType(tableName, column, children) {
1157
1731
  if (isFileRefStruct(children)) {
1158
1732
  return "fileRef";
1159
1733
  }
1160
- throw new LakeStorageError8(`[SixbDuckLake] DuckDB column type '${formatDuckLakeCatalogColumnType(column, children)}' in dataset table '${tableName}' cannot be mapped to a Sixb dataset column type.`);
1734
+ throw new LakeStorageError12(`[SixbDuckLake] DuckDB column type '${formatDuckLakeCatalogColumnType(column, children)}' in dataset table '${tableName}' cannot be mapped to a Sixb dataset column type.`);
1161
1735
  }
1162
1736
  function compareDuckLakeCatalogColumnOrder(left, right) {
1163
1737
  return Number(left.columnOrder - right.columnOrder);
@@ -1179,84 +1753,7 @@ function formatDuckLakeCatalogColumnType(column, children) {
1179
1753
  }
1180
1754
 
1181
1755
  // src/internal/ducklake-current-dataset-definitions.ts
1182
- import { LakeStorageError as LakeStorageError10 } from "@sixb/core/lake-storage";
1183
-
1184
- // src/internal/names.ts
1185
- import { LakeStorageError as LakeStorageError9 } from "@sixb/core/lake-storage";
1186
- var DATASET_TABLE_PREFIX = "sixb__ds__";
1187
- var textEncoder = new TextEncoder;
1188
- var textDecoder = new TextDecoder("utf-8", { fatal: true });
1189
- function isInlineByte(byte) {
1190
- return byte >= 97 && byte <= 122 || byte >= 48 && byte <= 57;
1191
- }
1192
- function isHexPair(value) {
1193
- return /^[0-9a-f]{2}$/.test(value);
1194
- }
1195
- function encodeDatasetId(datasetId) {
1196
- if (datasetId.trim().length === 0) {
1197
- throw new LakeStorageError9("[SixbDuckLake] Dataset id must not be empty.");
1198
- }
1199
- let encoded = "";
1200
- for (const byte of textEncoder.encode(datasetId)) {
1201
- if (isInlineByte(byte)) {
1202
- encoded += String.fromCharCode(byte);
1203
- } else if (byte === 46) {
1204
- encoded += "__";
1205
- } else {
1206
- encoded += `_${byte.toString(16).padStart(2, "0")}`;
1207
- }
1208
- }
1209
- return encoded;
1210
- }
1211
- function decodeDatasetId(encoded) {
1212
- if (encoded.length === 0) {
1213
- return null;
1214
- }
1215
- const bytes = [];
1216
- let index = 0;
1217
- while (index < encoded.length) {
1218
- const char = encoded[index];
1219
- const byte = char.charCodeAt(0);
1220
- if (isInlineByte(byte)) {
1221
- bytes.push(byte);
1222
- index += 1;
1223
- continue;
1224
- }
1225
- if (char !== "_") {
1226
- return null;
1227
- }
1228
- if (encoded[index + 1] === "_") {
1229
- bytes.push(46);
1230
- index += 2;
1231
- continue;
1232
- }
1233
- const hex = encoded.slice(index + 1, index + 3);
1234
- if (!isHexPair(hex)) {
1235
- return null;
1236
- }
1237
- bytes.push(Number.parseInt(hex, 16));
1238
- index += 3;
1239
- }
1240
- try {
1241
- return textDecoder.decode(new Uint8Array(bytes));
1242
- } catch {
1243
- return null;
1244
- }
1245
- }
1246
- function encodeDatasetTableName(datasetId) {
1247
- return `${DATASET_TABLE_PREFIX}${encodeDatasetId(datasetId)}`;
1248
- }
1249
- function encodeDatasetSequenceTableName(datasetId) {
1250
- return `sixb__seq__${encodeDatasetTableName(datasetId)}`;
1251
- }
1252
- function decodeDatasetTableName(tableName) {
1253
- if (!tableName.startsWith(DATASET_TABLE_PREFIX)) {
1254
- return null;
1255
- }
1256
- return decodeDatasetId(tableName.slice(DATASET_TABLE_PREFIX.length));
1257
- }
1258
-
1259
- // src/internal/ducklake-current-dataset-definitions.ts
1756
+ import { LakeStorageError as LakeStorageError13 } from "@sixb/core/lake-storage";
1260
1757
  async function readCurrentDatasetDefinitions(input) {
1261
1758
  if (input.datasetIds?.length === 0) {
1262
1759
  return new Map;
@@ -1383,7 +1880,7 @@ async function readCurrentDatasetPartitions(input, tableNames) {
1383
1880
  transform: getString(row, "transform")
1384
1881
  };
1385
1882
  if (partition.transform !== "identity") {
1386
- throw new LakeStorageError10(`[SixbDuckLake] Dataset table '${partition.tableName}' uses unsupported DuckLake partition transform '${partition.transform}'.`);
1883
+ throw new LakeStorageError13(`[SixbDuckLake] Dataset table '${partition.tableName}' uses unsupported DuckLake partition transform '${partition.transform}'.`);
1387
1884
  }
1388
1885
  return partition;
1389
1886
  });
@@ -1485,7 +1982,7 @@ class DuckLakeDatasetCatalog {
1485
1982
  if (failures.length > 0) {
1486
1983
  const details = failures.join(`
1487
1984
  `);
1488
- throw new LakeStorageError11(`[SixbLake] Lake dataset definition check failed for ${failures.length} dataset(s).
1985
+ throw new LakeStorageError14(`[SixbLake] Lake dataset definition check failed for ${failures.length} dataset(s).
1489
1986
  ${details}`);
1490
1987
  }
1491
1988
  }
@@ -1521,12 +2018,12 @@ ${details}`);
1521
2018
  assertDuckLakeSnapshotId(snapshotId);
1522
2019
  const columns = await this.readDatasetColumnsAtSnapshot(runtime, tableName, tableId, snapshotId);
1523
2020
  if (columns.length === 0) {
1524
- throw new LakeStorageError11(`[SixbDuckLake] Could not reconstruct schema for dataset '${datasetId}' at DuckLake snapshot '${snapshotId}'.`);
2021
+ throw new LakeStorageError14(`[SixbDuckLake] Could not reconstruct schema for dataset '${datasetId}' at DuckLake snapshot '${snapshotId}'.`);
1525
2022
  }
1526
2023
  return duckLakeCatalogColumnsToDatasetSchema(tableName, columns);
1527
2024
  }
1528
2025
  assertSchema(definition) {
1529
- assertDatasetDefinition(definition, (message) => new LakeStorageError11(`[SixbDuckLake] ${message}`));
2026
+ assertDatasetDefinition(definition, (message) => new LakeStorageError14(`[SixbDuckLake] ${message}`));
1530
2027
  }
1531
2028
  async applyExistingDatasetUpdate(plan) {
1532
2029
  const tableName = encodeDatasetTableName(plan.definition.id);
@@ -1573,7 +2070,7 @@ ${details}`);
1573
2070
  await runtime.run(`ALTER TABLE ${qualifiedName} SET PARTITIONED BY (${definition.partitionBy.map((columnName) => quoteIdentifier(columnName)).join(", ")})`);
1574
2071
  } catch (error) {
1575
2072
  const message = error instanceof Error ? error.message : String(error);
1576
- throw new LakeStorageError11(`[SixbDuckLake] Dataset '${definition.id}' cannot apply partitionBy because DuckLake rejected ALTER TABLE SET PARTITIONED BY: ${message}`);
2073
+ throw new LakeStorageError14(`[SixbDuckLake] Dataset '${definition.id}' cannot apply partitionBy because DuckLake rejected ALTER TABLE SET PARTITIONED BY: ${message}`);
1577
2074
  }
1578
2075
  }
1579
2076
  async applyPrimaryKey(runtime, qualifiedName, definition) {
@@ -1590,7 +2087,7 @@ ${details}`);
1590
2087
  sixb: {
1591
2088
  kind: "datasetVersion",
1592
2089
  datasetId,
1593
- commitId: randomUUID(),
2090
+ commitId: randomUUID2(),
1594
2091
  mode: "schema",
1595
2092
  schemaChange: {
1596
2093
  addColumns: plan.columns.map((column) => column.name)
@@ -1632,12 +2129,12 @@ ${details}`);
1632
2129
  }
1633
2130
  function assertDuckLakeSnapshotId(snapshotId) {
1634
2131
  if (!/^\d+$/.test(snapshotId)) {
1635
- throw new LakeStorageError11(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`);
2132
+ throw new LakeStorageError14(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`);
1636
2133
  }
1637
2134
  }
1638
2135
 
1639
2136
  // src/internal/ducklake-maintenance.ts
1640
- import { LakeStorageError as LakeStorageError12 } from "@sixb/core/lake-storage";
2137
+ import { LakeStorageError as LakeStorageError15 } from "@sixb/core/lake-storage";
1641
2138
  var DEFAULT_RETENTION = "7 days";
1642
2139
 
1643
2140
  class DuckLakeMaintenance {
@@ -1653,9 +2150,9 @@ class DuckLakeMaintenance {
1653
2150
  const deleteOlderThan = options.deleteOlderThan ?? expireOlderThan;
1654
2151
  const dryRun = options.dryRun ?? false;
1655
2152
  if (this.options.readOnly && !dryRun) {
1656
- throw new LakeStorageError12("[SixbDuckLake] DuckLake maintenance cannot run in read-only mode.");
2153
+ throw new LakeStorageError15("[SixbDuckLake] DuckLake maintenance cannot run in read-only mode.");
1657
2154
  }
1658
- return this.connections.withExclusiveAttached(async (runtime) => {
2155
+ return this.connections.withMaintenanceRuntime(async (runtime) => {
1659
2156
  const counts = await runMaintenanceProcedures(runtime, this.options, {
1660
2157
  expireOlderThan,
1661
2158
  deleteOlderThan
@@ -1690,112 +2187,7 @@ function maintenanceProcedureSql(procedure, aliasSql, olderThan, dryRun) {
1690
2187
  }
1691
2188
 
1692
2189
  // src/internal/ducklake-row-reader.ts
1693
- import { LakeStorageError as LakeStorageError14 } from "@sixb/core/lake-storage";
1694
-
1695
- // src/internal/ducklake-dataset-table-ref.ts
1696
- async function resolveDatasetTableRef(options, runtime, datasetId) {
1697
- const tableName = encodeDatasetTableName(datasetId);
1698
- const ducklakeTable = duckLakeMetadataTableName(options, "ducklake_table");
1699
- const [row] = await runtime.query(`
1700
- SELECT table_id
1701
- FROM ${ducklakeTable}
1702
- WHERE table_name = ${quoteSqlString(tableName)}
1703
- AND end_snapshot IS NULL
1704
- LIMIT 1
1705
- `);
1706
- return row === undefined ? null : {
1707
- datasetId,
1708
- tableName,
1709
- tableId: getBigIntLike(row, "table_id")
1710
- };
1711
- }
1712
-
1713
- // src/internal/versions.ts
1714
- import { LakeStorageError as LakeStorageError13 } from "@sixb/core/lake-storage";
1715
- function parseVersionId(versionId) {
1716
- if (!versionId.startsWith("ducklake:")) {
1717
- throw new LakeStorageError13(`[SixbDuckLake] Invalid DuckLake version id '${versionId}'.`);
1718
- }
1719
- const snapshotId = versionId.slice("ducklake:".length);
1720
- if (!/^\d+$/.test(snapshotId)) {
1721
- throw new LakeStorageError13(`[SixbDuckLake] Invalid DuckLake version id '${versionId}'.`);
1722
- }
1723
- return snapshotId;
1724
- }
1725
- function toVersionId(snapshotId) {
1726
- return `ducklake:${snapshotId}`;
1727
- }
1728
- function parseCommitMetadata(value) {
1729
- if (typeof value !== "string") {
1730
- return;
1731
- }
1732
- let parsed;
1733
- try {
1734
- parsed = JSON.parse(value);
1735
- } catch {
1736
- return;
1737
- }
1738
- if (!isRecord(parsed) || !isRecord(parsed.sixb) || parsed.sixb.kind !== "datasetVersion" || typeof parsed.sixb.datasetId !== "string") {
1739
- return;
1740
- }
1741
- const mode = parsed.sixb.mode;
1742
- const commitId = parsed.sixb.commitId;
1743
- const rowCount = parsed.sixb.rowCount;
1744
- const validatedPrimaryKeyColumns = parsed.sixb.validatedPrimaryKeyColumns;
1745
- return {
1746
- kind: "datasetVersion",
1747
- datasetId: parsed.sixb.datasetId,
1748
- ...typeof commitId === "string" ? { commitId } : {},
1749
- ...mode === "snapshot" || mode === "append" || mode === "merge" || mode === "schema" ? { mode } : {},
1750
- ...isDatasetProducer(parsed.sixb.producer) ? { producer: parsed.sixb.producer } : {},
1751
- ...isDatasetVersionRefs(parsed.sixb.inputs) ? { inputs: parsed.sixb.inputs } : {},
1752
- ...typeof commitId === "string" && isRowCount(rowCount) ? { rowCount } : {},
1753
- ...typeof commitId === "string" && isValidatedPrimaryKeyColumns(validatedPrimaryKeyColumns) ? { validatedPrimaryKeyColumns } : {},
1754
- ...isSchemaChangeMetadata(parsed.sixb.schemaChange) ? { schemaChange: parsed.sixb.schemaChange } : {}
1755
- };
1756
- }
1757
- function parseInlineDataChange(changesMade, tableId) {
1758
- const tableIdText = tableId.toString();
1759
- let hasDataChange = false;
1760
- let hasDeleteChange = false;
1761
- for (const change of changesMade.split(",")) {
1762
- const [kind, changedTableId] = change.split(":");
1763
- if (changedTableId !== tableIdText) {
1764
- continue;
1765
- }
1766
- const normalizedKind = kind.toLowerCase();
1767
- if (normalizedKind.includes("insert") || normalizedKind.includes("delete")) {
1768
- hasDataChange = true;
1769
- }
1770
- if (normalizedKind.includes("delete")) {
1771
- hasDeleteChange = true;
1772
- }
1773
- }
1774
- return { hasDataChange, hasDeleteChange };
1775
- }
1776
- function isRecord(value) {
1777
- return typeof value === "object" && value !== null && !Array.isArray(value);
1778
- }
1779
- function isDatasetProducer(value) {
1780
- return isRecord(value) && (value.kind === "sync" || value.kind === "pipeline" || value.kind === "ingest") && optionalString(value.id) && optionalString(value.runId);
1781
- }
1782
- function isDatasetVersionRefs(value) {
1783
- return Array.isArray(value) && value.every((item) => isRecord(item) && typeof item.datasetId === "string" && typeof item.versionId === "string");
1784
- }
1785
- function isSchemaChangeMetadata(value) {
1786
- return isRecord(value) && (value.addColumns === undefined || Array.isArray(value.addColumns) && value.addColumns.every((columnName) => typeof columnName === "string"));
1787
- }
1788
- function optionalString(value) {
1789
- return value === undefined || typeof value === "string";
1790
- }
1791
- function isRowCount(value) {
1792
- return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
1793
- }
1794
- function isValidatedPrimaryKeyColumns(value) {
1795
- return Array.isArray(value) && value.length > 0 && value.every((column) => typeof column === "string" && column.trim().length > 0) && new Set(value).size === value.length;
1796
- }
1797
-
1798
- // src/internal/ducklake-row-reader.ts
2190
+ import { LakeStorageError as LakeStorageError16 } from "@sixb/core/lake-storage";
1799
2191
  var READ_PAGE_ROWS = 5000;
1800
2192
  var PHYSICAL_ROW_ID_ALIAS = "__sixb_physical_row_id";
1801
2193
 
@@ -1809,58 +2201,68 @@ class DuckLakeRowReader {
1809
2201
  this.snapshots = snapshots;
1810
2202
  }
1811
2203
  async* readRows(input) {
1812
- this.connections.assertOpen();
1813
- const lease = await this.connections.acquireAttachedRuntime();
2204
+ const read = await this.connections.acquireRead(input.signal);
2205
+ let reader = null;
1814
2206
  try {
1815
- const runtime = lease.runtime;
1816
- const tableRef = await resolveDatasetTableRef(this.options, runtime, input.datasetId);
1817
- if (!tableRef) {
1818
- throw new LakeStorageError14(`[SixbDuckLake] Unknown dataset '${input.datasetId}'.`);
1819
- }
1820
- const version = await this.resolveVersion(runtime, tableRef, input.versionId);
1821
- const selectedColumns = this.resolveReadColumns(tableRef.datasetId, version.schema, input.columns);
1822
- const snapshotId = parseVersionId(version.versionId);
1823
- const columnsSql = selectedColumns.map((column) => quoteIdentifier(column.name)).join(", ");
1824
- const tableSql = qualifiedTableName(this.options, tableRef.tableName);
1825
- const schemaNames = new Set(version.schema.columns.map((column) => column.name.toLowerCase()));
1826
- const canUsePhysicalRowId = !schemaNames.has("rowid") && !schemaNames.has(PHYSICAL_ROW_ID_ALIAS.toLowerCase());
1827
- const orderSql = canUsePhysicalRowId ? " ORDER BY rowid" : "";
1828
- const baseSql = `SELECT ${columnsSql} FROM ${tableSql} AT (VERSION => ${snapshotId})`;
1829
- const requestedOffset = Math.max(0, Math.trunc(input.offset ?? 0));
1830
- let remaining = input.limit === undefined ? undefined : Math.max(0, Math.trunc(input.limit));
1831
- if (remaining === 0)
2207
+ const prepared = await this.connections.withAttachedRuntime(async (runtime) => {
2208
+ read.signal.throwIfAborted();
2209
+ const tableRef = await resolveDatasetTableRef(this.options, runtime, input.datasetId);
2210
+ if (!tableRef) {
2211
+ throw new LakeStorageError16(`[SixbDuckLake] Unknown dataset '${input.datasetId}'.`);
2212
+ }
2213
+ const version = await this.resolveVersion(runtime, tableRef, input.versionId);
2214
+ const columns = this.resolveReadColumns(tableRef.datasetId, version.schema, input.columns);
2215
+ const names = new Set(version.schema.columns.map((column) => column.name.toLowerCase()));
2216
+ const physicalOrder = !names.has("rowid") && !names.has(PHYSICAL_ROW_ID_ALIAS);
2217
+ const relation = `${qualifiedTableName(this.options, tableRef.tableName)} AT (VERSION => ${parseVersionId(version.versionId)})`;
2218
+ const columnsSql = columns.map((column) => quoteIdentifier(column.name)).join(", ");
2219
+ const offset = Math.max(0, Math.trunc(input.offset ?? 0));
2220
+ const limit = input.limit === undefined ? undefined : Math.max(0, Math.trunc(input.limit));
2221
+ const sql = `SELECT ${columnsSql} FROM ${relation}${physicalOrder ? " ORDER BY rowid" : ""}${limit === undefined ? "" : ` LIMIT ${limit}`}${offset > 0 ? ` OFFSET ${offset}` : ""}`;
2222
+ const cursor = limit === undefined || limit > READ_PAGE_ROWS ? await this.connections.openReader(runtime, sql, read.signal, read.release) : null;
2223
+ const query = { columnsSql, relation, physicalOrder };
2224
+ const firstPage = cursor || limit === 0 ? [] : await runtime.query(pageSql(query, Math.min(limit ?? READ_PAGE_ROWS, READ_PAGE_ROWS), offset, null));
2225
+ return { columns, ...query, offset, limit, cursor, firstPage };
2226
+ });
2227
+ reader = prepared.cursor;
2228
+ if (!reader)
2229
+ read.signal.addEventListener("abort", read.release, { once: true });
2230
+ read.signal.throwIfAborted();
2231
+ if (reader) {
2232
+ yield* this.normalizeRows(reader.rows(), prepared.columns, read.signal);
1832
2233
  return;
1833
- let offset = requestedOffset;
1834
- let physicalCursor = null;
1835
- while (true) {
1836
- const pageRows = remaining === undefined ? READ_PAGE_ROWS : Math.min(READ_PAGE_ROWS, remaining);
1837
- const pageSql = canUsePhysicalRowId ? `SELECT rowid AS ${quoteIdentifier(PHYSICAL_ROW_ID_ALIAS)}, ${columnsSql}
1838
- FROM ${tableSql} AT (VERSION => ${snapshotId})
1839
- ${physicalCursor === null ? "" : `WHERE rowid > ${physicalCursor}`}
1840
- ORDER BY rowid LIMIT ${pageRows}
1841
- ${physicalCursor === null && offset > 0 ? `OFFSET ${offset}` : ""}` : `${baseSql}${orderSql} LIMIT ${pageRows} OFFSET ${offset}`;
1842
- const rows = await runtime.query(pageSql);
1843
- if (rows.length === 0)
1844
- return;
1845
- yield* this.normalizeRows(rows, selectedColumns);
1846
- offset += rows.length;
1847
- if (remaining !== undefined) {
2234
+ }
2235
+ let offset = prepared.offset;
2236
+ let remaining = prepared.limit;
2237
+ let cursor = null;
2238
+ let page = prepared.firstPage;
2239
+ while (remaining !== 0) {
2240
+ read.signal.throwIfAborted();
2241
+ const pageSize = Math.min(remaining ?? READ_PAGE_ROWS, READ_PAGE_ROWS);
2242
+ const rows = page ?? await this.connections.withAttachedRuntime((runtime) => runtime.query(pageSql(prepared, pageSize, offset, cursor)));
2243
+ page = undefined;
2244
+ read.signal.throwIfAborted();
2245
+ yield* this.normalizeRows(rows, prepared.columns, read.signal);
2246
+ if (remaining !== undefined)
1848
2247
  remaining -= rows.length;
1849
- if (remaining === 0)
1850
- return;
1851
- }
1852
- if (canUsePhysicalRowId) {
1853
- physicalCursor = getBigIntLike(rows[rows.length - 1], PHYSICAL_ROW_ID_ALIAS);
1854
- }
1855
- if (rows.length < pageRows)
2248
+ if (rows.length < pageSize)
1856
2249
  return;
2250
+ if (prepared.physicalOrder)
2251
+ cursor = getBigIntLike(rows[rows.length - 1], PHYSICAL_ROW_ID_ALIAS);
2252
+ offset += rows.length;
1857
2253
  }
1858
2254
  } finally {
1859
- await lease.release();
2255
+ read.signal.removeEventListener("abort", read.release);
2256
+ try {
2257
+ await reader?.close();
2258
+ } finally {
2259
+ read.release();
2260
+ }
1860
2261
  }
1861
2262
  }
1862
- async* normalizeRows(rows, selectedColumns) {
2263
+ async* normalizeRows(rows, selectedColumns, signal) {
1863
2264
  for await (const row of rows) {
2265
+ signal.throwIfAborted();
1864
2266
  const output = {};
1865
2267
  for (const column of selectedColumns) {
1866
2268
  output[column.name] = normalizeReadValue(row[column.name], column);
@@ -1891,18 +2293,23 @@ class DuckLakeRowReader {
1891
2293
  return columns.map((columnName) => {
1892
2294
  const column = columnsByName.get(columnName);
1893
2295
  if (!column) {
1894
- throw new LakeStorageError14(`[SixbDuckLake] Dataset '${datasetId}' does not have column '${columnName}' at the requested version.`);
2296
+ throw new LakeStorageError16(`[SixbDuckLake] Dataset '${datasetId}' does not have column '${columnName}' at the requested version.`);
1895
2297
  }
1896
2298
  return column;
1897
2299
  });
1898
2300
  }
1899
2301
  throwNoCommittedVersion(datasetId) {
1900
- throw new LakeStorageError14(`[SixbDuckLake] No committed version found for dataset '${datasetId}'.`);
2302
+ throw new LakeStorageError16(`[SixbDuckLake] No committed version found for dataset '${datasetId}'.`);
1901
2303
  }
1902
2304
  }
2305
+ function pageSql(read, limit, offset, cursor) {
2306
+ return read.physicalOrder ? `SELECT rowid AS ${quoteIdentifier(PHYSICAL_ROW_ID_ALIAS)}, ${read.columnsSql}
2307
+ FROM ${read.relation} ${cursor === null ? "" : `WHERE rowid > ${cursor}`}
2308
+ ORDER BY rowid LIMIT ${limit} ${cursor === null && offset > 0 ? `OFFSET ${offset}` : ""}` : `SELECT ${read.columnsSql} FROM ${read.relation} LIMIT ${limit} OFFSET ${offset}`;
2309
+ }
1903
2310
 
1904
2311
  // src/internal/ducklake-snapshot-reader.ts
1905
- import { LakeStorageError as LakeStorageError15 } from "@sixb/core/lake-storage";
2312
+ import { LakeStorageError as LakeStorageError17 } from "@sixb/core/lake-storage";
1906
2313
  var SNAPSHOT_ROW_BATCH_SIZE = 128;
1907
2314
  var CATALOG_SNAPSHOT_SCAN_LIMIT = 512;
1908
2315
 
@@ -2115,7 +2522,7 @@ class DuckLakeSnapshotReader {
2115
2522
  }
2116
2523
  assertNoMetadataConflict(snapshotId, datasetId, metadata) {
2117
2524
  if (metadata !== undefined && metadata.datasetId !== datasetId) {
2118
- throw new LakeStorageError15(`[SixbDuckLake] DuckLake snapshot '${snapshotId}' changed dataset '${datasetId}' but Sixb commit metadata references dataset '${metadata.datasetId}'.`);
2525
+ throw new LakeStorageError17(`[SixbDuckLake] DuckLake snapshot '${snapshotId}' changed dataset '${datasetId}' but Sixb commit metadata references dataset '${metadata.datasetId}'.`);
2119
2526
  }
2120
2527
  }
2121
2528
  async getLatestVersionForDefinition(runtime, dataset) {
@@ -2260,7 +2667,10 @@ class DuckLakeSnapshotReader {
2260
2667
  if (input.limit !== undefined && input.limit <= 0) {
2261
2668
  return [];
2262
2669
  }
2263
- const where = [];
2670
+ const ducklakeTable = duckLakeMetadataTableName(this.options, "ducklake_table");
2671
+ const where = [
2672
+ `snapshot.snapshot_id >= (SELECT min(begin_snapshot) FROM ${ducklakeTable} WHERE table_id = ${input.tableId})`
2673
+ ];
2264
2674
  if (input.exactSnapshotId !== undefined) {
2265
2675
  assertDuckLakeSnapshotId2(input.exactSnapshotId);
2266
2676
  where.push(`snapshot.snapshot_id = ${input.exactSnapshotId}`);
@@ -2269,7 +2679,7 @@ class DuckLakeSnapshotReader {
2269
2679
  assertDuckLakeSnapshotId2(input.beforeSnapshotId);
2270
2680
  where.push(`snapshot.snapshot_id < ${input.beforeSnapshotId}`);
2271
2681
  }
2272
- const whereSql = where.length === 0 ? "" : `WHERE ${where.join(" AND ")}`;
2682
+ const whereSql = `WHERE ${where.join(" AND ")}`;
2273
2683
  const limitSql = input.limit === undefined ? "" : `LIMIT ${Math.max(0, Math.trunc(input.limit))}`;
2274
2684
  const ducklakeSnapshot = duckLakeMetadataTableName(this.options, "ducklake_snapshot");
2275
2685
  const ducklakeSnapshotChanges = duckLakeMetadataTableName(this.options, "ducklake_snapshot_changes");
@@ -2395,7 +2805,7 @@ class DuckLakeSnapshotReader {
2395
2805
  }
2396
2806
  function assertDuckLakeSnapshotId2(snapshotId) {
2397
2807
  if (!/^\d+$/.test(snapshotId)) {
2398
- throw new LakeStorageError15(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`);
2808
+ throw new LakeStorageError17(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`);
2399
2809
  }
2400
2810
  }
2401
2811
  function fileChangeKey(snapshotId, tableId) {
@@ -2403,19 +2813,19 @@ function fileChangeKey(snapshotId, tableId) {
2403
2813
  }
2404
2814
 
2405
2815
  // src/internal/ducklake-sql-executor.ts
2406
- import { randomUUID as randomUUID3 } from "crypto";
2816
+ import { randomUUID as randomUUID4 } from "crypto";
2407
2817
  import {
2408
- LakeStorageError as LakeStorageError18
2818
+ LakeStorageError as LakeStorageError20
2409
2819
  } from "@sixb/core/lake-storage";
2410
2820
 
2411
2821
  // src/internal/dataset-row-commit.ts
2412
2822
  import {
2413
2823
  getDatasetPrimaryKeyColumns,
2414
- LakeStorageError as LakeStorageError16
2824
+ LakeStorageError as LakeStorageError18
2415
2825
  } from "@sixb/core/lake-storage";
2416
2826
  function assertDatasetWriteMode(mode, operation) {
2417
2827
  if (mode !== "snapshot" && mode !== "append") {
2418
- throw new LakeStorageError16(`[SixbDuckLake] Invalid ${operation} mode '${String(mode)}'. Expected 'snapshot' or 'append'.`);
2828
+ throw new LakeStorageError18(`[SixbDuckLake] Invalid ${operation} mode '${String(mode)}'. Expected 'snapshot' or 'append'.`);
2419
2829
  }
2420
2830
  }
2421
2831
  async function applyDatasetRowsFromRelation(input) {
@@ -2475,7 +2885,7 @@ async function applyDatasetRowsFromRelation(input) {
2475
2885
  async function inspectUniqueKeyedRelation(input) {
2476
2886
  const primaryKeyColumns = getDatasetPrimaryKeyColumns(input.dataset);
2477
2887
  if (primaryKeyColumns === null) {
2478
- throw new LakeStorageError16(`[SixbDuckLake] Dataset '${input.dataset.id}' must define a primaryKey before rows can be keyed.`);
2888
+ throw new LakeStorageError18(`[SixbDuckLake] Dataset '${input.dataset.id}' must define a primaryKey before rows can be keyed.`);
2479
2889
  }
2480
2890
  const keysSql = primaryKeyColumns.map((column) => quoteIdentifier(column)).join(", ");
2481
2891
  const [row] = await input.runtime.query(`
@@ -2490,7 +2900,7 @@ async function inspectUniqueKeyedRelation(input) {
2490
2900
  const rowCount = row === undefined ? 0 : Number(getBigIntLike(row, "row_count"));
2491
2901
  const maxKeyCount = row === undefined ? 0 : Number(getBigIntLike(row, "max_key_count"));
2492
2902
  if (maxKeyCount > 1) {
2493
- throw new LakeStorageError16(`[SixbDuckLake] Dataset '${input.dataset.id}' ${input.context} contains duplicate primary key.`);
2903
+ throw new LakeStorageError18(`[SixbDuckLake] Dataset '${input.dataset.id}' ${input.context} contains duplicate primary key.`);
2494
2904
  }
2495
2905
  return rowCount;
2496
2906
  }
@@ -2507,7 +2917,7 @@ async function assertNoPrimaryKeyOverlap(input) {
2507
2917
  LIMIT 1
2508
2918
  `);
2509
2919
  if (collision !== undefined) {
2510
- throw new LakeStorageError16(`[SixbDuckLake] Dataset '${input.dataset.id}' ${input.context} contains duplicate primary key.`);
2920
+ throw new LakeStorageError18(`[SixbDuckLake] Dataset '${input.dataset.id}' ${input.context} contains duplicate primary key.`);
2511
2921
  }
2512
2922
  }
2513
2923
  async function fingerprintRelation(runtime, relationSql, columnsSql) {
@@ -2539,9 +2949,9 @@ async function countRows(runtime, relationSql) {
2539
2949
  }
2540
2950
 
2541
2951
  // src/internal/sql-transform-relations.ts
2542
- import { randomUUID as randomUUID2 } from "crypto";
2952
+ import { randomUUID as randomUUID3 } from "crypto";
2543
2953
  import {
2544
- LakeStorageError as LakeStorageError17
2954
+ LakeStorageError as LakeStorageError19
2545
2955
  } from "@sixb/core/lake-storage";
2546
2956
  var RELATION_PLACEHOLDER_PREFIX = "__sixb_sql_transform_relation_";
2547
2957
  var RELATION_PLACEHOLDER_PATTERN = /__sixb_sql_transform_relation_[0-9a-f]{32}_[0-9]+__/g;
@@ -2559,12 +2969,12 @@ function renderDuckLakeSqlTransformSql(input) {
2559
2969
  const context = createDuckLakeSqlTransformRelationContext(input);
2560
2970
  const sql = input.sql(context.relations);
2561
2971
  if (typeof sql !== "string") {
2562
- throw new LakeStorageError17("[SixbDuckLake] SQL transform body must return a SQL string.");
2972
+ throw new LakeStorageError19("[SixbDuckLake] SQL transform body must return a SQL string.");
2563
2973
  }
2564
2974
  return context.render(sql);
2565
2975
  }
2566
2976
  function createDuckLakeSqlTransformRelationContext(input) {
2567
- const token = randomUUID2().replaceAll("-", "");
2977
+ const token = randomUUID3().replaceAll("-", "");
2568
2978
  const relations = Object.create(null);
2569
2979
  const replacements = new Map;
2570
2980
  let index = 0;
@@ -2597,7 +3007,7 @@ function renderDuckLakeSourceRelation(options, source) {
2597
3007
  return `(SELECT * FROM ${qualifiedTableName(options, tableName)} AT (VERSION => ${snapshotId}))`;
2598
3008
  }
2599
3009
  function throwUnresolvedPlaceholder() {
2600
- throw new LakeStorageError17("[SixbDuckLake] SQL transform contains an unresolved relation placeholder.");
3010
+ throw new LakeStorageError19("[SixbDuckLake] SQL transform contains an unresolved relation placeholder.");
2601
3011
  }
2602
3012
 
2603
3013
  // src/internal/ducklake-sql-executor.ts
@@ -2691,14 +3101,14 @@ class DuckLakeSqlExecutor {
2691
3101
  async resolveSourceDataset(runtime, dataset) {
2692
3102
  const definition = await this.datasets.getDatasetOnRuntime(runtime, dataset.id);
2693
3103
  if (!definition) {
2694
- throw new LakeStorageError18(`[SixbDuckLake] Unknown SQL transform source dataset '${dataset.id}'.`);
3104
+ throw new LakeStorageError20(`[SixbDuckLake] Unknown SQL transform source dataset '${dataset.id}'.`);
2695
3105
  }
2696
3106
  return definition;
2697
3107
  }
2698
3108
  async resolveTargetDataset(runtime, dataset) {
2699
3109
  const definition = await this.datasets.getDatasetOnRuntime(runtime, dataset.id);
2700
3110
  if (!definition) {
2701
- throw new LakeStorageError18(`[SixbDuckLake] Unknown SQL transform target dataset '${dataset.id}'.`);
3111
+ throw new LakeStorageError20(`[SixbDuckLake] Unknown SQL transform target dataset '${dataset.id}'.`);
2702
3112
  }
2703
3113
  this.datasets.assertSchema(definition);
2704
3114
  return definition;
@@ -2748,7 +3158,7 @@ class DuckLakeSqlExecutor {
2748
3158
  return checkedCasts;
2749
3159
  }
2750
3160
  async applySqlTransform(input) {
2751
- const tempTableName = `sixb_sql_transform_${randomUUID3().replaceAll("-", "")}`;
3161
+ const tempTableName = `sixb_sql_transform_${randomUUID4().replaceAll("-", "")}`;
2752
3162
  const tempTable = quoteIdentifier(tempTableName);
2753
3163
  await input.runtime.run(`CREATE TEMP TABLE ${tempTable} AS SELECT * FROM (${input.sql}) AS sixb_sql_transform_result`);
2754
3164
  let result;
@@ -2819,33 +3229,33 @@ function normalizePreviewValue(value) {
2819
3229
  return value;
2820
3230
  }
2821
3231
  function throwNoCommittedSourceVersion(datasetId) {
2822
- throw new LakeStorageError18(`[SixbDuckLake] No committed version found for SQL transform source dataset '${datasetId}'.`);
3232
+ throw new LakeStorageError20(`[SixbDuckLake] No committed version found for SQL transform source dataset '${datasetId}'.`);
2823
3233
  }
2824
3234
  function throwResultSchemaMismatch(datasetId, detail) {
2825
- throw new LakeStorageError18(`[SixbDuckLake] SQL transform result schema does not match target dataset '${datasetId}': ${detail}.`);
3235
+ throw new LakeStorageError20(`[SixbDuckLake] SQL transform result schema does not match target dataset '${datasetId}': ${detail}.`);
2826
3236
  }
2827
3237
 
2828
3238
  // src/internal/ducklake-write-coordinator.ts
2829
- import { randomUUID as randomUUID6 } from "crypto";
3239
+ import { randomUUID as randomUUID7 } from "crypto";
2830
3240
  import {
2831
3241
  assertUnsequencedDatasetWrite,
2832
3242
  getDatasetPrimaryKeyColumns as getDatasetPrimaryKeyColumns4,
2833
3243
  hasDatasetInputChanges,
2834
3244
  LakeConcurrencyError,
2835
- LakeStorageError as LakeStorageError22,
3245
+ LakeStorageError as LakeStorageError24,
2836
3246
  retryDatasetMergeCommit
2837
3247
  } from "@sixb/core/lake-storage";
2838
3248
 
2839
3249
  // src/internal/dataset-row-merge.ts
2840
3250
  import {
2841
3251
  getDatasetPrimaryKeyColumns as getDatasetPrimaryKeyColumns2,
2842
- LakeStorageError as LakeStorageError19,
3252
+ LakeStorageError as LakeStorageError21,
2843
3253
  reconcileDatasetSequences
2844
3254
  } from "@sixb/core/lake-storage";
2845
3255
  async function applyDatasetMergeFromRelation(input) {
2846
3256
  const primaryKeyColumns = getDatasetPrimaryKeyColumns2(input.dataset);
2847
3257
  if (primaryKeyColumns === null) {
2848
- throw new LakeStorageError19(`[SixbDuckLake] Dataset '${input.dataset.id}' must define a primaryKey before it can be merged.`);
3258
+ throw new LakeStorageError21(`[SixbDuckLake] Dataset '${input.dataset.id}' must define a primaryKey before it can be merged.`);
2849
3259
  }
2850
3260
  const stagingTable = quoteIdentifier(input.stagingTableName);
2851
3261
  const targetTable = qualifiedTableName(input.options, encodeDatasetTableName(input.dataset.id));
@@ -3006,7 +3416,7 @@ function finalChangesSelectSql(input, primaryKeyColumns) {
3006
3416
  async function countMergeEffects(input, targetTable, finalChanges, primaryKeyColumns) {
3007
3417
  const matchColumn = primaryKeyColumns[0];
3008
3418
  if (matchColumn === undefined) {
3009
- throw new LakeStorageError19(`[SixbDuckLake] Dataset '${input.dataset.id}' primaryKey must contain at least one column.`);
3419
+ throw new LakeStorageError21(`[SixbDuckLake] Dataset '${input.dataset.id}' primaryKey must contain at least one column.`);
3010
3420
  }
3011
3421
  const keyMatchSql = primaryKeyMatchSql(primaryKeyColumns, "target", "source");
3012
3422
  const targetMatch = `target.${quoteIdentifier(matchColumn)} IS NOT NULL`;
@@ -3055,12 +3465,12 @@ function sameColumns(left, right) {
3055
3465
  }
3056
3466
 
3057
3467
  // src/internal/ducklake-merge-session.ts
3058
- import { randomUUID as randomUUID4 } from "crypto";
3468
+ import { randomUUID as randomUUID5 } from "crypto";
3059
3469
  import {
3060
3470
  cloneDatasetMergeChange,
3061
3471
  datasetSequenceChange,
3062
3472
  getDatasetMergeChangeValidationError,
3063
- LakeStorageError as LakeStorageError20
3473
+ LakeStorageError as LakeStorageError22
3064
3474
  } from "@sixb/core/lake-storage";
3065
3475
 
3066
3476
  // src/internal/row-appender.ts
@@ -3119,7 +3529,7 @@ function fileRefToDuckDbStruct(fileRef) {
3119
3529
  // src/internal/ducklake-merge-session.ts
3120
3530
  var MERGE_CHANGE_BATCH_SIZE = 1000;
3121
3531
  async function createDuckLakeMergeSession(input) {
3122
- const suffix = randomUUID4().replaceAll("-", "");
3532
+ const suffix = randomUUID5().replaceAll("-", "");
3123
3533
  const stagingTableName = `sixb_merge_${suffix}`;
3124
3534
  const sequenceColumnName = `sixb_merge_sequence_${suffix}`;
3125
3535
  const kindColumnName = `sixb_merge_kind_${suffix}`;
@@ -3161,7 +3571,7 @@ class DuckLakeMergeSession {
3161
3571
  for await (const change of changes) {
3162
3572
  const validationError = getDatasetMergeChangeValidationError(change, this.input.dataset);
3163
3573
  if (validationError) {
3164
- throw new LakeStorageError20(`[SixbDuckLake] ${validationError}`);
3574
+ throw new LakeStorageError22(`[SixbDuckLake] ${validationError}`);
3165
3575
  }
3166
3576
  batch.push({
3167
3577
  sequence: this.nextSequence,
@@ -3224,7 +3634,7 @@ class DuckLakeMergeSession {
3224
3634
  }
3225
3635
  assertOpen() {
3226
3636
  if (this.closed) {
3227
- throw new LakeStorageError20("[SixbDuckLake] Merge session is already closed.");
3637
+ throw new LakeStorageError22("[SixbDuckLake] Merge session is already closed.");
3228
3638
  }
3229
3639
  }
3230
3640
  async cleanup() {
@@ -3239,16 +3649,16 @@ class DuckLakeMergeSession {
3239
3649
  }
3240
3650
 
3241
3651
  // src/internal/ducklake-write-session.ts
3242
- import { randomUUID as randomUUID5 } from "crypto";
3652
+ import { randomUUID as randomUUID6 } from "crypto";
3243
3653
  import { getDatasetRowValidationError } from "@sixb/core";
3244
3654
  import {
3245
3655
  encodeDatasetPrimaryKey,
3246
3656
  getDatasetPrimaryKeyColumns as getDatasetPrimaryKeyColumns3,
3247
- LakeStorageError as LakeStorageError21
3657
+ LakeStorageError as LakeStorageError23
3248
3658
  } from "@sixb/core/lake-storage";
3249
3659
  var WRITE_ROW_BATCH_SIZE = 1000;
3250
3660
  async function createDuckLakeWriteSession(input) {
3251
- const stagingTableName = `sixb_write_${randomUUID5().replaceAll("-", "")}`;
3661
+ const stagingTableName = `sixb_write_${randomUUID6().replaceAll("-", "")}`;
3252
3662
  await input.runtime.run(`CREATE TEMP TABLE ${quoteIdentifier(stagingTableName)} (${datasetSchemaToDuckDbColumnsSql(input.write.dataset.schema)})`);
3253
3663
  return new DuckLakeWriteSession(input.commitWrite, input.runtime, input.write, stagingTableName);
3254
3664
  }
@@ -3275,13 +3685,13 @@ class DuckLakeWriteSession {
3275
3685
  for await (const row of rows) {
3276
3686
  const validationError = getDatasetRowValidationError(row, this.input.dataset);
3277
3687
  if (validationError) {
3278
- throw new LakeStorageError21(`[SixbDuckLake] ${validationError}`);
3688
+ throw new LakeStorageError23(`[SixbDuckLake] ${validationError}`);
3279
3689
  }
3280
3690
  const clonedRow = structuredClone(row);
3281
3691
  if (getDatasetPrimaryKeyColumns3(this.input.dataset) !== null) {
3282
3692
  const primaryKey = encodeDatasetPrimaryKey(this.input.dataset, clonedRow);
3283
3693
  if (this.stagedPrimaryKeys.has(primaryKey) || batchPrimaryKeys.has(primaryKey)) {
3284
- throw new LakeStorageError21(`[SixbDuckLake] Dataset '${this.input.dataset.id}' ${this.input.mode ?? "snapshot"} source contains duplicate primary key.`);
3694
+ throw new LakeStorageError23(`[SixbDuckLake] Dataset '${this.input.dataset.id}' ${this.input.mode ?? "snapshot"} source contains duplicate primary key.`);
3285
3695
  }
3286
3696
  batchPrimaryKeys.add(primaryKey);
3287
3697
  }
@@ -3338,7 +3748,7 @@ class DuckLakeWriteSession {
3338
3748
  }
3339
3749
  assertOpen() {
3340
3750
  if (this.closed) {
3341
- throw new LakeStorageError21("[SixbDuckLake] Write session is already closed.");
3751
+ throw new LakeStorageError23("[SixbDuckLake] Write session is already closed.");
3342
3752
  }
3343
3753
  }
3344
3754
  async cleanup() {
@@ -3346,6 +3756,7 @@ class DuckLakeWriteSession {
3346
3756
  return;
3347
3757
  }
3348
3758
  this.cleanedUp = true;
3759
+ this.stagedPrimaryKeys.clear();
3349
3760
  await this.dropStagingTable();
3350
3761
  }
3351
3762
  async dropStagingTable() {
@@ -3371,7 +3782,7 @@ class DuckLakeWriteCoordinator {
3371
3782
  this.connections.assertOpen();
3372
3783
  const definition = await this.datasets.getDataset(input.dataset.id);
3373
3784
  if (!definition) {
3374
- throw new LakeStorageError22(`[SixbDuckLake] Unknown dataset '${input.dataset.id}'.`);
3785
+ throw new LakeStorageError24(`[SixbDuckLake] Unknown dataset '${input.dataset.id}'.`);
3375
3786
  }
3376
3787
  this.datasets.assertSchema(definition);
3377
3788
  assertUnsequencedDatasetWrite(definition);
@@ -3392,10 +3803,10 @@ class DuckLakeWriteCoordinator {
3392
3803
  this.connections.assertOpen();
3393
3804
  const definition = await this.datasets.getDataset(input.dataset.id);
3394
3805
  if (!definition) {
3395
- throw new LakeStorageError22(`[SixbDuckLake] Unknown dataset '${input.dataset.id}'.`);
3806
+ throw new LakeStorageError24(`[SixbDuckLake] Unknown dataset '${input.dataset.id}'.`);
3396
3807
  }
3397
3808
  if (getDatasetPrimaryKeyColumns4(definition) === null) {
3398
- throw new LakeStorageError22(`[SixbDuckLake] Dataset '${definition.id}' must define a primaryKey before it can be merged.`);
3809
+ throw new LakeStorageError24(`[SixbDuckLake] Dataset '${definition.id}' must define a primaryKey before it can be merged.`);
3399
3810
  }
3400
3811
  this.datasets.assertSchema(definition);
3401
3812
  const latestVersion = await this.connections.withAttachedRuntime((runtime) => this.snapshots.getLatestVersionForDefinition(runtime, definition));
@@ -3413,7 +3824,7 @@ class DuckLakeWriteCoordinator {
3413
3824
  async commitWrite(input) {
3414
3825
  const definition = await this.datasets.getDataset(input.write.dataset.id);
3415
3826
  if (!definition) {
3416
- throw new LakeStorageError22(`[SixbDuckLake] Unknown dataset '${input.write.dataset.id}'.`);
3827
+ throw new LakeStorageError24(`[SixbDuckLake] Unknown dataset '${input.write.dataset.id}'.`);
3417
3828
  }
3418
3829
  const mode = input.write.mode ?? "snapshot";
3419
3830
  assertDatasetWriteMode(mode, "write");
@@ -3434,10 +3845,10 @@ class DuckLakeWriteCoordinator {
3434
3845
  async commitMerge(input) {
3435
3846
  const definition = await this.datasets.getDataset(input.merge.dataset.id);
3436
3847
  if (!definition) {
3437
- throw new LakeStorageError22(`[SixbDuckLake] Unknown dataset '${input.merge.dataset.id}'.`);
3848
+ throw new LakeStorageError24(`[SixbDuckLake] Unknown dataset '${input.merge.dataset.id}'.`);
3438
3849
  }
3439
3850
  if (getDatasetPrimaryKeyColumns4(definition) === null) {
3440
- throw new LakeStorageError22(`[SixbDuckLake] Dataset '${definition.id}' must define a primaryKey before it can be merged.`);
3851
+ throw new LakeStorageError24(`[SixbDuckLake] Dataset '${definition.id}' must define a primaryKey before it can be merged.`);
3441
3852
  }
3442
3853
  return retryDatasetMergeCommit(input.merge, input.commit, (rebase) => this.withCommitRuntime(async (runtime) => {
3443
3854
  input.commit?.signal?.throwIfAborted();
@@ -3465,7 +3876,7 @@ class DuckLakeWriteCoordinator {
3465
3876
  validatedPrimaryKeyColumns: context.validatedPrimaryKeyColumns
3466
3877
  });
3467
3878
  if (result.sourceRowCount !== input.changesWritten) {
3468
- throw new LakeStorageError22(`[SixbDuckLake] Staged merge for dataset '${definition.id}' accepted ${input.changesWritten} change(s), but DuckLake saw ${result.sourceRowCount} source change(s) at commit time.`);
3879
+ throw new LakeStorageError24(`[SixbDuckLake] Staged merge for dataset '${definition.id}' accepted ${input.changesWritten} change(s), but DuckLake saw ${result.sourceRowCount} source change(s) at commit time.`);
3469
3880
  }
3470
3881
  return result;
3471
3882
  }
@@ -3487,7 +3898,7 @@ class DuckLakeWriteCoordinator {
3487
3898
  disableRetries: input.mode === "append" && getDatasetPrimaryKeyColumns4(input.dataset) !== null
3488
3899
  });
3489
3900
  if (result.version === null) {
3490
- throw new LakeStorageError22(`[SixbDuckLake] No DuckLake changes were committed for dataset '${input.dataset.id}', and no previous version exists.`);
3901
+ throw new LakeStorageError24(`[SixbDuckLake] No DuckLake changes were committed for dataset '${input.dataset.id}', and no previous version exists.`);
3491
3902
  }
3492
3903
  return { ...result.version, outcome: result.outcome };
3493
3904
  }
@@ -3518,10 +3929,9 @@ class DuckLakeWriteCoordinator {
3518
3929
  }
3519
3930
  async commitDatasetVersionUnlocked(input) {
3520
3931
  const previousWriteSnapshotId = await this.lastCommittedSnapshotId(input.runtime);
3521
- await input.runtime.run("BEGIN TRANSACTION");
3522
3932
  let committed = false;
3523
3933
  try {
3524
- const latestVersion = await this.latestVersionForCommit(input);
3934
+ const latestVersion = await this.beginVersionTransaction(input);
3525
3935
  if (input.expectedLatestVersionId !== undefined) {
3526
3936
  this.assertExpectedLatestVersion({
3527
3937
  datasetId: input.dataset.id,
@@ -3536,7 +3946,7 @@ class DuckLakeWriteCoordinator {
3536
3946
  });
3537
3947
  input.signal?.throwIfAborted();
3538
3948
  await this.ensureNoOpHasSnapshot(input, changeResult, latestVersion);
3539
- const commitId = randomUUID6();
3949
+ const commitId = randomUUID7();
3540
3950
  await this.setCommitMetadata(input, commitId, commitRowCount(input, changeResult, latestVersion));
3541
3951
  await input.runtime.run("COMMIT");
3542
3952
  committed = true;
@@ -3569,7 +3979,7 @@ class DuckLakeWriteCoordinator {
3569
3979
  if (version) {
3570
3980
  return { outcome: "created", version };
3571
3981
  }
3572
- throw new LakeStorageError22(`[SixbDuckLake] DuckLake committed snapshot '${ownSnapshotId}' for dataset '${input.dataset.id}', but Sixb could not hydrate it as a dataset version.`);
3982
+ throw new LakeStorageError24(`[SixbDuckLake] DuckLake committed snapshot '${ownSnapshotId}' for dataset '${input.dataset.id}', but Sixb could not hydrate it as a dataset version.`);
3573
3983
  }
3574
3984
  if (!input.changeResult.dataChangeExpected) {
3575
3985
  const version = await this.versionForNoOpCommit(input.runtime, input.dataset, input.allowInitialNoOp ?? false);
@@ -3587,9 +3997,9 @@ class DuckLakeWriteCoordinator {
3587
3997
  };
3588
3998
  }
3589
3999
  if (input.committedWriteSnapshotId === input.previousWriteSnapshotId) {
3590
- throw new LakeStorageError22(`[SixbDuckLake] DuckLake commit for dataset '${input.dataset.id}' completed without producing a snapshot for this non-empty write.`);
4000
+ throw new LakeStorageError24(`[SixbDuckLake] DuckLake commit for dataset '${input.dataset.id}' completed without producing a snapshot for this non-empty write.`);
3591
4001
  }
3592
- throw new LakeStorageError22(`[SixbDuckLake] DuckLake commit for dataset '${input.dataset.id}' completed, but Sixb could not find a matching snapshot for this write.`);
4002
+ throw new LakeStorageError24(`[SixbDuckLake] DuckLake commit for dataset '${input.dataset.id}' completed, but Sixb could not find a matching snapshot for this write.`);
3593
4003
  }
3594
4004
  async applyStagedRows(input) {
3595
4005
  const mode = input.write.mode ?? "snapshot";
@@ -3602,7 +4012,7 @@ class DuckLakeWriteCoordinator {
3602
4012
  previousRowCount: input.previousRowCount
3603
4013
  });
3604
4014
  if (result.sourceRowCount !== input.rowsWritten) {
3605
- throw new LakeStorageError22(`[SixbDuckLake] Staged write for dataset '${input.write.dataset.id}' accepted ${input.rowsWritten} row(s), but DuckLake saw ${result.sourceRowCount} source row(s) at commit time.`);
4015
+ throw new LakeStorageError24(`[SixbDuckLake] Staged write for dataset '${input.write.dataset.id}' accepted ${input.rowsWritten} row(s), but DuckLake saw ${result.sourceRowCount} source row(s) at commit time.`);
3606
4016
  }
3607
4017
  return result;
3608
4018
  }
@@ -3624,7 +4034,7 @@ class DuckLakeWriteCoordinator {
3624
4034
  if (changeResult.dataChangeExpected) {
3625
4035
  return;
3626
4036
  }
3627
- const latestVersion = knownLatestVersion ?? await this.snapshots.getLatestVersionSummaryForDefinition(input.runtime, input.dataset);
4037
+ const latestVersion = knownLatestVersion === undefined ? await this.snapshots.getLatestVersionSummaryForDefinition(input.runtime, input.dataset) : knownLatestVersion;
3628
4038
  if (latestVersion ? input.mode === "merge" || !hasDatasetInputChanges(latestVersion.inputs, input.inputs) : input.allowInitialNoOp) {
3629
4039
  return;
3630
4040
  }
@@ -3640,18 +4050,39 @@ class DuckLakeWriteCoordinator {
3640
4050
  if (allowInitialNoOp) {
3641
4051
  return null;
3642
4052
  }
3643
- throw new LakeStorageError22(`[SixbDuckLake] No DuckLake changes were committed for dataset '${definition.id}', and no previous version exists.`);
4053
+ throw new LakeStorageError24(`[SixbDuckLake] No DuckLake changes were committed for dataset '${definition.id}', and no previous version exists.`);
3644
4054
  }
3645
4055
  async rollbackTransaction(runtime) {
3646
4056
  try {
3647
4057
  await runtime.run("ROLLBACK");
3648
4058
  } catch {}
3649
4059
  }
3650
- async latestVersionForCommit(input) {
3651
- if (input.expectedLatestVersionId === undefined && input.mode !== "append") {
3652
- return null;
4060
+ async beginVersionTransaction(input) {
4061
+ if (!isSqliteCatalog(this.options.catalog)) {
4062
+ await input.runtime.run("BEGIN TRANSACTION");
4063
+ if (input.expectedLatestVersionId === undefined && input.mode !== "append") {
4064
+ return;
4065
+ }
4066
+ return this.snapshots.getLatestVersionSummaryForDefinition(input.runtime, input.dataset);
3653
4067
  }
3654
- return this.snapshots.getLatestVersionSummaryForDefinition(input.runtime, input.dataset);
4068
+ for (let attempt = 0;attempt < 3; attempt++) {
4069
+ input.signal?.throwIfAborted();
4070
+ const snapshotId = await this.currentSnapshotId(input.runtime);
4071
+ const latestVersion = await this.snapshots.getLatestVersionSummaryForDefinition(input.runtime, input.dataset);
4072
+ await input.runtime.run("BEGIN TRANSACTION");
4073
+ if (await this.currentSnapshotId(input.runtime) === snapshotId) {
4074
+ return latestVersion;
4075
+ }
4076
+ await input.runtime.run("ROLLBACK");
4077
+ }
4078
+ throw new LakeConcurrencyError(`[SixbDuckLake] Catalog changed during three write preparations for dataset '${input.dataset.id}'. Retry the write.`);
4079
+ }
4080
+ async currentSnapshotId(runtime) {
4081
+ const [row] = await runtime.query(`SELECT id FROM ${quoteIdentifier(duckLakeAlias(this.options))}.current_snapshot()`);
4082
+ if (row === undefined) {
4083
+ throw new LakeStorageError24("[SixbDuckLake] DuckLake did not return a current snapshot.");
4084
+ }
4085
+ return String(getBigIntLike(row, "id"));
3655
4086
  }
3656
4087
  async lastCommittedSnapshotId(runtime) {
3657
4088
  const [row] = await runtime.query(`SELECT id FROM ${quoteIdentifier(duckLakeAlias(this.options))}.last_committed_snapshot()`);
@@ -3700,6 +4131,9 @@ class DuckLakeWriteCoordinator {
3700
4131
  }
3701
4132
  }
3702
4133
  }
4134
+ function isSqliteCatalog(catalog) {
4135
+ return catalog.type === "sqlite" || catalog.type === "custom" && catalog.uri.toLowerCase().startsWith("sqlite:");
4136
+ }
3703
4137
  function commitRowCount(input, changeResult, latestVersion) {
3704
4138
  if (input.mode === "append" && input.expectedLatestVersionId === undefined) {
3705
4139
  return { kind: "unknown" };
@@ -3714,7 +4148,7 @@ function commitRowCount(input, changeResult, latestVersion) {
3714
4148
  }
3715
4149
  function assertDuckLakeSnapshotId3(snapshotId) {
3716
4150
  if (!/^\d+$/.test(snapshotId)) {
3717
- throw new LakeStorageError22(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`);
4151
+ throw new LakeStorageError24(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`);
3718
4152
  }
3719
4153
  }
3720
4154
  var localCatalogCommitLocks = new Map;
@@ -3750,6 +4184,7 @@ class DuckLakeStorage {
3750
4184
  datasets;
3751
4185
  maintenance;
3752
4186
  rows;
4187
+ changes;
3753
4188
  snapshotReader;
3754
4189
  writes;
3755
4190
  constructor(options) {
@@ -3768,6 +4203,7 @@ class DuckLakeStorage {
3768
4203
  this.maintenance = new DuckLakeMaintenance(normalizedOptions, this.connections);
3769
4204
  this.snapshotReader = new DuckLakeSnapshotReader(normalizedOptions, this.connections, this.datasets);
3770
4205
  this.rows = new DuckLakeRowReader(normalizedOptions, this.connections, this.snapshotReader);
4206
+ this.changes = new DuckLakeChangeReader(normalizedOptions, this.connections, this.snapshotReader);
3771
4207
  this.writes = new DuckLakeWriteCoordinator(normalizedOptions, this.connections, this.datasets, this.snapshotReader);
3772
4208
  this.sql = new DuckLakeSqlExecutor(normalizedOptions, this.connections, this.datasets, this.snapshotReader, this.writes);
3773
4209
  }
@@ -3804,6 +4240,9 @@ class DuckLakeStorage {
3804
4240
  readRows(input) {
3805
4241
  return this.rows.readRows(input);
3806
4242
  }
4243
+ readChanges(input) {
4244
+ return this.changes.readChanges(input);
4245
+ }
3807
4246
  async runMaintenance(options) {
3808
4247
  return this.maintenance.runMaintenance(options);
3809
4248
  }
@@ -3815,4 +4254,4 @@ export {
3815
4254
  DuckLakeStorage
3816
4255
  };
3817
4256
 
3818
- //# debugId=75A88322BA3CC9E264756E2164756E21
4257
+ //# debugId=35ABBEC5A94F36A964756E2164756E21