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