@sixb/ducklake 0.1.1 → 0.1.3
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 +15 -4
- package/dist/index.js +77 -13
- package/dist/index.js.map +7 -7
- package/dist/internal/ducklake-row-reader.d.ts.map +1 -1
- package/dist/internal/ducklake-sql-executor.d.ts.map +1 -1
- package/dist/internal/ducklake-write-coordinator.d.ts +1 -0
- package/dist/internal/ducklake-write-coordinator.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/internal/ducklake-merge-session.ts +1 -0
- package/src/internal/ducklake-row-reader.ts +51 -27
- package/src/internal/ducklake-sql-executor.ts +54 -11
- package/src/internal/ducklake-write-coordinator.ts +35 -1
- package/src/internal/ducklake-write-session.ts +4 -0
package/README.md
CHANGED
|
@@ -94,15 +94,22 @@ const report = await lakeStorage.runMaintenance({
|
|
|
94
94
|
Reads, writes, SQL previews, and SQL transforms use the same DuckDB runtime and
|
|
95
95
|
the same DuckLake PostgreSQL metadata pool. Work is serialized inside one
|
|
96
96
|
`DuckLakeStorage` instance, so a queued DuckDB operation makes later operations
|
|
97
|
-
wait. Only actual DuckDB work holds the runtime:
|
|
98
|
-
transforms, and metadata reads.
|
|
99
|
-
|
|
97
|
+
wait. Only actual DuckDB work holds the runtime: bounded read pages, batch
|
|
98
|
+
appends, commits, SQL transforms, and metadata reads.
|
|
99
|
+
|
|
100
|
+
Large `readRows(...)` calls materialize bounded pages, using a physical-row keyset when available,
|
|
101
|
+
and release the DuckDB queue between pages. This also applies when an explicit limit exceeds one
|
|
102
|
+
page. A JavaScript pipeline can therefore stream one DuckLake dataset directly into another on the
|
|
103
|
+
same provider without buffering the complete input or deadlocking at the destination appender's
|
|
104
|
+
first flush. The read remains pinned to one immutable snapshot and keeps its attachment lease until
|
|
105
|
+
the iterator completes or closes.
|
|
100
106
|
|
|
101
107
|
A write does not hold the runtime while its source iterable is producing rows.
|
|
102
108
|
`writeRows(...)` validates and stages rows in bounded in-memory batches, then
|
|
103
109
|
takes a queue slot only to flush each batch into the staging table. Slow external
|
|
104
110
|
reads (pagination, APIs, SFTP, retries) run outside the queue, so other reads and
|
|
105
|
-
write batches can interleave between a write's batches.
|
|
111
|
+
write batches can interleave between a write's batches. Large in-memory writes
|
|
112
|
+
also yield to the event loop between batches.
|
|
106
113
|
|
|
107
114
|
```txt
|
|
108
115
|
same provider instance:
|
|
@@ -263,6 +270,10 @@ new rows to the existing target. Omitting a source `versionId` resolves the late
|
|
|
263
270
|
committed version at execution time; providing a `versionId` pins that source for
|
|
264
271
|
repeatable transforms.
|
|
265
272
|
|
|
273
|
+
DuckDB promotes `sum(BIGINT)` results to `HUGEINT`. When the matching target column is a Sixb
|
|
274
|
+
`int64`, the executor applies a checked conversion back to `BIGINT` before comparing or writing
|
|
275
|
+
rows. Values outside the signed 64-bit range fail the transform instead of being truncated.
|
|
276
|
+
|
|
266
277
|
## Definition Updates
|
|
267
278
|
|
|
268
279
|
DuckLake supports schema evolution, and this provider exposes a small safe
|
package/dist/index.js
CHANGED
|
@@ -1784,6 +1784,9 @@ function isValidatedPrimaryKeyColumns(value) {
|
|
|
1784
1784
|
}
|
|
1785
1785
|
|
|
1786
1786
|
// src/internal/ducklake-row-reader.ts
|
|
1787
|
+
var READ_PAGE_ROWS = 5000;
|
|
1788
|
+
var PHYSICAL_ROW_ID_ALIAS = "__sixb_physical_row_id";
|
|
1789
|
+
|
|
1787
1790
|
class DuckLakeRowReader {
|
|
1788
1791
|
options;
|
|
1789
1792
|
connections;
|
|
@@ -1803,16 +1806,43 @@ class DuckLakeRowReader {
|
|
|
1803
1806
|
throw new LakeStorageError14(`[SixbDuckLake] Unknown dataset '${input.datasetId}'.`);
|
|
1804
1807
|
}
|
|
1805
1808
|
const version = await this.resolveVersion(runtime, tableRef, input.versionId);
|
|
1806
|
-
const snapshotId = parseVersionId(version.versionId);
|
|
1807
1809
|
const selectedColumns = this.resolveReadColumns(tableRef.datasetId, version.schema, input.columns);
|
|
1810
|
+
const snapshotId = parseVersionId(version.versionId);
|
|
1808
1811
|
const columnsSql = selectedColumns.map((column) => quoteIdentifier(column.name)).join(", ");
|
|
1809
1812
|
const tableSql = qualifiedTableName(this.options, tableRef.tableName);
|
|
1810
|
-
const
|
|
1811
|
-
const
|
|
1812
|
-
const
|
|
1813
|
-
const
|
|
1814
|
-
const
|
|
1815
|
-
|
|
1813
|
+
const schemaNames = new Set(version.schema.columns.map((column) => column.name.toLowerCase()));
|
|
1814
|
+
const canUsePhysicalRowId = !schemaNames.has("rowid") && !schemaNames.has(PHYSICAL_ROW_ID_ALIAS.toLowerCase());
|
|
1815
|
+
const orderSql = canUsePhysicalRowId ? " ORDER BY rowid" : "";
|
|
1816
|
+
const baseSql = `SELECT ${columnsSql} FROM ${tableSql} AT (VERSION => ${snapshotId})`;
|
|
1817
|
+
const requestedOffset = Math.max(0, Math.trunc(input.offset ?? 0));
|
|
1818
|
+
let remaining = input.limit === undefined ? undefined : Math.max(0, Math.trunc(input.limit));
|
|
1819
|
+
if (remaining === 0)
|
|
1820
|
+
return;
|
|
1821
|
+
let offset = requestedOffset;
|
|
1822
|
+
let physicalCursor = null;
|
|
1823
|
+
while (true) {
|
|
1824
|
+
const pageRows = remaining === undefined ? READ_PAGE_ROWS : Math.min(READ_PAGE_ROWS, remaining);
|
|
1825
|
+
const pageSql = canUsePhysicalRowId ? `SELECT rowid AS ${quoteIdentifier(PHYSICAL_ROW_ID_ALIAS)}, ${columnsSql}
|
|
1826
|
+
FROM ${tableSql} AT (VERSION => ${snapshotId})
|
|
1827
|
+
${physicalCursor === null ? "" : `WHERE rowid > ${physicalCursor}`}
|
|
1828
|
+
ORDER BY rowid LIMIT ${pageRows}
|
|
1829
|
+
${physicalCursor === null && offset > 0 ? `OFFSET ${offset}` : ""}` : `${baseSql}${orderSql} LIMIT ${pageRows} OFFSET ${offset}`;
|
|
1830
|
+
const rows = await runtime.query(pageSql);
|
|
1831
|
+
if (rows.length === 0)
|
|
1832
|
+
return;
|
|
1833
|
+
yield* this.normalizeRows(rows, selectedColumns);
|
|
1834
|
+
offset += rows.length;
|
|
1835
|
+
if (remaining !== undefined) {
|
|
1836
|
+
remaining -= rows.length;
|
|
1837
|
+
if (remaining === 0)
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
if (canUsePhysicalRowId) {
|
|
1841
|
+
physicalCursor = getBigIntLike(rows[rows.length - 1], PHYSICAL_ROW_ID_ALIAS);
|
|
1842
|
+
}
|
|
1843
|
+
if (rows.length < pageRows)
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1816
1846
|
} finally {
|
|
1817
1847
|
await lease.release();
|
|
1818
1848
|
}
|
|
@@ -2606,7 +2636,7 @@ class DuckLakeSqlExecutor {
|
|
|
2606
2636
|
sources: sources.relations,
|
|
2607
2637
|
sql: input.sql
|
|
2608
2638
|
});
|
|
2609
|
-
await this.assertResultSchemaMatchesTarget(runtime, target, sql);
|
|
2639
|
+
const checkedCasts = await this.assertResultSchemaMatchesTarget(runtime, target, sql);
|
|
2610
2640
|
return this.writes.commitDatasetVersionOnExclusiveRuntime(runtime, {
|
|
2611
2641
|
dataset: target,
|
|
2612
2642
|
mode: input.mode,
|
|
@@ -2619,6 +2649,7 @@ class DuckLakeSqlExecutor {
|
|
|
2619
2649
|
target,
|
|
2620
2650
|
mode: input.mode,
|
|
2621
2651
|
sql,
|
|
2652
|
+
checkedCasts,
|
|
2622
2653
|
previousRowCount: context.previousRowCount
|
|
2623
2654
|
})
|
|
2624
2655
|
});
|
|
@@ -2678,6 +2709,7 @@ class DuckLakeSqlExecutor {
|
|
|
2678
2709
|
const rows = await runtime.query(`DESCRIBE SELECT * FROM (${sql}) AS sixb_sql_transform_result_schema`);
|
|
2679
2710
|
const actualColumns = rows.map((row) => resultColumnFromDescribeRow(row));
|
|
2680
2711
|
const expectedColumns = target.schema.columns;
|
|
2712
|
+
const checkedCasts = [];
|
|
2681
2713
|
if (actualColumns.length !== expectedColumns.length) {
|
|
2682
2714
|
throwResultSchemaMismatch(target.id, `expected ${expectedColumns.length} columns, got ${actualColumns.length}`);
|
|
2683
2715
|
}
|
|
@@ -2687,10 +2719,20 @@ class DuckLakeSqlExecutor {
|
|
|
2687
2719
|
if (actual.name !== expected.name) {
|
|
2688
2720
|
throwResultSchemaMismatch(target.id, `column ${index + 1} should be '${expected.name}', got '${actual.name}'`);
|
|
2689
2721
|
}
|
|
2690
|
-
|
|
2691
|
-
|
|
2722
|
+
const actualDatasetType = findDatasetColumnTypeForDuckDbSql(actual.typeSql);
|
|
2723
|
+
if (actualDatasetType === expected.type) {
|
|
2724
|
+
continue;
|
|
2692
2725
|
}
|
|
2726
|
+
if (isCheckedInt64Cast(actual.typeSql, expected.type)) {
|
|
2727
|
+
checkedCasts.push({
|
|
2728
|
+
columnName: expected.name,
|
|
2729
|
+
targetTypeSql: datasetColumnTypeToDuckDbSql(expected.type)
|
|
2730
|
+
});
|
|
2731
|
+
continue;
|
|
2732
|
+
}
|
|
2733
|
+
throwResultSchemaMismatch(target.id, `column '${expected.name}' should have type '${expected.type}', got '${actualDatasetType ?? actual.typeSql}'`);
|
|
2693
2734
|
}
|
|
2735
|
+
return checkedCasts;
|
|
2694
2736
|
}
|
|
2695
2737
|
async applySqlTransform(input) {
|
|
2696
2738
|
const tempTableName = `sixb_sql_transform_${randomUUID3().replaceAll("-", "")}`;
|
|
@@ -2698,6 +2740,9 @@ class DuckLakeSqlExecutor {
|
|
|
2698
2740
|
await input.runtime.run(`CREATE TEMP TABLE ${tempTable} AS SELECT * FROM (${input.sql}) AS sixb_sql_transform_result`);
|
|
2699
2741
|
let result;
|
|
2700
2742
|
try {
|
|
2743
|
+
for (const checkedCast of input.checkedCasts) {
|
|
2744
|
+
await input.runtime.run(`ALTER TABLE ${tempTable} ALTER COLUMN ${quoteIdentifier(checkedCast.columnName)} SET DATA TYPE ${checkedCast.targetTypeSql}`);
|
|
2745
|
+
}
|
|
2701
2746
|
result = await applyDatasetRowsFromRelation({
|
|
2702
2747
|
options: this.options,
|
|
2703
2748
|
runtime: input.runtime,
|
|
@@ -2727,8 +2772,12 @@ function buildPreviewSql(sql, limit) {
|
|
|
2727
2772
|
}
|
|
2728
2773
|
function resultColumnFromDescribeRow(row) {
|
|
2729
2774
|
const name = getString(row, "column_name");
|
|
2730
|
-
const
|
|
2731
|
-
return { name,
|
|
2775
|
+
const typeSql = getString(row, "column_type");
|
|
2776
|
+
return { name, typeSql };
|
|
2777
|
+
}
|
|
2778
|
+
function isCheckedInt64Cast(actualTypeSql, expectedType) {
|
|
2779
|
+
const normalizedType = actualTypeSql.trim().toUpperCase();
|
|
2780
|
+
return expectedType === "int64" && (normalizedType === "HUGEINT" || normalizedType === "INT128");
|
|
2732
2781
|
}
|
|
2733
2782
|
function normalizePreviewRow(row) {
|
|
2734
2783
|
const normalized = {};
|
|
@@ -3049,6 +3098,7 @@ class DuckLakeMergeSession {
|
|
|
3049
3098
|
if (batch.length >= MERGE_CHANGE_BATCH_SIZE) {
|
|
3050
3099
|
await this.appendBatchToStagingTable(batch);
|
|
3051
3100
|
batch = [];
|
|
3101
|
+
await Bun.sleep(0);
|
|
3052
3102
|
}
|
|
3053
3103
|
}
|
|
3054
3104
|
if (batch.length > 0) {
|
|
@@ -3164,6 +3214,7 @@ class DuckLakeWriteSession {
|
|
|
3164
3214
|
await this.appendBatchToStagingTable(batch, batchPrimaryKeys);
|
|
3165
3215
|
batch = [];
|
|
3166
3216
|
batchPrimaryKeys = new Set;
|
|
3217
|
+
await Bun.sleep(0);
|
|
3167
3218
|
}
|
|
3168
3219
|
}
|
|
3169
3220
|
if (batch.length > 0) {
|
|
@@ -3400,6 +3451,7 @@ class DuckLakeWriteCoordinator {
|
|
|
3400
3451
|
previousRowCount: latestVersion?.rowCount,
|
|
3401
3452
|
validatedPrimaryKeyColumns: latestVersion?.validatedPrimaryKeyColumns
|
|
3402
3453
|
});
|
|
3454
|
+
await this.ensureInitialNoOpHasSnapshot(input, changeResult, latestVersion);
|
|
3403
3455
|
const commitId = randomUUID6();
|
|
3404
3456
|
await this.setCommitMetadata(input, commitId, commitRowCount(input, changeResult, latestVersion));
|
|
3405
3457
|
await input.runtime.run("COMMIT");
|
|
@@ -3481,6 +3533,18 @@ class DuckLakeWriteCoordinator {
|
|
|
3481
3533
|
};
|
|
3482
3534
|
await input.runtime.run(`CALL ${quoteIdentifier(duckLakeAlias(this.options))}.set_commit_message(${quoteSqlString("Sixb")}, ${quoteSqlString(input.commitMessage)}, extra_info => ${quoteSqlString(JSON.stringify({ sixb: metadata }))})`);
|
|
3483
3535
|
}
|
|
3536
|
+
async ensureInitialNoOpHasSnapshot(input, changeResult, knownLatestVersion) {
|
|
3537
|
+
if (changeResult.dataChangeExpected || input.allowInitialNoOp) {
|
|
3538
|
+
return;
|
|
3539
|
+
}
|
|
3540
|
+
const latestVersion = knownLatestVersion ?? await this.snapshots.getLatestVersionSummaryForDefinition(input.runtime, input.dataset);
|
|
3541
|
+
if (latestVersion) {
|
|
3542
|
+
return;
|
|
3543
|
+
}
|
|
3544
|
+
const table = qualifiedTableName(this.options, encodeDatasetTableName(input.dataset.id));
|
|
3545
|
+
const descriptionSql = input.dataset.description === undefined ? "NULL" : quoteSqlString(input.dataset.description);
|
|
3546
|
+
await input.runtime.run(`COMMENT ON TABLE ${table} IS ${descriptionSql}`);
|
|
3547
|
+
}
|
|
3484
3548
|
async versionForNoOpCommit(runtime, definition, allowInitialNoOp) {
|
|
3485
3549
|
const latestVersion = await this.snapshots.getLatestVersionForDefinition(runtime, definition);
|
|
3486
3550
|
if (latestVersion) {
|
|
@@ -3664,4 +3728,4 @@ export {
|
|
|
3664
3728
|
DuckLakeStorage
|
|
3665
3729
|
};
|
|
3666
3730
|
|
|
3667
|
-
//# debugId=
|
|
3731
|
+
//# debugId=85151472B319A47E64756E2164756E21
|