@sixb/ducklake 0.1.6 → 0.1.8

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 (35) hide show
  1. package/README.md +25 -27
  2. package/dist/ducklake-storage.d.ts +2 -1
  3. package/dist/ducklake-storage.d.ts.map +1 -1
  4. package/dist/index.js +736 -244
  5. package/dist/index.js.map +14 -12
  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-connection-manager.d.ts +19 -3
  11. package/dist/internal/ducklake-connection-manager.d.ts.map +1 -1
  12. package/dist/internal/ducklake-read-gate.d.ts +11 -0
  13. package/dist/internal/ducklake-read-gate.d.ts.map +1 -0
  14. package/dist/internal/ducklake-row-reader.d.ts.map +1 -1
  15. package/dist/internal/ducklake-snapshot-reader.d.ts +26 -1
  16. package/dist/internal/ducklake-snapshot-reader.d.ts.map +1 -1
  17. package/dist/internal/ducklake-write-coordinator.d.ts +2 -1
  18. package/dist/internal/ducklake-write-coordinator.d.ts.map +1 -1
  19. package/dist/internal/sql.d.ts +11 -2
  20. package/dist/internal/sql.d.ts.map +1 -1
  21. package/dist/types.d.ts +2 -0
  22. package/dist/types.d.ts.map +1 -1
  23. package/package.json +4 -4
  24. package/src/ducklake-storage.ts +8 -0
  25. package/src/internal/duckdb-reader.ts +129 -0
  26. package/src/internal/duckdb-runtime.ts +75 -51
  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 +331 -79
  32. package/src/internal/ducklake-write-coordinator.ts +56 -15
  33. package/src/internal/ducklake-write-session.ts +1 -0
  34. package/src/internal/sql.ts +35 -6
  35. package/src/types.ts +2 -0
package/dist/index.js CHANGED
@@ -158,8 +158,20 @@ function buildSetPostgresPoolSql(options) {
158
158
  return postgresPoolParameters(options.postgresPool).map(({ name, value }) => `SET pg_pool_${name} = ${value}`);
159
159
  }
160
160
  function duckLakeMetadataTableName(options, tableName) {
161
- const schemaName = options.catalog.type === "postgres" ? options.catalog.metadataSchema ?? "main" : "main";
162
- return `${quoteIdentifier(duckLakeMetadataCatalog(options))}.${quoteIdentifier(schemaName)}.${quoteIdentifier(tableName)}`;
161
+ return `${quoteIdentifier(duckLakeMetadataCatalog(options))}.${quoteIdentifier(duckLakeMetadataSchema(options))}.${quoteIdentifier(tableName)}`;
162
+ }
163
+ function buildDuckLakeMetadataQuery(options, build) {
164
+ if (options.catalog.type !== "postgres") {
165
+ return build((tableName) => duckLakeMetadataTableName(options, tableName));
166
+ }
167
+ const schema = options.catalog.metadataSchema;
168
+ const sql = build((tableName) => schema === undefined ? quoteIdentifier(tableName) : `${quoteIdentifier(schema)}.${quoteIdentifier(tableName)}`);
169
+ return `SELECT * FROM postgres_query(${quoteSqlString(duckLakeMetadataCatalog(options))}, ${quoteSqlString(sql)})`;
170
+ }
171
+ function duckLakeMetadataSchema(options) {
172
+ if (options.catalog.type !== "postgres")
173
+ return "main";
174
+ return options.catalog.metadataSchema ?? "public";
163
175
  }
164
176
  function quoteIdentifier(identifier) {
165
177
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
@@ -771,7 +783,7 @@ function rowImage(row, prefix, columns) {
771
783
  }
772
784
 
773
785
  // src/internal/ducklake-connection-manager.ts
774
- import { LakeStorageError as LakeStorageError10 } from "@sixb/core/lake-storage";
786
+ import { LakeStorageError as LakeStorageError11 } from "@sixb/core/lake-storage";
775
787
 
776
788
  // src/internal/catalog-key.ts
777
789
  import { resolve } from "path";
@@ -791,9 +803,14 @@ function localCatalogCoordinationKey(options) {
791
803
  }
792
804
 
793
805
  // src/internal/duckdb-runtime.ts
806
+ import { mkdir, mkdtemp, rm } from "fs/promises";
807
+ import { join, resolve as resolve2 } from "path";
794
808
  import {
795
809
  DuckDBInstance
796
810
  } from "@duckdb/node-api";
811
+ import { LakeStorageError as LakeStorageError10 } from "@sixb/core/lake-storage";
812
+
813
+ // src/internal/duckdb-reader.ts
797
814
  import { LakeStorageError as LakeStorageError9 } from "@sixb/core/lake-storage";
798
815
 
799
816
  // src/internal/duckdb-value-converter.ts
@@ -816,16 +833,147 @@ var sixbDuckDbValueConverter = (value, type, converter) => {
816
833
  return JSDuckDBValueConverter(value, type, converter);
817
834
  };
818
835
 
836
+ // src/internal/duckdb-reader.ts
837
+ class NodeDuckDbReader {
838
+ connection;
839
+ options;
840
+ resultPromise;
841
+ pending;
842
+ closePromise;
843
+ finished = false;
844
+ consumed = false;
845
+ onAbort = () => {
846
+ this.close().catch(() => {});
847
+ };
848
+ constructor(connection, options) {
849
+ this.connection = connection;
850
+ this.options = options;
851
+ }
852
+ async start(sql) {
853
+ this.options.signal?.throwIfAborted();
854
+ this.options.signal?.addEventListener("abort", this.onAbort, { once: true });
855
+ await this.track(this.connection.run("SET preserve_insertion_order = true"));
856
+ this.assertOpen();
857
+ const prepared = await this.track(this.connection.prepare(sql));
858
+ try {
859
+ this.assertOpen();
860
+ this.resultPromise = this.track(prepared.startStream().getResult()).finally(() => {
861
+ prepared.destroySync();
862
+ });
863
+ this.resultPromise.catch(() => {});
864
+ } catch (error) {
865
+ prepared.destroySync();
866
+ throw error;
867
+ }
868
+ }
869
+ async* rows() {
870
+ if (this.consumed)
871
+ throw new LakeStorageError9("[SixbDuckLake] Reader can only be consumed once.");
872
+ this.consumed = true;
873
+ try {
874
+ this.assertOpen();
875
+ const result = await this.resultPromise;
876
+ this.assertOpen();
877
+ const batches = result.yieldConvertedRowObjects(sixbDuckDbValueConverter)[Symbol.asyncIterator]();
878
+ while (true) {
879
+ this.assertOpen();
880
+ const batch = await this.track(batches.next());
881
+ this.assertOpen();
882
+ if (batch.done) {
883
+ this.finished = true;
884
+ return;
885
+ }
886
+ for (const row of batch.value) {
887
+ this.assertOpen();
888
+ yield row;
889
+ }
890
+ }
891
+ } finally {
892
+ await this.close();
893
+ }
894
+ }
895
+ close() {
896
+ if (!this.closePromise) {
897
+ this.options.signal?.removeEventListener("abort", this.onAbort);
898
+ if (this.pending)
899
+ this.connection.interrupt();
900
+ this.closePromise = this.finish();
901
+ }
902
+ return this.closePromise;
903
+ }
904
+ async finish() {
905
+ try {
906
+ await this.pending?.catch(() => {});
907
+ if (!this.finished) {
908
+ await this.connection.run("SELECT 1");
909
+ }
910
+ } finally {
911
+ this.resultPromise = undefined;
912
+ try {
913
+ this.connection.closeSync();
914
+ } finally {
915
+ await this.options.onClose?.();
916
+ }
917
+ }
918
+ }
919
+ async track(operation) {
920
+ this.pending = operation;
921
+ try {
922
+ return await operation;
923
+ } catch (error) {
924
+ this.assertOpen();
925
+ throw error;
926
+ } finally {
927
+ if (this.pending === operation)
928
+ this.pending = undefined;
929
+ }
930
+ }
931
+ assertOpen() {
932
+ this.options.signal?.throwIfAborted();
933
+ if (this.closePromise)
934
+ throw new LakeStorageError9("[SixbDuckLake] Reader is closed.");
935
+ }
936
+ }
937
+
819
938
  // src/internal/duckdb-runtime.ts
820
939
  class NodeDuckDbRuntime {
821
940
  instance;
822
941
  connection;
942
+ temporaryDirectory;
823
943
  closing = false;
824
944
  closed = false;
825
945
  operations = Promise.resolve();
826
- constructor(instance, connection) {
946
+ readers = new Set;
947
+ closePromise;
948
+ constructor(instance, connection, temporaryDirectory) {
827
949
  this.instance = instance;
828
950
  this.connection = connection;
951
+ this.temporaryDirectory = temporaryDirectory;
952
+ }
953
+ async openReader(sql, options = {}) {
954
+ return this.enqueue(async () => {
955
+ this.assertAcceptingOperations();
956
+ const connection = await this.instance.connect();
957
+ if (this.closing) {
958
+ connection.closeSync();
959
+ this.assertAcceptingOperations();
960
+ }
961
+ const reader = new NodeDuckDbReader(connection, {
962
+ ...options,
963
+ onClose: async () => {
964
+ this.readers.delete(reader);
965
+ await options.onClose?.();
966
+ }
967
+ });
968
+ this.readers.add(reader);
969
+ try {
970
+ await reader.start(sql);
971
+ return reader;
972
+ } catch (error) {
973
+ await reader.close();
974
+ throw error;
975
+ }
976
+ });
829
977
  }
830
978
  async run(sql, values) {
831
979
  await this.enqueue(async () => {
@@ -845,28 +993,6 @@ class NodeDuckDbRuntime {
845
993
  return reader.convertRowObjects(sixbDuckDbValueConverter);
846
994
  });
847
995
  }
848
- async* streamRows(sql, values) {
849
- this.assertAcceptingOperations();
850
- let releaseOperation;
851
- const operationFinished = new Promise((resolve) => {
852
- releaseOperation = resolve;
853
- });
854
- const waitForTurn = this.operations.then(() => {
855
- this.assertNotClosed();
856
- });
857
- this.operations = waitForTurn.then(() => operationFinished).then(() => {}, () => {});
858
- try {
859
- await waitForTurn;
860
- const result = await this.connection.stream(sql, values === undefined ? undefined : [...values]);
861
- for await (const batch of result.yieldConvertedRowObjects(sixbDuckDbValueConverter)) {
862
- for (const row of batch) {
863
- yield row;
864
- }
865
- }
866
- } finally {
867
- releaseOperation?.();
868
- }
869
- }
870
996
  async withAppender(tableName, useAppender) {
871
997
  return this.enqueue(async () => {
872
998
  const appender = await this.connection.createAppender(tableName);
@@ -882,15 +1008,28 @@ class NodeDuckDbRuntime {
882
1008
  return useRuntime(new NodeDuckDbExclusiveRuntime(this.connection));
883
1009
  });
884
1010
  }
885
- async close() {
886
- if (this.closed) {
887
- return;
888
- }
1011
+ close() {
1012
+ this.closePromise ??= this.finish();
1013
+ return this.closePromise;
1014
+ }
1015
+ async stopReaders() {
1016
+ const results = await Promise.allSettled([...this.readers].map((reader) => reader.close()));
1017
+ const failed = results.find((result) => result.status === "rejected");
1018
+ if (failed?.status === "rejected")
1019
+ throw failed.reason;
1020
+ }
1021
+ async finish() {
889
1022
  this.closing = true;
1023
+ const results = await Promise.allSettled([...this.readers].map((reader) => reader.close()));
890
1024
  await this.operations.catch(() => {});
891
1025
  this.closed = true;
892
1026
  this.connection.closeSync();
893
1027
  this.instance.closeSync();
1028
+ if (this.temporaryDirectory)
1029
+ await rm(this.temporaryDirectory, { recursive: true, force: true });
1030
+ const failed = results.find((result) => result.status === "rejected");
1031
+ if (failed?.status === "rejected")
1032
+ throw failed.reason;
894
1033
  }
895
1034
  enqueue(operation) {
896
1035
  this.assertAcceptingOperations();
@@ -903,12 +1042,12 @@ class NodeDuckDbRuntime {
903
1042
  }
904
1043
  assertAcceptingOperations() {
905
1044
  if (this.closing || this.closed) {
906
- throw new LakeStorageError9("[SixbDuckLake] DuckDB runtime is closed.");
1045
+ throw new LakeStorageError10("[SixbDuckLake] DuckDB runtime is closed.");
907
1046
  }
908
1047
  }
909
1048
  assertNotClosed() {
910
1049
  if (this.closed) {
911
- throw new LakeStorageError9("[SixbDuckLake] DuckDB runtime is closed.");
1050
+ throw new LakeStorageError10("[SixbDuckLake] DuckDB runtime is closed.");
912
1051
  }
913
1052
  }
914
1053
  }
@@ -968,8 +1107,27 @@ class NodeDuckDbAppenderAdapter {
968
1107
  }
969
1108
  }
970
1109
  async function createDuckDbRuntime(options = {}) {
971
- const instance = await DuckDBInstance.create(options.path ?? ":memory:", options.config ?? {});
972
- return new NodeDuckDbRuntime(instance, await instance.connect());
1110
+ const path = options.path ?? ":memory:";
1111
+ const base = options.config?.temp_directory ?? (path === ":memory:" ? ".tmp" : `${path}.tmp`);
1112
+ let temporaryDirectory;
1113
+ if (base !== "") {
1114
+ const directory = resolve2(base);
1115
+ await mkdir(directory, { recursive: true });
1116
+ temporaryDirectory = await mkdtemp(join(directory, "sixb-"));
1117
+ }
1118
+ let instance;
1119
+ try {
1120
+ instance = await DuckDBInstance.create(path, {
1121
+ ...options.config,
1122
+ ...temporaryDirectory ? { temp_directory: temporaryDirectory } : {}
1123
+ });
1124
+ return new NodeDuckDbRuntime(instance, await instance.connect(), temporaryDirectory);
1125
+ } catch (error) {
1126
+ instance?.closeSync();
1127
+ if (temporaryDirectory)
1128
+ await rm(temporaryDirectory, { recursive: true, force: true });
1129
+ throw error;
1130
+ }
973
1131
  }
974
1132
  async function installDuckLakeExtensions(runtime, options) {
975
1133
  await runtime.run("INSTALL ducklake");
@@ -1002,6 +1160,62 @@ async function setupDuckLake(runtime, options, setupOptions = {}) {
1002
1160
  }
1003
1161
  }
1004
1162
 
1163
+ // src/internal/ducklake-read-gate.ts
1164
+ class DuckLakeReadGate {
1165
+ active = 0;
1166
+ maintenance = false;
1167
+ waiters = new Set;
1168
+ async acquire(signal) {
1169
+ while (true) {
1170
+ signal.throwIfAborted();
1171
+ if (!this.maintenance) {
1172
+ this.active++;
1173
+ let released = false;
1174
+ return () => {
1175
+ if (released)
1176
+ return;
1177
+ released = true;
1178
+ this.active--;
1179
+ this.wake();
1180
+ };
1181
+ }
1182
+ await this.wait(signal);
1183
+ }
1184
+ }
1185
+ async withMaintenance(signal, run) {
1186
+ while (this.active > 0 || this.maintenance)
1187
+ await this.wait(signal);
1188
+ signal.throwIfAborted();
1189
+ this.maintenance = true;
1190
+ try {
1191
+ return await run();
1192
+ } finally {
1193
+ this.maintenance = false;
1194
+ this.wake();
1195
+ }
1196
+ }
1197
+ wait(signal) {
1198
+ return new Promise((resolve, reject) => {
1199
+ const wake = () => {
1200
+ signal.removeEventListener("abort", abort);
1201
+ this.waiters.delete(wake);
1202
+ resolve();
1203
+ };
1204
+ const abort = () => {
1205
+ this.waiters.delete(wake);
1206
+ reject(signal.reason);
1207
+ };
1208
+ signal.throwIfAborted();
1209
+ this.waiters.add(wake);
1210
+ signal.addEventListener("abort", abort, { once: true });
1211
+ });
1212
+ }
1213
+ wake() {
1214
+ for (const wake of this.waiters)
1215
+ wake();
1216
+ }
1217
+ }
1218
+
1005
1219
  // src/internal/ducklake-connection-manager.ts
1006
1220
  class DuckLakeConnectionManager {
1007
1221
  options;
@@ -1009,18 +1223,77 @@ class DuckLakeConnectionManager {
1009
1223
  installExtensionsPromise;
1010
1224
  attachPromise;
1011
1225
  refreshPromise;
1012
- localAttachmentLock = Promise.resolve();
1226
+ attachmentLock = Promise.resolve();
1013
1227
  observedLocalCatalogGeneration;
1014
1228
  attached = false;
1015
1229
  closed = false;
1230
+ closePromise;
1016
1231
  runtimePoisoned = false;
1232
+ pinnedReaders = 0;
1233
+ refreshRequested = false;
1234
+ shutdown = new AbortController;
1235
+ reads = new DuckLakeReadGate;
1236
+ maxStreamingReads;
1017
1237
  constructor(options) {
1018
1238
  this.options = options;
1019
1239
  this.observedLocalCatalogGeneration = currentLocalCatalogGeneration(options);
1240
+ this.maxStreamingReads = options.maxStreamingReads ?? 4;
1241
+ if (!Number.isSafeInteger(this.maxStreamingReads) || this.maxStreamingReads < 1) {
1242
+ throw new LakeStorageError11("[SixbDuckLake] maxStreamingReads must be a positive integer.");
1243
+ }
1244
+ }
1245
+ async acquireRead(signal) {
1246
+ this.assertOpen();
1247
+ const combined = signal ? AbortSignal.any([signal, this.shutdown.signal]) : this.shutdown.signal;
1248
+ return { signal: combined, release: await this.reads.acquire(combined) };
1249
+ }
1250
+ async nativeReaderLimit(runtime) {
1251
+ if (this.options.catalog.type === "duckdb")
1252
+ return this.maxStreamingReads;
1253
+ if (this.options.catalog.type !== "postgres")
1254
+ return 0;
1255
+ const [setting] = await runtime.query("SELECT current_setting('pg_pool_max_connections') AS value");
1256
+ const budget = Number(setting?.value);
1257
+ return Number.isFinite(budget) ? Math.min(this.maxStreamingReads, Math.max(0, budget - 4)) : 0;
1258
+ }
1259
+ async openReader(runtime, sql, signal, releaseRead) {
1260
+ const limit = await this.nativeReaderLimit(runtime);
1261
+ signal.throwIfAborted();
1262
+ if (this.pinnedReaders >= limit)
1263
+ return null;
1264
+ this.pinnedReaders++;
1265
+ let starting = true;
1266
+ let released = false;
1267
+ const onClose = async () => {
1268
+ if (released)
1269
+ return;
1270
+ released = true;
1271
+ this.pinnedReaders--;
1272
+ if (signal.aborted || this.closed || this.runtimePoisoned)
1273
+ releaseRead();
1274
+ if (starting || this.closed || this.runtimePoisoned)
1275
+ return;
1276
+ await this.withAttachmentLock(async () => {
1277
+ if (this.pinnedReaders === 0 && this.refreshRequested)
1278
+ await this.resetRuntimeUnlocked();
1279
+ await this.detachDuckDbCatalogAfterLease();
1280
+ });
1281
+ };
1282
+ try {
1283
+ return await runtime.openReader(sql, { signal, onClose });
1284
+ } catch (error) {
1285
+ await onClose();
1286
+ throw error;
1287
+ } finally {
1288
+ starting = false;
1289
+ }
1290
+ }
1291
+ async withMaintenanceRuntime(run) {
1292
+ return this.reads.withMaintenance(this.shutdown.signal, () => this.withExclusiveAttached(run));
1020
1293
  }
1021
1294
  assertOpen() {
1022
1295
  if (this.closed) {
1023
- throw new LakeStorageError10("[SixbDuckLake] DuckLakeStorage is closed.");
1296
+ throw new LakeStorageError11("[SixbDuckLake] DuckLakeStorage is closed.");
1024
1297
  }
1025
1298
  }
1026
1299
  async runtime() {
@@ -1051,7 +1324,7 @@ class DuckLakeConnectionManager {
1051
1324
  }
1052
1325
  async acquireAttachedRuntime() {
1053
1326
  this.assertOpen();
1054
- const releaseLock = await this.acquireLocalAttachmentLock();
1327
+ const releaseLock = await this.acquireAttachmentLock();
1055
1328
  let released = false;
1056
1329
  const release = async () => {
1057
1330
  if (released) {
@@ -1060,8 +1333,8 @@ class DuckLakeConnectionManager {
1060
1333
  released = true;
1061
1334
  try {
1062
1335
  if (this.runtimePoisoned) {
1063
- this.runtimePoisoned = false;
1064
1336
  await this.discardRuntimeUnlocked();
1337
+ this.runtimePoisoned = false;
1065
1338
  } else {
1066
1339
  await this.detachDuckDbCatalogAfterLease();
1067
1340
  }
@@ -1110,7 +1383,7 @@ class DuckLakeConnectionManager {
1110
1383
  }
1111
1384
  }
1112
1385
  async resetRuntime() {
1113
- const releaseLock = await this.acquireLocalAttachmentLock();
1386
+ const releaseLock = await this.acquireAttachmentLock();
1114
1387
  try {
1115
1388
  await this.resetRuntimeUnlocked();
1116
1389
  } finally {
@@ -1118,6 +1391,11 @@ class DuckLakeConnectionManager {
1118
1391
  }
1119
1392
  }
1120
1393
  async resetRuntimeUnlocked() {
1394
+ if (this.pinnedReaders > 0) {
1395
+ this.refreshRequested = true;
1396
+ return;
1397
+ }
1398
+ this.refreshRequested = false;
1121
1399
  const runtimePromise = this.runtimePromise;
1122
1400
  if (runtimePromise === undefined) {
1123
1401
  return;
@@ -1158,7 +1436,7 @@ class DuckLakeConnectionManager {
1158
1436
  }
1159
1437
  if (this.refreshPromise === undefined) {
1160
1438
  let refreshPromise;
1161
- refreshPromise = this.withLocalAttachmentLock(() => this.refreshForExternalChangesUnlocked()).finally(() => {
1439
+ refreshPromise = this.withAttachmentLock(() => this.refreshForExternalChangesUnlocked()).finally(() => {
1162
1440
  if (this.refreshPromise === refreshPromise) {
1163
1441
  this.refreshPromise = undefined;
1164
1442
  }
@@ -1178,8 +1456,10 @@ class DuckLakeConnectionManager {
1178
1456
  return;
1179
1457
  }
1180
1458
  try {
1181
- await runtime.run(`DETACH ${quoteIdentifier(duckLakeAlias(this.options))}`);
1182
- this.attached = false;
1459
+ if (this.pinnedReaders === 0) {
1460
+ await runtime.run(`DETACH ${quoteIdentifier(duckLakeAlias(this.options))}`);
1461
+ this.attached = false;
1462
+ }
1183
1463
  } catch {} finally {
1184
1464
  this.markLocalCatalogChanged();
1185
1465
  }
@@ -1195,11 +1475,13 @@ class DuckLakeConnectionManager {
1195
1475
  needsLocalCatalogRefresh() {
1196
1476
  return this.observedLocalCatalogGeneration !== currentLocalCatalogGeneration(this.options);
1197
1477
  }
1198
- async close() {
1199
- if (this.closed) {
1200
- return;
1201
- }
1478
+ close() {
1479
+ this.closePromise ??= this.finishClose();
1480
+ return this.closePromise;
1481
+ }
1482
+ async finishClose() {
1202
1483
  this.closed = true;
1484
+ this.shutdown.abort(new LakeStorageError11("[SixbDuckLake] DuckLakeStorage is closed."));
1203
1485
  const runtimePromise = this.runtimePromise;
1204
1486
  this.runtimePromise = undefined;
1205
1487
  await this.closeRuntimePromise(runtimePromise, {
@@ -1258,13 +1540,13 @@ class DuckLakeConnectionManager {
1258
1540
  ]);
1259
1541
  }
1260
1542
  async detachRuntime() {
1261
- await this.withLocalAttachmentLock(() => this.detachRuntimeUnlocked());
1543
+ await this.withAttachmentLock(() => this.detachRuntimeUnlocked());
1262
1544
  }
1263
1545
  poisonRuntime() {
1264
1546
  this.runtimePoisoned = true;
1265
1547
  }
1266
1548
  async detachDuckDbCatalogAfterLease() {
1267
- if (this.options.catalog.type !== "duckdb" || !this.attached) {
1549
+ if (this.options.catalog.type !== "duckdb" || !this.attached || this.pinnedReaders > 0) {
1268
1550
  return;
1269
1551
  }
1270
1552
  try {
@@ -1280,6 +1562,8 @@ class DuckLakeConnectionManager {
1280
1562
  await this.closeRuntimePromise(runtimePromise, { detach: false }).catch(() => {});
1281
1563
  }
1282
1564
  async detachRuntimeUnlocked() {
1565
+ if (this.pinnedReaders > 0)
1566
+ return;
1283
1567
  if (this.attachPromise !== undefined) {
1284
1568
  await this.attachPromise;
1285
1569
  }
@@ -1290,24 +1574,21 @@ class DuckLakeConnectionManager {
1290
1574
  await runtime.run(`DETACH ${quoteIdentifier(duckLakeAlias(this.options))}`);
1291
1575
  this.attached = false;
1292
1576
  }
1293
- async withLocalAttachmentLock(run) {
1294
- const releaseLock = await this.acquireLocalAttachmentLock();
1577
+ async withAttachmentLock(run) {
1578
+ const releaseLock = await this.acquireAttachmentLock();
1295
1579
  try {
1296
1580
  return await run();
1297
1581
  } finally {
1298
1582
  releaseLock();
1299
1583
  }
1300
1584
  }
1301
- async acquireLocalAttachmentLock() {
1302
- if (this.options.catalog.type === "postgres") {
1303
- return () => {};
1304
- }
1305
- const previous = this.localAttachmentLock.catch(() => {});
1585
+ async acquireAttachmentLock() {
1586
+ const previous = this.attachmentLock.catch(() => {});
1306
1587
  let release;
1307
1588
  const current = new Promise((resolve) => {
1308
1589
  release = resolve;
1309
1590
  });
1310
- this.localAttachmentLock = previous.then(() => current);
1591
+ this.attachmentLock = previous.then(() => current);
1311
1592
  await previous;
1312
1593
  let released = false;
1313
1594
  return () => {
@@ -1331,12 +1612,16 @@ class DuckLakeConnectionManager {
1331
1612
  await this.closeRuntime(runtime, options);
1332
1613
  }
1333
1614
  async closeRuntime(runtime, options) {
1334
- if (options.detach) {
1335
- try {
1336
- await runtime.run(`DETACH ${quoteIdentifier(duckLakeAlias(this.options))}`);
1337
- } catch {}
1615
+ try {
1616
+ await runtime.stopReaders();
1617
+ if (options.detach) {
1618
+ try {
1619
+ await runtime.run(`DETACH ${quoteIdentifier(duckLakeAlias(this.options))}`);
1620
+ } catch {}
1621
+ }
1622
+ } finally {
1623
+ await runtime.close();
1338
1624
  }
1339
- await runtime.close();
1340
1625
  }
1341
1626
  }
1342
1627
  var localCatalogGenerations = new Map;
@@ -1358,12 +1643,12 @@ function incrementLocalCatalogGeneration(options) {
1358
1643
  import { randomUUID as randomUUID2 } from "crypto";
1359
1644
  import {
1360
1645
  assertDatasetDefinition,
1361
- LakeStorageError as LakeStorageError13,
1646
+ LakeStorageError as LakeStorageError14,
1362
1647
  planDatasetDefinitionUpdate
1363
1648
  } from "@sixb/core/lake-storage";
1364
1649
 
1365
1650
  // src/internal/ducklake-catalog-schema.ts
1366
- import { LakeStorageError as LakeStorageError11 } from "@sixb/core/lake-storage";
1651
+ import { LakeStorageError as LakeStorageError12 } from "@sixb/core/lake-storage";
1367
1652
  var SIXB_PRIMARY_KEY_COMMENT_NAMESPACE = "sixb:primary-key:";
1368
1653
  var DATASET_SEQUENCE_COLUMN_COMMENT = "sixb.sequenceBy.v1";
1369
1654
  var SIXB_PRIMARY_KEY_COMMENT_PREFIX = `${SIXB_PRIMARY_KEY_COMMENT_NAMESPACE}v1:`;
@@ -1414,14 +1699,14 @@ function duckLakeCatalogColumnsToDatasetPrimaryKey(tableName, rows) {
1414
1699
  throwInvalidPrimaryKeyMetadata(tableName, column, comment);
1415
1700
  }
1416
1701
  if (seenOrdinals.has(ordinal)) {
1417
- throw new LakeStorageError11(`[SixbDuckLake] Dataset table '${tableName}' has duplicate primary-key ordinal '${ordinal}'.`);
1702
+ throw new LakeStorageError12(`[SixbDuckLake] Dataset table '${tableName}' has duplicate primary-key ordinal '${ordinal}'.`);
1418
1703
  }
1419
1704
  seenOrdinals.add(ordinal);
1420
1705
  if (duckDbTypeToDatasetColumnType(column.columnType) !== "string") {
1421
- throw new LakeStorageError11(`[SixbDuckLake] Dataset table '${tableName}' primary-key column '${column.columnName}' must map to type 'string'.`);
1706
+ throw new LakeStorageError12(`[SixbDuckLake] Dataset table '${tableName}' primary-key column '${column.columnName}' must map to type 'string'.`);
1422
1707
  }
1423
1708
  if (column.nullsAllowed) {
1424
- throw new LakeStorageError11(`[SixbDuckLake] Dataset table '${tableName}' primary-key column '${column.columnName}' must not be nullable.`);
1709
+ throw new LakeStorageError12(`[SixbDuckLake] Dataset table '${tableName}' primary-key column '${column.columnName}' must not be nullable.`);
1425
1710
  }
1426
1711
  keyedColumns.push({ column, ordinal });
1427
1712
  }
@@ -1431,7 +1716,7 @@ function duckLakeCatalogColumnsToDatasetPrimaryKey(tableName, rows) {
1431
1716
  keyedColumns.sort((left, right) => left.ordinal - right.ordinal);
1432
1717
  for (const [expectedOrdinal, keyedColumn] of keyedColumns.entries()) {
1433
1718
  if (keyedColumn.ordinal !== expectedOrdinal) {
1434
- throw new LakeStorageError11(`[SixbDuckLake] Dataset table '${tableName}' primary-key ordinals must be contiguous from 0.`);
1719
+ throw new LakeStorageError12(`[SixbDuckLake] Dataset table '${tableName}' primary-key ordinals must be contiguous from 0.`);
1435
1720
  }
1436
1721
  }
1437
1722
  const columnNames = keyedColumns.map(({ column }) => column.columnName);
@@ -1441,7 +1726,7 @@ function duckLakeCatalogColumnsToDatasetPrimaryKey(tableName, rows) {
1441
1726
  return columnNames;
1442
1727
  }
1443
1728
  function throwInvalidPrimaryKeyMetadata(tableName, column, comment) {
1444
- throw new LakeStorageError11(`[SixbDuckLake] Dataset table '${tableName}' has malformed primary-key metadata '${comment}' on column '${column.columnName}'.`);
1729
+ throw new LakeStorageError12(`[SixbDuckLake] Dataset table '${tableName}' has malformed primary-key metadata '${comment}' on column '${column.columnName}'.`);
1445
1730
  }
1446
1731
  function duckLakeCatalogColumnToDefinition(tableName, column, childrenByParentId) {
1447
1732
  const children = [...childrenByParentId.get(column.columnId) ?? []].sort(compareDuckLakeCatalogColumnOrder);
@@ -1458,7 +1743,7 @@ function duckLakeCatalogColumnTypeToDatasetType(tableName, column, children) {
1458
1743
  if (isFileRefStruct(children)) {
1459
1744
  return "fileRef";
1460
1745
  }
1461
- throw new LakeStorageError11(`[SixbDuckLake] DuckDB column type '${formatDuckLakeCatalogColumnType(column, children)}' in dataset table '${tableName}' cannot be mapped to a Sixb dataset column type.`);
1746
+ throw new LakeStorageError12(`[SixbDuckLake] DuckDB column type '${formatDuckLakeCatalogColumnType(column, children)}' in dataset table '${tableName}' cannot be mapped to a Sixb dataset column type.`);
1462
1747
  }
1463
1748
  function compareDuckLakeCatalogColumnOrder(left, right) {
1464
1749
  return Number(left.columnOrder - right.columnOrder);
@@ -1480,7 +1765,7 @@ function formatDuckLakeCatalogColumnType(column, children) {
1480
1765
  }
1481
1766
 
1482
1767
  // src/internal/ducklake-current-dataset-definitions.ts
1483
- import { LakeStorageError as LakeStorageError12 } from "@sixb/core/lake-storage";
1768
+ import { LakeStorageError as LakeStorageError13 } from "@sixb/core/lake-storage";
1484
1769
  async function readCurrentDatasetDefinitions(input) {
1485
1770
  if (input.datasetIds?.length === 0) {
1486
1771
  return new Map;
@@ -1607,7 +1892,7 @@ async function readCurrentDatasetPartitions(input, tableNames) {
1607
1892
  transform: getString(row, "transform")
1608
1893
  };
1609
1894
  if (partition.transform !== "identity") {
1610
- throw new LakeStorageError12(`[SixbDuckLake] Dataset table '${partition.tableName}' uses unsupported DuckLake partition transform '${partition.transform}'.`);
1895
+ throw new LakeStorageError13(`[SixbDuckLake] Dataset table '${partition.tableName}' uses unsupported DuckLake partition transform '${partition.transform}'.`);
1611
1896
  }
1612
1897
  return partition;
1613
1898
  });
@@ -1709,7 +1994,7 @@ class DuckLakeDatasetCatalog {
1709
1994
  if (failures.length > 0) {
1710
1995
  const details = failures.join(`
1711
1996
  `);
1712
- throw new LakeStorageError13(`[SixbLake] Lake dataset definition check failed for ${failures.length} dataset(s).
1997
+ throw new LakeStorageError14(`[SixbLake] Lake dataset definition check failed for ${failures.length} dataset(s).
1713
1998
  ${details}`);
1714
1999
  }
1715
2000
  }
@@ -1745,12 +2030,12 @@ ${details}`);
1745
2030
  assertDuckLakeSnapshotId(snapshotId);
1746
2031
  const columns = await this.readDatasetColumnsAtSnapshot(runtime, tableName, tableId, snapshotId);
1747
2032
  if (columns.length === 0) {
1748
- throw new LakeStorageError13(`[SixbDuckLake] Could not reconstruct schema for dataset '${datasetId}' at DuckLake snapshot '${snapshotId}'.`);
2033
+ throw new LakeStorageError14(`[SixbDuckLake] Could not reconstruct schema for dataset '${datasetId}' at DuckLake snapshot '${snapshotId}'.`);
1749
2034
  }
1750
2035
  return duckLakeCatalogColumnsToDatasetSchema(tableName, columns);
1751
2036
  }
1752
2037
  assertSchema(definition) {
1753
- assertDatasetDefinition(definition, (message) => new LakeStorageError13(`[SixbDuckLake] ${message}`));
2038
+ assertDatasetDefinition(definition, (message) => new LakeStorageError14(`[SixbDuckLake] ${message}`));
1754
2039
  }
1755
2040
  async applyExistingDatasetUpdate(plan) {
1756
2041
  const tableName = encodeDatasetTableName(plan.definition.id);
@@ -1797,7 +2082,7 @@ ${details}`);
1797
2082
  await runtime.run(`ALTER TABLE ${qualifiedName} SET PARTITIONED BY (${definition.partitionBy.map((columnName) => quoteIdentifier(columnName)).join(", ")})`);
1798
2083
  } catch (error) {
1799
2084
  const message = error instanceof Error ? error.message : String(error);
1800
- throw new LakeStorageError13(`[SixbDuckLake] Dataset '${definition.id}' cannot apply partitionBy because DuckLake rejected ALTER TABLE SET PARTITIONED BY: ${message}`);
2085
+ throw new LakeStorageError14(`[SixbDuckLake] Dataset '${definition.id}' cannot apply partitionBy because DuckLake rejected ALTER TABLE SET PARTITIONED BY: ${message}`);
1801
2086
  }
1802
2087
  }
1803
2088
  async applyPrimaryKey(runtime, qualifiedName, definition) {
@@ -1856,12 +2141,12 @@ ${details}`);
1856
2141
  }
1857
2142
  function assertDuckLakeSnapshotId(snapshotId) {
1858
2143
  if (!/^\d+$/.test(snapshotId)) {
1859
- throw new LakeStorageError13(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`);
2144
+ throw new LakeStorageError14(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`);
1860
2145
  }
1861
2146
  }
1862
2147
 
1863
2148
  // src/internal/ducklake-maintenance.ts
1864
- import { LakeStorageError as LakeStorageError14 } from "@sixb/core/lake-storage";
2149
+ import { LakeStorageError as LakeStorageError15 } from "@sixb/core/lake-storage";
1865
2150
  var DEFAULT_RETENTION = "7 days";
1866
2151
 
1867
2152
  class DuckLakeMaintenance {
@@ -1877,9 +2162,9 @@ class DuckLakeMaintenance {
1877
2162
  const deleteOlderThan = options.deleteOlderThan ?? expireOlderThan;
1878
2163
  const dryRun = options.dryRun ?? false;
1879
2164
  if (this.options.readOnly && !dryRun) {
1880
- throw new LakeStorageError14("[SixbDuckLake] DuckLake maintenance cannot run in read-only mode.");
2165
+ throw new LakeStorageError15("[SixbDuckLake] DuckLake maintenance cannot run in read-only mode.");
1881
2166
  }
1882
- return this.connections.withExclusiveAttached(async (runtime) => {
2167
+ return this.connections.withMaintenanceRuntime(async (runtime) => {
1883
2168
  const counts = await runMaintenanceProcedures(runtime, this.options, {
1884
2169
  expireOlderThan,
1885
2170
  deleteOlderThan
@@ -1914,7 +2199,7 @@ function maintenanceProcedureSql(procedure, aliasSql, olderThan, dryRun) {
1914
2199
  }
1915
2200
 
1916
2201
  // src/internal/ducklake-row-reader.ts
1917
- import { LakeStorageError as LakeStorageError15 } from "@sixb/core/lake-storage";
2202
+ import { LakeStorageError as LakeStorageError16 } from "@sixb/core/lake-storage";
1918
2203
  var READ_PAGE_ROWS = 5000;
1919
2204
  var PHYSICAL_ROW_ID_ALIAS = "__sixb_physical_row_id";
1920
2205
 
@@ -1928,58 +2213,68 @@ class DuckLakeRowReader {
1928
2213
  this.snapshots = snapshots;
1929
2214
  }
1930
2215
  async* readRows(input) {
1931
- this.connections.assertOpen();
1932
- const lease = await this.connections.acquireAttachedRuntime();
2216
+ const read = await this.connections.acquireRead(input.signal);
2217
+ let reader = null;
1933
2218
  try {
1934
- const runtime = lease.runtime;
1935
- const tableRef = await resolveDatasetTableRef(this.options, runtime, input.datasetId);
1936
- if (!tableRef) {
1937
- throw new LakeStorageError15(`[SixbDuckLake] Unknown dataset '${input.datasetId}'.`);
1938
- }
1939
- const version = await this.resolveVersion(runtime, tableRef, input.versionId);
1940
- const selectedColumns = this.resolveReadColumns(tableRef.datasetId, version.schema, input.columns);
1941
- const snapshotId = parseVersionId(version.versionId);
1942
- const columnsSql = selectedColumns.map((column) => quoteIdentifier(column.name)).join(", ");
1943
- const tableSql = qualifiedTableName(this.options, tableRef.tableName);
1944
- const schemaNames = new Set(version.schema.columns.map((column) => column.name.toLowerCase()));
1945
- const canUsePhysicalRowId = !schemaNames.has("rowid") && !schemaNames.has(PHYSICAL_ROW_ID_ALIAS.toLowerCase());
1946
- const orderSql = canUsePhysicalRowId ? " ORDER BY rowid" : "";
1947
- const baseSql = `SELECT ${columnsSql} FROM ${tableSql} AT (VERSION => ${snapshotId})`;
1948
- const requestedOffset = Math.max(0, Math.trunc(input.offset ?? 0));
1949
- let remaining = input.limit === undefined ? undefined : Math.max(0, Math.trunc(input.limit));
1950
- if (remaining === 0)
2219
+ const prepared = await this.connections.withAttachedRuntime(async (runtime) => {
2220
+ read.signal.throwIfAborted();
2221
+ const tableRef = await resolveDatasetTableRef(this.options, runtime, input.datasetId);
2222
+ if (!tableRef) {
2223
+ throw new LakeStorageError16(`[SixbDuckLake] Unknown dataset '${input.datasetId}'.`);
2224
+ }
2225
+ const version = await this.resolveVersion(runtime, tableRef, input.versionId);
2226
+ const columns = this.resolveReadColumns(tableRef.datasetId, version.schema, input.columns);
2227
+ const names = new Set(version.schema.columns.map((column) => column.name.toLowerCase()));
2228
+ const physicalOrder = !names.has("rowid") && !names.has(PHYSICAL_ROW_ID_ALIAS);
2229
+ const relation = `${qualifiedTableName(this.options, tableRef.tableName)} AT (VERSION => ${parseVersionId(version.versionId)})`;
2230
+ const columnsSql = columns.map((column) => quoteIdentifier(column.name)).join(", ");
2231
+ const offset = Math.max(0, Math.trunc(input.offset ?? 0));
2232
+ const limit = input.limit === undefined ? undefined : Math.max(0, Math.trunc(input.limit));
2233
+ const sql = `SELECT ${columnsSql} FROM ${relation}${physicalOrder ? " ORDER BY rowid" : ""}${limit === undefined ? "" : ` LIMIT ${limit}`}${offset > 0 ? ` OFFSET ${offset}` : ""}`;
2234
+ const cursor = limit === undefined || limit > READ_PAGE_ROWS ? await this.connections.openReader(runtime, sql, read.signal, read.release) : null;
2235
+ const query = { columnsSql, relation, physicalOrder };
2236
+ const firstPage = cursor || limit === 0 ? [] : await runtime.query(pageSql(query, Math.min(limit ?? READ_PAGE_ROWS, READ_PAGE_ROWS), offset, null));
2237
+ return { columns, ...query, offset, limit, cursor, firstPage };
2238
+ });
2239
+ reader = prepared.cursor;
2240
+ if (!reader)
2241
+ read.signal.addEventListener("abort", read.release, { once: true });
2242
+ read.signal.throwIfAborted();
2243
+ if (reader) {
2244
+ yield* this.normalizeRows(reader.rows(), prepared.columns, read.signal);
1951
2245
  return;
1952
- let offset = requestedOffset;
1953
- let physicalCursor = null;
1954
- while (true) {
1955
- const pageRows = remaining === undefined ? READ_PAGE_ROWS : Math.min(READ_PAGE_ROWS, remaining);
1956
- const pageSql = canUsePhysicalRowId ? `SELECT rowid AS ${quoteIdentifier(PHYSICAL_ROW_ID_ALIAS)}, ${columnsSql}
1957
- FROM ${tableSql} AT (VERSION => ${snapshotId})
1958
- ${physicalCursor === null ? "" : `WHERE rowid > ${physicalCursor}`}
1959
- ORDER BY rowid LIMIT ${pageRows}
1960
- ${physicalCursor === null && offset > 0 ? `OFFSET ${offset}` : ""}` : `${baseSql}${orderSql} LIMIT ${pageRows} OFFSET ${offset}`;
1961
- const rows = await runtime.query(pageSql);
1962
- if (rows.length === 0)
1963
- return;
1964
- yield* this.normalizeRows(rows, selectedColumns);
1965
- offset += rows.length;
1966
- if (remaining !== undefined) {
2246
+ }
2247
+ let offset = prepared.offset;
2248
+ let remaining = prepared.limit;
2249
+ let cursor = null;
2250
+ let page = prepared.firstPage;
2251
+ while (remaining !== 0) {
2252
+ read.signal.throwIfAborted();
2253
+ const pageSize = Math.min(remaining ?? READ_PAGE_ROWS, READ_PAGE_ROWS);
2254
+ const rows = page ?? await this.connections.withAttachedRuntime((runtime) => runtime.query(pageSql(prepared, pageSize, offset, cursor)));
2255
+ page = undefined;
2256
+ read.signal.throwIfAborted();
2257
+ yield* this.normalizeRows(rows, prepared.columns, read.signal);
2258
+ if (remaining !== undefined)
1967
2259
  remaining -= rows.length;
1968
- if (remaining === 0)
1969
- return;
1970
- }
1971
- if (canUsePhysicalRowId) {
1972
- physicalCursor = getBigIntLike(rows[rows.length - 1], PHYSICAL_ROW_ID_ALIAS);
1973
- }
1974
- if (rows.length < pageRows)
2260
+ if (rows.length < pageSize)
1975
2261
  return;
2262
+ if (prepared.physicalOrder)
2263
+ cursor = getBigIntLike(rows[rows.length - 1], PHYSICAL_ROW_ID_ALIAS);
2264
+ offset += rows.length;
1976
2265
  }
1977
2266
  } finally {
1978
- await lease.release();
2267
+ read.signal.removeEventListener("abort", read.release);
2268
+ try {
2269
+ await reader?.close();
2270
+ } finally {
2271
+ read.release();
2272
+ }
1979
2273
  }
1980
2274
  }
1981
- async* normalizeRows(rows, selectedColumns) {
2275
+ async* normalizeRows(rows, selectedColumns, signal) {
1982
2276
  for await (const row of rows) {
2277
+ signal.throwIfAborted();
1983
2278
  const output = {};
1984
2279
  for (const column of selectedColumns) {
1985
2280
  output[column.name] = normalizeReadValue(row[column.name], column);
@@ -2010,19 +2305,25 @@ class DuckLakeRowReader {
2010
2305
  return columns.map((columnName) => {
2011
2306
  const column = columnsByName.get(columnName);
2012
2307
  if (!column) {
2013
- throw new LakeStorageError15(`[SixbDuckLake] Dataset '${datasetId}' does not have column '${columnName}' at the requested version.`);
2308
+ throw new LakeStorageError16(`[SixbDuckLake] Dataset '${datasetId}' does not have column '${columnName}' at the requested version.`);
2014
2309
  }
2015
2310
  return column;
2016
2311
  });
2017
2312
  }
2018
2313
  throwNoCommittedVersion(datasetId) {
2019
- throw new LakeStorageError15(`[SixbDuckLake] No committed version found for dataset '${datasetId}'.`);
2314
+ throw new LakeStorageError16(`[SixbDuckLake] No committed version found for dataset '${datasetId}'.`);
2020
2315
  }
2021
2316
  }
2317
+ function pageSql(read, limit, offset, cursor) {
2318
+ return read.physicalOrder ? `SELECT rowid AS ${quoteIdentifier(PHYSICAL_ROW_ID_ALIAS)}, ${read.columnsSql}
2319
+ FROM ${read.relation} ${cursor === null ? "" : `WHERE rowid > ${cursor}`}
2320
+ ORDER BY rowid LIMIT ${limit} ${cursor === null && offset > 0 ? `OFFSET ${offset}` : ""}` : `SELECT ${read.columnsSql} FROM ${read.relation} LIMIT ${limit} OFFSET ${offset}`;
2321
+ }
2022
2322
 
2023
2323
  // src/internal/ducklake-snapshot-reader.ts
2024
- import { LakeStorageError as LakeStorageError16 } from "@sixb/core/lake-storage";
2324
+ import { LakeStorageError as LakeStorageError17 } from "@sixb/core/lake-storage";
2025
2325
  var SNAPSHOT_ROW_BATCH_SIZE = 128;
2326
+ var FEED_PAGE_SIZE = 512;
2026
2327
  var CATALOG_SNAPSHOT_SCAN_LIMIT = 512;
2027
2328
 
2028
2329
  class DuckLakeSnapshotReader {
@@ -2088,6 +2389,135 @@ class DuckLakeSnapshotReader {
2088
2389
  };
2089
2390
  });
2090
2391
  }
2392
+ async listLatestVersionsSince(input) {
2393
+ this.connections.assertOpen();
2394
+ return this.connections.withAttachedRuntime(async (runtime) => {
2395
+ if (input.cursor === null) {
2396
+ return { cursor: await this.queryHeadSnapshotId(runtime), versions: [] };
2397
+ }
2398
+ if (!/^\d+$/.test(input.cursor)) {
2399
+ return null;
2400
+ }
2401
+ const snapshots = await this.querySnapshotsFrom(runtime, input.cursor);
2402
+ if (snapshots[0]?.snapshotId !== input.cursor) {
2403
+ return null;
2404
+ }
2405
+ const newSnapshots = snapshots.slice(1);
2406
+ if (newSnapshots.length === 0) {
2407
+ return { cursor: input.cursor, versions: [] };
2408
+ }
2409
+ const tables = await this.resolveFeedTables(runtime, input.datasetIds);
2410
+ const latest = await this.collectFeedPage(runtime, tables, newSnapshots);
2411
+ return {
2412
+ cursor: newSnapshots[newSnapshots.length - 1]?.snapshotId ?? input.cursor,
2413
+ versions: [...latest].map(([datasetId, row]) => this.snapshotRowToSummary(datasetId, row))
2414
+ };
2415
+ });
2416
+ }
2417
+ async queryHeadSnapshotId(runtime) {
2418
+ const [row] = await runtime.query(buildDuckLakeMetadataQuery(this.options, (table) => `SELECT max(snapshot_id) AS snapshot_id FROM ${table("ducklake_snapshot")}`));
2419
+ if (row?.snapshot_id === null || row?.snapshot_id === undefined) {
2420
+ throw new LakeStorageError17("[SixbDuckLake] DuckLake catalog has no snapshots.");
2421
+ }
2422
+ return String(getBigIntLike(row, "snapshot_id"));
2423
+ }
2424
+ async querySnapshotsFrom(runtime, cursor) {
2425
+ const rows = await runtime.query(buildDuckLakeMetadataQuery(this.options, (table) => `
2426
+ SELECT snapshot_id, snapshot_time
2427
+ FROM ${table("ducklake_snapshot")}
2428
+ WHERE snapshot_id >= ${cursor}
2429
+ ORDER BY snapshot_id
2430
+ LIMIT ${FEED_PAGE_SIZE + 1}
2431
+ `));
2432
+ return rows.map((row) => ({
2433
+ snapshotId: String(getBigIntLike(row, "snapshot_id")),
2434
+ createdAt: getDate(row, "snapshot_time")
2435
+ }));
2436
+ }
2437
+ async resolveFeedTables(runtime, datasetIds) {
2438
+ const requested = datasetIds === undefined ? null : new Set(datasetIds);
2439
+ const rows = await runtime.query(`
2440
+ SELECT table_id, table_name, begin_snapshot, end_snapshot
2441
+ FROM ${duckLakeMetadataTableName(this.options, "ducklake_table")}
2442
+ `);
2443
+ const startByTableId = new Map;
2444
+ const current = [];
2445
+ for (const row of rows) {
2446
+ const tableId = getBigIntLike(row, "table_id");
2447
+ const begin = getBigIntLike(row, "begin_snapshot");
2448
+ const start = startByTableId.get(tableId);
2449
+ if (start === undefined || begin < start) {
2450
+ startByTableId.set(tableId, begin);
2451
+ }
2452
+ if (row.end_snapshot !== null && row.end_snapshot !== undefined) {
2453
+ continue;
2454
+ }
2455
+ const datasetId = decodeDatasetTableName(getString(row, "table_name"));
2456
+ if (datasetId !== null && (requested === null || requested.has(datasetId))) {
2457
+ current.push({ datasetId, tableId });
2458
+ }
2459
+ }
2460
+ return current.map((table) => ({
2461
+ ...table,
2462
+ startSnapshotId: startByTableId.get(table.tableId) ?? 0n
2463
+ }));
2464
+ }
2465
+ async collectFeedPage(runtime, tables, snapshots) {
2466
+ const latest = new Map;
2467
+ const first = snapshots[0]?.snapshotId;
2468
+ const last = snapshots[snapshots.length - 1]?.snapshotId;
2469
+ if (first === undefined || last === undefined || tables.length === 0) {
2470
+ return latest;
2471
+ }
2472
+ const changeRows = await runtime.query(buildDuckLakeMetadataQuery(this.options, (table) => `
2473
+ SELECT snapshot_id, changes_made, commit_extra_info
2474
+ FROM ${table("ducklake_snapshot_changes")}
2475
+ WHERE snapshot_id >= ${first} AND snapshot_id <= ${last}
2476
+ `));
2477
+ const fileFlags = await this.queryFileChangeFlags(runtime, snapshots.map((snapshot) => snapshot.snapshotId), tables.map((table) => table.tableId));
2478
+ const changesBySnapshotId = new Map(changeRows.map((row) => [String(getBigIntLike(row, "snapshot_id")), row]));
2479
+ const tablesByTableId = new Map(tables.map((table) => [table.tableId.toString(), table]));
2480
+ const tablesByDatasetId = new Map(tables.map((table) => [table.datasetId, table]));
2481
+ for (const snapshot of snapshots) {
2482
+ const changes = changesBySnapshotId.get(snapshot.snapshotId);
2483
+ if (changes === undefined) {
2484
+ continue;
2485
+ }
2486
+ const changesMade = getString(changes, "changes_made");
2487
+ const metadata = parseCommitMetadata(changes.commit_extra_info);
2488
+ const touched = new Set;
2489
+ for (const change of changesMade.split(",")) {
2490
+ const table = tablesByTableId.get(change.split(":")[1] ?? "");
2491
+ if (table)
2492
+ touched.add(table);
2493
+ }
2494
+ for (const table of tables) {
2495
+ if (fileFlags.has(fileChangeKey(snapshot.snapshotId, table.tableId)))
2496
+ touched.add(table);
2497
+ }
2498
+ const tagged = metadata === undefined ? undefined : tablesByDatasetId.get(metadata.datasetId);
2499
+ if (tagged)
2500
+ touched.add(tagged);
2501
+ for (const table of touched) {
2502
+ if (BigInt(snapshot.snapshotId) < table.startSnapshotId) {
2503
+ continue;
2504
+ }
2505
+ const flags = fileFlags.get(fileChangeKey(snapshot.snapshotId, table.tableId));
2506
+ const row = this.candidateToSnapshotRow(table.datasetId, table.tableId, {
2507
+ snapshotId: snapshot.snapshotId,
2508
+ createdAt: snapshot.createdAt,
2509
+ changesMade,
2510
+ hasFileChange: flags?.hasFileChange ?? false,
2511
+ hasFileDeleteChange: flags?.hasFileDeleteChange ?? false,
2512
+ ...metadata !== undefined ? { metadata } : {}
2513
+ });
2514
+ if (row && row.mode !== "schema") {
2515
+ latest.set(table.datasetId, row);
2516
+ }
2517
+ }
2518
+ }
2519
+ return latest;
2520
+ }
2091
2521
  snapshotRowToSummary(datasetId, row) {
2092
2522
  return {
2093
2523
  datasetId,
@@ -2234,7 +2664,7 @@ class DuckLakeSnapshotReader {
2234
2664
  }
2235
2665
  assertNoMetadataConflict(snapshotId, datasetId, metadata) {
2236
2666
  if (metadata !== undefined && metadata.datasetId !== datasetId) {
2237
- throw new LakeStorageError16(`[SixbDuckLake] DuckLake snapshot '${snapshotId}' changed dataset '${datasetId}' but Sixb commit metadata references dataset '${metadata.datasetId}'.`);
2667
+ throw new LakeStorageError17(`[SixbDuckLake] DuckLake snapshot '${snapshotId}' changed dataset '${datasetId}' but Sixb commit metadata references dataset '${metadata.datasetId}'.`);
2238
2668
  }
2239
2669
  }
2240
2670
  async getLatestVersionForDefinition(runtime, dataset) {
@@ -2351,6 +2781,7 @@ class DuckLakeSnapshotReader {
2351
2781
  let beforeSnapshotId = input.beforeSnapshotId;
2352
2782
  while (input.visibleRowLimit === undefined || snapshots.length < input.visibleRowLimit) {
2353
2783
  const candidates = await this.querySnapshotCandidates(runtime, {
2784
+ datasetId: input.datasetId,
2354
2785
  tableId: input.tableId,
2355
2786
  exactSnapshotId: input.exactSnapshotId,
2356
2787
  beforeSnapshotId,
@@ -2360,6 +2791,9 @@ class DuckLakeSnapshotReader {
2360
2791
  break;
2361
2792
  }
2362
2793
  for (const candidate of candidates) {
2794
+ if ("missing" in candidate) {
2795
+ continue;
2796
+ }
2363
2797
  const snapshot = this.candidateToSnapshotRow(input.datasetId, input.tableId, candidate);
2364
2798
  if (snapshot) {
2365
2799
  snapshots.push(snapshot);
@@ -2376,87 +2810,118 @@ class DuckLakeSnapshotReader {
2376
2810
  return snapshots;
2377
2811
  }
2378
2812
  async querySnapshotCandidates(runtime, input) {
2379
- if (input.limit !== undefined && input.limit <= 0) {
2813
+ const tableId = input.tableId.toString();
2814
+ const limit = Math.max(0, Math.trunc(input.limit));
2815
+ if (limit === 0) {
2380
2816
  return [];
2381
2817
  }
2382
- const where = [];
2383
2818
  if (input.exactSnapshotId !== undefined) {
2384
2819
  assertDuckLakeSnapshotId2(input.exactSnapshotId);
2385
- where.push(`snapshot.snapshot_id = ${input.exactSnapshotId}`);
2386
2820
  }
2387
2821
  if (input.beforeSnapshotId !== undefined) {
2388
2822
  assertDuckLakeSnapshotId2(input.beforeSnapshotId);
2389
- where.push(`snapshot.snapshot_id < ${input.beforeSnapshotId}`);
2390
2823
  }
2391
- const whereSql = where.length === 0 ? "" : `WHERE ${where.join(" AND ")}`;
2392
- const limitSql = input.limit === undefined ? "" : `LIMIT ${Math.max(0, Math.trunc(input.limit))}`;
2393
- const ducklakeSnapshot = duckLakeMetadataTableName(this.options, "ducklake_snapshot");
2394
- const ducklakeSnapshotChanges = duckLakeMetadataTableName(this.options, "ducklake_snapshot_changes");
2395
- const ducklakeDataFile = duckLakeMetadataTableName(this.options, "ducklake_data_file");
2396
- const ducklakeDeleteFile = duckLakeMetadataTableName(this.options, "ducklake_delete_file");
2397
- const rows = await runtime.query(`
2398
- WITH candidate_snapshots AS (
2399
- -- Recent DuckLake snapshots to inspect; Sixb visibility is filtered later.
2400
- SELECT
2401
- snapshot.snapshot_id,
2402
- snapshot.snapshot_time,
2403
- changes.changes_made,
2404
- changes.commit_extra_info
2405
- FROM ${ducklakeSnapshot} snapshot
2406
- JOIN ${ducklakeSnapshotChanges} changes
2407
- ON changes.snapshot_id = snapshot.snapshot_id
2408
- ${whereSql}
2409
- ORDER BY snapshot.snapshot_id DESC
2410
- ${limitSql}
2411
- ),
2412
- file_changes AS (
2413
- -- File metadata tells whether this table changed in each candidate snapshot.
2414
- SELECT begin_snapshot AS snapshot_id, false AS is_delete_change
2415
- FROM ${ducklakeDataFile}
2416
- WHERE table_id = ${input.tableId}
2417
- AND begin_snapshot IN (SELECT snapshot_id FROM candidate_snapshots)
2418
- UNION ALL
2419
- SELECT end_snapshot AS snapshot_id, true AS is_delete_change
2420
- FROM ${ducklakeDataFile}
2421
- WHERE table_id = ${input.tableId}
2422
- AND end_snapshot IN (SELECT snapshot_id FROM candidate_snapshots)
2423
- UNION ALL
2424
- SELECT begin_snapshot AS snapshot_id, true AS is_delete_change
2425
- FROM ${ducklakeDeleteFile}
2426
- WHERE table_id = ${input.tableId}
2427
- AND begin_snapshot IN (SELECT snapshot_id FROM candidate_snapshots)
2428
- UNION ALL
2429
- SELECT end_snapshot AS snapshot_id, true AS is_delete_change
2430
- FROM ${ducklakeDeleteFile}
2431
- WHERE table_id = ${input.tableId}
2432
- AND end_snapshot IN (SELECT snapshot_id FROM candidate_snapshots)
2433
- ),
2434
- file_changes_by_snapshot AS (
2435
- -- Collapse file-level changes into one row per snapshot.
2436
- SELECT
2437
- snapshot_id,
2438
- count(*) > 0 AS has_file_change,
2439
- count(*) FILTER (WHERE is_delete_change) > 0 AS has_file_delete_change
2440
- FROM file_changes
2441
- GROUP BY snapshot_id
2442
- )
2443
- SELECT
2444
- -- Keep metadata-only candidates; Sixb filters them with commit_extra_info.
2445
- candidate.snapshot_id,
2446
- candidate.snapshot_time,
2447
- candidate.changes_made,
2448
- candidate.commit_extra_info,
2449
- coalesce(file_changes.has_file_change, false) AS has_file_change,
2450
- coalesce(file_changes.has_file_delete_change, false) AS has_file_delete_change
2451
- FROM candidate_snapshots candidate
2452
- LEFT JOIN file_changes_by_snapshot file_changes
2453
- ON file_changes.snapshot_id = candidate.snapshot_id
2454
- ORDER BY candidate.snapshot_id DESC
2455
- `);
2824
+ const rows = await runtime.query(buildDuckLakeMetadataQuery(this.options, (table) => {
2825
+ const tableStart = `(SELECT min(begin_snapshot) FROM ${table("ducklake_table")} WHERE table_id = ${tableId})`;
2826
+ const candidateIds = input.exactSnapshotId !== undefined ? `SELECT CAST(${input.exactSnapshotId} AS BIGINT) AS snapshot_id
2827
+ WHERE ${input.exactSnapshotId} >= ${tableStart}` : `SELECT DISTINCT snapshot_id
2828
+ FROM (
2829
+ SELECT begin_snapshot AS snapshot_id
2830
+ FROM ${table("ducklake_data_file")} WHERE table_id = ${tableId}
2831
+ UNION ALL
2832
+ SELECT end_snapshot
2833
+ FROM ${table("ducklake_data_file")}
2834
+ WHERE table_id = ${tableId} AND end_snapshot IS NOT NULL
2835
+ UNION ALL
2836
+ SELECT begin_snapshot
2837
+ FROM ${table("ducklake_delete_file")} WHERE table_id = ${tableId}
2838
+ UNION ALL
2839
+ SELECT end_snapshot
2840
+ FROM ${table("ducklake_delete_file")}
2841
+ WHERE table_id = ${tableId} AND end_snapshot IS NOT NULL
2842
+ UNION ALL
2843
+ SELECT snapshot_id
2844
+ FROM ${table("ducklake_snapshot_changes")}
2845
+ WHERE strpos(',' || changes_made || ',', ${quoteSqlString(`:${tableId},`)}) > 0
2846
+ OR strpos(
2847
+ commit_extra_info,
2848
+ ${quoteSqlString(`"datasetId":${JSON.stringify(input.datasetId)}`)}
2849
+ ) > 0
2850
+ ) touched
2851
+ WHERE snapshot_id >= ${tableStart}
2852
+ ${input.beforeSnapshotId === undefined ? "" : `AND snapshot_id < ${input.beforeSnapshotId}`}
2853
+ ORDER BY snapshot_id DESC
2854
+ LIMIT ${limit}`;
2855
+ return `
2856
+ WITH candidate_ids AS (
2857
+ ${candidateIds}
2858
+ ),
2859
+ candidate_snapshots AS (
2860
+ SELECT
2861
+ snapshot.snapshot_id,
2862
+ snapshot.snapshot_time,
2863
+ changes.changes_made,
2864
+ changes.commit_extra_info
2865
+ FROM ${table("ducklake_snapshot")} snapshot
2866
+ JOIN ${table("ducklake_snapshot_changes")} changes
2867
+ ON changes.snapshot_id = snapshot.snapshot_id
2868
+ WHERE snapshot.snapshot_id IN (SELECT snapshot_id FROM candidate_ids)
2869
+ ),
2870
+ file_changes AS (
2871
+ -- File metadata tells whether this table changed in each candidate snapshot.
2872
+ SELECT begin_snapshot AS snapshot_id, false AS is_delete_change
2873
+ FROM ${table("ducklake_data_file")}
2874
+ WHERE table_id = ${tableId}
2875
+ AND begin_snapshot IN (SELECT snapshot_id FROM candidate_ids)
2876
+ UNION ALL
2877
+ SELECT end_snapshot AS snapshot_id, true AS is_delete_change
2878
+ FROM ${table("ducklake_data_file")}
2879
+ WHERE table_id = ${tableId}
2880
+ AND end_snapshot IN (SELECT snapshot_id FROM candidate_ids)
2881
+ UNION ALL
2882
+ SELECT begin_snapshot AS snapshot_id, true AS is_delete_change
2883
+ FROM ${table("ducklake_delete_file")}
2884
+ WHERE table_id = ${tableId}
2885
+ AND begin_snapshot IN (SELECT snapshot_id FROM candidate_ids)
2886
+ UNION ALL
2887
+ SELECT end_snapshot AS snapshot_id, true AS is_delete_change
2888
+ FROM ${table("ducklake_delete_file")}
2889
+ WHERE table_id = ${tableId}
2890
+ AND end_snapshot IN (SELECT snapshot_id FROM candidate_ids)
2891
+ ),
2892
+ file_changes_by_snapshot AS (
2893
+ -- Collapse file-level changes into one row per snapshot.
2894
+ SELECT
2895
+ snapshot_id,
2896
+ count(*) > 0 AS has_file_change,
2897
+ count(*) FILTER (WHERE is_delete_change) > 0 AS has_file_delete_change
2898
+ FROM file_changes
2899
+ GROUP BY snapshot_id
2900
+ )
2901
+ SELECT
2902
+ -- Keep metadata-only candidates; Sixb filters them with commit_extra_info.
2903
+ candidate_ids.snapshot_id,
2904
+ candidate.snapshot_time,
2905
+ candidate.changes_made,
2906
+ candidate.commit_extra_info,
2907
+ coalesce(file_changes.has_file_change, false) AS has_file_change,
2908
+ coalesce(file_changes.has_file_delete_change, false) AS has_file_delete_change
2909
+ FROM candidate_ids
2910
+ LEFT JOIN candidate_snapshots candidate
2911
+ ON candidate.snapshot_id = candidate_ids.snapshot_id
2912
+ LEFT JOIN file_changes_by_snapshot file_changes
2913
+ ON file_changes.snapshot_id = candidate_ids.snapshot_id
2914
+ ORDER BY candidate_ids.snapshot_id DESC
2915
+ `;
2916
+ }));
2456
2917
  return rows.map((row) => {
2918
+ const snapshotId = String(getBigIntLike(row, "snapshot_id"));
2919
+ if (row.snapshot_time === null || row.snapshot_time === undefined) {
2920
+ return { snapshotId, missing: true };
2921
+ }
2457
2922
  const metadata = parseCommitMetadata(row.commit_extra_info);
2458
2923
  return {
2459
- snapshotId: String(getBigIntLike(row, "snapshot_id")),
2924
+ snapshotId,
2460
2925
  createdAt: getDate(row, "snapshot_time"),
2461
2926
  changesMade: getString(row, "changes_made"),
2462
2927
  hasFileChange: getBoolean(row, "has_file_change"),
@@ -2514,7 +2979,7 @@ class DuckLakeSnapshotReader {
2514
2979
  }
2515
2980
  function assertDuckLakeSnapshotId2(snapshotId) {
2516
2981
  if (!/^\d+$/.test(snapshotId)) {
2517
- throw new LakeStorageError16(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`);
2982
+ throw new LakeStorageError17(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`);
2518
2983
  }
2519
2984
  }
2520
2985
  function fileChangeKey(snapshotId, tableId) {
@@ -2524,17 +2989,17 @@ function fileChangeKey(snapshotId, tableId) {
2524
2989
  // src/internal/ducklake-sql-executor.ts
2525
2990
  import { randomUUID as randomUUID4 } from "crypto";
2526
2991
  import {
2527
- LakeStorageError as LakeStorageError19
2992
+ LakeStorageError as LakeStorageError20
2528
2993
  } from "@sixb/core/lake-storage";
2529
2994
 
2530
2995
  // src/internal/dataset-row-commit.ts
2531
2996
  import {
2532
2997
  getDatasetPrimaryKeyColumns,
2533
- LakeStorageError as LakeStorageError17
2998
+ LakeStorageError as LakeStorageError18
2534
2999
  } from "@sixb/core/lake-storage";
2535
3000
  function assertDatasetWriteMode(mode, operation) {
2536
3001
  if (mode !== "snapshot" && mode !== "append") {
2537
- throw new LakeStorageError17(`[SixbDuckLake] Invalid ${operation} mode '${String(mode)}'. Expected 'snapshot' or 'append'.`);
3002
+ throw new LakeStorageError18(`[SixbDuckLake] Invalid ${operation} mode '${String(mode)}'. Expected 'snapshot' or 'append'.`);
2538
3003
  }
2539
3004
  }
2540
3005
  async function applyDatasetRowsFromRelation(input) {
@@ -2594,7 +3059,7 @@ async function applyDatasetRowsFromRelation(input) {
2594
3059
  async function inspectUniqueKeyedRelation(input) {
2595
3060
  const primaryKeyColumns = getDatasetPrimaryKeyColumns(input.dataset);
2596
3061
  if (primaryKeyColumns === null) {
2597
- throw new LakeStorageError17(`[SixbDuckLake] Dataset '${input.dataset.id}' must define a primaryKey before rows can be keyed.`);
3062
+ throw new LakeStorageError18(`[SixbDuckLake] Dataset '${input.dataset.id}' must define a primaryKey before rows can be keyed.`);
2598
3063
  }
2599
3064
  const keysSql = primaryKeyColumns.map((column) => quoteIdentifier(column)).join(", ");
2600
3065
  const [row] = await input.runtime.query(`
@@ -2609,7 +3074,7 @@ async function inspectUniqueKeyedRelation(input) {
2609
3074
  const rowCount = row === undefined ? 0 : Number(getBigIntLike(row, "row_count"));
2610
3075
  const maxKeyCount = row === undefined ? 0 : Number(getBigIntLike(row, "max_key_count"));
2611
3076
  if (maxKeyCount > 1) {
2612
- throw new LakeStorageError17(`[SixbDuckLake] Dataset '${input.dataset.id}' ${input.context} contains duplicate primary key.`);
3077
+ throw new LakeStorageError18(`[SixbDuckLake] Dataset '${input.dataset.id}' ${input.context} contains duplicate primary key.`);
2613
3078
  }
2614
3079
  return rowCount;
2615
3080
  }
@@ -2626,7 +3091,7 @@ async function assertNoPrimaryKeyOverlap(input) {
2626
3091
  LIMIT 1
2627
3092
  `);
2628
3093
  if (collision !== undefined) {
2629
- throw new LakeStorageError17(`[SixbDuckLake] Dataset '${input.dataset.id}' ${input.context} contains duplicate primary key.`);
3094
+ throw new LakeStorageError18(`[SixbDuckLake] Dataset '${input.dataset.id}' ${input.context} contains duplicate primary key.`);
2630
3095
  }
2631
3096
  }
2632
3097
  async function fingerprintRelation(runtime, relationSql, columnsSql) {
@@ -2660,7 +3125,7 @@ async function countRows(runtime, relationSql) {
2660
3125
  // src/internal/sql-transform-relations.ts
2661
3126
  import { randomUUID as randomUUID3 } from "crypto";
2662
3127
  import {
2663
- LakeStorageError as LakeStorageError18
3128
+ LakeStorageError as LakeStorageError19
2664
3129
  } from "@sixb/core/lake-storage";
2665
3130
  var RELATION_PLACEHOLDER_PREFIX = "__sixb_sql_transform_relation_";
2666
3131
  var RELATION_PLACEHOLDER_PATTERN = /__sixb_sql_transform_relation_[0-9a-f]{32}_[0-9]+__/g;
@@ -2678,7 +3143,7 @@ function renderDuckLakeSqlTransformSql(input) {
2678
3143
  const context = createDuckLakeSqlTransformRelationContext(input);
2679
3144
  const sql = input.sql(context.relations);
2680
3145
  if (typeof sql !== "string") {
2681
- throw new LakeStorageError18("[SixbDuckLake] SQL transform body must return a SQL string.");
3146
+ throw new LakeStorageError19("[SixbDuckLake] SQL transform body must return a SQL string.");
2682
3147
  }
2683
3148
  return context.render(sql);
2684
3149
  }
@@ -2716,7 +3181,7 @@ function renderDuckLakeSourceRelation(options, source) {
2716
3181
  return `(SELECT * FROM ${qualifiedTableName(options, tableName)} AT (VERSION => ${snapshotId}))`;
2717
3182
  }
2718
3183
  function throwUnresolvedPlaceholder() {
2719
- throw new LakeStorageError18("[SixbDuckLake] SQL transform contains an unresolved relation placeholder.");
3184
+ throw new LakeStorageError19("[SixbDuckLake] SQL transform contains an unresolved relation placeholder.");
2720
3185
  }
2721
3186
 
2722
3187
  // src/internal/ducklake-sql-executor.ts
@@ -2810,14 +3275,14 @@ class DuckLakeSqlExecutor {
2810
3275
  async resolveSourceDataset(runtime, dataset) {
2811
3276
  const definition = await this.datasets.getDatasetOnRuntime(runtime, dataset.id);
2812
3277
  if (!definition) {
2813
- throw new LakeStorageError19(`[SixbDuckLake] Unknown SQL transform source dataset '${dataset.id}'.`);
3278
+ throw new LakeStorageError20(`[SixbDuckLake] Unknown SQL transform source dataset '${dataset.id}'.`);
2814
3279
  }
2815
3280
  return definition;
2816
3281
  }
2817
3282
  async resolveTargetDataset(runtime, dataset) {
2818
3283
  const definition = await this.datasets.getDatasetOnRuntime(runtime, dataset.id);
2819
3284
  if (!definition) {
2820
- throw new LakeStorageError19(`[SixbDuckLake] Unknown SQL transform target dataset '${dataset.id}'.`);
3285
+ throw new LakeStorageError20(`[SixbDuckLake] Unknown SQL transform target dataset '${dataset.id}'.`);
2821
3286
  }
2822
3287
  this.datasets.assertSchema(definition);
2823
3288
  return definition;
@@ -2938,10 +3403,10 @@ function normalizePreviewValue(value) {
2938
3403
  return value;
2939
3404
  }
2940
3405
  function throwNoCommittedSourceVersion(datasetId) {
2941
- throw new LakeStorageError19(`[SixbDuckLake] No committed version found for SQL transform source dataset '${datasetId}'.`);
3406
+ throw new LakeStorageError20(`[SixbDuckLake] No committed version found for SQL transform source dataset '${datasetId}'.`);
2942
3407
  }
2943
3408
  function throwResultSchemaMismatch(datasetId, detail) {
2944
- throw new LakeStorageError19(`[SixbDuckLake] SQL transform result schema does not match target dataset '${datasetId}': ${detail}.`);
3409
+ throw new LakeStorageError20(`[SixbDuckLake] SQL transform result schema does not match target dataset '${datasetId}': ${detail}.`);
2945
3410
  }
2946
3411
 
2947
3412
  // src/internal/ducklake-write-coordinator.ts
@@ -2951,20 +3416,20 @@ import {
2951
3416
  getDatasetPrimaryKeyColumns as getDatasetPrimaryKeyColumns4,
2952
3417
  hasDatasetInputChanges,
2953
3418
  LakeConcurrencyError,
2954
- LakeStorageError as LakeStorageError23,
3419
+ LakeStorageError as LakeStorageError24,
2955
3420
  retryDatasetMergeCommit
2956
3421
  } from "@sixb/core/lake-storage";
2957
3422
 
2958
3423
  // src/internal/dataset-row-merge.ts
2959
3424
  import {
2960
3425
  getDatasetPrimaryKeyColumns as getDatasetPrimaryKeyColumns2,
2961
- LakeStorageError as LakeStorageError20,
3426
+ LakeStorageError as LakeStorageError21,
2962
3427
  reconcileDatasetSequences
2963
3428
  } from "@sixb/core/lake-storage";
2964
3429
  async function applyDatasetMergeFromRelation(input) {
2965
3430
  const primaryKeyColumns = getDatasetPrimaryKeyColumns2(input.dataset);
2966
3431
  if (primaryKeyColumns === null) {
2967
- throw new LakeStorageError20(`[SixbDuckLake] Dataset '${input.dataset.id}' must define a primaryKey before it can be merged.`);
3432
+ throw new LakeStorageError21(`[SixbDuckLake] Dataset '${input.dataset.id}' must define a primaryKey before it can be merged.`);
2968
3433
  }
2969
3434
  const stagingTable = quoteIdentifier(input.stagingTableName);
2970
3435
  const targetTable = qualifiedTableName(input.options, encodeDatasetTableName(input.dataset.id));
@@ -3125,7 +3590,7 @@ function finalChangesSelectSql(input, primaryKeyColumns) {
3125
3590
  async function countMergeEffects(input, targetTable, finalChanges, primaryKeyColumns) {
3126
3591
  const matchColumn = primaryKeyColumns[0];
3127
3592
  if (matchColumn === undefined) {
3128
- throw new LakeStorageError20(`[SixbDuckLake] Dataset '${input.dataset.id}' primaryKey must contain at least one column.`);
3593
+ throw new LakeStorageError21(`[SixbDuckLake] Dataset '${input.dataset.id}' primaryKey must contain at least one column.`);
3129
3594
  }
3130
3595
  const keyMatchSql = primaryKeyMatchSql(primaryKeyColumns, "target", "source");
3131
3596
  const targetMatch = `target.${quoteIdentifier(matchColumn)} IS NOT NULL`;
@@ -3179,7 +3644,7 @@ import {
3179
3644
  cloneDatasetMergeChange,
3180
3645
  datasetSequenceChange,
3181
3646
  getDatasetMergeChangeValidationError,
3182
- LakeStorageError as LakeStorageError21
3647
+ LakeStorageError as LakeStorageError22
3183
3648
  } from "@sixb/core/lake-storage";
3184
3649
 
3185
3650
  // src/internal/row-appender.ts
@@ -3280,7 +3745,7 @@ class DuckLakeMergeSession {
3280
3745
  for await (const change of changes) {
3281
3746
  const validationError = getDatasetMergeChangeValidationError(change, this.input.dataset);
3282
3747
  if (validationError) {
3283
- throw new LakeStorageError21(`[SixbDuckLake] ${validationError}`);
3748
+ throw new LakeStorageError22(`[SixbDuckLake] ${validationError}`);
3284
3749
  }
3285
3750
  batch.push({
3286
3751
  sequence: this.nextSequence,
@@ -3343,7 +3808,7 @@ class DuckLakeMergeSession {
3343
3808
  }
3344
3809
  assertOpen() {
3345
3810
  if (this.closed) {
3346
- throw new LakeStorageError21("[SixbDuckLake] Merge session is already closed.");
3811
+ throw new LakeStorageError22("[SixbDuckLake] Merge session is already closed.");
3347
3812
  }
3348
3813
  }
3349
3814
  async cleanup() {
@@ -3363,7 +3828,7 @@ import { getDatasetRowValidationError } from "@sixb/core";
3363
3828
  import {
3364
3829
  encodeDatasetPrimaryKey,
3365
3830
  getDatasetPrimaryKeyColumns as getDatasetPrimaryKeyColumns3,
3366
- LakeStorageError as LakeStorageError22
3831
+ LakeStorageError as LakeStorageError23
3367
3832
  } from "@sixb/core/lake-storage";
3368
3833
  var WRITE_ROW_BATCH_SIZE = 1000;
3369
3834
  async function createDuckLakeWriteSession(input) {
@@ -3394,13 +3859,13 @@ class DuckLakeWriteSession {
3394
3859
  for await (const row of rows) {
3395
3860
  const validationError = getDatasetRowValidationError(row, this.input.dataset);
3396
3861
  if (validationError) {
3397
- throw new LakeStorageError22(`[SixbDuckLake] ${validationError}`);
3862
+ throw new LakeStorageError23(`[SixbDuckLake] ${validationError}`);
3398
3863
  }
3399
3864
  const clonedRow = structuredClone(row);
3400
3865
  if (getDatasetPrimaryKeyColumns3(this.input.dataset) !== null) {
3401
3866
  const primaryKey = encodeDatasetPrimaryKey(this.input.dataset, clonedRow);
3402
3867
  if (this.stagedPrimaryKeys.has(primaryKey) || batchPrimaryKeys.has(primaryKey)) {
3403
- throw new LakeStorageError22(`[SixbDuckLake] Dataset '${this.input.dataset.id}' ${this.input.mode ?? "snapshot"} source contains duplicate primary key.`);
3868
+ throw new LakeStorageError23(`[SixbDuckLake] Dataset '${this.input.dataset.id}' ${this.input.mode ?? "snapshot"} source contains duplicate primary key.`);
3404
3869
  }
3405
3870
  batchPrimaryKeys.add(primaryKey);
3406
3871
  }
@@ -3457,7 +3922,7 @@ class DuckLakeWriteSession {
3457
3922
  }
3458
3923
  assertOpen() {
3459
3924
  if (this.closed) {
3460
- throw new LakeStorageError22("[SixbDuckLake] Write session is already closed.");
3925
+ throw new LakeStorageError23("[SixbDuckLake] Write session is already closed.");
3461
3926
  }
3462
3927
  }
3463
3928
  async cleanup() {
@@ -3465,6 +3930,7 @@ class DuckLakeWriteSession {
3465
3930
  return;
3466
3931
  }
3467
3932
  this.cleanedUp = true;
3933
+ this.stagedPrimaryKeys.clear();
3468
3934
  await this.dropStagingTable();
3469
3935
  }
3470
3936
  async dropStagingTable() {
@@ -3490,7 +3956,7 @@ class DuckLakeWriteCoordinator {
3490
3956
  this.connections.assertOpen();
3491
3957
  const definition = await this.datasets.getDataset(input.dataset.id);
3492
3958
  if (!definition) {
3493
- throw new LakeStorageError23(`[SixbDuckLake] Unknown dataset '${input.dataset.id}'.`);
3959
+ throw new LakeStorageError24(`[SixbDuckLake] Unknown dataset '${input.dataset.id}'.`);
3494
3960
  }
3495
3961
  this.datasets.assertSchema(definition);
3496
3962
  assertUnsequencedDatasetWrite(definition);
@@ -3511,10 +3977,10 @@ class DuckLakeWriteCoordinator {
3511
3977
  this.connections.assertOpen();
3512
3978
  const definition = await this.datasets.getDataset(input.dataset.id);
3513
3979
  if (!definition) {
3514
- throw new LakeStorageError23(`[SixbDuckLake] Unknown dataset '${input.dataset.id}'.`);
3980
+ throw new LakeStorageError24(`[SixbDuckLake] Unknown dataset '${input.dataset.id}'.`);
3515
3981
  }
3516
3982
  if (getDatasetPrimaryKeyColumns4(definition) === null) {
3517
- throw new LakeStorageError23(`[SixbDuckLake] Dataset '${definition.id}' must define a primaryKey before it can be merged.`);
3983
+ throw new LakeStorageError24(`[SixbDuckLake] Dataset '${definition.id}' must define a primaryKey before it can be merged.`);
3518
3984
  }
3519
3985
  this.datasets.assertSchema(definition);
3520
3986
  const latestVersion = await this.connections.withAttachedRuntime((runtime) => this.snapshots.getLatestVersionForDefinition(runtime, definition));
@@ -3532,7 +3998,7 @@ class DuckLakeWriteCoordinator {
3532
3998
  async commitWrite(input) {
3533
3999
  const definition = await this.datasets.getDataset(input.write.dataset.id);
3534
4000
  if (!definition) {
3535
- throw new LakeStorageError23(`[SixbDuckLake] Unknown dataset '${input.write.dataset.id}'.`);
4001
+ throw new LakeStorageError24(`[SixbDuckLake] Unknown dataset '${input.write.dataset.id}'.`);
3536
4002
  }
3537
4003
  const mode = input.write.mode ?? "snapshot";
3538
4004
  assertDatasetWriteMode(mode, "write");
@@ -3553,10 +4019,10 @@ class DuckLakeWriteCoordinator {
3553
4019
  async commitMerge(input) {
3554
4020
  const definition = await this.datasets.getDataset(input.merge.dataset.id);
3555
4021
  if (!definition) {
3556
- throw new LakeStorageError23(`[SixbDuckLake] Unknown dataset '${input.merge.dataset.id}'.`);
4022
+ throw new LakeStorageError24(`[SixbDuckLake] Unknown dataset '${input.merge.dataset.id}'.`);
3557
4023
  }
3558
4024
  if (getDatasetPrimaryKeyColumns4(definition) === null) {
3559
- throw new LakeStorageError23(`[SixbDuckLake] Dataset '${definition.id}' must define a primaryKey before it can be merged.`);
4025
+ throw new LakeStorageError24(`[SixbDuckLake] Dataset '${definition.id}' must define a primaryKey before it can be merged.`);
3560
4026
  }
3561
4027
  return retryDatasetMergeCommit(input.merge, input.commit, (rebase) => this.withCommitRuntime(async (runtime) => {
3562
4028
  input.commit?.signal?.throwIfAborted();
@@ -3584,7 +4050,7 @@ class DuckLakeWriteCoordinator {
3584
4050
  validatedPrimaryKeyColumns: context.validatedPrimaryKeyColumns
3585
4051
  });
3586
4052
  if (result.sourceRowCount !== input.changesWritten) {
3587
- throw new LakeStorageError23(`[SixbDuckLake] Staged merge for dataset '${definition.id}' accepted ${input.changesWritten} change(s), but DuckLake saw ${result.sourceRowCount} source change(s) at commit time.`);
4053
+ 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.`);
3588
4054
  }
3589
4055
  return result;
3590
4056
  }
@@ -3606,7 +4072,7 @@ class DuckLakeWriteCoordinator {
3606
4072
  disableRetries: input.mode === "append" && getDatasetPrimaryKeyColumns4(input.dataset) !== null
3607
4073
  });
3608
4074
  if (result.version === null) {
3609
- throw new LakeStorageError23(`[SixbDuckLake] No DuckLake changes were committed for dataset '${input.dataset.id}', and no previous version exists.`);
4075
+ throw new LakeStorageError24(`[SixbDuckLake] No DuckLake changes were committed for dataset '${input.dataset.id}', and no previous version exists.`);
3610
4076
  }
3611
4077
  return { ...result.version, outcome: result.outcome };
3612
4078
  }
@@ -3637,10 +4103,9 @@ class DuckLakeWriteCoordinator {
3637
4103
  }
3638
4104
  async commitDatasetVersionUnlocked(input) {
3639
4105
  const previousWriteSnapshotId = await this.lastCommittedSnapshotId(input.runtime);
3640
- await input.runtime.run("BEGIN TRANSACTION");
3641
4106
  let committed = false;
3642
4107
  try {
3643
- const latestVersion = await this.latestVersionForCommit(input);
4108
+ const latestVersion = await this.beginVersionTransaction(input);
3644
4109
  if (input.expectedLatestVersionId !== undefined) {
3645
4110
  this.assertExpectedLatestVersion({
3646
4111
  datasetId: input.dataset.id,
@@ -3688,7 +4153,7 @@ class DuckLakeWriteCoordinator {
3688
4153
  if (version) {
3689
4154
  return { outcome: "created", version };
3690
4155
  }
3691
- throw new LakeStorageError23(`[SixbDuckLake] DuckLake committed snapshot '${ownSnapshotId}' for dataset '${input.dataset.id}', but Sixb could not hydrate it as a dataset version.`);
4156
+ throw new LakeStorageError24(`[SixbDuckLake] DuckLake committed snapshot '${ownSnapshotId}' for dataset '${input.dataset.id}', but Sixb could not hydrate it as a dataset version.`);
3692
4157
  }
3693
4158
  if (!input.changeResult.dataChangeExpected) {
3694
4159
  const version = await this.versionForNoOpCommit(input.runtime, input.dataset, input.allowInitialNoOp ?? false);
@@ -3706,9 +4171,9 @@ class DuckLakeWriteCoordinator {
3706
4171
  };
3707
4172
  }
3708
4173
  if (input.committedWriteSnapshotId === input.previousWriteSnapshotId) {
3709
- throw new LakeStorageError23(`[SixbDuckLake] DuckLake commit for dataset '${input.dataset.id}' completed without producing a snapshot for this non-empty write.`);
4174
+ throw new LakeStorageError24(`[SixbDuckLake] DuckLake commit for dataset '${input.dataset.id}' completed without producing a snapshot for this non-empty write.`);
3710
4175
  }
3711
- throw new LakeStorageError23(`[SixbDuckLake] DuckLake commit for dataset '${input.dataset.id}' completed, but Sixb could not find a matching snapshot for this write.`);
4176
+ throw new LakeStorageError24(`[SixbDuckLake] DuckLake commit for dataset '${input.dataset.id}' completed, but Sixb could not find a matching snapshot for this write.`);
3712
4177
  }
3713
4178
  async applyStagedRows(input) {
3714
4179
  const mode = input.write.mode ?? "snapshot";
@@ -3721,7 +4186,7 @@ class DuckLakeWriteCoordinator {
3721
4186
  previousRowCount: input.previousRowCount
3722
4187
  });
3723
4188
  if (result.sourceRowCount !== input.rowsWritten) {
3724
- throw new LakeStorageError23(`[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.`);
4189
+ 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.`);
3725
4190
  }
3726
4191
  return result;
3727
4192
  }
@@ -3743,7 +4208,7 @@ class DuckLakeWriteCoordinator {
3743
4208
  if (changeResult.dataChangeExpected) {
3744
4209
  return;
3745
4210
  }
3746
- const latestVersion = knownLatestVersion ?? await this.snapshots.getLatestVersionSummaryForDefinition(input.runtime, input.dataset);
4211
+ const latestVersion = knownLatestVersion === undefined ? await this.snapshots.getLatestVersionSummaryForDefinition(input.runtime, input.dataset) : knownLatestVersion;
3747
4212
  if (latestVersion ? input.mode === "merge" || !hasDatasetInputChanges(latestVersion.inputs, input.inputs) : input.allowInitialNoOp) {
3748
4213
  return;
3749
4214
  }
@@ -3759,18 +4224,39 @@ class DuckLakeWriteCoordinator {
3759
4224
  if (allowInitialNoOp) {
3760
4225
  return null;
3761
4226
  }
3762
- throw new LakeStorageError23(`[SixbDuckLake] No DuckLake changes were committed for dataset '${definition.id}', and no previous version exists.`);
4227
+ throw new LakeStorageError24(`[SixbDuckLake] No DuckLake changes were committed for dataset '${definition.id}', and no previous version exists.`);
3763
4228
  }
3764
4229
  async rollbackTransaction(runtime) {
3765
4230
  try {
3766
4231
  await runtime.run("ROLLBACK");
3767
4232
  } catch {}
3768
4233
  }
3769
- async latestVersionForCommit(input) {
3770
- if (input.expectedLatestVersionId === undefined && input.mode !== "append") {
3771
- return null;
4234
+ async beginVersionTransaction(input) {
4235
+ if (!isSqliteCatalog(this.options.catalog)) {
4236
+ await input.runtime.run("BEGIN TRANSACTION");
4237
+ if (input.expectedLatestVersionId === undefined && input.mode !== "append") {
4238
+ return;
4239
+ }
4240
+ return this.snapshots.getLatestVersionSummaryForDefinition(input.runtime, input.dataset);
4241
+ }
4242
+ for (let attempt = 0;attempt < 3; attempt++) {
4243
+ input.signal?.throwIfAborted();
4244
+ const snapshotId = await this.currentSnapshotId(input.runtime);
4245
+ const latestVersion = await this.snapshots.getLatestVersionSummaryForDefinition(input.runtime, input.dataset);
4246
+ await input.runtime.run("BEGIN TRANSACTION");
4247
+ if (await this.currentSnapshotId(input.runtime) === snapshotId) {
4248
+ return latestVersion;
4249
+ }
4250
+ await input.runtime.run("ROLLBACK");
3772
4251
  }
3773
- return this.snapshots.getLatestVersionSummaryForDefinition(input.runtime, input.dataset);
4252
+ throw new LakeConcurrencyError(`[SixbDuckLake] Catalog changed during three write preparations for dataset '${input.dataset.id}'. Retry the write.`);
4253
+ }
4254
+ async currentSnapshotId(runtime) {
4255
+ const [row] = await runtime.query(`SELECT id FROM ${quoteIdentifier(duckLakeAlias(this.options))}.current_snapshot()`);
4256
+ if (row === undefined) {
4257
+ throw new LakeStorageError24("[SixbDuckLake] DuckLake did not return a current snapshot.");
4258
+ }
4259
+ return String(getBigIntLike(row, "id"));
3774
4260
  }
3775
4261
  async lastCommittedSnapshotId(runtime) {
3776
4262
  const [row] = await runtime.query(`SELECT id FROM ${quoteIdentifier(duckLakeAlias(this.options))}.last_committed_snapshot()`);
@@ -3819,6 +4305,9 @@ class DuckLakeWriteCoordinator {
3819
4305
  }
3820
4306
  }
3821
4307
  }
4308
+ function isSqliteCatalog(catalog) {
4309
+ return catalog.type === "sqlite" || catalog.type === "custom" && catalog.uri.toLowerCase().startsWith("sqlite:");
4310
+ }
3822
4311
  function commitRowCount(input, changeResult, latestVersion) {
3823
4312
  if (input.mode === "append" && input.expectedLatestVersionId === undefined) {
3824
4313
  return { kind: "unknown" };
@@ -3833,7 +4322,7 @@ function commitRowCount(input, changeResult, latestVersion) {
3833
4322
  }
3834
4323
  function assertDuckLakeSnapshotId3(snapshotId) {
3835
4324
  if (!/^\d+$/.test(snapshotId)) {
3836
- throw new LakeStorageError23(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`);
4325
+ throw new LakeStorageError24(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`);
3837
4326
  }
3838
4327
  }
3839
4328
  var localCatalogCommitLocks = new Map;
@@ -3922,6 +4411,9 @@ class DuckLakeStorage {
3922
4411
  async getVersion(datasetId, versionId) {
3923
4412
  return this.snapshotReader.getVersion(datasetId, versionId);
3924
4413
  }
4414
+ async listLatestVersionsSince(input) {
4415
+ return this.snapshotReader.listLatestVersionsSince(input);
4416
+ }
3925
4417
  readRows(input) {
3926
4418
  return this.rows.readRows(input);
3927
4419
  }
@@ -3939,4 +4431,4 @@ export {
3939
4431
  DuckLakeStorage
3940
4432
  };
3941
4433
 
3942
- //# debugId=555594FFA13F16F264756E2164756E21
4434
+ //# debugId=99E61447F4C74C9E64756E2164756E21