@sixb/ducklake 0.1.4 → 0.1.6
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/dist/ducklake-storage.d.ts +3 -1
- package/dist/ducklake-storage.d.ts.map +1 -1
- package/dist/index.js +691 -567
- package/dist/index.js.map +13 -12
- package/dist/internal/ducklake-change-reader.d.ts +13 -0
- package/dist/internal/ducklake-change-reader.d.ts.map +1 -0
- package/package.json +5 -5
- package/src/ducklake-storage.ts +13 -0
- package/src/internal/ducklake-change-reader.ts +157 -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,23 +381,441 @@ 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}'.`);
|
|
389
|
+
}
|
|
390
|
+
names.add(normalizedName);
|
|
391
|
+
parameters.push(`${name}=${quotePostgresConnectionValue(String(value))}`);
|
|
392
|
+
}
|
|
393
|
+
function quotePostgresConnectionValue(value) {
|
|
394
|
+
return `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`;
|
|
395
|
+
}
|
|
396
|
+
function pathScheme(path) {
|
|
397
|
+
const match = /^([a-z][a-z0-9+.-]*):\/\//i.exec(path);
|
|
398
|
+
return match ? match[1].toLowerCase() : null;
|
|
399
|
+
}
|
|
400
|
+
|
|
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
|
+
}
|
|
456
|
+
}
|
|
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.`);
|
|
468
|
+
}
|
|
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.`);
|
|
474
|
+
}
|
|
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.`);
|
|
479
|
+
}
|
|
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 LakeStorageError10 } 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;
|
|
296
790
|
}
|
|
297
|
-
names.add(normalizedName);
|
|
298
|
-
parameters.push(`${name}=${quotePostgresConnectionValue(String(value))}`);
|
|
299
|
-
}
|
|
300
|
-
function quotePostgresConnectionValue(value) {
|
|
301
|
-
return `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`;
|
|
302
|
-
}
|
|
303
|
-
function pathScheme(path) {
|
|
304
|
-
const match = /^([a-z][a-z0-9+.-]*):\/\//i.exec(path);
|
|
305
|
-
return match ? match[1].toLowerCase() : null;
|
|
306
791
|
}
|
|
307
792
|
|
|
793
|
+
// src/internal/duckdb-runtime.ts
|
|
794
|
+
import {
|
|
795
|
+
DuckDBInstance
|
|
796
|
+
} from "@duckdb/node-api";
|
|
797
|
+
import { LakeStorageError as LakeStorageError9 } from "@sixb/core/lake-storage";
|
|
798
|
+
|
|
799
|
+
// src/internal/duckdb-value-converter.ts
|
|
800
|
+
import {
|
|
801
|
+
DuckDBDecimalValue,
|
|
802
|
+
DuckDBTypeId,
|
|
803
|
+
JSDuckDBValueConverter
|
|
804
|
+
} from "@duckdb/node-api";
|
|
805
|
+
import { LakeStorageError as LakeStorageError8 } from "@sixb/core/lake-storage";
|
|
806
|
+
var sixbDuckDbValueConverter = (value, type, converter) => {
|
|
807
|
+
if (value === null) {
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
if (type.typeId === DuckDBTypeId.DECIMAL) {
|
|
811
|
+
if (!(value instanceof DuckDBDecimalValue)) {
|
|
812
|
+
throw new LakeStorageError8("[SixbDuckLake] DuckDB returned an invalid DECIMAL value.");
|
|
813
|
+
}
|
|
814
|
+
return value.toString();
|
|
815
|
+
}
|
|
816
|
+
return JSDuckDBValueConverter(value, type, converter);
|
|
817
|
+
};
|
|
818
|
+
|
|
308
819
|
// src/internal/duckdb-runtime.ts
|
|
309
820
|
class NodeDuckDbRuntime {
|
|
310
821
|
instance;
|
|
@@ -337,8 +848,8 @@ class NodeDuckDbRuntime {
|
|
|
337
848
|
async* streamRows(sql, values) {
|
|
338
849
|
this.assertAcceptingOperations();
|
|
339
850
|
let releaseOperation;
|
|
340
|
-
const operationFinished = new Promise((
|
|
341
|
-
releaseOperation =
|
|
851
|
+
const operationFinished = new Promise((resolve) => {
|
|
852
|
+
releaseOperation = resolve;
|
|
342
853
|
});
|
|
343
854
|
const waitForTurn = this.operations.then(() => {
|
|
344
855
|
this.assertNotClosed();
|
|
@@ -392,12 +903,12 @@ class NodeDuckDbRuntime {
|
|
|
392
903
|
}
|
|
393
904
|
assertAcceptingOperations() {
|
|
394
905
|
if (this.closing || this.closed) {
|
|
395
|
-
throw new
|
|
906
|
+
throw new LakeStorageError9("[SixbDuckLake] DuckDB runtime is closed.");
|
|
396
907
|
}
|
|
397
908
|
}
|
|
398
909
|
assertNotClosed() {
|
|
399
910
|
if (this.closed) {
|
|
400
|
-
throw new
|
|
911
|
+
throw new LakeStorageError9("[SixbDuckLake] DuckDB runtime is closed.");
|
|
401
912
|
}
|
|
402
913
|
}
|
|
403
914
|
}
|
|
@@ -509,7 +1020,7 @@ class DuckLakeConnectionManager {
|
|
|
509
1020
|
}
|
|
510
1021
|
assertOpen() {
|
|
511
1022
|
if (this.closed) {
|
|
512
|
-
throw new
|
|
1023
|
+
throw new LakeStorageError10("[SixbDuckLake] DuckLakeStorage is closed.");
|
|
513
1024
|
}
|
|
514
1025
|
}
|
|
515
1026
|
async runtime() {
|
|
@@ -793,8 +1304,8 @@ class DuckLakeConnectionManager {
|
|
|
793
1304
|
}
|
|
794
1305
|
const previous = this.localAttachmentLock.catch(() => {});
|
|
795
1306
|
let release;
|
|
796
|
-
const current = new Promise((
|
|
797
|
-
release =
|
|
1307
|
+
const current = new Promise((resolve) => {
|
|
1308
|
+
release = resolve;
|
|
798
1309
|
});
|
|
799
1310
|
this.localAttachmentLock = previous.then(() => current);
|
|
800
1311
|
await previous;
|
|
@@ -805,264 +1316,54 @@ class DuckLakeConnectionManager {
|
|
|
805
1316
|
}
|
|
806
1317
|
released = true;
|
|
807
1318
|
release();
|
|
808
|
-
};
|
|
809
|
-
}
|
|
810
|
-
async closeRuntimePromise(runtimePromise, options) {
|
|
811
|
-
if (runtimePromise === undefined) {
|
|
812
|
-
return;
|
|
813
|
-
}
|
|
814
|
-
let runtime;
|
|
815
|
-
try {
|
|
816
|
-
runtime = await runtimePromise;
|
|
817
|
-
} catch {
|
|
818
|
-
return;
|
|
819
|
-
}
|
|
820
|
-
await this.closeRuntime(runtime, options);
|
|
821
|
-
}
|
|
822
|
-
async closeRuntime(runtime, options) {
|
|
823
|
-
if (options.detach) {
|
|
824
|
-
try {
|
|
825
|
-
await runtime.run(`DETACH ${quoteIdentifier(duckLakeAlias(this.options))}`);
|
|
826
|
-
} catch {}
|
|
827
|
-
}
|
|
828
|
-
await runtime.close();
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
var localCatalogGenerations = new Map;
|
|
832
|
-
function currentLocalCatalogGeneration(options) {
|
|
833
|
-
const key = localCatalogCoordinationKey(options);
|
|
834
|
-
return key === undefined ? 0 : localCatalogGenerations.get(key) ?? 0;
|
|
835
|
-
}
|
|
836
|
-
function incrementLocalCatalogGeneration(options) {
|
|
837
|
-
const key = localCatalogCoordinationKey(options);
|
|
838
|
-
if (key === undefined) {
|
|
839
|
-
return 0;
|
|
840
|
-
}
|
|
841
|
-
const nextGeneration = (localCatalogGenerations.get(key) ?? 0) + 1;
|
|
842
|
-
localCatalogGenerations.set(key, nextGeneration);
|
|
843
|
-
return nextGeneration;
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
// src/internal/ducklake-dataset-catalog.ts
|
|
847
|
-
import { randomUUID } from "crypto";
|
|
848
|
-
import {
|
|
849
|
-
assertDatasetDefinition,
|
|
850
|
-
LakeStorageError as LakeStorageError11,
|
|
851
|
-
planDatasetDefinitionUpdate
|
|
852
|
-
} from "@sixb/core/lake-storage";
|
|
853
|
-
|
|
854
|
-
// src/internal/duckdb-row.ts
|
|
855
|
-
import { LakeStorageError as LakeStorageError5 } from "@sixb/core/lake-storage";
|
|
856
|
-
function getString(row, key) {
|
|
857
|
-
const value = row[key];
|
|
858
|
-
if (typeof value !== "string") {
|
|
859
|
-
throw new LakeStorageError5(`[SixbDuckLake] Expected DuckDB column '${key}' to be a string, got ${typeof value}.`);
|
|
860
|
-
}
|
|
861
|
-
return value;
|
|
862
|
-
}
|
|
863
|
-
function getOptionalString(row, key) {
|
|
864
|
-
const value = row[key];
|
|
865
|
-
if (value === null || value === undefined) {
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
if (typeof value !== "string") {
|
|
869
|
-
throw new LakeStorageError5(`[SixbDuckLake] Expected DuckDB column '${key}' to be a string, got ${typeof value}.`);
|
|
870
|
-
}
|
|
871
|
-
return value;
|
|
872
|
-
}
|
|
873
|
-
function getBoolean(row, key) {
|
|
874
|
-
const value = row[key];
|
|
875
|
-
if (typeof value !== "boolean") {
|
|
876
|
-
throw new LakeStorageError5(`[SixbDuckLake] Expected DuckDB column '${key}' to be a boolean, got ${typeof value}.`);
|
|
877
|
-
}
|
|
878
|
-
return value;
|
|
879
|
-
}
|
|
880
|
-
function getBigIntLike(row, key) {
|
|
881
|
-
const value = row[key];
|
|
882
|
-
if (typeof value === "bigint") {
|
|
883
|
-
return value;
|
|
884
|
-
}
|
|
885
|
-
if (typeof value === "number" && Number.isInteger(value)) {
|
|
886
|
-
return BigInt(value);
|
|
887
|
-
}
|
|
888
|
-
if (typeof value === "string" && /^-?\d+$/.test(value)) {
|
|
889
|
-
return BigInt(value);
|
|
890
|
-
}
|
|
891
|
-
throw new LakeStorageError5(`[SixbDuckLake] Expected DuckDB column '${key}' to be an integer, got ${typeof value}.`);
|
|
892
|
-
}
|
|
893
|
-
function getOptionalBigIntLike(row, key) {
|
|
894
|
-
const value = row[key];
|
|
895
|
-
if (value === null || value === undefined) {
|
|
896
|
-
return;
|
|
897
|
-
}
|
|
898
|
-
return getBigIntLike(row, key);
|
|
899
|
-
}
|
|
900
|
-
function getDate(row, key) {
|
|
901
|
-
const value = row[key];
|
|
902
|
-
if (value instanceof Date) {
|
|
903
|
-
return value;
|
|
904
|
-
}
|
|
905
|
-
if (typeof value === "string") {
|
|
906
|
-
return new Date(value);
|
|
907
|
-
}
|
|
908
|
-
throw new LakeStorageError5(`[SixbDuckLake] Expected DuckDB column '${key}' to be a date, got ${typeof value}.`);
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
// src/internal/ducklake-catalog-schema.ts
|
|
912
|
-
import { LakeStorageError as LakeStorageError8 } from "@sixb/core/lake-storage";
|
|
913
|
-
|
|
914
|
-
// src/internal/schema.ts
|
|
915
|
-
import { LakeStorageError as LakeStorageError7 } from "@sixb/core/lake-storage";
|
|
916
|
-
|
|
917
|
-
// src/internal/duckdb-column-types.ts
|
|
918
|
-
function normalizeDuckDbTypeSql(typeSql) {
|
|
919
|
-
return typeSql.trim().toUpperCase().replace(/\s+/g, " ").replace(/\s*([(),])\s*/g, "$1");
|
|
920
|
-
}
|
|
921
|
-
function defineDuckDbColumnType(sql, aliases = []) {
|
|
922
|
-
const acceptedTypes = new Set([sql, ...aliases].map(normalizeDuckDbTypeSql));
|
|
923
|
-
return {
|
|
924
|
-
sql,
|
|
925
|
-
matches: (typeSql) => acceptedTypes.has(normalizeDuckDbTypeSql(typeSql))
|
|
926
|
-
};
|
|
927
|
-
}
|
|
928
|
-
function defineDuckDbDecimalType(precision, scale) {
|
|
929
|
-
return {
|
|
930
|
-
...defineDuckDbColumnType(`DECIMAL(${precision}, ${scale})`),
|
|
931
|
-
precision,
|
|
932
|
-
scale
|
|
933
|
-
};
|
|
934
|
-
}
|
|
935
|
-
var DUCKDB_COLUMN_TYPES = {
|
|
936
|
-
string: defineDuckDbColumnType("VARCHAR"),
|
|
937
|
-
boolean: defineDuckDbColumnType("BOOLEAN"),
|
|
938
|
-
int64: defineDuckDbColumnType("BIGINT", ["INT64"]),
|
|
939
|
-
float64: defineDuckDbColumnType("DOUBLE", ["FLOAT64"]),
|
|
940
|
-
decimal: defineDuckDbDecimalType(38, 9),
|
|
941
|
-
date: defineDuckDbColumnType("DATE"),
|
|
942
|
-
timestamp: defineDuckDbColumnType("TIMESTAMPTZ", ["TIMESTAMP WITH TIME ZONE"]),
|
|
943
|
-
json: defineDuckDbColumnType("JSON"),
|
|
944
|
-
fileRef: defineDuckDbColumnType("STRUCT(blobId VARCHAR, digest VARCHAR, sizeBytes BIGINT, fileName VARCHAR, mediaType VARCHAR, logicalPath VARCHAR)")
|
|
945
|
-
};
|
|
946
|
-
function findDatasetColumnTypeForDuckDbSql(typeSql) {
|
|
947
|
-
for (const type of Object.keys(DUCKDB_COLUMN_TYPES)) {
|
|
948
|
-
if (DUCKDB_COLUMN_TYPES[type].matches(typeSql)) {
|
|
949
|
-
return type;
|
|
950
|
-
}
|
|
951
|
-
}
|
|
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.`);
|
|
1319
|
+
};
|
|
1041
1320
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1321
|
+
async closeRuntimePromise(runtimePromise, options) {
|
|
1322
|
+
if (runtimePromise === undefined) {
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
let runtime;
|
|
1326
|
+
try {
|
|
1327
|
+
runtime = await runtimePromise;
|
|
1328
|
+
} catch {
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
await this.closeRuntime(runtime, options);
|
|
1048
1332
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1333
|
+
async closeRuntime(runtime, options) {
|
|
1334
|
+
if (options.detach) {
|
|
1335
|
+
try {
|
|
1336
|
+
await runtime.run(`DETACH ${quoteIdentifier(duckLakeAlias(this.options))}`);
|
|
1337
|
+
} catch {}
|
|
1338
|
+
}
|
|
1339
|
+
await runtime.close();
|
|
1051
1340
|
}
|
|
1052
|
-
return value;
|
|
1053
1341
|
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1342
|
+
var localCatalogGenerations = new Map;
|
|
1343
|
+
function currentLocalCatalogGeneration(options) {
|
|
1344
|
+
const key = localCatalogCoordinationKey(options);
|
|
1345
|
+
return key === undefined ? 0 : localCatalogGenerations.get(key) ?? 0;
|
|
1346
|
+
}
|
|
1347
|
+
function incrementLocalCatalogGeneration(options) {
|
|
1348
|
+
const key = localCatalogCoordinationKey(options);
|
|
1349
|
+
if (key === undefined) {
|
|
1350
|
+
return 0;
|
|
1061
1351
|
}
|
|
1062
|
-
|
|
1352
|
+
const nextGeneration = (localCatalogGenerations.get(key) ?? 0) + 1;
|
|
1353
|
+
localCatalogGenerations.set(key, nextGeneration);
|
|
1354
|
+
return nextGeneration;
|
|
1063
1355
|
}
|
|
1064
1356
|
|
|
1357
|
+
// src/internal/ducklake-dataset-catalog.ts
|
|
1358
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1359
|
+
import {
|
|
1360
|
+
assertDatasetDefinition,
|
|
1361
|
+
LakeStorageError as LakeStorageError13,
|
|
1362
|
+
planDatasetDefinitionUpdate
|
|
1363
|
+
} from "@sixb/core/lake-storage";
|
|
1364
|
+
|
|
1065
1365
|
// src/internal/ducklake-catalog-schema.ts
|
|
1366
|
+
import { LakeStorageError as LakeStorageError11 } from "@sixb/core/lake-storage";
|
|
1066
1367
|
var SIXB_PRIMARY_KEY_COMMENT_NAMESPACE = "sixb:primary-key:";
|
|
1067
1368
|
var DATASET_SEQUENCE_COLUMN_COMMENT = "sixb.sequenceBy.v1";
|
|
1068
1369
|
var SIXB_PRIMARY_KEY_COMMENT_PREFIX = `${SIXB_PRIMARY_KEY_COMMENT_NAMESPACE}v1:`;
|
|
@@ -1113,14 +1414,14 @@ function duckLakeCatalogColumnsToDatasetPrimaryKey(tableName, rows) {
|
|
|
1113
1414
|
throwInvalidPrimaryKeyMetadata(tableName, column, comment);
|
|
1114
1415
|
}
|
|
1115
1416
|
if (seenOrdinals.has(ordinal)) {
|
|
1116
|
-
throw new
|
|
1417
|
+
throw new LakeStorageError11(`[SixbDuckLake] Dataset table '${tableName}' has duplicate primary-key ordinal '${ordinal}'.`);
|
|
1117
1418
|
}
|
|
1118
1419
|
seenOrdinals.add(ordinal);
|
|
1119
1420
|
if (duckDbTypeToDatasetColumnType(column.columnType) !== "string") {
|
|
1120
|
-
throw new
|
|
1421
|
+
throw new LakeStorageError11(`[SixbDuckLake] Dataset table '${tableName}' primary-key column '${column.columnName}' must map to type 'string'.`);
|
|
1121
1422
|
}
|
|
1122
1423
|
if (column.nullsAllowed) {
|
|
1123
|
-
throw new
|
|
1424
|
+
throw new LakeStorageError11(`[SixbDuckLake] Dataset table '${tableName}' primary-key column '${column.columnName}' must not be nullable.`);
|
|
1124
1425
|
}
|
|
1125
1426
|
keyedColumns.push({ column, ordinal });
|
|
1126
1427
|
}
|
|
@@ -1130,7 +1431,7 @@ function duckLakeCatalogColumnsToDatasetPrimaryKey(tableName, rows) {
|
|
|
1130
1431
|
keyedColumns.sort((left, right) => left.ordinal - right.ordinal);
|
|
1131
1432
|
for (const [expectedOrdinal, keyedColumn] of keyedColumns.entries()) {
|
|
1132
1433
|
if (keyedColumn.ordinal !== expectedOrdinal) {
|
|
1133
|
-
throw new
|
|
1434
|
+
throw new LakeStorageError11(`[SixbDuckLake] Dataset table '${tableName}' primary-key ordinals must be contiguous from 0.`);
|
|
1134
1435
|
}
|
|
1135
1436
|
}
|
|
1136
1437
|
const columnNames = keyedColumns.map(({ column }) => column.columnName);
|
|
@@ -1140,7 +1441,7 @@ function duckLakeCatalogColumnsToDatasetPrimaryKey(tableName, rows) {
|
|
|
1140
1441
|
return columnNames;
|
|
1141
1442
|
}
|
|
1142
1443
|
function throwInvalidPrimaryKeyMetadata(tableName, column, comment) {
|
|
1143
|
-
throw new
|
|
1444
|
+
throw new LakeStorageError11(`[SixbDuckLake] Dataset table '${tableName}' has malformed primary-key metadata '${comment}' on column '${column.columnName}'.`);
|
|
1144
1445
|
}
|
|
1145
1446
|
function duckLakeCatalogColumnToDefinition(tableName, column, childrenByParentId) {
|
|
1146
1447
|
const children = [...childrenByParentId.get(column.columnId) ?? []].sort(compareDuckLakeCatalogColumnOrder);
|
|
@@ -1157,7 +1458,7 @@ function duckLakeCatalogColumnTypeToDatasetType(tableName, column, children) {
|
|
|
1157
1458
|
if (isFileRefStruct(children)) {
|
|
1158
1459
|
return "fileRef";
|
|
1159
1460
|
}
|
|
1160
|
-
throw new
|
|
1461
|
+
throw new LakeStorageError11(`[SixbDuckLake] DuckDB column type '${formatDuckLakeCatalogColumnType(column, children)}' in dataset table '${tableName}' cannot be mapped to a Sixb dataset column type.`);
|
|
1161
1462
|
}
|
|
1162
1463
|
function compareDuckLakeCatalogColumnOrder(left, right) {
|
|
1163
1464
|
return Number(left.columnOrder - right.columnOrder);
|
|
@@ -1179,84 +1480,7 @@ function formatDuckLakeCatalogColumnType(column, children) {
|
|
|
1179
1480
|
}
|
|
1180
1481
|
|
|
1181
1482
|
// 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
|
|
1483
|
+
import { LakeStorageError as LakeStorageError12 } from "@sixb/core/lake-storage";
|
|
1260
1484
|
async function readCurrentDatasetDefinitions(input) {
|
|
1261
1485
|
if (input.datasetIds?.length === 0) {
|
|
1262
1486
|
return new Map;
|
|
@@ -1383,7 +1607,7 @@ async function readCurrentDatasetPartitions(input, tableNames) {
|
|
|
1383
1607
|
transform: getString(row, "transform")
|
|
1384
1608
|
};
|
|
1385
1609
|
if (partition.transform !== "identity") {
|
|
1386
|
-
throw new
|
|
1610
|
+
throw new LakeStorageError12(`[SixbDuckLake] Dataset table '${partition.tableName}' uses unsupported DuckLake partition transform '${partition.transform}'.`);
|
|
1387
1611
|
}
|
|
1388
1612
|
return partition;
|
|
1389
1613
|
});
|
|
@@ -1485,7 +1709,7 @@ class DuckLakeDatasetCatalog {
|
|
|
1485
1709
|
if (failures.length > 0) {
|
|
1486
1710
|
const details = failures.join(`
|
|
1487
1711
|
`);
|
|
1488
|
-
throw new
|
|
1712
|
+
throw new LakeStorageError13(`[SixbLake] Lake dataset definition check failed for ${failures.length} dataset(s).
|
|
1489
1713
|
${details}`);
|
|
1490
1714
|
}
|
|
1491
1715
|
}
|
|
@@ -1521,12 +1745,12 @@ ${details}`);
|
|
|
1521
1745
|
assertDuckLakeSnapshotId(snapshotId);
|
|
1522
1746
|
const columns = await this.readDatasetColumnsAtSnapshot(runtime, tableName, tableId, snapshotId);
|
|
1523
1747
|
if (columns.length === 0) {
|
|
1524
|
-
throw new
|
|
1748
|
+
throw new LakeStorageError13(`[SixbDuckLake] Could not reconstruct schema for dataset '${datasetId}' at DuckLake snapshot '${snapshotId}'.`);
|
|
1525
1749
|
}
|
|
1526
1750
|
return duckLakeCatalogColumnsToDatasetSchema(tableName, columns);
|
|
1527
1751
|
}
|
|
1528
1752
|
assertSchema(definition) {
|
|
1529
|
-
assertDatasetDefinition(definition, (message) => new
|
|
1753
|
+
assertDatasetDefinition(definition, (message) => new LakeStorageError13(`[SixbDuckLake] ${message}`));
|
|
1530
1754
|
}
|
|
1531
1755
|
async applyExistingDatasetUpdate(plan) {
|
|
1532
1756
|
const tableName = encodeDatasetTableName(plan.definition.id);
|
|
@@ -1573,7 +1797,7 @@ ${details}`);
|
|
|
1573
1797
|
await runtime.run(`ALTER TABLE ${qualifiedName} SET PARTITIONED BY (${definition.partitionBy.map((columnName) => quoteIdentifier(columnName)).join(", ")})`);
|
|
1574
1798
|
} catch (error) {
|
|
1575
1799
|
const message = error instanceof Error ? error.message : String(error);
|
|
1576
|
-
throw new
|
|
1800
|
+
throw new LakeStorageError13(`[SixbDuckLake] Dataset '${definition.id}' cannot apply partitionBy because DuckLake rejected ALTER TABLE SET PARTITIONED BY: ${message}`);
|
|
1577
1801
|
}
|
|
1578
1802
|
}
|
|
1579
1803
|
async applyPrimaryKey(runtime, qualifiedName, definition) {
|
|
@@ -1590,7 +1814,7 @@ ${details}`);
|
|
|
1590
1814
|
sixb: {
|
|
1591
1815
|
kind: "datasetVersion",
|
|
1592
1816
|
datasetId,
|
|
1593
|
-
commitId:
|
|
1817
|
+
commitId: randomUUID2(),
|
|
1594
1818
|
mode: "schema",
|
|
1595
1819
|
schemaChange: {
|
|
1596
1820
|
addColumns: plan.columns.map((column) => column.name)
|
|
@@ -1632,12 +1856,12 @@ ${details}`);
|
|
|
1632
1856
|
}
|
|
1633
1857
|
function assertDuckLakeSnapshotId(snapshotId) {
|
|
1634
1858
|
if (!/^\d+$/.test(snapshotId)) {
|
|
1635
|
-
throw new
|
|
1859
|
+
throw new LakeStorageError13(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`);
|
|
1636
1860
|
}
|
|
1637
1861
|
}
|
|
1638
1862
|
|
|
1639
1863
|
// src/internal/ducklake-maintenance.ts
|
|
1640
|
-
import { LakeStorageError as
|
|
1864
|
+
import { LakeStorageError as LakeStorageError14 } from "@sixb/core/lake-storage";
|
|
1641
1865
|
var DEFAULT_RETENTION = "7 days";
|
|
1642
1866
|
|
|
1643
1867
|
class DuckLakeMaintenance {
|
|
@@ -1653,7 +1877,7 @@ class DuckLakeMaintenance {
|
|
|
1653
1877
|
const deleteOlderThan = options.deleteOlderThan ?? expireOlderThan;
|
|
1654
1878
|
const dryRun = options.dryRun ?? false;
|
|
1655
1879
|
if (this.options.readOnly && !dryRun) {
|
|
1656
|
-
throw new
|
|
1880
|
+
throw new LakeStorageError14("[SixbDuckLake] DuckLake maintenance cannot run in read-only mode.");
|
|
1657
1881
|
}
|
|
1658
1882
|
return this.connections.withExclusiveAttached(async (runtime) => {
|
|
1659
1883
|
const counts = await runMaintenanceProcedures(runtime, this.options, {
|
|
@@ -1690,112 +1914,7 @@ function maintenanceProcedureSql(procedure, aliasSql, olderThan, dryRun) {
|
|
|
1690
1914
|
}
|
|
1691
1915
|
|
|
1692
1916
|
// 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
|
|
1917
|
+
import { LakeStorageError as LakeStorageError15 } from "@sixb/core/lake-storage";
|
|
1799
1918
|
var READ_PAGE_ROWS = 5000;
|
|
1800
1919
|
var PHYSICAL_ROW_ID_ALIAS = "__sixb_physical_row_id";
|
|
1801
1920
|
|
|
@@ -1815,7 +1934,7 @@ class DuckLakeRowReader {
|
|
|
1815
1934
|
const runtime = lease.runtime;
|
|
1816
1935
|
const tableRef = await resolveDatasetTableRef(this.options, runtime, input.datasetId);
|
|
1817
1936
|
if (!tableRef) {
|
|
1818
|
-
throw new
|
|
1937
|
+
throw new LakeStorageError15(`[SixbDuckLake] Unknown dataset '${input.datasetId}'.`);
|
|
1819
1938
|
}
|
|
1820
1939
|
const version = await this.resolveVersion(runtime, tableRef, input.versionId);
|
|
1821
1940
|
const selectedColumns = this.resolveReadColumns(tableRef.datasetId, version.schema, input.columns);
|
|
@@ -1891,18 +2010,18 @@ class DuckLakeRowReader {
|
|
|
1891
2010
|
return columns.map((columnName) => {
|
|
1892
2011
|
const column = columnsByName.get(columnName);
|
|
1893
2012
|
if (!column) {
|
|
1894
|
-
throw new
|
|
2013
|
+
throw new LakeStorageError15(`[SixbDuckLake] Dataset '${datasetId}' does not have column '${columnName}' at the requested version.`);
|
|
1895
2014
|
}
|
|
1896
2015
|
return column;
|
|
1897
2016
|
});
|
|
1898
2017
|
}
|
|
1899
2018
|
throwNoCommittedVersion(datasetId) {
|
|
1900
|
-
throw new
|
|
2019
|
+
throw new LakeStorageError15(`[SixbDuckLake] No committed version found for dataset '${datasetId}'.`);
|
|
1901
2020
|
}
|
|
1902
2021
|
}
|
|
1903
2022
|
|
|
1904
2023
|
// src/internal/ducklake-snapshot-reader.ts
|
|
1905
|
-
import { LakeStorageError as
|
|
2024
|
+
import { LakeStorageError as LakeStorageError16 } from "@sixb/core/lake-storage";
|
|
1906
2025
|
var SNAPSHOT_ROW_BATCH_SIZE = 128;
|
|
1907
2026
|
var CATALOG_SNAPSHOT_SCAN_LIMIT = 512;
|
|
1908
2027
|
|
|
@@ -2115,7 +2234,7 @@ class DuckLakeSnapshotReader {
|
|
|
2115
2234
|
}
|
|
2116
2235
|
assertNoMetadataConflict(snapshotId, datasetId, metadata) {
|
|
2117
2236
|
if (metadata !== undefined && metadata.datasetId !== datasetId) {
|
|
2118
|
-
throw new
|
|
2237
|
+
throw new LakeStorageError16(`[SixbDuckLake] DuckLake snapshot '${snapshotId}' changed dataset '${datasetId}' but Sixb commit metadata references dataset '${metadata.datasetId}'.`);
|
|
2119
2238
|
}
|
|
2120
2239
|
}
|
|
2121
2240
|
async getLatestVersionForDefinition(runtime, dataset) {
|
|
@@ -2395,7 +2514,7 @@ class DuckLakeSnapshotReader {
|
|
|
2395
2514
|
}
|
|
2396
2515
|
function assertDuckLakeSnapshotId2(snapshotId) {
|
|
2397
2516
|
if (!/^\d+$/.test(snapshotId)) {
|
|
2398
|
-
throw new
|
|
2517
|
+
throw new LakeStorageError16(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`);
|
|
2399
2518
|
}
|
|
2400
2519
|
}
|
|
2401
2520
|
function fileChangeKey(snapshotId, tableId) {
|
|
@@ -2403,19 +2522,19 @@ function fileChangeKey(snapshotId, tableId) {
|
|
|
2403
2522
|
}
|
|
2404
2523
|
|
|
2405
2524
|
// src/internal/ducklake-sql-executor.ts
|
|
2406
|
-
import { randomUUID as
|
|
2525
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
2407
2526
|
import {
|
|
2408
|
-
LakeStorageError as
|
|
2527
|
+
LakeStorageError as LakeStorageError19
|
|
2409
2528
|
} from "@sixb/core/lake-storage";
|
|
2410
2529
|
|
|
2411
2530
|
// src/internal/dataset-row-commit.ts
|
|
2412
2531
|
import {
|
|
2413
2532
|
getDatasetPrimaryKeyColumns,
|
|
2414
|
-
LakeStorageError as
|
|
2533
|
+
LakeStorageError as LakeStorageError17
|
|
2415
2534
|
} from "@sixb/core/lake-storage";
|
|
2416
2535
|
function assertDatasetWriteMode(mode, operation) {
|
|
2417
2536
|
if (mode !== "snapshot" && mode !== "append") {
|
|
2418
|
-
throw new
|
|
2537
|
+
throw new LakeStorageError17(`[SixbDuckLake] Invalid ${operation} mode '${String(mode)}'. Expected 'snapshot' or 'append'.`);
|
|
2419
2538
|
}
|
|
2420
2539
|
}
|
|
2421
2540
|
async function applyDatasetRowsFromRelation(input) {
|
|
@@ -2475,7 +2594,7 @@ async function applyDatasetRowsFromRelation(input) {
|
|
|
2475
2594
|
async function inspectUniqueKeyedRelation(input) {
|
|
2476
2595
|
const primaryKeyColumns = getDatasetPrimaryKeyColumns(input.dataset);
|
|
2477
2596
|
if (primaryKeyColumns === null) {
|
|
2478
|
-
throw new
|
|
2597
|
+
throw new LakeStorageError17(`[SixbDuckLake] Dataset '${input.dataset.id}' must define a primaryKey before rows can be keyed.`);
|
|
2479
2598
|
}
|
|
2480
2599
|
const keysSql = primaryKeyColumns.map((column) => quoteIdentifier(column)).join(", ");
|
|
2481
2600
|
const [row] = await input.runtime.query(`
|
|
@@ -2490,7 +2609,7 @@ async function inspectUniqueKeyedRelation(input) {
|
|
|
2490
2609
|
const rowCount = row === undefined ? 0 : Number(getBigIntLike(row, "row_count"));
|
|
2491
2610
|
const maxKeyCount = row === undefined ? 0 : Number(getBigIntLike(row, "max_key_count"));
|
|
2492
2611
|
if (maxKeyCount > 1) {
|
|
2493
|
-
throw new
|
|
2612
|
+
throw new LakeStorageError17(`[SixbDuckLake] Dataset '${input.dataset.id}' ${input.context} contains duplicate primary key.`);
|
|
2494
2613
|
}
|
|
2495
2614
|
return rowCount;
|
|
2496
2615
|
}
|
|
@@ -2507,7 +2626,7 @@ async function assertNoPrimaryKeyOverlap(input) {
|
|
|
2507
2626
|
LIMIT 1
|
|
2508
2627
|
`);
|
|
2509
2628
|
if (collision !== undefined) {
|
|
2510
|
-
throw new
|
|
2629
|
+
throw new LakeStorageError17(`[SixbDuckLake] Dataset '${input.dataset.id}' ${input.context} contains duplicate primary key.`);
|
|
2511
2630
|
}
|
|
2512
2631
|
}
|
|
2513
2632
|
async function fingerprintRelation(runtime, relationSql, columnsSql) {
|
|
@@ -2539,9 +2658,9 @@ async function countRows(runtime, relationSql) {
|
|
|
2539
2658
|
}
|
|
2540
2659
|
|
|
2541
2660
|
// src/internal/sql-transform-relations.ts
|
|
2542
|
-
import { randomUUID as
|
|
2661
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
2543
2662
|
import {
|
|
2544
|
-
LakeStorageError as
|
|
2663
|
+
LakeStorageError as LakeStorageError18
|
|
2545
2664
|
} from "@sixb/core/lake-storage";
|
|
2546
2665
|
var RELATION_PLACEHOLDER_PREFIX = "__sixb_sql_transform_relation_";
|
|
2547
2666
|
var RELATION_PLACEHOLDER_PATTERN = /__sixb_sql_transform_relation_[0-9a-f]{32}_[0-9]+__/g;
|
|
@@ -2559,12 +2678,12 @@ function renderDuckLakeSqlTransformSql(input) {
|
|
|
2559
2678
|
const context = createDuckLakeSqlTransformRelationContext(input);
|
|
2560
2679
|
const sql = input.sql(context.relations);
|
|
2561
2680
|
if (typeof sql !== "string") {
|
|
2562
|
-
throw new
|
|
2681
|
+
throw new LakeStorageError18("[SixbDuckLake] SQL transform body must return a SQL string.");
|
|
2563
2682
|
}
|
|
2564
2683
|
return context.render(sql);
|
|
2565
2684
|
}
|
|
2566
2685
|
function createDuckLakeSqlTransformRelationContext(input) {
|
|
2567
|
-
const token =
|
|
2686
|
+
const token = randomUUID3().replaceAll("-", "");
|
|
2568
2687
|
const relations = Object.create(null);
|
|
2569
2688
|
const replacements = new Map;
|
|
2570
2689
|
let index = 0;
|
|
@@ -2597,7 +2716,7 @@ function renderDuckLakeSourceRelation(options, source) {
|
|
|
2597
2716
|
return `(SELECT * FROM ${qualifiedTableName(options, tableName)} AT (VERSION => ${snapshotId}))`;
|
|
2598
2717
|
}
|
|
2599
2718
|
function throwUnresolvedPlaceholder() {
|
|
2600
|
-
throw new
|
|
2719
|
+
throw new LakeStorageError18("[SixbDuckLake] SQL transform contains an unresolved relation placeholder.");
|
|
2601
2720
|
}
|
|
2602
2721
|
|
|
2603
2722
|
// src/internal/ducklake-sql-executor.ts
|
|
@@ -2657,8 +2776,8 @@ class DuckLakeSqlExecutor {
|
|
|
2657
2776
|
commitMessage: input.commitMessage ?? `execute SQL transform for dataset ${target.id}`,
|
|
2658
2777
|
producer: input.producer,
|
|
2659
2778
|
inputs: sources.inputs,
|
|
2660
|
-
applyChanges: (
|
|
2661
|
-
runtime
|
|
2779
|
+
applyChanges: (runtime, context) => this.applySqlTransform({
|
|
2780
|
+
runtime,
|
|
2662
2781
|
target,
|
|
2663
2782
|
mode: input.mode,
|
|
2664
2783
|
sql,
|
|
@@ -2691,14 +2810,14 @@ class DuckLakeSqlExecutor {
|
|
|
2691
2810
|
async resolveSourceDataset(runtime, dataset) {
|
|
2692
2811
|
const definition = await this.datasets.getDatasetOnRuntime(runtime, dataset.id);
|
|
2693
2812
|
if (!definition) {
|
|
2694
|
-
throw new
|
|
2813
|
+
throw new LakeStorageError19(`[SixbDuckLake] Unknown SQL transform source dataset '${dataset.id}'.`);
|
|
2695
2814
|
}
|
|
2696
2815
|
return definition;
|
|
2697
2816
|
}
|
|
2698
2817
|
async resolveTargetDataset(runtime, dataset) {
|
|
2699
2818
|
const definition = await this.datasets.getDatasetOnRuntime(runtime, dataset.id);
|
|
2700
2819
|
if (!definition) {
|
|
2701
|
-
throw new
|
|
2820
|
+
throw new LakeStorageError19(`[SixbDuckLake] Unknown SQL transform target dataset '${dataset.id}'.`);
|
|
2702
2821
|
}
|
|
2703
2822
|
this.datasets.assertSchema(definition);
|
|
2704
2823
|
return definition;
|
|
@@ -2748,7 +2867,7 @@ class DuckLakeSqlExecutor {
|
|
|
2748
2867
|
return checkedCasts;
|
|
2749
2868
|
}
|
|
2750
2869
|
async applySqlTransform(input) {
|
|
2751
|
-
const tempTableName = `sixb_sql_transform_${
|
|
2870
|
+
const tempTableName = `sixb_sql_transform_${randomUUID4().replaceAll("-", "")}`;
|
|
2752
2871
|
const tempTable = quoteIdentifier(tempTableName);
|
|
2753
2872
|
await input.runtime.run(`CREATE TEMP TABLE ${tempTable} AS SELECT * FROM (${input.sql}) AS sixb_sql_transform_result`);
|
|
2754
2873
|
let result;
|
|
@@ -2819,33 +2938,33 @@ function normalizePreviewValue(value) {
|
|
|
2819
2938
|
return value;
|
|
2820
2939
|
}
|
|
2821
2940
|
function throwNoCommittedSourceVersion(datasetId) {
|
|
2822
|
-
throw new
|
|
2941
|
+
throw new LakeStorageError19(`[SixbDuckLake] No committed version found for SQL transform source dataset '${datasetId}'.`);
|
|
2823
2942
|
}
|
|
2824
2943
|
function throwResultSchemaMismatch(datasetId, detail) {
|
|
2825
|
-
throw new
|
|
2944
|
+
throw new LakeStorageError19(`[SixbDuckLake] SQL transform result schema does not match target dataset '${datasetId}': ${detail}.`);
|
|
2826
2945
|
}
|
|
2827
2946
|
|
|
2828
2947
|
// src/internal/ducklake-write-coordinator.ts
|
|
2829
|
-
import { randomUUID as
|
|
2948
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
2830
2949
|
import {
|
|
2831
2950
|
assertUnsequencedDatasetWrite,
|
|
2832
2951
|
getDatasetPrimaryKeyColumns as getDatasetPrimaryKeyColumns4,
|
|
2833
2952
|
hasDatasetInputChanges,
|
|
2834
2953
|
LakeConcurrencyError,
|
|
2835
|
-
LakeStorageError as
|
|
2954
|
+
LakeStorageError as LakeStorageError23,
|
|
2836
2955
|
retryDatasetMergeCommit
|
|
2837
2956
|
} from "@sixb/core/lake-storage";
|
|
2838
2957
|
|
|
2839
2958
|
// src/internal/dataset-row-merge.ts
|
|
2840
2959
|
import {
|
|
2841
2960
|
getDatasetPrimaryKeyColumns as getDatasetPrimaryKeyColumns2,
|
|
2842
|
-
LakeStorageError as
|
|
2961
|
+
LakeStorageError as LakeStorageError20,
|
|
2843
2962
|
reconcileDatasetSequences
|
|
2844
2963
|
} from "@sixb/core/lake-storage";
|
|
2845
2964
|
async function applyDatasetMergeFromRelation(input) {
|
|
2846
2965
|
const primaryKeyColumns = getDatasetPrimaryKeyColumns2(input.dataset);
|
|
2847
2966
|
if (primaryKeyColumns === null) {
|
|
2848
|
-
throw new
|
|
2967
|
+
throw new LakeStorageError20(`[SixbDuckLake] Dataset '${input.dataset.id}' must define a primaryKey before it can be merged.`);
|
|
2849
2968
|
}
|
|
2850
2969
|
const stagingTable = quoteIdentifier(input.stagingTableName);
|
|
2851
2970
|
const targetTable = qualifiedTableName(input.options, encodeDatasetTableName(input.dataset.id));
|
|
@@ -3006,7 +3125,7 @@ function finalChangesSelectSql(input, primaryKeyColumns) {
|
|
|
3006
3125
|
async function countMergeEffects(input, targetTable, finalChanges, primaryKeyColumns) {
|
|
3007
3126
|
const matchColumn = primaryKeyColumns[0];
|
|
3008
3127
|
if (matchColumn === undefined) {
|
|
3009
|
-
throw new
|
|
3128
|
+
throw new LakeStorageError20(`[SixbDuckLake] Dataset '${input.dataset.id}' primaryKey must contain at least one column.`);
|
|
3010
3129
|
}
|
|
3011
3130
|
const keyMatchSql = primaryKeyMatchSql(primaryKeyColumns, "target", "source");
|
|
3012
3131
|
const targetMatch = `target.${quoteIdentifier(matchColumn)} IS NOT NULL`;
|
|
@@ -3055,12 +3174,12 @@ function sameColumns(left, right) {
|
|
|
3055
3174
|
}
|
|
3056
3175
|
|
|
3057
3176
|
// src/internal/ducklake-merge-session.ts
|
|
3058
|
-
import { randomUUID as
|
|
3177
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
3059
3178
|
import {
|
|
3060
3179
|
cloneDatasetMergeChange,
|
|
3061
3180
|
datasetSequenceChange,
|
|
3062
3181
|
getDatasetMergeChangeValidationError,
|
|
3063
|
-
LakeStorageError as
|
|
3182
|
+
LakeStorageError as LakeStorageError21
|
|
3064
3183
|
} from "@sixb/core/lake-storage";
|
|
3065
3184
|
|
|
3066
3185
|
// src/internal/row-appender.ts
|
|
@@ -3119,7 +3238,7 @@ function fileRefToDuckDbStruct(fileRef) {
|
|
|
3119
3238
|
// src/internal/ducklake-merge-session.ts
|
|
3120
3239
|
var MERGE_CHANGE_BATCH_SIZE = 1000;
|
|
3121
3240
|
async function createDuckLakeMergeSession(input) {
|
|
3122
|
-
const suffix =
|
|
3241
|
+
const suffix = randomUUID5().replaceAll("-", "");
|
|
3123
3242
|
const stagingTableName = `sixb_merge_${suffix}`;
|
|
3124
3243
|
const sequenceColumnName = `sixb_merge_sequence_${suffix}`;
|
|
3125
3244
|
const kindColumnName = `sixb_merge_kind_${suffix}`;
|
|
@@ -3161,7 +3280,7 @@ class DuckLakeMergeSession {
|
|
|
3161
3280
|
for await (const change of changes) {
|
|
3162
3281
|
const validationError = getDatasetMergeChangeValidationError(change, this.input.dataset);
|
|
3163
3282
|
if (validationError) {
|
|
3164
|
-
throw new
|
|
3283
|
+
throw new LakeStorageError21(`[SixbDuckLake] ${validationError}`);
|
|
3165
3284
|
}
|
|
3166
3285
|
batch.push({
|
|
3167
3286
|
sequence: this.nextSequence,
|
|
@@ -3224,7 +3343,7 @@ class DuckLakeMergeSession {
|
|
|
3224
3343
|
}
|
|
3225
3344
|
assertOpen() {
|
|
3226
3345
|
if (this.closed) {
|
|
3227
|
-
throw new
|
|
3346
|
+
throw new LakeStorageError21("[SixbDuckLake] Merge session is already closed.");
|
|
3228
3347
|
}
|
|
3229
3348
|
}
|
|
3230
3349
|
async cleanup() {
|
|
@@ -3239,16 +3358,16 @@ class DuckLakeMergeSession {
|
|
|
3239
3358
|
}
|
|
3240
3359
|
|
|
3241
3360
|
// src/internal/ducklake-write-session.ts
|
|
3242
|
-
import { randomUUID as
|
|
3361
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
3243
3362
|
import { getDatasetRowValidationError } from "@sixb/core";
|
|
3244
3363
|
import {
|
|
3245
3364
|
encodeDatasetPrimaryKey,
|
|
3246
3365
|
getDatasetPrimaryKeyColumns as getDatasetPrimaryKeyColumns3,
|
|
3247
|
-
LakeStorageError as
|
|
3366
|
+
LakeStorageError as LakeStorageError22
|
|
3248
3367
|
} from "@sixb/core/lake-storage";
|
|
3249
3368
|
var WRITE_ROW_BATCH_SIZE = 1000;
|
|
3250
3369
|
async function createDuckLakeWriteSession(input) {
|
|
3251
|
-
const stagingTableName = `sixb_write_${
|
|
3370
|
+
const stagingTableName = `sixb_write_${randomUUID6().replaceAll("-", "")}`;
|
|
3252
3371
|
await input.runtime.run(`CREATE TEMP TABLE ${quoteIdentifier(stagingTableName)} (${datasetSchemaToDuckDbColumnsSql(input.write.dataset.schema)})`);
|
|
3253
3372
|
return new DuckLakeWriteSession(input.commitWrite, input.runtime, input.write, stagingTableName);
|
|
3254
3373
|
}
|
|
@@ -3275,13 +3394,13 @@ class DuckLakeWriteSession {
|
|
|
3275
3394
|
for await (const row of rows) {
|
|
3276
3395
|
const validationError = getDatasetRowValidationError(row, this.input.dataset);
|
|
3277
3396
|
if (validationError) {
|
|
3278
|
-
throw new
|
|
3397
|
+
throw new LakeStorageError22(`[SixbDuckLake] ${validationError}`);
|
|
3279
3398
|
}
|
|
3280
3399
|
const clonedRow = structuredClone(row);
|
|
3281
3400
|
if (getDatasetPrimaryKeyColumns3(this.input.dataset) !== null) {
|
|
3282
3401
|
const primaryKey = encodeDatasetPrimaryKey(this.input.dataset, clonedRow);
|
|
3283
3402
|
if (this.stagedPrimaryKeys.has(primaryKey) || batchPrimaryKeys.has(primaryKey)) {
|
|
3284
|
-
throw new
|
|
3403
|
+
throw new LakeStorageError22(`[SixbDuckLake] Dataset '${this.input.dataset.id}' ${this.input.mode ?? "snapshot"} source contains duplicate primary key.`);
|
|
3285
3404
|
}
|
|
3286
3405
|
batchPrimaryKeys.add(primaryKey);
|
|
3287
3406
|
}
|
|
@@ -3338,7 +3457,7 @@ class DuckLakeWriteSession {
|
|
|
3338
3457
|
}
|
|
3339
3458
|
assertOpen() {
|
|
3340
3459
|
if (this.closed) {
|
|
3341
|
-
throw new
|
|
3460
|
+
throw new LakeStorageError22("[SixbDuckLake] Write session is already closed.");
|
|
3342
3461
|
}
|
|
3343
3462
|
}
|
|
3344
3463
|
async cleanup() {
|
|
@@ -3371,7 +3490,7 @@ class DuckLakeWriteCoordinator {
|
|
|
3371
3490
|
this.connections.assertOpen();
|
|
3372
3491
|
const definition = await this.datasets.getDataset(input.dataset.id);
|
|
3373
3492
|
if (!definition) {
|
|
3374
|
-
throw new
|
|
3493
|
+
throw new LakeStorageError23(`[SixbDuckLake] Unknown dataset '${input.dataset.id}'.`);
|
|
3375
3494
|
}
|
|
3376
3495
|
this.datasets.assertSchema(definition);
|
|
3377
3496
|
assertUnsequencedDatasetWrite(definition);
|
|
@@ -3392,13 +3511,13 @@ class DuckLakeWriteCoordinator {
|
|
|
3392
3511
|
this.connections.assertOpen();
|
|
3393
3512
|
const definition = await this.datasets.getDataset(input.dataset.id);
|
|
3394
3513
|
if (!definition) {
|
|
3395
|
-
throw new
|
|
3514
|
+
throw new LakeStorageError23(`[SixbDuckLake] Unknown dataset '${input.dataset.id}'.`);
|
|
3396
3515
|
}
|
|
3397
3516
|
if (getDatasetPrimaryKeyColumns4(definition) === null) {
|
|
3398
|
-
throw new
|
|
3517
|
+
throw new LakeStorageError23(`[SixbDuckLake] Dataset '${definition.id}' must define a primaryKey before it can be merged.`);
|
|
3399
3518
|
}
|
|
3400
3519
|
this.datasets.assertSchema(definition);
|
|
3401
|
-
const latestVersion = await this.connections.withAttachedRuntime((
|
|
3520
|
+
const latestVersion = await this.connections.withAttachedRuntime((runtime) => this.snapshots.getLatestVersionForDefinition(runtime, definition));
|
|
3402
3521
|
if (input.expectedLatestVersionId !== undefined && latestVersion?.versionId !== input.expectedLatestVersionId) {
|
|
3403
3522
|
throw new LakeConcurrencyError(`[SixbDuckLake] Optimistic merge start failed for dataset '${definition.id}': expected latest version '${input.expectedLatestVersionId}', found '${latestVersion?.versionId ?? "none"}'.`);
|
|
3404
3523
|
}
|
|
@@ -3413,7 +3532,7 @@ class DuckLakeWriteCoordinator {
|
|
|
3413
3532
|
async commitWrite(input) {
|
|
3414
3533
|
const definition = await this.datasets.getDataset(input.write.dataset.id);
|
|
3415
3534
|
if (!definition) {
|
|
3416
|
-
throw new
|
|
3535
|
+
throw new LakeStorageError23(`[SixbDuckLake] Unknown dataset '${input.write.dataset.id}'.`);
|
|
3417
3536
|
}
|
|
3418
3537
|
const mode = input.write.mode ?? "snapshot";
|
|
3419
3538
|
assertDatasetWriteMode(mode, "write");
|
|
@@ -3434,10 +3553,10 @@ class DuckLakeWriteCoordinator {
|
|
|
3434
3553
|
async commitMerge(input) {
|
|
3435
3554
|
const definition = await this.datasets.getDataset(input.merge.dataset.id);
|
|
3436
3555
|
if (!definition) {
|
|
3437
|
-
throw new
|
|
3556
|
+
throw new LakeStorageError23(`[SixbDuckLake] Unknown dataset '${input.merge.dataset.id}'.`);
|
|
3438
3557
|
}
|
|
3439
3558
|
if (getDatasetPrimaryKeyColumns4(definition) === null) {
|
|
3440
|
-
throw new
|
|
3559
|
+
throw new LakeStorageError23(`[SixbDuckLake] Dataset '${definition.id}' must define a primaryKey before it can be merged.`);
|
|
3441
3560
|
}
|
|
3442
3561
|
return retryDatasetMergeCommit(input.merge, input.commit, (rebase) => this.withCommitRuntime(async (runtime) => {
|
|
3443
3562
|
input.commit?.signal?.throwIfAborted();
|
|
@@ -3465,7 +3584,7 @@ class DuckLakeWriteCoordinator {
|
|
|
3465
3584
|
validatedPrimaryKeyColumns: context.validatedPrimaryKeyColumns
|
|
3466
3585
|
});
|
|
3467
3586
|
if (result.sourceRowCount !== input.changesWritten) {
|
|
3468
|
-
throw new
|
|
3587
|
+
throw new LakeStorageError23(`[SixbDuckLake] Staged merge for dataset '${definition.id}' accepted ${input.changesWritten} change(s), but DuckLake saw ${result.sourceRowCount} source change(s) at commit time.`);
|
|
3469
3588
|
}
|
|
3470
3589
|
return result;
|
|
3471
3590
|
}
|
|
@@ -3487,7 +3606,7 @@ class DuckLakeWriteCoordinator {
|
|
|
3487
3606
|
disableRetries: input.mode === "append" && getDatasetPrimaryKeyColumns4(input.dataset) !== null
|
|
3488
3607
|
});
|
|
3489
3608
|
if (result.version === null) {
|
|
3490
|
-
throw new
|
|
3609
|
+
throw new LakeStorageError23(`[SixbDuckLake] No DuckLake changes were committed for dataset '${input.dataset.id}', and no previous version exists.`);
|
|
3491
3610
|
}
|
|
3492
3611
|
return { ...result.version, outcome: result.outcome };
|
|
3493
3612
|
}
|
|
@@ -3536,7 +3655,7 @@ class DuckLakeWriteCoordinator {
|
|
|
3536
3655
|
});
|
|
3537
3656
|
input.signal?.throwIfAborted();
|
|
3538
3657
|
await this.ensureNoOpHasSnapshot(input, changeResult, latestVersion);
|
|
3539
|
-
const commitId =
|
|
3658
|
+
const commitId = randomUUID7();
|
|
3540
3659
|
await this.setCommitMetadata(input, commitId, commitRowCount(input, changeResult, latestVersion));
|
|
3541
3660
|
await input.runtime.run("COMMIT");
|
|
3542
3661
|
committed = true;
|
|
@@ -3569,7 +3688,7 @@ class DuckLakeWriteCoordinator {
|
|
|
3569
3688
|
if (version) {
|
|
3570
3689
|
return { outcome: "created", version };
|
|
3571
3690
|
}
|
|
3572
|
-
throw new
|
|
3691
|
+
throw new LakeStorageError23(`[SixbDuckLake] DuckLake committed snapshot '${ownSnapshotId}' for dataset '${input.dataset.id}', but Sixb could not hydrate it as a dataset version.`);
|
|
3573
3692
|
}
|
|
3574
3693
|
if (!input.changeResult.dataChangeExpected) {
|
|
3575
3694
|
const version = await this.versionForNoOpCommit(input.runtime, input.dataset, input.allowInitialNoOp ?? false);
|
|
@@ -3587,9 +3706,9 @@ class DuckLakeWriteCoordinator {
|
|
|
3587
3706
|
};
|
|
3588
3707
|
}
|
|
3589
3708
|
if (input.committedWriteSnapshotId === input.previousWriteSnapshotId) {
|
|
3590
|
-
throw new
|
|
3709
|
+
throw new LakeStorageError23(`[SixbDuckLake] DuckLake commit for dataset '${input.dataset.id}' completed without producing a snapshot for this non-empty write.`);
|
|
3591
3710
|
}
|
|
3592
|
-
throw new
|
|
3711
|
+
throw new LakeStorageError23(`[SixbDuckLake] DuckLake commit for dataset '${input.dataset.id}' completed, but Sixb could not find a matching snapshot for this write.`);
|
|
3593
3712
|
}
|
|
3594
3713
|
async applyStagedRows(input) {
|
|
3595
3714
|
const mode = input.write.mode ?? "snapshot";
|
|
@@ -3602,7 +3721,7 @@ class DuckLakeWriteCoordinator {
|
|
|
3602
3721
|
previousRowCount: input.previousRowCount
|
|
3603
3722
|
});
|
|
3604
3723
|
if (result.sourceRowCount !== input.rowsWritten) {
|
|
3605
|
-
throw new
|
|
3724
|
+
throw new LakeStorageError23(`[SixbDuckLake] Staged write for dataset '${input.write.dataset.id}' accepted ${input.rowsWritten} row(s), but DuckLake saw ${result.sourceRowCount} source row(s) at commit time.`);
|
|
3606
3725
|
}
|
|
3607
3726
|
return result;
|
|
3608
3727
|
}
|
|
@@ -3640,7 +3759,7 @@ class DuckLakeWriteCoordinator {
|
|
|
3640
3759
|
if (allowInitialNoOp) {
|
|
3641
3760
|
return null;
|
|
3642
3761
|
}
|
|
3643
|
-
throw new
|
|
3762
|
+
throw new LakeStorageError23(`[SixbDuckLake] No DuckLake changes were committed for dataset '${definition.id}', and no previous version exists.`);
|
|
3644
3763
|
}
|
|
3645
3764
|
async rollbackTransaction(runtime) {
|
|
3646
3765
|
try {
|
|
@@ -3714,7 +3833,7 @@ function commitRowCount(input, changeResult, latestVersion) {
|
|
|
3714
3833
|
}
|
|
3715
3834
|
function assertDuckLakeSnapshotId3(snapshotId) {
|
|
3716
3835
|
if (!/^\d+$/.test(snapshotId)) {
|
|
3717
|
-
throw new
|
|
3836
|
+
throw new LakeStorageError23(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`);
|
|
3718
3837
|
}
|
|
3719
3838
|
}
|
|
3720
3839
|
var localCatalogCommitLocks = new Map;
|
|
@@ -3726,8 +3845,8 @@ async function withCatalogCommitLock(options, run) {
|
|
|
3726
3845
|
const previous = localCatalogCommitLocks.get(key) ?? Promise.resolve();
|
|
3727
3846
|
const ready = previous.catch(() => {});
|
|
3728
3847
|
let release;
|
|
3729
|
-
const next = new Promise((
|
|
3730
|
-
release =
|
|
3848
|
+
const next = new Promise((resolve) => {
|
|
3849
|
+
release = resolve;
|
|
3731
3850
|
});
|
|
3732
3851
|
const current = ready.then(() => next);
|
|
3733
3852
|
localCatalogCommitLocks.set(key, current);
|
|
@@ -3750,6 +3869,7 @@ class DuckLakeStorage {
|
|
|
3750
3869
|
datasets;
|
|
3751
3870
|
maintenance;
|
|
3752
3871
|
rows;
|
|
3872
|
+
changes;
|
|
3753
3873
|
snapshotReader;
|
|
3754
3874
|
writes;
|
|
3755
3875
|
constructor(options) {
|
|
@@ -3768,6 +3888,7 @@ class DuckLakeStorage {
|
|
|
3768
3888
|
this.maintenance = new DuckLakeMaintenance(normalizedOptions, this.connections);
|
|
3769
3889
|
this.snapshotReader = new DuckLakeSnapshotReader(normalizedOptions, this.connections, this.datasets);
|
|
3770
3890
|
this.rows = new DuckLakeRowReader(normalizedOptions, this.connections, this.snapshotReader);
|
|
3891
|
+
this.changes = new DuckLakeChangeReader(normalizedOptions, this.connections, this.snapshotReader);
|
|
3771
3892
|
this.writes = new DuckLakeWriteCoordinator(normalizedOptions, this.connections, this.datasets, this.snapshotReader);
|
|
3772
3893
|
this.sql = new DuckLakeSqlExecutor(normalizedOptions, this.connections, this.datasets, this.snapshotReader, this.writes);
|
|
3773
3894
|
}
|
|
@@ -3804,6 +3925,9 @@ class DuckLakeStorage {
|
|
|
3804
3925
|
readRows(input) {
|
|
3805
3926
|
return this.rows.readRows(input);
|
|
3806
3927
|
}
|
|
3928
|
+
readChanges(input) {
|
|
3929
|
+
return this.changes.readChanges(input);
|
|
3930
|
+
}
|
|
3807
3931
|
async runMaintenance(options) {
|
|
3808
3932
|
return this.maintenance.runMaintenance(options);
|
|
3809
3933
|
}
|
|
@@ -3815,4 +3939,4 @@ export {
|
|
|
3815
3939
|
DuckLakeStorage
|
|
3816
3940
|
};
|
|
3817
3941
|
|
|
3818
|
-
//# debugId=
|
|
3942
|
+
//# debugId=555594FFA13F16F264756E2164756E21
|