@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 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: batch appends, commits, SQL
98
- transforms, and metadata reads. A large streaming read can also make a later
99
- write wait until the stream is consumed or closed.
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 orderSql = version.schema.columns.some((column) => column.name.toLowerCase() === "rowid") ? "" : " ORDER BY rowid";
1811
- const limitSql = input.limit === undefined ? "" : ` LIMIT ${Math.max(0, Math.trunc(input.limit))}`;
1812
- const offsetSql = input.offset === undefined ? "" : ` OFFSET ${Math.max(0, Math.trunc(input.offset))}`;
1813
- const sql = `SELECT ${columnsSql} FROM ${tableSql} AT (VERSION => ${snapshotId})${orderSql}${limitSql}${offsetSql}`;
1814
- const rows = input.limit === undefined ? runtime.streamRows(sql) : await runtime.query(sql);
1815
- yield* this.normalizeRows(rows, selectedColumns);
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
- if (actual.type !== expected.type) {
2691
- throwResultSchemaMismatch(target.id, `column '${expected.name}' should have type '${expected.type}', got '${actual.type}'`);
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 type = duckDbTypeToDatasetColumnType(getString(row, "column_type"));
2731
- return { name, type };
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=8AEF2C0D635BEB8A64756E2164756E21
3731
+ //# debugId=85151472B319A47E64756E2164756E21