@sixb/ducklake 0.0.1
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/LICENSE +21 -0
- package/README.md +490 -0
- package/dist/ducklake-storage.d.ts +42 -0
- package/dist/ducklake-storage.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3112 -0
- package/dist/index.js.map +34 -0
- package/dist/internal/catalog-key.d.ts +11 -0
- package/dist/internal/catalog-key.d.ts.map +1 -0
- package/dist/internal/dataset-row-commit.d.ts +26 -0
- package/dist/internal/dataset-row-commit.d.ts.map +1 -0
- package/dist/internal/duckdb-column-types.d.ts +45 -0
- package/dist/internal/duckdb-column-types.d.ts.map +1 -0
- package/dist/internal/duckdb-decimal.d.ts +6 -0
- package/dist/internal/duckdb-decimal.d.ts.map +1 -0
- package/dist/internal/duckdb-row.d.ts +8 -0
- package/dist/internal/duckdb-row.d.ts.map +1 -0
- package/dist/internal/duckdb-runtime.d.ts +63 -0
- package/dist/internal/duckdb-runtime.d.ts.map +1 -0
- package/dist/internal/duckdb-value-converter.d.ts +7 -0
- package/dist/internal/duckdb-value-converter.d.ts.map +1 -0
- package/dist/internal/ducklake-catalog-schema.d.ts +12 -0
- package/dist/internal/ducklake-catalog-schema.d.ts.map +1 -0
- package/dist/internal/ducklake-connection-manager.d.ts +67 -0
- package/dist/internal/ducklake-connection-manager.d.ts.map +1 -0
- package/dist/internal/ducklake-current-dataset-definitions.d.ts +10 -0
- package/dist/internal/ducklake-current-dataset-definitions.d.ts.map +1 -0
- package/dist/internal/ducklake-dataset-catalog.d.ts +33 -0
- package/dist/internal/ducklake-dataset-catalog.d.ts.map +1 -0
- package/dist/internal/ducklake-dataset-table-ref.d.ts +9 -0
- package/dist/internal/ducklake-dataset-table-ref.d.ts.map +1 -0
- package/dist/internal/ducklake-maintenance.d.ts +9 -0
- package/dist/internal/ducklake-maintenance.d.ts.map +1 -0
- package/dist/internal/ducklake-row-reader.d.ts +24 -0
- package/dist/internal/ducklake-row-reader.d.ts.map +1 -0
- package/dist/internal/ducklake-snapshot-reader.d.ts +73 -0
- package/dist/internal/ducklake-snapshot-reader.d.ts.map +1 -0
- package/dist/internal/ducklake-sql-executor.d.ts +33 -0
- package/dist/internal/ducklake-sql-executor.d.ts.map +1 -0
- package/dist/internal/ducklake-write-coordinator.d.ts +53 -0
- package/dist/internal/ducklake-write-coordinator.d.ts.map +1 -0
- package/dist/internal/ducklake-write-session.d.ts +17 -0
- package/dist/internal/ducklake-write-session.d.ts.map +1 -0
- package/dist/internal/names.d.ts +13 -0
- package/dist/internal/names.d.ts.map +1 -0
- package/dist/internal/row-appender.d.ts +7 -0
- package/dist/internal/row-appender.d.ts.map +1 -0
- package/dist/internal/schema.d.ts +35 -0
- package/dist/internal/schema.d.ts.map +1 -0
- package/dist/internal/sql-transform-relations.d.ts +13 -0
- package/dist/internal/sql-transform-relations.d.ts.map +1 -0
- package/dist/internal/sql.d.ts +62 -0
- package/dist/internal/sql.d.ts.map +1 -0
- package/dist/internal/versions.d.ts +22 -0
- package/dist/internal/versions.d.ts.map +1 -0
- package/dist/tsconfig.json +5 -0
- package/dist/types.d.ts +147 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +64 -0
- package/src/ducklake-storage.ts +130 -0
- package/src/index.ts +14 -0
- package/src/internal/catalog-key.ts +28 -0
- package/src/internal/dataset-row-commit.ts +128 -0
- package/src/internal/duckdb-column-types.ts +68 -0
- package/src/internal/duckdb-decimal.ts +36 -0
- package/src/internal/duckdb-row.ts +83 -0
- package/src/internal/duckdb-runtime.ts +347 -0
- package/src/internal/duckdb-value-converter.ts +27 -0
- package/src/internal/ducklake-catalog-schema.ts +116 -0
- package/src/internal/ducklake-connection-manager.ts +510 -0
- package/src/internal/ducklake-current-dataset-definitions.ts +228 -0
- package/src/internal/ducklake-dataset-catalog.ts +363 -0
- package/src/internal/ducklake-dataset-table-ref.ts +35 -0
- package/src/internal/ducklake-maintenance.ts +116 -0
- package/src/internal/ducklake-row-reader.ts +143 -0
- package/src/internal/ducklake-snapshot-reader.ts +826 -0
- package/src/internal/ducklake-sql-executor.ts +334 -0
- package/src/internal/ducklake-write-coordinator.ts +532 -0
- package/src/internal/ducklake-write-session.ts +170 -0
- package/src/internal/names.ts +111 -0
- package/src/internal/row-appender.ts +73 -0
- package/src/internal/schema.ts +149 -0
- package/src/internal/sql-transform-relations.ts +99 -0
- package/src/internal/sql.ts +440 -0
- package/src/internal/versions.ts +140 -0
- package/src/types.ts +159 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto"
|
|
2
|
+
import type { DatasetColumnDefinition, DatasetDefinition, DatasetRow } from "@sixb/core"
|
|
3
|
+
import {
|
|
4
|
+
type DatasetVersionRef,
|
|
5
|
+
type DatasetWriteCommitResult,
|
|
6
|
+
type DatasetWriteMode,
|
|
7
|
+
type ExecuteSqlTransformInput,
|
|
8
|
+
type LakeSqlExecutor,
|
|
9
|
+
LakeStorageError,
|
|
10
|
+
type PreviewSqlTransformInput,
|
|
11
|
+
} from "@sixb/core/lake-storage"
|
|
12
|
+
import type { DuckLakeStorageOptions } from "../types"
|
|
13
|
+
import {
|
|
14
|
+
type ApplyDatasetRowsResult,
|
|
15
|
+
applyDatasetRowsFromRelation,
|
|
16
|
+
assertDatasetWriteMode,
|
|
17
|
+
} from "./dataset-row-commit"
|
|
18
|
+
import { getString } from "./duckdb-row"
|
|
19
|
+
import type { DuckDbQueryRuntime } from "./duckdb-runtime"
|
|
20
|
+
import type { DuckLakeConnectionManager } from "./ducklake-connection-manager"
|
|
21
|
+
import type { DuckLakeDatasetCatalog } from "./ducklake-dataset-catalog"
|
|
22
|
+
import type { DuckLakeSnapshotReader } from "./ducklake-snapshot-reader"
|
|
23
|
+
import type { DuckLakeWriteCoordinator } from "./ducklake-write-coordinator"
|
|
24
|
+
import { duckDbTypeToDatasetColumnType } from "./schema"
|
|
25
|
+
import { quoteIdentifier } from "./sql"
|
|
26
|
+
import {
|
|
27
|
+
type DuckLakeSqlTransformSourceRelation,
|
|
28
|
+
renderDuckLakeSqlTransformSql,
|
|
29
|
+
} from "./sql-transform-relations"
|
|
30
|
+
import { parseVersionId } from "./versions"
|
|
31
|
+
|
|
32
|
+
const DEFAULT_PREVIEW_LIMIT = 100
|
|
33
|
+
const MAX_PREVIEW_LIMIT = 1_000
|
|
34
|
+
|
|
35
|
+
interface ResolvedDuckLakeSqlTransformSources {
|
|
36
|
+
readonly relations: Readonly<Record<string, DuckLakeSqlTransformSourceRelation>>
|
|
37
|
+
readonly inputs: readonly DatasetVersionRef[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface ApplySqlTransformInput {
|
|
41
|
+
readonly runtime: DuckDbQueryRuntime
|
|
42
|
+
readonly target: DatasetDefinition
|
|
43
|
+
readonly mode: DatasetWriteMode
|
|
44
|
+
readonly sql: string
|
|
45
|
+
readonly previousRowCount?: number
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* DuckDB-backed SQL transform executor for DuckLakeStorage.
|
|
50
|
+
*/
|
|
51
|
+
export class DuckLakeSqlExecutor implements LakeSqlExecutor<"duckdb"> {
|
|
52
|
+
readonly dialect = "duckdb" as const
|
|
53
|
+
readonly capabilities = {
|
|
54
|
+
preview: true,
|
|
55
|
+
supportsAppend: true,
|
|
56
|
+
supportsSnapshot: true,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
constructor(
|
|
60
|
+
private readonly options: DuckLakeStorageOptions,
|
|
61
|
+
private readonly connections: DuckLakeConnectionManager,
|
|
62
|
+
private readonly datasets: DuckLakeDatasetCatalog,
|
|
63
|
+
private readonly snapshots: DuckLakeSnapshotReader,
|
|
64
|
+
private readonly writes: DuckLakeWriteCoordinator
|
|
65
|
+
) {}
|
|
66
|
+
|
|
67
|
+
async *preview(input: PreviewSqlTransformInput<"duckdb">): AsyncIterable<DatasetRow> {
|
|
68
|
+
this.connections.assertOpen()
|
|
69
|
+
|
|
70
|
+
const rows = await this.connections.withAttachedRuntime(async (runtime) => {
|
|
71
|
+
const sources = await this.resolveSources(runtime, input.sources)
|
|
72
|
+
const sql = renderDuckLakeSqlTransformSql({
|
|
73
|
+
options: this.options,
|
|
74
|
+
sources: sources.relations,
|
|
75
|
+
sql: input.sql,
|
|
76
|
+
})
|
|
77
|
+
return runtime.query(buildPreviewSql(sql, previewLimit(input.limit)))
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
for (const row of rows) {
|
|
81
|
+
yield normalizePreviewRow(row)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async execute(input: ExecuteSqlTransformInput<"duckdb">): Promise<DatasetWriteCommitResult> {
|
|
86
|
+
this.connections.assertOpen()
|
|
87
|
+
assertDatasetWriteMode(input.mode, "SQL transform")
|
|
88
|
+
|
|
89
|
+
return this.writes.withCommitRuntime(async (runtime) => {
|
|
90
|
+
const target = await this.resolveTargetDataset(runtime, input.target)
|
|
91
|
+
const sources = await this.resolveSources(runtime, input.sources)
|
|
92
|
+
const sql = renderDuckLakeSqlTransformSql({
|
|
93
|
+
options: this.options,
|
|
94
|
+
sources: sources.relations,
|
|
95
|
+
sql: input.sql,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
await this.assertResultSchemaMatchesTarget(runtime, target, sql)
|
|
99
|
+
|
|
100
|
+
return this.writes.commitDatasetVersionOnExclusiveRuntime(runtime, {
|
|
101
|
+
dataset: target,
|
|
102
|
+
mode: input.mode,
|
|
103
|
+
expectedLatestVersionId: input.expectedLatestVersionId,
|
|
104
|
+
commitMessage: input.commitMessage ?? `execute SQL transform for dataset ${target.id}`,
|
|
105
|
+
producer: input.producer,
|
|
106
|
+
inputs: sources.inputs,
|
|
107
|
+
applyChanges: (runtime, context) =>
|
|
108
|
+
this.applySqlTransform({
|
|
109
|
+
runtime,
|
|
110
|
+
target,
|
|
111
|
+
mode: input.mode,
|
|
112
|
+
sql,
|
|
113
|
+
previousRowCount: context.previousRowCount,
|
|
114
|
+
}),
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private async resolveSources(
|
|
120
|
+
runtime: DuckDbQueryRuntime,
|
|
121
|
+
sources: PreviewSqlTransformInput<"duckdb">["sources"]
|
|
122
|
+
): Promise<ResolvedDuckLakeSqlTransformSources> {
|
|
123
|
+
const relations: Record<string, DuckLakeSqlTransformSourceRelation> = Object.create(null)
|
|
124
|
+
const inputs: DatasetVersionRef[] = []
|
|
125
|
+
|
|
126
|
+
for (const [sourceName, source] of Object.entries(sources)) {
|
|
127
|
+
const definition = await this.resolveSourceDataset(runtime, source.dataset)
|
|
128
|
+
const versionRef = await this.resolveSourceVersion(runtime, definition, source.versionId)
|
|
129
|
+
|
|
130
|
+
relations[sourceName] = {
|
|
131
|
+
datasetId: definition.id,
|
|
132
|
+
versionId: versionRef.versionId,
|
|
133
|
+
}
|
|
134
|
+
inputs.push({
|
|
135
|
+
datasetId: definition.id,
|
|
136
|
+
versionId: versionRef.versionId,
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
relations: Object.freeze(relations),
|
|
142
|
+
inputs: Object.freeze(inputs),
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private async resolveSourceDataset(
|
|
147
|
+
runtime: DuckDbQueryRuntime,
|
|
148
|
+
dataset: DatasetDefinition
|
|
149
|
+
): Promise<DatasetDefinition> {
|
|
150
|
+
const definition = await this.datasets.getDatasetOnRuntime(runtime, dataset.id)
|
|
151
|
+
if (!definition) {
|
|
152
|
+
throw new LakeStorageError(
|
|
153
|
+
`[SixbDuckLake] Unknown SQL transform source dataset '${dataset.id}'.`
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return definition
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private async resolveTargetDataset(
|
|
161
|
+
runtime: DuckDbQueryRuntime,
|
|
162
|
+
dataset: DatasetDefinition
|
|
163
|
+
): Promise<DatasetDefinition> {
|
|
164
|
+
const definition = await this.datasets.getDatasetOnRuntime(runtime, dataset.id)
|
|
165
|
+
if (!definition) {
|
|
166
|
+
throw new LakeStorageError(
|
|
167
|
+
`[SixbDuckLake] Unknown SQL transform target dataset '${dataset.id}'.`
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.datasets.assertSchema(definition)
|
|
172
|
+
return definition
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private async resolveSourceVersion(
|
|
176
|
+
runtime: DuckDbQueryRuntime,
|
|
177
|
+
dataset: DatasetDefinition,
|
|
178
|
+
versionId?: string
|
|
179
|
+
): Promise<DatasetVersionRef> {
|
|
180
|
+
if (versionId !== undefined) {
|
|
181
|
+
const snapshotId = parseVersionId(versionId)
|
|
182
|
+
const versionRef = await this.snapshots.getVersionRefForSnapshot(runtime, dataset, snapshotId)
|
|
183
|
+
if (versionRef) {
|
|
184
|
+
return versionRef
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
throwNoCommittedSourceVersion(dataset.id)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const latestVersionRef = await this.snapshots.getLatestVersionRefForDefinition(runtime, dataset)
|
|
191
|
+
if (latestVersionRef) {
|
|
192
|
+
return latestVersionRef
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
throwNoCommittedSourceVersion(dataset.id)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private async assertResultSchemaMatchesTarget(
|
|
199
|
+
runtime: DuckDbQueryRuntime,
|
|
200
|
+
target: DatasetDefinition,
|
|
201
|
+
sql: string
|
|
202
|
+
): Promise<void> {
|
|
203
|
+
const rows = await runtime.query(
|
|
204
|
+
`DESCRIBE SELECT * FROM (${sql}) AS sixb_sql_transform_result_schema`
|
|
205
|
+
)
|
|
206
|
+
const actualColumns = rows.map((row) => resultColumnFromDescribeRow(row))
|
|
207
|
+
const expectedColumns = target.schema.columns
|
|
208
|
+
|
|
209
|
+
if (actualColumns.length !== expectedColumns.length) {
|
|
210
|
+
throwResultSchemaMismatch(
|
|
211
|
+
target.id,
|
|
212
|
+
`expected ${expectedColumns.length} columns, got ${actualColumns.length}`
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
for (let index = 0; index < expectedColumns.length; index += 1) {
|
|
217
|
+
const expected = expectedColumns[index]
|
|
218
|
+
const actual = actualColumns[index]
|
|
219
|
+
|
|
220
|
+
if (actual.name !== expected.name) {
|
|
221
|
+
throwResultSchemaMismatch(
|
|
222
|
+
target.id,
|
|
223
|
+
`column ${index + 1} should be '${expected.name}', got '${actual.name}'`
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (actual.type !== expected.type) {
|
|
228
|
+
throwResultSchemaMismatch(
|
|
229
|
+
target.id,
|
|
230
|
+
`column '${expected.name}' should have type '${expected.type}', got '${actual.type}'`
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private async applySqlTransform(input: ApplySqlTransformInput): Promise<ApplyDatasetRowsResult> {
|
|
237
|
+
const tempTableName = `sixb_sql_transform_${randomUUID().replaceAll("-", "")}`
|
|
238
|
+
const tempTable = quoteIdentifier(tempTableName)
|
|
239
|
+
|
|
240
|
+
await input.runtime.run(
|
|
241
|
+
`CREATE TEMP TABLE ${tempTable} AS SELECT * FROM (${input.sql}) AS sixb_sql_transform_result`
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
let result: ApplyDatasetRowsResult
|
|
245
|
+
try {
|
|
246
|
+
result = await applyDatasetRowsFromRelation({
|
|
247
|
+
options: this.options,
|
|
248
|
+
runtime: input.runtime,
|
|
249
|
+
dataset: input.target,
|
|
250
|
+
mode: input.mode,
|
|
251
|
+
sourceRelationSql: tempTable,
|
|
252
|
+
previousRowCount: input.previousRowCount,
|
|
253
|
+
})
|
|
254
|
+
} catch (error) {
|
|
255
|
+
try {
|
|
256
|
+
await input.runtime.run(`DROP TABLE IF EXISTS ${tempTable}`)
|
|
257
|
+
} catch {
|
|
258
|
+
// Preserve the write failure. DuckDB rejects cleanup statements while
|
|
259
|
+
// the transaction is aborted; the coordinator will roll it back next.
|
|
260
|
+
}
|
|
261
|
+
throw error
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
await input.runtime.run(`DROP TABLE IF EXISTS ${tempTable}`)
|
|
265
|
+
return result
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function previewLimit(limit: number | undefined): number {
|
|
270
|
+
if (limit === undefined || !Number.isFinite(limit)) {
|
|
271
|
+
return DEFAULT_PREVIEW_LIMIT
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return Math.min(Math.max(0, Math.trunc(limit)), MAX_PREVIEW_LIMIT)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function buildPreviewSql(sql: string, limit: number): string {
|
|
278
|
+
return `SELECT * FROM (${sql}) AS sixb_sql_transform_preview LIMIT ${limit}`
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function resultColumnFromDescribeRow(
|
|
282
|
+
row: Readonly<Record<string, unknown>>
|
|
283
|
+
): DatasetColumnDefinition {
|
|
284
|
+
const name = getString(row, "column_name")
|
|
285
|
+
const type = duckDbTypeToDatasetColumnType(getString(row, "column_type"))
|
|
286
|
+
|
|
287
|
+
return { name, type }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function normalizePreviewRow(row: Readonly<Record<string, unknown>>): DatasetRow {
|
|
291
|
+
const normalized: Record<string, unknown> = {}
|
|
292
|
+
|
|
293
|
+
for (const [key, value] of Object.entries(row)) {
|
|
294
|
+
normalized[key] = normalizePreviewValue(value)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return normalized
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function normalizePreviewValue(value: unknown): unknown {
|
|
301
|
+
if (typeof value === "bigint") {
|
|
302
|
+
return value.toString()
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (value instanceof Date) {
|
|
306
|
+
return value.toISOString()
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (Array.isArray(value)) {
|
|
310
|
+
return value.map((item) => normalizePreviewValue(item))
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (typeof value === "object" && value !== null) {
|
|
314
|
+
const normalized: Record<string, unknown> = {}
|
|
315
|
+
for (const [key, child] of Object.entries(value)) {
|
|
316
|
+
normalized[key] = normalizePreviewValue(child)
|
|
317
|
+
}
|
|
318
|
+
return normalized
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return value
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function throwNoCommittedSourceVersion(datasetId: string): never {
|
|
325
|
+
throw new LakeStorageError(
|
|
326
|
+
`[SixbDuckLake] No committed version found for SQL transform source dataset '${datasetId}'.`
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function throwResultSchemaMismatch(datasetId: string, detail: string): never {
|
|
331
|
+
throw new LakeStorageError(
|
|
332
|
+
`[SixbDuckLake] SQL transform result schema does not match target dataset '${datasetId}': ${detail}.`
|
|
333
|
+
)
|
|
334
|
+
}
|