@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ducklake-row-reader.d.ts","sourceRoot":"","sources":["../../src/internal/ducklake-row-reader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA2B,UAAU,EAAE,MAAM,YAAY,CAAA;AACrE,OAAO,KAAK,EAAkB,oBAAoB,EAAE,MAAM,yBAAyB,CAAA;AAEnF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"ducklake-row-reader.d.ts","sourceRoot":"","sources":["../../src/internal/ducklake-row-reader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA2B,UAAU,EAAE,MAAM,YAAY,CAAA;AACrE,OAAO,KAAK,EAAkB,oBAAoB,EAAE,MAAM,yBAAyB,CAAA;AAEnF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;AAGtD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAA;AAE9E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAA;AAYxE;;;;;;GAMG;AACH,qBAAa,iBAAiB;IAE1B,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAFT,OAAO,EAAE,sBAAsB,EAC/B,WAAW,EAAE,yBAAyB,EACtC,SAAS,EAAE,sBAAsB;IAG7C,QAAQ,CAAC,KAAK,EAAE,oBAAoB,GAAG,aAAa,CAAC,UAAU,CAAC;YAoExD,aAAa;YAad,cAAc;IAuB5B,OAAO,CAAC,kBAAkB;IAuB1B,OAAO,CAAC,uBAAuB;CAKhC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ducklake-sql-executor.d.ts","sourceRoot":"","sources":["../../src/internal/ducklake-sql-executor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAA8C,UAAU,EAAE,MAAM,YAAY,CAAA;AACxF,OAAO,EAEL,KAAK,wBAAwB,EAE7B,KAAK,wBAAwB,EAC7B,KAAK,eAAe,EAEpB,KAAK,wBAAwB,EAC9B,MAAM,yBAAyB,CAAA;AAChC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"ducklake-sql-executor.d.ts","sourceRoot":"","sources":["../../src/internal/ducklake-sql-executor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAA8C,UAAU,EAAE,MAAM,YAAY,CAAA;AACxF,OAAO,EAEL,KAAK,wBAAwB,EAE7B,KAAK,wBAAwB,EAC7B,KAAK,eAAe,EAEpB,KAAK,wBAAwB,EAC9B,MAAM,yBAAyB,CAAA;AAChC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;AAStD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAA;AAC9E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAA;AACxE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAA;AACxE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAA;AAoC5E;;GAEG;AACH,qBAAa,mBAAoB,YAAW,eAAe,CAAC,QAAQ,CAAC;IASjE,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,MAAM;IAZzB,QAAQ,CAAC,OAAO,EAAG,QAAQ,CAAS;IACpC,QAAQ,CAAC,YAAY;;;;MAIpB;gBAGkB,OAAO,EAAE,sBAAsB,EAC/B,WAAW,EAAE,yBAAyB,EACtC,QAAQ,EAAE,sBAAsB,EAChC,SAAS,EAAE,sBAAsB,EACjC,MAAM,EAAE,wBAAwB;IAG5C,OAAO,CAAC,KAAK,EAAE,wBAAwB,CAAC,QAAQ,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC;IAkB9E,OAAO,CAAC,KAAK,EAAE,wBAAwB,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,wBAAwB,CAAC;YAmC7E,cAAc;YA2Bd,oBAAoB;YAcpB,oBAAoB;YAepB,oBAAoB;YAuBpB,+BAA+B;YAoD/B,iBAAiB;CAuChC"}
|
|
@@ -46,6 +46,7 @@ export declare class DuckLakeWriteCoordinator {
|
|
|
46
46
|
private versionForCommittedWrite;
|
|
47
47
|
private applyStagedRows;
|
|
48
48
|
private setCommitMetadata;
|
|
49
|
+
private ensureInitialNoOpHasSnapshot;
|
|
49
50
|
private versionForNoOpCommit;
|
|
50
51
|
private rollbackTransaction;
|
|
51
52
|
private latestVersionForCommit;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ducklake-write-coordinator.d.ts","sourceRoot":"","sources":["../../src/internal/ducklake-write-coordinator.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AACnD,OAAO,KAAK,EACV,sBAAsB,EACtB,sBAAsB,EAGtB,wBAAwB,EACxB,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EACjB,MAAM,yBAAyB,CAAA;AAEhC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;AAEtD,OAAO,EACL,KAAK,sBAAsB,EAI5B,MAAM,sBAAsB,CAAA;AAG7B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAC1D,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAA;AAC9E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAA;AAExE,OAAO,KAAK,EAAE,sBAAsB,EAA0B,MAAM,4BAA4B,CAAA;
|
|
1
|
+
{"version":3,"file":"ducklake-write-coordinator.d.ts","sourceRoot":"","sources":["../../src/internal/ducklake-write-coordinator.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AACnD,OAAO,KAAK,EACV,sBAAsB,EACtB,sBAAsB,EAGtB,wBAAwB,EACxB,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EACjB,MAAM,yBAAyB,CAAA;AAEhC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;AAEtD,OAAO,EACL,KAAK,sBAAsB,EAI5B,MAAM,sBAAsB,CAAA;AAG7B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAC1D,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAA;AAC9E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAA;AAExE,OAAO,KAAK,EAAE,sBAAsB,EAA0B,MAAM,4BAA4B,CAAA;AAYhG,MAAM,WAAW,iCAAiC;IAChD,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAA;IACnC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAA;IAC/B,QAAQ,CAAC,uBAAuB,CAAC,EAAE,MAAM,CAAA;IACzC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,sBAAsB,CAAC,UAAU,CAAC,CAAA;IACtD,QAAQ,CAAC,MAAM,CAAC,EAAE,sBAAsB,CAAC,QAAQ,CAAC,CAAA;IAClD,YAAY,CACV,OAAO,EAAE,kBAAkB,EAC3B,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,sBAAsB,CAAC,CAAA;CACnC;AAeD,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;IAClC,QAAQ,CAAC,0BAA0B,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACxD;AAED;;;;;;;GAOG;AACH,qBAAa,wBAAwB;IAEjC,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAHT,OAAO,EAAE,sBAAsB,EAC/B,WAAW,EAAE,yBAAyB,EACtC,QAAQ,EAAE,sBAAsB,EAChC,SAAS,EAAE,sBAAsB;IAG9C,UAAU,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA4BpE,UAAU,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,gBAAgB,CAAC;YAkC5D,WAAW;YAyBX,WAAW;IA4CnB,oBAAoB,CACxB,KAAK,EAAE,iCAAiC,GACvC,OAAO,CAAC,wBAAwB,CAAC;IAM9B,iBAAiB,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,kBAAkB,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAQlF,sCAAsC,CAC1C,OAAO,EAAE,kBAAkB,EAC3B,KAAK,EAAE,iCAAiC,GACvC,OAAO,CAAC,wBAAwB,CAAC;YAiBtB,sCAAsC;YAUtC,uBAAuB;YA2CvB,4BAA4B;YA2D5B,wBAAwB;YAiExB,eAAe;YAyBf,iBAAiB;YAkCjB,4BAA4B;YA0B5B,oBAAoB;YAmBpB,mBAAmB;YASnB,sBAAsB;YAUtB,uBAAuB;YAYvB,wBAAwB;YAiCxB,iBAAiB;IA0B/B,OAAO,CAAC,2BAA2B;CAgBpC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sixb/ducklake",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "DuckDB/DuckLake lake storage provider for Sixb",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sixb",
|
|
@@ -44,10 +44,10 @@
|
|
|
44
44
|
"@duckdb/node-api": "^1.5.2-r.1"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"@sixb/core": "^0.1.
|
|
47
|
+
"@sixb/core": "^0.1.4"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@sixb/core": "0.1.
|
|
50
|
+
"@sixb/core": "0.1.4",
|
|
51
51
|
"@types/bun": "latest",
|
|
52
52
|
"typescript": "^5.7.0"
|
|
53
53
|
},
|
|
@@ -2,6 +2,7 @@ import type { DatasetColumnDefinition, DatasetRow } from "@sixb/core"
|
|
|
2
2
|
import type { DatasetVersion, ReadDatasetRowsInput } from "@sixb/core/lake-storage"
|
|
3
3
|
import { LakeStorageError } from "@sixb/core/lake-storage"
|
|
4
4
|
import type { DuckLakeStorageOptions } from "../types"
|
|
5
|
+
import { getBigIntLike } from "./duckdb-row"
|
|
5
6
|
import type { DuckDbQueryRuntime } from "./duckdb-runtime"
|
|
6
7
|
import type { DuckLakeConnectionManager } from "./ducklake-connection-manager"
|
|
7
8
|
import { type DatasetTableRef, resolveDatasetTableRef } from "./ducklake-dataset-table-ref"
|
|
@@ -10,6 +11,13 @@ import { normalizeReadValue } from "./schema"
|
|
|
10
11
|
import { qualifiedTableName, quoteIdentifier } from "./sql"
|
|
11
12
|
import { parseVersionId } from "./versions"
|
|
12
13
|
|
|
14
|
+
// Large reads release the provider's single DuckDB queue slot between bounded pages. Besides
|
|
15
|
+
// bounding converted-row memory, this lets a JavaScript pipeline feed one DuckLake dataset into
|
|
16
|
+
// another: the destination appender can use the runtime after each page instead of waiting forever
|
|
17
|
+
// behind its own still-open source stream.
|
|
18
|
+
const READ_PAGE_ROWS = 5_000
|
|
19
|
+
const PHYSICAL_ROW_ID_ALIAS = "__sixb_physical_row_id"
|
|
20
|
+
|
|
13
21
|
/**
|
|
14
22
|
* Reads dataset rows from DuckLake snapshots.
|
|
15
23
|
*
|
|
@@ -27,50 +35,66 @@ export class DuckLakeRowReader {
|
|
|
27
35
|
async *readRows(input: ReadDatasetRowsInput): AsyncIterable<DatasetRow> {
|
|
28
36
|
this.connections.assertOpen()
|
|
29
37
|
|
|
38
|
+
// Keep the attachment lease across pages so local catalogs do not pay DETACH/ATTACH for every
|
|
39
|
+
// page. Individual query() calls still release the DuckDB queue before rows are yielded.
|
|
30
40
|
const lease = await this.connections.acquireAttachedRuntime()
|
|
31
41
|
try {
|
|
32
42
|
const runtime = lease.runtime
|
|
33
|
-
|
|
34
|
-
//
|
|
35
|
-
// previews must not go through broad catalog introspection.
|
|
43
|
+
// Resolve latest exactly once so a paged read stays pinned even if another writer commits
|
|
44
|
+
// between pages. DuckLake snapshots are immutable; every page names this snapshot.
|
|
36
45
|
const tableRef = await resolveDatasetTableRef(this.options, runtime, input.datasetId)
|
|
37
46
|
if (!tableRef) {
|
|
38
47
|
throw new LakeStorageError(`[SixbDuckLake] Unknown dataset '${input.datasetId}'.`)
|
|
39
48
|
}
|
|
40
|
-
|
|
41
49
|
const version = await this.resolveVersion(runtime, tableRef, input.versionId)
|
|
42
|
-
const snapshotId = parseVersionId(version.versionId)
|
|
43
50
|
const selectedColumns = this.resolveReadColumns(
|
|
44
51
|
tableRef.datasetId,
|
|
45
52
|
version.schema,
|
|
46
53
|
input.columns
|
|
47
54
|
)
|
|
48
|
-
|
|
49
|
-
// Step 2: query DuckLake at the exact snapshot id. Version ids are just
|
|
50
|
-
// `ducklake:<snapshot_id>`, so reads can use native DuckLake time travel
|
|
51
|
-
// directly after validation.
|
|
55
|
+
const snapshotId = parseVersionId(version.versionId)
|
|
52
56
|
const columnsSql = selectedColumns.map((column) => quoteIdentifier(column.name)).join(", ")
|
|
53
57
|
const tableSql = qualifiedTableName(this.options, tableRef.tableName)
|
|
54
58
|
// A user column named `rowid` shadows DuckDB's virtual row id. In that edge case the
|
|
55
59
|
// connection-level invariant still preserves insertion order; otherwise make it explicit.
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
60
|
+
const schemaNames = new Set(version.schema.columns.map((column) => column.name.toLowerCase()))
|
|
61
|
+
const canUsePhysicalRowId =
|
|
62
|
+
!schemaNames.has("rowid") && !schemaNames.has(PHYSICAL_ROW_ID_ALIAS.toLowerCase())
|
|
63
|
+
const orderSql = canUsePhysicalRowId ? " ORDER BY rowid" : ""
|
|
64
|
+
const baseSql = `SELECT ${columnsSql} FROM ${tableSql} AT (VERSION => ${snapshotId})`
|
|
65
|
+
const requestedOffset = Math.max(0, Math.trunc(input.offset ?? 0))
|
|
66
|
+
let remaining = input.limit === undefined ? undefined : Math.max(0, Math.trunc(input.limit))
|
|
67
|
+
if (remaining === 0) return
|
|
68
|
+
|
|
69
|
+
// Materialize one bounded page and release its queue operation before yielding it. A
|
|
70
|
+
// consumer may enqueue another DuckLake operation while processing the yielded rows.
|
|
71
|
+
// Holding a native stream open here would make that operation wait behind itself.
|
|
72
|
+
let offset = requestedOffset
|
|
73
|
+
let physicalCursor: bigint | null = null
|
|
74
|
+
while (true) {
|
|
75
|
+
const pageRows =
|
|
76
|
+
remaining === undefined ? READ_PAGE_ROWS : Math.min(READ_PAGE_ROWS, remaining)
|
|
77
|
+
const pageSql = canUsePhysicalRowId
|
|
78
|
+
? `SELECT rowid AS ${quoteIdentifier(PHYSICAL_ROW_ID_ALIAS)}, ${columnsSql}
|
|
79
|
+
FROM ${tableSql} AT (VERSION => ${snapshotId})
|
|
80
|
+
${physicalCursor === null ? "" : `WHERE rowid > ${physicalCursor}`}
|
|
81
|
+
ORDER BY rowid LIMIT ${pageRows}
|
|
82
|
+
${physicalCursor === null && offset > 0 ? `OFFSET ${offset}` : ""}`
|
|
83
|
+
: `${baseSql}${orderSql} LIMIT ${pageRows} OFFSET ${offset}`
|
|
84
|
+
const rows = await runtime.query(pageSql)
|
|
85
|
+
if (rows.length === 0) return
|
|
86
|
+
|
|
87
|
+
yield* this.normalizeRows(rows, selectedColumns)
|
|
88
|
+
offset += rows.length
|
|
89
|
+
if (remaining !== undefined) {
|
|
90
|
+
remaining -= rows.length
|
|
91
|
+
if (remaining === 0) return
|
|
92
|
+
}
|
|
93
|
+
if (canUsePhysicalRowId) {
|
|
94
|
+
physicalCursor = getBigIntLike(rows[rows.length - 1]!, PHYSICAL_ROW_ID_ALIAS)
|
|
95
|
+
}
|
|
96
|
+
if (rows.length < pageRows) return
|
|
97
|
+
}
|
|
74
98
|
} finally {
|
|
75
99
|
await lease.release()
|
|
76
100
|
}
|
|
@@ -15,13 +15,14 @@ import {
|
|
|
15
15
|
applyDatasetRowsFromRelation,
|
|
16
16
|
assertDatasetWriteMode,
|
|
17
17
|
} from "./dataset-row-commit"
|
|
18
|
+
import { findDatasetColumnTypeForDuckDbSql } from "./duckdb-column-types"
|
|
18
19
|
import { getString } from "./duckdb-row"
|
|
19
20
|
import type { DuckDbQueryRuntime } from "./duckdb-runtime"
|
|
20
21
|
import type { DuckLakeConnectionManager } from "./ducklake-connection-manager"
|
|
21
22
|
import type { DuckLakeDatasetCatalog } from "./ducklake-dataset-catalog"
|
|
22
23
|
import type { DuckLakeSnapshotReader } from "./ducklake-snapshot-reader"
|
|
23
24
|
import type { DuckLakeWriteCoordinator } from "./ducklake-write-coordinator"
|
|
24
|
-
import {
|
|
25
|
+
import { datasetColumnTypeToDuckDbSql } from "./schema"
|
|
25
26
|
import { quoteIdentifier } from "./sql"
|
|
26
27
|
import {
|
|
27
28
|
type DuckLakeSqlTransformSourceRelation,
|
|
@@ -42,9 +43,20 @@ interface ApplySqlTransformInput {
|
|
|
42
43
|
readonly target: DatasetDefinition
|
|
43
44
|
readonly mode: DatasetWriteMode
|
|
44
45
|
readonly sql: string
|
|
46
|
+
readonly checkedCasts: readonly SqlTransformCheckedCast[]
|
|
45
47
|
readonly previousRowCount?: number
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
interface SqlTransformResultColumn {
|
|
51
|
+
readonly name: string
|
|
52
|
+
readonly typeSql: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface SqlTransformCheckedCast {
|
|
56
|
+
readonly columnName: string
|
|
57
|
+
readonly targetTypeSql: string
|
|
58
|
+
}
|
|
59
|
+
|
|
48
60
|
/**
|
|
49
61
|
* DuckDB-backed SQL transform executor for DuckLakeStorage.
|
|
50
62
|
*/
|
|
@@ -95,7 +107,7 @@ export class DuckLakeSqlExecutor implements LakeSqlExecutor<"duckdb"> {
|
|
|
95
107
|
sql: input.sql,
|
|
96
108
|
})
|
|
97
109
|
|
|
98
|
-
await this.assertResultSchemaMatchesTarget(runtime, target, sql)
|
|
110
|
+
const checkedCasts = await this.assertResultSchemaMatchesTarget(runtime, target, sql)
|
|
99
111
|
|
|
100
112
|
return this.writes.commitDatasetVersionOnExclusiveRuntime(runtime, {
|
|
101
113
|
dataset: target,
|
|
@@ -110,6 +122,7 @@ export class DuckLakeSqlExecutor implements LakeSqlExecutor<"duckdb"> {
|
|
|
110
122
|
target,
|
|
111
123
|
mode: input.mode,
|
|
112
124
|
sql,
|
|
125
|
+
checkedCasts,
|
|
113
126
|
previousRowCount: context.previousRowCount,
|
|
114
127
|
}),
|
|
115
128
|
})
|
|
@@ -199,12 +212,13 @@ export class DuckLakeSqlExecutor implements LakeSqlExecutor<"duckdb"> {
|
|
|
199
212
|
runtime: DuckDbQueryRuntime,
|
|
200
213
|
target: DatasetDefinition,
|
|
201
214
|
sql: string
|
|
202
|
-
): Promise<
|
|
215
|
+
): Promise<readonly SqlTransformCheckedCast[]> {
|
|
203
216
|
const rows = await runtime.query(
|
|
204
217
|
`DESCRIBE SELECT * FROM (${sql}) AS sixb_sql_transform_result_schema`
|
|
205
218
|
)
|
|
206
219
|
const actualColumns = rows.map((row) => resultColumnFromDescribeRow(row))
|
|
207
220
|
const expectedColumns = target.schema.columns
|
|
221
|
+
const checkedCasts: SqlTransformCheckedCast[] = []
|
|
208
222
|
|
|
209
223
|
if (actualColumns.length !== expectedColumns.length) {
|
|
210
224
|
throwResultSchemaMismatch(
|
|
@@ -224,13 +238,26 @@ export class DuckLakeSqlExecutor implements LakeSqlExecutor<"duckdb"> {
|
|
|
224
238
|
)
|
|
225
239
|
}
|
|
226
240
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
`column '${expected.name}' should have type '${expected.type}', got '${actual.type}'`
|
|
231
|
-
)
|
|
241
|
+
const actualDatasetType = findDatasetColumnTypeForDuckDbSql(actual.typeSql)
|
|
242
|
+
if (actualDatasetType === expected.type) {
|
|
243
|
+
continue
|
|
232
244
|
}
|
|
245
|
+
|
|
246
|
+
if (isCheckedInt64Cast(actual.typeSql, expected.type)) {
|
|
247
|
+
checkedCasts.push({
|
|
248
|
+
columnName: expected.name,
|
|
249
|
+
targetTypeSql: datasetColumnTypeToDuckDbSql(expected.type),
|
|
250
|
+
})
|
|
251
|
+
continue
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
throwResultSchemaMismatch(
|
|
255
|
+
target.id,
|
|
256
|
+
`column '${expected.name}' should have type '${expected.type}', got '${actualDatasetType ?? actual.typeSql}'`
|
|
257
|
+
)
|
|
233
258
|
}
|
|
259
|
+
|
|
260
|
+
return checkedCasts
|
|
234
261
|
}
|
|
235
262
|
|
|
236
263
|
private async applySqlTransform(input: ApplySqlTransformInput): Promise<ApplyDatasetRowsResult> {
|
|
@@ -243,6 +270,14 @@ export class DuckLakeSqlExecutor implements LakeSqlExecutor<"duckdb"> {
|
|
|
243
270
|
|
|
244
271
|
let result: ApplyDatasetRowsResult
|
|
245
272
|
try {
|
|
273
|
+
for (const checkedCast of input.checkedCasts) {
|
|
274
|
+
await input.runtime.run(
|
|
275
|
+
`ALTER TABLE ${tempTable} ALTER COLUMN ${quoteIdentifier(
|
|
276
|
+
checkedCast.columnName
|
|
277
|
+
)} SET DATA TYPE ${checkedCast.targetTypeSql}`
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
246
281
|
result = await applyDatasetRowsFromRelation({
|
|
247
282
|
options: this.options,
|
|
248
283
|
runtime: input.runtime,
|
|
@@ -280,11 +315,19 @@ function buildPreviewSql(sql: string, limit: number): string {
|
|
|
280
315
|
|
|
281
316
|
function resultColumnFromDescribeRow(
|
|
282
317
|
row: Readonly<Record<string, unknown>>
|
|
283
|
-
):
|
|
318
|
+
): SqlTransformResultColumn {
|
|
284
319
|
const name = getString(row, "column_name")
|
|
285
|
-
const
|
|
320
|
+
const typeSql = getString(row, "column_type")
|
|
321
|
+
|
|
322
|
+
return { name, typeSql }
|
|
323
|
+
}
|
|
286
324
|
|
|
287
|
-
|
|
325
|
+
function isCheckedInt64Cast(
|
|
326
|
+
actualTypeSql: string,
|
|
327
|
+
expectedType: DatasetColumnDefinition["type"]
|
|
328
|
+
): boolean {
|
|
329
|
+
const normalizedType = actualTypeSql.trim().toUpperCase()
|
|
330
|
+
return expectedType === "int64" && (normalizedType === "HUGEINT" || normalizedType === "INT128")
|
|
288
331
|
}
|
|
289
332
|
|
|
290
333
|
function normalizePreviewRow(row: Readonly<Record<string, unknown>>): DatasetRow {
|
|
@@ -27,7 +27,14 @@ import type { DuckLakeDatasetCatalog } from "./ducklake-dataset-catalog"
|
|
|
27
27
|
import { createDuckLakeMergeSession, type DuckLakeCommitMergeInput } from "./ducklake-merge-session"
|
|
28
28
|
import type { DuckLakeSnapshotReader, DuckLakeVersionSummary } from "./ducklake-snapshot-reader"
|
|
29
29
|
import { createDuckLakeWriteSession, type DuckLakeCommitWriteInput } from "./ducklake-write-session"
|
|
30
|
-
import {
|
|
30
|
+
import { encodeDatasetTableName } from "./names"
|
|
31
|
+
import {
|
|
32
|
+
duckLakeAlias,
|
|
33
|
+
duckLakeMetadataTableName,
|
|
34
|
+
qualifiedTableName,
|
|
35
|
+
quoteIdentifier,
|
|
36
|
+
quoteSqlString,
|
|
37
|
+
} from "./sql"
|
|
31
38
|
import { parseCommitMetadata, type SixbCommitMetadata } from "./versions"
|
|
32
39
|
|
|
33
40
|
export interface DuckLakeCommitDatasetVersionInput {
|
|
@@ -325,6 +332,7 @@ export class DuckLakeWriteCoordinator {
|
|
|
325
332
|
previousRowCount: latestVersion?.rowCount,
|
|
326
333
|
validatedPrimaryKeyColumns: latestVersion?.validatedPrimaryKeyColumns,
|
|
327
334
|
})
|
|
335
|
+
await this.ensureInitialNoOpHasSnapshot(input, changeResult, latestVersion)
|
|
328
336
|
const commitId = randomUUID()
|
|
329
337
|
await this.setCommitMetadata(
|
|
330
338
|
input,
|
|
@@ -479,6 +487,32 @@ export class DuckLakeWriteCoordinator {
|
|
|
479
487
|
)
|
|
480
488
|
}
|
|
481
489
|
|
|
490
|
+
private async ensureInitialNoOpHasSnapshot(
|
|
491
|
+
input: DuckLakeCommitVersionOutcomeRuntimeInput,
|
|
492
|
+
changeResult: ApplyDatasetRowsResult,
|
|
493
|
+
knownLatestVersion: DuckLakeVersionSummary | null
|
|
494
|
+
): Promise<void> {
|
|
495
|
+
if (changeResult.dataChangeExpected || input.allowInitialNoOp) {
|
|
496
|
+
return
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const latestVersion =
|
|
500
|
+
knownLatestVersion ??
|
|
501
|
+
(await this.snapshots.getLatestVersionSummaryForDefinition(input.runtime, input.dataset))
|
|
502
|
+
if (latestVersion) {
|
|
503
|
+
return
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// DuckLake 1.5.2 does not create a snapshot for a transaction whose data and metadata are
|
|
507
|
+
// unchanged, and set_commit_message alone cannot force one. Reapplying the current table
|
|
508
|
+
// comment is an idempotent metadata write: it leaves the visible DatasetDefinition unchanged
|
|
509
|
+
// while giving the first empty write a real DuckLake snapshot for time travel and lineage.
|
|
510
|
+
const table = qualifiedTableName(this.options, encodeDatasetTableName(input.dataset.id))
|
|
511
|
+
const descriptionSql =
|
|
512
|
+
input.dataset.description === undefined ? "NULL" : quoteSqlString(input.dataset.description)
|
|
513
|
+
await input.runtime.run(`COMMENT ON TABLE ${table} IS ${descriptionSql}`)
|
|
514
|
+
}
|
|
515
|
+
|
|
482
516
|
private async versionForNoOpCommit(
|
|
483
517
|
runtime: DuckDbQueryRuntime,
|
|
484
518
|
definition: DatasetDefinition,
|
|
@@ -106,6 +106,10 @@ class DuckLakeWriteSession implements LakeWriteSession {
|
|
|
106
106
|
await this.appendBatchToStagingTable(batch, batchPrimaryKeys)
|
|
107
107
|
batch = []
|
|
108
108
|
batchPrimaryKeys = new Set()
|
|
109
|
+
// Native appender promises can settle through microtasks quickly enough to starve timers
|
|
110
|
+
// across a large in-memory iterable. Yield once per bounded batch so API traffic remains
|
|
111
|
+
// responsive without adding per-row scheduling overhead.
|
|
112
|
+
await Bun.sleep(0)
|
|
109
113
|
}
|
|
110
114
|
}
|
|
111
115
|
|