@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,111 @@
|
|
|
1
|
+
import { LakeStorageError } from "@sixb/core/lake-storage"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Prefix used for all Sixb dataset tables inside the DuckLake schema.
|
|
5
|
+
*/
|
|
6
|
+
export const DATASET_TABLE_PREFIX = "sixb__ds__"
|
|
7
|
+
|
|
8
|
+
const textEncoder = new TextEncoder()
|
|
9
|
+
const textDecoder = new TextDecoder("utf-8", { fatal: true })
|
|
10
|
+
|
|
11
|
+
function isInlineByte(byte: number): boolean {
|
|
12
|
+
return (byte >= 0x61 && byte <= 0x7a) || (byte >= 0x30 && byte <= 0x39)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isHexPair(value: string): boolean {
|
|
16
|
+
return /^[0-9a-f]{2}$/.test(value)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Encode a Sixb dataset id into a SQL identifier-safe table suffix.
|
|
21
|
+
*
|
|
22
|
+
* Common lowercase dotted ids stay readable by mapping "." to "__". Literal
|
|
23
|
+
* underscores and all other non-inline UTF-8 bytes are encoded as "_xx" hex
|
|
24
|
+
* escapes so decoding is collision-free.
|
|
25
|
+
*/
|
|
26
|
+
function encodeDatasetId(datasetId: string): string {
|
|
27
|
+
if (datasetId.trim().length === 0) {
|
|
28
|
+
throw new LakeStorageError("[SixbDuckLake] Dataset id must not be empty.")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let encoded = ""
|
|
32
|
+
for (const byte of textEncoder.encode(datasetId)) {
|
|
33
|
+
if (isInlineByte(byte)) {
|
|
34
|
+
encoded += String.fromCharCode(byte)
|
|
35
|
+
} else if (byte === 0x2e) {
|
|
36
|
+
encoded += "__"
|
|
37
|
+
} else {
|
|
38
|
+
encoded += `_${byte.toString(16).padStart(2, "0")}`
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return encoded
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Decode a table suffix created by `encodeDatasetId`.
|
|
47
|
+
*
|
|
48
|
+
* Returns null for malformed provider table names so metadata scans can filter
|
|
49
|
+
* defensively instead of throwing on unrelated tables.
|
|
50
|
+
*/
|
|
51
|
+
function decodeDatasetId(encoded: string): string | null {
|
|
52
|
+
if (encoded.length === 0) {
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const bytes: number[] = []
|
|
57
|
+
let index = 0
|
|
58
|
+
|
|
59
|
+
while (index < encoded.length) {
|
|
60
|
+
const char = encoded[index]
|
|
61
|
+
const byte = char.charCodeAt(0)
|
|
62
|
+
|
|
63
|
+
if (isInlineByte(byte)) {
|
|
64
|
+
bytes.push(byte)
|
|
65
|
+
index += 1
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (char !== "_") {
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (encoded[index + 1] === "_") {
|
|
74
|
+
bytes.push(0x2e)
|
|
75
|
+
index += 2
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const hex = encoded.slice(index + 1, index + 3)
|
|
80
|
+
if (!isHexPair(hex)) {
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
bytes.push(Number.parseInt(hex, 16))
|
|
85
|
+
index += 3
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
return textDecoder.decode(new Uint8Array(bytes))
|
|
90
|
+
} catch {
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Convert a Sixb dataset id to its physical DuckLake table name.
|
|
97
|
+
*/
|
|
98
|
+
export function encodeDatasetTableName(datasetId: string): string {
|
|
99
|
+
return `${DATASET_TABLE_PREFIX}${encodeDatasetId(datasetId)}`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Decode a physical DuckLake table name back to a Sixb dataset id.
|
|
104
|
+
*/
|
|
105
|
+
export function decodeDatasetTableName(tableName: string): string | null {
|
|
106
|
+
if (!tableName.startsWith(DATASET_TABLE_PREFIX)) {
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return decodeDatasetId(tableName.slice(DATASET_TABLE_PREFIX.length))
|
|
111
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { DuckDBValue } from "@duckdb/node-api"
|
|
2
|
+
import type { DatasetColumnDefinition, DatasetRow, DatasetSchema, FileRef } from "@sixb/core"
|
|
3
|
+
import { normalizeDuckDbDecimalValue } from "./duckdb-decimal"
|
|
4
|
+
import type { DuckDbAppender } from "./duckdb-runtime"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Append one validated dataset row into the staging table's DuckDB appender.
|
|
8
|
+
*/
|
|
9
|
+
export function appendDatasetRow(
|
|
10
|
+
appender: DuckDbAppender,
|
|
11
|
+
schema: DatasetSchema,
|
|
12
|
+
row: DatasetRow
|
|
13
|
+
): void {
|
|
14
|
+
for (const column of schema.columns) {
|
|
15
|
+
appendColumnValue(appender, column, row[column.name] ?? null)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
appender.endRow()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function appendColumnValue(
|
|
22
|
+
appender: DuckDbAppender,
|
|
23
|
+
column: DatasetColumnDefinition,
|
|
24
|
+
value: unknown
|
|
25
|
+
): void {
|
|
26
|
+
if (value === null || value === undefined) {
|
|
27
|
+
appender.appendNull()
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
switch (column.type) {
|
|
32
|
+
case "string":
|
|
33
|
+
appender.appendVarchar(value as string)
|
|
34
|
+
return
|
|
35
|
+
case "boolean":
|
|
36
|
+
appender.appendBoolean(value as boolean)
|
|
37
|
+
return
|
|
38
|
+
case "int64":
|
|
39
|
+
appender.appendBigInt(typeof value === "bigint" ? value : BigInt(value as string | number))
|
|
40
|
+
return
|
|
41
|
+
case "float64":
|
|
42
|
+
appender.appendDouble(value as number)
|
|
43
|
+
return
|
|
44
|
+
case "decimal":
|
|
45
|
+
appender.appendVarchar(normalizeDuckDbDecimalValue(value, column.name))
|
|
46
|
+
return
|
|
47
|
+
case "date":
|
|
48
|
+
appender.appendVarchar(
|
|
49
|
+
value instanceof Date ? value.toISOString().slice(0, 10) : String(value)
|
|
50
|
+
)
|
|
51
|
+
return
|
|
52
|
+
case "timestamp":
|
|
53
|
+
appender.appendVarchar(value instanceof Date ? value.toISOString() : String(value))
|
|
54
|
+
return
|
|
55
|
+
case "json":
|
|
56
|
+
appender.appendVarchar(JSON.stringify(value))
|
|
57
|
+
return
|
|
58
|
+
case "fileRef":
|
|
59
|
+
appender.appendStruct(fileRefToDuckDbStruct(value as FileRef))
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function fileRefToDuckDbStruct(fileRef: FileRef): Readonly<Record<string, DuckDBValue>> {
|
|
65
|
+
return {
|
|
66
|
+
blobId: fileRef.blobId,
|
|
67
|
+
digest: fileRef.digest,
|
|
68
|
+
sizeBytes: BigInt(fileRef.sizeBytes),
|
|
69
|
+
fileName: fileRef.fileName ?? null,
|
|
70
|
+
mediaType: fileRef.mediaType ?? null,
|
|
71
|
+
logicalPath: fileRef.logicalPath ?? null,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { DatasetColumnDefinition, DatasetColumnType, DatasetSchema, FileRef } from "@sixb/core"
|
|
2
|
+
import { LakeStorageError } from "@sixb/core/lake-storage"
|
|
3
|
+
import { DUCKDB_COLUMN_TYPES, findDatasetColumnTypeForDuckDbSql } from "./duckdb-column-types"
|
|
4
|
+
import { normalizeDuckDbDecimalValue } from "./duckdb-decimal"
|
|
5
|
+
import { quoteIdentifier } from "./sql"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Normalized column metadata read from DuckDB/DuckLake introspection queries.
|
|
9
|
+
*/
|
|
10
|
+
export interface DuckDbColumnMetadata {
|
|
11
|
+
readonly name: string
|
|
12
|
+
readonly type: string
|
|
13
|
+
readonly nullable: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Map a Sixb dataset column type to the DuckDB type used in CREATE TABLE.
|
|
18
|
+
*/
|
|
19
|
+
export function datasetColumnTypeToDuckDbSql(type: DatasetColumnType): string {
|
|
20
|
+
return DUCKDB_COLUMN_TYPES[type].sql
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Reconstruct a Sixb dataset column type from DuckDB column metadata.
|
|
25
|
+
*/
|
|
26
|
+
export function duckDbTypeToDatasetColumnType(typeSql: string): DatasetColumnType {
|
|
27
|
+
const type = findDatasetColumnTypeForDuckDbSql(typeSql)
|
|
28
|
+
if (type) {
|
|
29
|
+
return type
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
throw new LakeStorageError(
|
|
33
|
+
`[SixbDuckLake] DuckDB column type '${typeSql}' cannot be mapped to a Sixb dataset column type.`
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Render one Sixb dataset column as DuckDB DDL.
|
|
39
|
+
*/
|
|
40
|
+
export function datasetColumnToDuckDbSql(column: DatasetColumnDefinition): string {
|
|
41
|
+
const nullableSql = column.nullable ? "" : " NOT NULL"
|
|
42
|
+
return `${quoteIdentifier(column.name)} ${datasetColumnTypeToDuckDbSql(column.type)}${nullableSql}`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Render the column list used by CREATE TABLE and staging table DDL.
|
|
47
|
+
*/
|
|
48
|
+
export function datasetSchemaToDuckDbColumnsSql(schema: DatasetSchema): string {
|
|
49
|
+
return schema.columns.map((column) => datasetColumnToDuckDbSql(column)).join(", ")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Render the column list used by INSERT ... SELECT operations.
|
|
54
|
+
*/
|
|
55
|
+
export function datasetSchemaColumnNamesSql(schema: DatasetSchema): string {
|
|
56
|
+
return schema.columns.map((column) => quoteIdentifier(column.name)).join(", ")
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Convert DuckDB column metadata back into a Sixb dataset schema.
|
|
61
|
+
*/
|
|
62
|
+
export function duckDbColumnsToDatasetSchema(
|
|
63
|
+
columns: readonly DuckDbColumnMetadata[]
|
|
64
|
+
): DatasetSchema {
|
|
65
|
+
return {
|
|
66
|
+
columns: columns.map((column) => ({
|
|
67
|
+
name: column.name,
|
|
68
|
+
type: duckDbTypeToDatasetColumnType(column.type),
|
|
69
|
+
...(column.nullable ? { nullable: true } : {}),
|
|
70
|
+
})),
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function normalizeReadValue(value: unknown, column: DatasetColumnDefinition): unknown {
|
|
75
|
+
if (value === null || value === undefined) {
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
switch (column.type) {
|
|
80
|
+
case "int64":
|
|
81
|
+
return typeof value === "bigint" ? value.toString() : value
|
|
82
|
+
case "decimal":
|
|
83
|
+
return normalizeDuckDbDecimalValue(value, column.name)
|
|
84
|
+
case "date":
|
|
85
|
+
return value instanceof Date ? value.toISOString().slice(0, 10) : String(value)
|
|
86
|
+
case "timestamp":
|
|
87
|
+
return value instanceof Date ? value.toISOString() : String(value)
|
|
88
|
+
case "json":
|
|
89
|
+
return typeof value === "string" ? JSON.parse(value) : value
|
|
90
|
+
case "fileRef":
|
|
91
|
+
return normalizeFileRef(value)
|
|
92
|
+
default:
|
|
93
|
+
return value
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeFileRef(value: unknown): FileRef {
|
|
98
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
99
|
+
throw new LakeStorageError("[SixbDuckLake] DuckDB fileRef value must be a struct.")
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const record = value as Record<string, unknown>
|
|
103
|
+
return {
|
|
104
|
+
blobId: getStringField(record, "blobId"),
|
|
105
|
+
digest: getStringField(record, "digest") as FileRef["digest"],
|
|
106
|
+
sizeBytes: getIntegerField(record, "sizeBytes"),
|
|
107
|
+
fileName: getOptionalStringField(record, "fileName"),
|
|
108
|
+
mediaType: getOptionalStringField(record, "mediaType"),
|
|
109
|
+
logicalPath: getOptionalStringField(record, "logicalPath"),
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getStringField(record: Readonly<Record<string, unknown>>, key: string): string {
|
|
114
|
+
const value = record[key]
|
|
115
|
+
if (typeof value !== "string") {
|
|
116
|
+
throw new LakeStorageError(`[SixbDuckLake] DuckDB fileRef field '${key}' must be a string.`)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return value
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getOptionalStringField(
|
|
123
|
+
record: Readonly<Record<string, unknown>>,
|
|
124
|
+
key: string
|
|
125
|
+
): string | undefined {
|
|
126
|
+
const value = record[key]
|
|
127
|
+
if (value === null || value === undefined) {
|
|
128
|
+
return undefined
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (typeof value !== "string") {
|
|
132
|
+
throw new LakeStorageError(`[SixbDuckLake] DuckDB fileRef field '${key}' must be a string.`)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return value
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function getIntegerField(record: Readonly<Record<string, unknown>>, key: string): number {
|
|
139
|
+
const value = record[key]
|
|
140
|
+
if (typeof value === "number" && Number.isInteger(value)) {
|
|
141
|
+
return value
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (typeof value === "bigint") {
|
|
145
|
+
return Number(value)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
throw new LakeStorageError(`[SixbDuckLake] DuckDB fileRef field '${key}' must be an integer.`)
|
|
149
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto"
|
|
2
|
+
import {
|
|
3
|
+
LakeStorageError,
|
|
4
|
+
type SqlTransformBody,
|
|
5
|
+
type SqlTransformRelation,
|
|
6
|
+
} from "@sixb/core/lake-storage"
|
|
7
|
+
import type { DuckLakeStorageOptions } from "../types"
|
|
8
|
+
import { encodeDatasetTableName } from "./names"
|
|
9
|
+
import { qualifiedTableName } from "./sql"
|
|
10
|
+
import { parseVersionId } from "./versions"
|
|
11
|
+
|
|
12
|
+
const RELATION_PLACEHOLDER_PREFIX = "__sixb_sql_transform_relation_"
|
|
13
|
+
const RELATION_PLACEHOLDER_PATTERN = /__sixb_sql_transform_relation_[0-9a-f]{32}_[0-9]+__/g
|
|
14
|
+
|
|
15
|
+
export interface DuckLakeSqlTransformSourceRelation {
|
|
16
|
+
readonly datasetId: string
|
|
17
|
+
readonly versionId: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface RenderDuckLakeSqlTransformSqlInput {
|
|
21
|
+
readonly options: Pick<DuckLakeStorageOptions, "alias">
|
|
22
|
+
readonly sources: Readonly<Record<string, DuckLakeSqlTransformSourceRelation>>
|
|
23
|
+
readonly sql: SqlTransformBody<"duckdb">
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface DuckLakeSqlTransformRelationContext {
|
|
27
|
+
readonly relations: Readonly<Record<string, SqlTransformRelation>>
|
|
28
|
+
render(sql: string): string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class DuckLakeSqlTransformRelation implements SqlTransformRelation {
|
|
32
|
+
constructor(private readonly placeholder: string) {}
|
|
33
|
+
|
|
34
|
+
toString(): string {
|
|
35
|
+
return this.placeholder
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function renderDuckLakeSqlTransformSql(input: RenderDuckLakeSqlTransformSqlInput): string {
|
|
40
|
+
const context = createDuckLakeSqlTransformRelationContext(input)
|
|
41
|
+
const sql = input.sql(context.relations)
|
|
42
|
+
|
|
43
|
+
if (typeof sql !== "string") {
|
|
44
|
+
throw new LakeStorageError("[SixbDuckLake] SQL transform body must return a SQL string.")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return context.render(sql)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function createDuckLakeSqlTransformRelationContext(
|
|
51
|
+
input: Omit<RenderDuckLakeSqlTransformSqlInput, "sql">
|
|
52
|
+
): DuckLakeSqlTransformRelationContext {
|
|
53
|
+
const token = randomUUID().replaceAll("-", "")
|
|
54
|
+
const relations: Record<string, SqlTransformRelation> = Object.create(null)
|
|
55
|
+
const replacements = new Map<string, string>()
|
|
56
|
+
|
|
57
|
+
let index = 0
|
|
58
|
+
for (const [sourceName, source] of Object.entries(input.sources)) {
|
|
59
|
+
const placeholder = `${RELATION_PLACEHOLDER_PREFIX}${token}_${index}__`
|
|
60
|
+
relations[sourceName] = Object.freeze(new DuckLakeSqlTransformRelation(placeholder))
|
|
61
|
+
replacements.set(placeholder, renderDuckLakeSourceRelation(input.options, source))
|
|
62
|
+
index += 1
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
relations: Object.freeze(relations),
|
|
67
|
+
render(sql) {
|
|
68
|
+
const rendered = sql.replace(RELATION_PLACEHOLDER_PATTERN, (placeholder) => {
|
|
69
|
+
const replacement = replacements.get(placeholder)
|
|
70
|
+
if (replacement === undefined) {
|
|
71
|
+
throwUnresolvedPlaceholder()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return replacement
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
if (rendered.includes(RELATION_PLACEHOLDER_PREFIX)) {
|
|
78
|
+
throwUnresolvedPlaceholder()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return rendered
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function renderDuckLakeSourceRelation(
|
|
87
|
+
options: Pick<DuckLakeStorageOptions, "alias">,
|
|
88
|
+
source: DuckLakeSqlTransformSourceRelation
|
|
89
|
+
): string {
|
|
90
|
+
const snapshotId = parseVersionId(source.versionId)
|
|
91
|
+
const tableName = encodeDatasetTableName(source.datasetId)
|
|
92
|
+
return `(SELECT * FROM ${qualifiedTableName(options, tableName)} AT (VERSION => ${snapshotId}))`
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function throwUnresolvedPlaceholder(): never {
|
|
96
|
+
throw new LakeStorageError(
|
|
97
|
+
"[SixbDuckLake] SQL transform contains an unresolved relation placeholder."
|
|
98
|
+
)
|
|
99
|
+
}
|