@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.
- package/README.md +25 -27
- package/dist/ducklake-storage.d.ts +2 -1
- package/dist/ducklake-storage.d.ts.map +1 -1
- package/dist/index.js +736 -244
- package/dist/index.js.map +14 -12
- package/dist/internal/duckdb-reader.d.ts +28 -0
- package/dist/internal/duckdb-reader.d.ts.map +1 -0
- package/dist/internal/duckdb-runtime.d.ts +5 -3
- package/dist/internal/duckdb-runtime.d.ts.map +1 -1
- package/dist/internal/ducklake-connection-manager.d.ts +19 -3
- package/dist/internal/ducklake-connection-manager.d.ts.map +1 -1
- package/dist/internal/ducklake-read-gate.d.ts +11 -0
- package/dist/internal/ducklake-read-gate.d.ts.map +1 -0
- package/dist/internal/ducklake-row-reader.d.ts.map +1 -1
- package/dist/internal/ducklake-snapshot-reader.d.ts +26 -1
- package/dist/internal/ducklake-snapshot-reader.d.ts.map +1 -1
- package/dist/internal/ducklake-write-coordinator.d.ts +2 -1
- package/dist/internal/ducklake-write-coordinator.d.ts.map +1 -1
- package/dist/internal/sql.d.ts +11 -2
- package/dist/internal/sql.d.ts.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/ducklake-storage.ts +8 -0
- package/src/internal/duckdb-reader.ts +129 -0
- package/src/internal/duckdb-runtime.ts +75 -51
- package/src/internal/ducklake-connection-manager.ts +117 -33
- package/src/internal/ducklake-maintenance.ts +1 -1
- package/src/internal/ducklake-read-gate.ts +58 -0
- package/src/internal/ducklake-row-reader.ts +84 -63
- package/src/internal/ducklake-snapshot-reader.ts +331 -79
- package/src/internal/ducklake-write-coordinator.ts +56 -15
- package/src/internal/ducklake-write-session.ts +1 -0
- package/src/internal/sql.ts +35 -6
- 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
|
-
|
|
162
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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
|
|
1045
|
+
throw new LakeStorageError10("[SixbDuckLake] DuckDB runtime is closed.");
|
|
907
1046
|
}
|
|
908
1047
|
}
|
|
909
1048
|
assertNotClosed() {
|
|
910
1049
|
if (this.closed) {
|
|
911
|
-
throw new
|
|
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
|
|
972
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1182
|
-
|
|
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
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
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.
|
|
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
|
|
1294
|
-
const releaseLock = await this.
|
|
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
|
|
1302
|
-
|
|
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.
|
|
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
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2165
|
+
throw new LakeStorageError15("[SixbDuckLake] DuckLake maintenance cannot run in read-only mode.");
|
|
1881
2166
|
}
|
|
1882
|
-
return this.connections.
|
|
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
|
|
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.
|
|
1932
|
-
|
|
2216
|
+
const read = await this.connections.acquireRead(input.signal);
|
|
2217
|
+
let reader = null;
|
|
1933
2218
|
try {
|
|
1934
|
-
const
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
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
|
-
|
|
1953
|
-
let
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3980
|
+
throw new LakeStorageError24(`[SixbDuckLake] Unknown dataset '${input.dataset.id}'.`);
|
|
3515
3981
|
}
|
|
3516
3982
|
if (getDatasetPrimaryKeyColumns4(definition) === null) {
|
|
3517
|
-
throw new
|
|
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
|
|
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
|
|
4022
|
+
throw new LakeStorageError24(`[SixbDuckLake] Unknown dataset '${input.merge.dataset.id}'.`);
|
|
3557
4023
|
}
|
|
3558
4024
|
if (getDatasetPrimaryKeyColumns4(definition) === null) {
|
|
3559
|
-
throw new
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3770
|
-
if (
|
|
3771
|
-
|
|
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
|
-
|
|
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
|
|
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=
|
|
4434
|
+
//# debugId=99E61447F4C74C9E64756E2164756E21
|