@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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +490 -0
  3. package/dist/ducklake-storage.d.ts +42 -0
  4. package/dist/ducklake-storage.d.ts.map +1 -0
  5. package/dist/index.d.ts +3 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +3112 -0
  8. package/dist/index.js.map +34 -0
  9. package/dist/internal/catalog-key.d.ts +11 -0
  10. package/dist/internal/catalog-key.d.ts.map +1 -0
  11. package/dist/internal/dataset-row-commit.d.ts +26 -0
  12. package/dist/internal/dataset-row-commit.d.ts.map +1 -0
  13. package/dist/internal/duckdb-column-types.d.ts +45 -0
  14. package/dist/internal/duckdb-column-types.d.ts.map +1 -0
  15. package/dist/internal/duckdb-decimal.d.ts +6 -0
  16. package/dist/internal/duckdb-decimal.d.ts.map +1 -0
  17. package/dist/internal/duckdb-row.d.ts +8 -0
  18. package/dist/internal/duckdb-row.d.ts.map +1 -0
  19. package/dist/internal/duckdb-runtime.d.ts +63 -0
  20. package/dist/internal/duckdb-runtime.d.ts.map +1 -0
  21. package/dist/internal/duckdb-value-converter.d.ts +7 -0
  22. package/dist/internal/duckdb-value-converter.d.ts.map +1 -0
  23. package/dist/internal/ducklake-catalog-schema.d.ts +12 -0
  24. package/dist/internal/ducklake-catalog-schema.d.ts.map +1 -0
  25. package/dist/internal/ducklake-connection-manager.d.ts +67 -0
  26. package/dist/internal/ducklake-connection-manager.d.ts.map +1 -0
  27. package/dist/internal/ducklake-current-dataset-definitions.d.ts +10 -0
  28. package/dist/internal/ducklake-current-dataset-definitions.d.ts.map +1 -0
  29. package/dist/internal/ducklake-dataset-catalog.d.ts +33 -0
  30. package/dist/internal/ducklake-dataset-catalog.d.ts.map +1 -0
  31. package/dist/internal/ducklake-dataset-table-ref.d.ts +9 -0
  32. package/dist/internal/ducklake-dataset-table-ref.d.ts.map +1 -0
  33. package/dist/internal/ducklake-maintenance.d.ts +9 -0
  34. package/dist/internal/ducklake-maintenance.d.ts.map +1 -0
  35. package/dist/internal/ducklake-row-reader.d.ts +24 -0
  36. package/dist/internal/ducklake-row-reader.d.ts.map +1 -0
  37. package/dist/internal/ducklake-snapshot-reader.d.ts +73 -0
  38. package/dist/internal/ducklake-snapshot-reader.d.ts.map +1 -0
  39. package/dist/internal/ducklake-sql-executor.d.ts +33 -0
  40. package/dist/internal/ducklake-sql-executor.d.ts.map +1 -0
  41. package/dist/internal/ducklake-write-coordinator.d.ts +53 -0
  42. package/dist/internal/ducklake-write-coordinator.d.ts.map +1 -0
  43. package/dist/internal/ducklake-write-session.d.ts +17 -0
  44. package/dist/internal/ducklake-write-session.d.ts.map +1 -0
  45. package/dist/internal/names.d.ts +13 -0
  46. package/dist/internal/names.d.ts.map +1 -0
  47. package/dist/internal/row-appender.d.ts +7 -0
  48. package/dist/internal/row-appender.d.ts.map +1 -0
  49. package/dist/internal/schema.d.ts +35 -0
  50. package/dist/internal/schema.d.ts.map +1 -0
  51. package/dist/internal/sql-transform-relations.d.ts +13 -0
  52. package/dist/internal/sql-transform-relations.d.ts.map +1 -0
  53. package/dist/internal/sql.d.ts +62 -0
  54. package/dist/internal/sql.d.ts.map +1 -0
  55. package/dist/internal/versions.d.ts +22 -0
  56. package/dist/internal/versions.d.ts.map +1 -0
  57. package/dist/tsconfig.json +5 -0
  58. package/dist/types.d.ts +147 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/package.json +64 -0
  61. package/src/ducklake-storage.ts +130 -0
  62. package/src/index.ts +14 -0
  63. package/src/internal/catalog-key.ts +28 -0
  64. package/src/internal/dataset-row-commit.ts +128 -0
  65. package/src/internal/duckdb-column-types.ts +68 -0
  66. package/src/internal/duckdb-decimal.ts +36 -0
  67. package/src/internal/duckdb-row.ts +83 -0
  68. package/src/internal/duckdb-runtime.ts +347 -0
  69. package/src/internal/duckdb-value-converter.ts +27 -0
  70. package/src/internal/ducklake-catalog-schema.ts +116 -0
  71. package/src/internal/ducklake-connection-manager.ts +510 -0
  72. package/src/internal/ducklake-current-dataset-definitions.ts +228 -0
  73. package/src/internal/ducklake-dataset-catalog.ts +363 -0
  74. package/src/internal/ducklake-dataset-table-ref.ts +35 -0
  75. package/src/internal/ducklake-maintenance.ts +116 -0
  76. package/src/internal/ducklake-row-reader.ts +143 -0
  77. package/src/internal/ducklake-snapshot-reader.ts +826 -0
  78. package/src/internal/ducklake-sql-executor.ts +334 -0
  79. package/src/internal/ducklake-write-coordinator.ts +532 -0
  80. package/src/internal/ducklake-write-session.ts +170 -0
  81. package/src/internal/names.ts +111 -0
  82. package/src/internal/row-appender.ts +73 -0
  83. package/src/internal/schema.ts +149 -0
  84. package/src/internal/sql-transform-relations.ts +99 -0
  85. package/src/internal/sql.ts +440 -0
  86. package/src/internal/versions.ts +140 -0
  87. 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
+ }