@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,440 @@
|
|
|
1
|
+
import { LakeStorageError } from "@sixb/core/lake-storage"
|
|
2
|
+
import type {
|
|
3
|
+
AzureSecretOptions,
|
|
4
|
+
DuckDbSecretOptions,
|
|
5
|
+
DuckLakeCatalogOptions,
|
|
6
|
+
DuckLakeStorageOptions,
|
|
7
|
+
GcsSecretOptions,
|
|
8
|
+
PostgresDuckLakeCatalogOptions,
|
|
9
|
+
R2SecretOptions,
|
|
10
|
+
S3SecretOptions,
|
|
11
|
+
} from "../types"
|
|
12
|
+
|
|
13
|
+
const DEFAULT_ALIAS = "sixb_lake"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolve the DuckLake attachment alias used by all provider SQL.
|
|
17
|
+
*/
|
|
18
|
+
export function duckLakeAlias(options: Pick<DuckLakeStorageOptions, "alias">): string {
|
|
19
|
+
return options.alias ?? DEFAULT_ALIAS
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* DuckLake exposes catalog metadata through an attached DuckDB database whose
|
|
24
|
+
* name is derived from the user-facing DuckLake alias.
|
|
25
|
+
*/
|
|
26
|
+
function duckLakeMetadataCatalog(options: Pick<DuckLakeStorageOptions, "alias">): string {
|
|
27
|
+
return `__ducklake_metadata_${duckLakeAlias(options)}`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Configure the PostgreSQL extension pool used by DuckLake's attached metadata
|
|
32
|
+
* catalog. DuckDB worker threads can otherwise pin pooled Postgres connections
|
|
33
|
+
* in the thread-local cache until the pool is exhausted during repeated
|
|
34
|
+
* DuckLake metadata reads.
|
|
35
|
+
*/
|
|
36
|
+
export function buildConfigurePostgresMetadataPoolSql(
|
|
37
|
+
options: Pick<DuckLakeStorageOptions, "alias" | "postgresPool">
|
|
38
|
+
): string {
|
|
39
|
+
const parameters = [
|
|
40
|
+
`catalog_name=${quoteSqlString(duckLakeMetadataCatalog(options))}`,
|
|
41
|
+
...postgresPoolParameters(options.postgresPool).map(({ name, value }) => `${name}=${value}`),
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
return `FROM postgres_configure_pool(${parameters.join(", ")})`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Configure DuckDB's PostgreSQL extension before ATTACH. These settings only
|
|
49
|
+
* affect databases attached after they are set.
|
|
50
|
+
*/
|
|
51
|
+
export function buildSetPostgresPoolSql(
|
|
52
|
+
options: Pick<DuckLakeStorageOptions, "postgresPool">
|
|
53
|
+
): readonly string[] {
|
|
54
|
+
return postgresPoolParameters(options.postgresPool).map(
|
|
55
|
+
({ name, value }) => `SET pg_pool_${name} = ${value}`
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Render a fully-qualified DuckLake metadata table path.
|
|
61
|
+
*
|
|
62
|
+
* PostgreSQL catalogs expose DuckLake metadata tables under the configured
|
|
63
|
+
* metadata schema inside the attached metadata catalog. Local catalogs use
|
|
64
|
+
* DuckDB's default `main` schema.
|
|
65
|
+
*/
|
|
66
|
+
export function duckLakeMetadataTableName(
|
|
67
|
+
options: Pick<DuckLakeStorageOptions, "alias" | "catalog">,
|
|
68
|
+
tableName: string
|
|
69
|
+
): string {
|
|
70
|
+
const schemaName =
|
|
71
|
+
options.catalog.type === "postgres" ? (options.catalog.metadataSchema ?? "main") : "main"
|
|
72
|
+
|
|
73
|
+
return `${quoteIdentifier(duckLakeMetadataCatalog(options))}.${quoteIdentifier(
|
|
74
|
+
schemaName
|
|
75
|
+
)}.${quoteIdentifier(tableName)}`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Quote a SQL identifier after validating it is already identifier-shaped.
|
|
80
|
+
*
|
|
81
|
+
* Dataset table names are produced by `names.ts`; user text should never be
|
|
82
|
+
* passed here without first going through a deterministic encoder.
|
|
83
|
+
*/
|
|
84
|
+
export function quoteIdentifier(identifier: string): string {
|
|
85
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
|
|
86
|
+
throw new LakeStorageError(`[SixbDuckLake] Invalid SQL identifier '${identifier}'.`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return `"${identifier}"`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Quote a SQL string literal for DDL statements that DuckDB does not parameterize.
|
|
94
|
+
*/
|
|
95
|
+
export function quoteSqlString(value: string): string {
|
|
96
|
+
return `'${value.replaceAll("'", "''")}'`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function quoteSqlStringList(values: readonly string[]): string {
|
|
100
|
+
return values.map((value) => quoteSqlString(value)).join(", ")
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Render a fully-qualified table path.
|
|
105
|
+
*/
|
|
106
|
+
export function qualifiedTableName(
|
|
107
|
+
options: Pick<DuckLakeStorageOptions, "alias">,
|
|
108
|
+
tableName: string
|
|
109
|
+
): string {
|
|
110
|
+
return `${quoteIdentifier(duckLakeAlias(options))}.main.${quoteIdentifier(tableName)}`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Build the catalog URI expected by DuckLake's ATTACH statement.
|
|
115
|
+
*/
|
|
116
|
+
export function catalogUri(catalog: DuckLakeCatalogOptions): string {
|
|
117
|
+
switch (catalog.type) {
|
|
118
|
+
case "duckdb":
|
|
119
|
+
return `ducklake:${catalog.path}`
|
|
120
|
+
case "sqlite":
|
|
121
|
+
return `ducklake:sqlite:${catalog.path}`
|
|
122
|
+
case "postgres":
|
|
123
|
+
return `ducklake:postgres:${postgresConnectionString(catalog)}`
|
|
124
|
+
case "custom":
|
|
125
|
+
return `ducklake:${catalog.uri}`
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Render typed PostgreSQL options to the libpq connection string DuckLake expects.
|
|
131
|
+
*/
|
|
132
|
+
export function postgresConnectionString(catalog: PostgresDuckLakeCatalogOptions): string {
|
|
133
|
+
const parameters: string[] = []
|
|
134
|
+
const names = new Set<string>()
|
|
135
|
+
|
|
136
|
+
appendPostgresConnectionParameter(parameters, names, "dbname", catalog.database)
|
|
137
|
+
appendPostgresConnectionParameter(parameters, names, "host", catalog.host)
|
|
138
|
+
appendPostgresConnectionParameter(parameters, names, "port", catalog.port)
|
|
139
|
+
appendPostgresConnectionParameter(parameters, names, "user", catalog.user)
|
|
140
|
+
appendPostgresConnectionParameter(parameters, names, "password", catalog.password)
|
|
141
|
+
appendPostgresConnectionParameter(parameters, names, "sslmode", catalog.sslMode)
|
|
142
|
+
appendPostgresConnectionParameter(parameters, names, "application_name", catalog.applicationName)
|
|
143
|
+
appendPostgresConnectionParameter(
|
|
144
|
+
parameters,
|
|
145
|
+
names,
|
|
146
|
+
"connect_timeout",
|
|
147
|
+
catalog.connectTimeoutSeconds
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
for (const [name, value] of Object.entries(catalog.parameters ?? {})) {
|
|
151
|
+
appendPostgresConnectionParameter(parameters, names, name, value)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return parameters.join(" ")
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Catalog extensions that must be available before ATTACH.
|
|
159
|
+
*/
|
|
160
|
+
function catalogExtensions(catalog: DuckLakeCatalogOptions): readonly string[] {
|
|
161
|
+
switch (catalog.type) {
|
|
162
|
+
case "duckdb":
|
|
163
|
+
return []
|
|
164
|
+
case "sqlite":
|
|
165
|
+
return ["sqlite"]
|
|
166
|
+
case "postgres":
|
|
167
|
+
return ["postgres"]
|
|
168
|
+
case "custom":
|
|
169
|
+
return catalog.extensions ?? []
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Extensions needed by the catalog, data path, and typed secrets.
|
|
175
|
+
*/
|
|
176
|
+
export function requiredExtensions(options: DuckLakeStorageOptions): readonly string[] {
|
|
177
|
+
const extensions = new Set<string>(catalogExtensions(options.catalog))
|
|
178
|
+
|
|
179
|
+
for (const extension of storageExtensions(options.dataPath)) {
|
|
180
|
+
extensions.add(extension)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const secret of options.secrets ?? []) {
|
|
184
|
+
for (const extension of secretExtensions(secret)) {
|
|
185
|
+
extensions.add(extension)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return [...extensions]
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Build a typed DuckDB CREATE SECRET statement.
|
|
194
|
+
*/
|
|
195
|
+
export function buildCreateSecretSql(secret: DuckDbSecretOptions): string {
|
|
196
|
+
const persistence = secret.persistent ? "PERSISTENT " : "TEMPORARY "
|
|
197
|
+
const nameSql = secret.name === undefined ? "" : `${quoteIdentifier(secret.name)} `
|
|
198
|
+
return `CREATE OR REPLACE ${persistence}SECRET ${nameSql}(${secretParameters(secret).join(", ")})`
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Build the DuckLake ATTACH statement from public provider options.
|
|
203
|
+
*/
|
|
204
|
+
export function buildAttachSql(options: DuckLakeStorageOptions): string {
|
|
205
|
+
const parameters: string[] = []
|
|
206
|
+
|
|
207
|
+
if (options.dataPath !== undefined) {
|
|
208
|
+
parameters.push(`DATA_PATH ${quoteSqlString(options.dataPath)}`)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
parameters.push(`CREATE_IF_NOT_EXISTS ${options.createIfNotExists ?? true}`)
|
|
212
|
+
|
|
213
|
+
if (options.readOnly) {
|
|
214
|
+
parameters.push("READ_ONLY")
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (options.catalog.type === "postgres" && options.catalog.metadataSchema !== undefined) {
|
|
218
|
+
parameters.push(`METADATA_SCHEMA ${quoteSqlString(options.catalog.metadataSchema)}`)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (
|
|
222
|
+
options.catalog.type === "custom" &&
|
|
223
|
+
options.catalog.metadataParameters !== undefined &&
|
|
224
|
+
Object.keys(options.catalog.metadataParameters).length > 0
|
|
225
|
+
) {
|
|
226
|
+
const entries = Object.entries(options.catalog.metadataParameters).map(
|
|
227
|
+
([key, value]) => `${quoteSqlString(key)}: ${quoteSqlString(value)}`
|
|
228
|
+
)
|
|
229
|
+
parameters.push(`METADATA_PARAMETERS MAP {${entries.join(", ")}}`)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return `ATTACH ${quoteSqlString(catalogUri(options.catalog))} AS ${quoteIdentifier(
|
|
233
|
+
duckLakeAlias(options)
|
|
234
|
+
)} (${parameters.join(", ")})`
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function storageExtensions(path: string | undefined): readonly string[] {
|
|
238
|
+
if (path === undefined) {
|
|
239
|
+
return []
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
switch (pathScheme(path)) {
|
|
243
|
+
case "s3":
|
|
244
|
+
case "r2":
|
|
245
|
+
case "gcs":
|
|
246
|
+
case "gs":
|
|
247
|
+
return ["httpfs"]
|
|
248
|
+
case "az":
|
|
249
|
+
case "azure":
|
|
250
|
+
case "abfss":
|
|
251
|
+
return ["azure"]
|
|
252
|
+
default:
|
|
253
|
+
return []
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The single source of truth for PostgreSQL pool options. Both the pre-attach
|
|
259
|
+
* `SET pg_pool_*` form and the post-attach `postgres_configure_pool(...)` form
|
|
260
|
+
* render from this list so the two stay in sync. `name` is the unprefixed
|
|
261
|
+
* option key; `value` is already rendered (numbers/booleans bare, strings
|
|
262
|
+
* quoted as SQL literals).
|
|
263
|
+
*/
|
|
264
|
+
function postgresPoolParameters(
|
|
265
|
+
pool: DuckLakeStorageOptions["postgresPool"]
|
|
266
|
+
): readonly { readonly name: string; readonly value: string }[] {
|
|
267
|
+
const parameters: { name: string; value: string }[] = [
|
|
268
|
+
{ name: "enable_thread_local_cache", value: String(pool?.enableThreadLocalCache ?? false) },
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
appendPoolNumber(parameters, "max_connections", pool?.maxConnections)
|
|
272
|
+
appendPoolNumber(parameters, "wait_timeout_millis", pool?.waitTimeoutMillis)
|
|
273
|
+
appendPoolNumber(parameters, "max_lifetime_millis", pool?.maxLifetimeMillis)
|
|
274
|
+
appendPoolNumber(parameters, "idle_timeout_millis", pool?.idleTimeoutMillis)
|
|
275
|
+
|
|
276
|
+
if (pool?.enableReaperThread !== undefined) {
|
|
277
|
+
parameters.push({ name: "enable_reaper_thread", value: String(pool.enableReaperThread) })
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (pool?.healthCheckQuery !== undefined) {
|
|
281
|
+
parameters.push({ name: "health_check_query", value: quoteSqlString(pool.healthCheckQuery) })
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return parameters
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function appendPoolNumber(
|
|
288
|
+
parameters: { name: string; value: string }[],
|
|
289
|
+
name: string,
|
|
290
|
+
value: number | undefined
|
|
291
|
+
): void {
|
|
292
|
+
if (value === undefined) {
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
parameters.push({ name, value: String(value) })
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function secretExtensions(secret: DuckDbSecretOptions): readonly string[] {
|
|
300
|
+
switch (secret.type) {
|
|
301
|
+
case "s3":
|
|
302
|
+
case "r2":
|
|
303
|
+
case "gcs":
|
|
304
|
+
return ["httpfs"]
|
|
305
|
+
case "azure":
|
|
306
|
+
return ["azure"]
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function secretParameters(secret: DuckDbSecretOptions): readonly string[] {
|
|
311
|
+
switch (secret.type) {
|
|
312
|
+
case "s3":
|
|
313
|
+
return withCommonSecretParameters(secret, [
|
|
314
|
+
"TYPE s3",
|
|
315
|
+
...s3ApiSecretParameters(secret),
|
|
316
|
+
...optionalStringParameter("KMS_KEY_ID", secret.kmsKeyId),
|
|
317
|
+
...optionalBooleanParameter("REQUESTER_PAYS", secret.requesterPays),
|
|
318
|
+
])
|
|
319
|
+
case "r2":
|
|
320
|
+
return withCommonSecretParameters(secret, [
|
|
321
|
+
"TYPE r2",
|
|
322
|
+
...s3ApiSecretParameters(secret),
|
|
323
|
+
...optionalStringParameter("ACCOUNT_ID", secret.accountId),
|
|
324
|
+
])
|
|
325
|
+
case "gcs":
|
|
326
|
+
return withCommonSecretParameters(secret, ["TYPE gcs", ...s3ApiSecretParameters(secret)])
|
|
327
|
+
case "azure":
|
|
328
|
+
return withCommonSecretParameters(secret, azureSecretParameters(secret))
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function s3ApiSecretParameters(
|
|
333
|
+
secret: S3SecretOptions | R2SecretOptions | GcsSecretOptions
|
|
334
|
+
): readonly string[] {
|
|
335
|
+
return [
|
|
336
|
+
...optionalIdentifierParameter("PROVIDER", secret.provider),
|
|
337
|
+
...optionalStringParameter("KEY_ID", secret.keyId),
|
|
338
|
+
...optionalStringParameter("SECRET", secret.secret),
|
|
339
|
+
...optionalStringParameter("SESSION_TOKEN", secret.sessionToken),
|
|
340
|
+
...optionalStringParameter("REGION", secret.region),
|
|
341
|
+
...optionalStringParameter("ENDPOINT", secret.endpoint),
|
|
342
|
+
...optionalStringParameter("URL_STYLE", secret.urlStyle),
|
|
343
|
+
...optionalBooleanParameter("USE_SSL", secret.useSsl),
|
|
344
|
+
...optionalBooleanParameter("VERIFY_SSL", secret.verifySsl),
|
|
345
|
+
...optionalStringParameter("CHAIN", secret.chain),
|
|
346
|
+
...optionalStringParameter("PROFILE", secret.profile),
|
|
347
|
+
]
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function azureSecretParameters(secret: AzureSecretOptions): readonly string[] {
|
|
351
|
+
if (
|
|
352
|
+
secret.tenantId !== undefined ||
|
|
353
|
+
secret.clientId !== undefined ||
|
|
354
|
+
secret.clientSecret !== undefined
|
|
355
|
+
) {
|
|
356
|
+
return [
|
|
357
|
+
"TYPE azure",
|
|
358
|
+
"PROVIDER service_principal",
|
|
359
|
+
...optionalStringParameter("TENANT_ID", secret.tenantId),
|
|
360
|
+
...optionalStringParameter("CLIENT_ID", secret.clientId),
|
|
361
|
+
...optionalStringParameter("CLIENT_SECRET", secret.clientSecret),
|
|
362
|
+
...optionalStringParameter("ACCOUNT_NAME", secret.accountName),
|
|
363
|
+
]
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const connectionString =
|
|
367
|
+
secret.connectionString ??
|
|
368
|
+
(secret.accountName !== undefined && secret.accountKey !== undefined
|
|
369
|
+
? `DefaultEndpointsProtocol=https;AccountName=${secret.accountName};AccountKey=${secret.accountKey};EndpointSuffix=core.windows.net`
|
|
370
|
+
: undefined)
|
|
371
|
+
|
|
372
|
+
if (connectionString !== undefined) {
|
|
373
|
+
return ["TYPE azure", ...optionalStringParameter("CONNECTION_STRING", connectionString)]
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return [
|
|
377
|
+
"TYPE azure",
|
|
378
|
+
"PROVIDER credential_chain",
|
|
379
|
+
...optionalStringParameter("ACCOUNT_NAME", secret.accountName),
|
|
380
|
+
]
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function withCommonSecretParameters(
|
|
384
|
+
secret: DuckDbSecretOptions,
|
|
385
|
+
parameters: readonly string[]
|
|
386
|
+
): readonly string[] {
|
|
387
|
+
return [...parameters, ...optionalStringParameter("SCOPE", secret.scope)]
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function optionalStringParameter(name: string, value: string | undefined): readonly string[] {
|
|
391
|
+
return value === undefined ? [] : [`${name} ${quoteSqlString(value)}`]
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function optionalBooleanParameter(name: string, value: boolean | undefined): readonly string[] {
|
|
395
|
+
return value === undefined ? [] : [`${name} ${value}`]
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function optionalIdentifierParameter(name: string, value: string | undefined): readonly string[] {
|
|
399
|
+
if (value === undefined) {
|
|
400
|
+
return []
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
|
|
404
|
+
throw new LakeStorageError(`[SixbDuckLake] Invalid SQL identifier '${value}'.`)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return [`${name} ${value}`]
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function appendPostgresConnectionParameter(
|
|
411
|
+
parameters: string[],
|
|
412
|
+
names: Set<string>,
|
|
413
|
+
name: string,
|
|
414
|
+
value: string | number | boolean | undefined
|
|
415
|
+
): void {
|
|
416
|
+
if (value === undefined) {
|
|
417
|
+
return
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
421
|
+
throw new LakeStorageError(`[SixbDuckLake] Invalid PostgreSQL catalog parameter '${name}'.`)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const normalizedName = name.toLowerCase()
|
|
425
|
+
if (names.has(normalizedName)) {
|
|
426
|
+
throw new LakeStorageError(`[SixbDuckLake] Duplicate PostgreSQL catalog parameter '${name}'.`)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
names.add(normalizedName)
|
|
430
|
+
parameters.push(`${name}=${quotePostgresConnectionValue(String(value))}`)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function quotePostgresConnectionValue(value: string): string {
|
|
434
|
+
return `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function pathScheme(path: string): string | null {
|
|
438
|
+
const match = /^([a-z][a-z0-9+.-]*):\/\//i.exec(path)
|
|
439
|
+
return match ? match[1].toLowerCase() : null
|
|
440
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { DatasetVersion } from "@sixb/core/lake-storage"
|
|
2
|
+
import { LakeStorageError } from "@sixb/core/lake-storage"
|
|
3
|
+
|
|
4
|
+
export interface SixbSchemaChangeMetadata {
|
|
5
|
+
readonly addColumns?: readonly string[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface SixbCommitMetadata {
|
|
9
|
+
readonly kind: "datasetVersion"
|
|
10
|
+
readonly datasetId: string
|
|
11
|
+
readonly commitId?: string
|
|
12
|
+
readonly mode?: DatasetVersion["mode"]
|
|
13
|
+
readonly producer?: DatasetVersion["producer"]
|
|
14
|
+
readonly inputs?: DatasetVersion["inputs"]
|
|
15
|
+
readonly rowCount?: number
|
|
16
|
+
readonly schemaChange?: SixbSchemaChangeMetadata
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function parseVersionId(versionId: string): string {
|
|
20
|
+
if (!versionId.startsWith("ducklake:")) {
|
|
21
|
+
throw new LakeStorageError(`[SixbDuckLake] Invalid DuckLake version id '${versionId}'.`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const snapshotId = versionId.slice("ducklake:".length)
|
|
25
|
+
if (!/^\d+$/.test(snapshotId)) {
|
|
26
|
+
throw new LakeStorageError(`[SixbDuckLake] Invalid DuckLake version id '${versionId}'.`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return snapshotId
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function toVersionId(snapshotId: string): string {
|
|
33
|
+
return `ducklake:${snapshotId}`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function parseCommitMetadata(value: unknown): SixbCommitMetadata | undefined {
|
|
37
|
+
if (typeof value !== "string") {
|
|
38
|
+
return undefined
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let parsed: unknown
|
|
42
|
+
try {
|
|
43
|
+
parsed = JSON.parse(value)
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (
|
|
49
|
+
!isRecord(parsed) ||
|
|
50
|
+
!isRecord(parsed.sixb) ||
|
|
51
|
+
parsed.sixb.kind !== "datasetVersion" ||
|
|
52
|
+
typeof parsed.sixb.datasetId !== "string"
|
|
53
|
+
) {
|
|
54
|
+
return undefined
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const mode = parsed.sixb.mode
|
|
58
|
+
const commitId = parsed.sixb.commitId
|
|
59
|
+
const rowCount = parsed.sixb.rowCount
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
kind: "datasetVersion",
|
|
63
|
+
datasetId: parsed.sixb.datasetId,
|
|
64
|
+
...(typeof commitId === "string" ? { commitId } : {}),
|
|
65
|
+
...(mode === "snapshot" || mode === "append" || mode === "schema" ? { mode } : {}),
|
|
66
|
+
...(isDatasetProducer(parsed.sixb.producer) ? { producer: parsed.sixb.producer } : {}),
|
|
67
|
+
...(isDatasetVersionRefs(parsed.sixb.inputs) ? { inputs: parsed.sixb.inputs } : {}),
|
|
68
|
+
...(typeof commitId === "string" && isRowCount(rowCount) ? { rowCount } : {}),
|
|
69
|
+
...(isSchemaChangeMetadata(parsed.sixb.schemaChange)
|
|
70
|
+
? { schemaChange: parsed.sixb.schemaChange }
|
|
71
|
+
: {}),
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function parseInlineDataChange(
|
|
76
|
+
changesMade: string,
|
|
77
|
+
tableId: bigint
|
|
78
|
+
): { readonly hasDataChange: boolean; readonly hasDeleteChange: boolean } {
|
|
79
|
+
const tableIdText = tableId.toString()
|
|
80
|
+
let hasDataChange = false
|
|
81
|
+
let hasDeleteChange = false
|
|
82
|
+
|
|
83
|
+
for (const change of changesMade.split(",")) {
|
|
84
|
+
const [kind, changedTableId] = change.split(":")
|
|
85
|
+
if (changedTableId !== tableIdText) {
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const normalizedKind = kind.toLowerCase()
|
|
90
|
+
if (normalizedKind.includes("insert") || normalizedKind.includes("delete")) {
|
|
91
|
+
hasDataChange = true
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (normalizedKind.includes("delete")) {
|
|
95
|
+
hasDeleteChange = true
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { hasDataChange, hasDeleteChange }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
103
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isDatasetProducer(value: unknown): value is NonNullable<DatasetVersion["producer"]> {
|
|
107
|
+
return (
|
|
108
|
+
isRecord(value) &&
|
|
109
|
+
(value.kind === "sync" || value.kind === "pipeline") &&
|
|
110
|
+
optionalString(value.id) &&
|
|
111
|
+
optionalString(value.runId)
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isDatasetVersionRefs(value: unknown): value is NonNullable<DatasetVersion["inputs"]> {
|
|
116
|
+
return (
|
|
117
|
+
Array.isArray(value) &&
|
|
118
|
+
value.every(
|
|
119
|
+
(item) =>
|
|
120
|
+
isRecord(item) && typeof item.datasetId === "string" && typeof item.versionId === "string"
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isSchemaChangeMetadata(value: unknown): value is SixbSchemaChangeMetadata {
|
|
126
|
+
return (
|
|
127
|
+
isRecord(value) &&
|
|
128
|
+
(value.addColumns === undefined ||
|
|
129
|
+
(Array.isArray(value.addColumns) &&
|
|
130
|
+
value.addColumns.every((columnName) => typeof columnName === "string")))
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function optionalString(value: unknown): boolean {
|
|
135
|
+
return value === undefined || typeof value === "string"
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isRowCount(value: unknown): value is number {
|
|
139
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0
|
|
140
|
+
}
|