@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,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
+ }