@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,116 @@
1
+ import { LakeStorageError } from "@sixb/core/lake-storage"
2
+ import type {
3
+ DuckLakeMaintenanceOptions,
4
+ DuckLakeMaintenanceReport,
5
+ DuckLakeStorageOptions,
6
+ } from "../types"
7
+ import type { DuckDbQueryRuntime } from "./duckdb-runtime"
8
+ import type { DuckLakeConnectionManager } from "./ducklake-connection-manager"
9
+ import { duckLakeAlias, quoteSqlString } from "./sql"
10
+
11
+ const DEFAULT_RETENTION = "7 days"
12
+
13
+ interface MaintenanceCounts {
14
+ readonly snapshots: number
15
+ readonly oldFiles: number
16
+ readonly orphanedFiles: number
17
+ }
18
+
19
+ export class DuckLakeMaintenance {
20
+ constructor(
21
+ private readonly options: DuckLakeStorageOptions,
22
+ private readonly connections: DuckLakeConnectionManager
23
+ ) {}
24
+
25
+ async runMaintenance(
26
+ options: DuckLakeMaintenanceOptions = {}
27
+ ): Promise<DuckLakeMaintenanceReport> {
28
+ this.connections.assertOpen()
29
+
30
+ const expireOlderThan = options.expireOlderThan ?? DEFAULT_RETENTION
31
+ const deleteOlderThan = options.deleteOlderThan ?? expireOlderThan
32
+ const dryRun = options.dryRun ?? false
33
+
34
+ if (this.options.readOnly && !dryRun) {
35
+ throw new LakeStorageError(
36
+ "[SixbDuckLake] DuckLake maintenance cannot run in read-only mode."
37
+ )
38
+ }
39
+
40
+ return this.connections.withExclusiveAttached(async (runtime) => {
41
+ const counts = await runMaintenanceProcedures(
42
+ runtime,
43
+ this.options,
44
+ {
45
+ expireOlderThan,
46
+ deleteOlderThan,
47
+ },
48
+ dryRun
49
+ )
50
+
51
+ if (!dryRun) {
52
+ this.connections.markLocalCatalogChanged()
53
+ }
54
+
55
+ return {
56
+ dryRun,
57
+ expireOlderThan,
58
+ deleteOlderThan,
59
+ ...counts,
60
+ }
61
+ })
62
+ }
63
+ }
64
+
65
+ async function runMaintenanceProcedures(
66
+ runtime: DuckDbQueryRuntime,
67
+ options: DuckLakeStorageOptions,
68
+ retention: Pick<DuckLakeMaintenanceReport, "expireOlderThan" | "deleteOlderThan">,
69
+ dryRun: boolean
70
+ ): Promise<MaintenanceCounts> {
71
+ const aliasSql = quoteSqlString(duckLakeAlias(options))
72
+ const snapshots = await runtime.query(
73
+ maintenanceProcedureSql(
74
+ "ducklake_expire_snapshots",
75
+ aliasSql,
76
+ retention.expireOlderThan,
77
+ dryRun
78
+ )
79
+ )
80
+ const oldFiles = await runtime.query(
81
+ maintenanceProcedureSql(
82
+ "ducklake_cleanup_old_files",
83
+ aliasSql,
84
+ retention.deleteOlderThan,
85
+ dryRun
86
+ )
87
+ )
88
+ const orphanedFiles = await runtime.query(
89
+ maintenanceProcedureSql(
90
+ "ducklake_delete_orphaned_files",
91
+ aliasSql,
92
+ retention.deleteOlderThan,
93
+ dryRun
94
+ )
95
+ )
96
+
97
+ return {
98
+ snapshots: snapshots.length,
99
+ oldFiles: oldFiles.length,
100
+ orphanedFiles: orphanedFiles.length,
101
+ }
102
+ }
103
+
104
+ function maintenanceProcedureSql(
105
+ procedure: string,
106
+ aliasSql: string,
107
+ olderThan: string,
108
+ dryRun: boolean
109
+ ): string {
110
+ const olderThanSql = quoteSqlString(olderThan)
111
+ const dryRunSql = dryRun ? "dry_run => true, " : ""
112
+ return (
113
+ `CALL ${procedure}(${aliasSql}, ${dryRunSql}` +
114
+ `older_than => now() - INTERVAL ${olderThanSql})`
115
+ )
116
+ }
@@ -0,0 +1,143 @@
1
+ import type { DatasetColumnDefinition, DatasetRow } from "@sixb/core"
2
+ import type { DatasetVersion, ReadDatasetRowsInput } from "@sixb/core/lake-storage"
3
+ import { LakeStorageError } from "@sixb/core/lake-storage"
4
+ import type { DuckLakeStorageOptions } from "../types"
5
+ import type { DuckDbQueryRuntime } from "./duckdb-runtime"
6
+ import type { DuckLakeConnectionManager } from "./ducklake-connection-manager"
7
+ import { type DatasetTableRef, resolveDatasetTableRef } from "./ducklake-dataset-table-ref"
8
+ import type { DuckLakeSnapshotReader } from "./ducklake-snapshot-reader"
9
+ import { normalizeReadValue } from "./schema"
10
+ import { qualifiedTableName, quoteIdentifier } from "./sql"
11
+ import { parseVersionId } from "./versions"
12
+
13
+ /**
14
+ * Reads dataset rows from DuckLake snapshots.
15
+ *
16
+ * This module owns the last mile of reads: resolve the requested Sixb version
17
+ * to a DuckLake snapshot id, render a DuckLake time-travel query, and normalize
18
+ * DuckDB values back into Sixb row shape.
19
+ */
20
+ export class DuckLakeRowReader {
21
+ constructor(
22
+ private readonly options: DuckLakeStorageOptions,
23
+ private readonly connections: DuckLakeConnectionManager,
24
+ private readonly snapshots: DuckLakeSnapshotReader
25
+ ) {}
26
+
27
+ async *readRows(input: ReadDatasetRowsInput): AsyncIterable<DatasetRow> {
28
+ this.connections.assertOpen()
29
+
30
+ const lease = await this.connections.acquireAttachedRuntime()
31
+ try {
32
+ const runtime = lease.runtime
33
+
34
+ // Step 1: resolve the physical table from DuckLake metadata directly. Row
35
+ // previews must not go through broad catalog introspection.
36
+ const tableRef = await resolveDatasetTableRef(this.options, runtime, input.datasetId)
37
+ if (!tableRef) {
38
+ throw new LakeStorageError(`[SixbDuckLake] Unknown dataset '${input.datasetId}'.`)
39
+ }
40
+
41
+ const version = await this.resolveVersion(runtime, tableRef, input.versionId)
42
+ const snapshotId = parseVersionId(version.versionId)
43
+ const selectedColumns = this.resolveReadColumns(
44
+ tableRef.datasetId,
45
+ version.schema,
46
+ input.columns
47
+ )
48
+
49
+ // Step 2: query DuckLake at the exact snapshot id. Version ids are just
50
+ // `ducklake:<snapshot_id>`, so reads can use native DuckLake time travel
51
+ // directly after validation.
52
+ const columnsSql = selectedColumns.map((column) => quoteIdentifier(column.name)).join(", ")
53
+ const tableSql = qualifiedTableName(this.options, tableRef.tableName)
54
+ // A user column named `rowid` shadows DuckDB's virtual row id. In that edge case the
55
+ // connection-level invariant still preserves insertion order; otherwise make it explicit.
56
+ const orderSql = version.schema.columns.some(
57
+ (column) => column.name.toLowerCase() === "rowid"
58
+ )
59
+ ? ""
60
+ : " ORDER BY rowid"
61
+ const limitSql =
62
+ input.limit === undefined ? "" : ` LIMIT ${Math.max(0, Math.trunc(input.limit))}`
63
+ const offsetSql =
64
+ input.offset === undefined ? "" : ` OFFSET ${Math.max(0, Math.trunc(input.offset))}`
65
+ const sql = `SELECT ${columnsSql} FROM ${tableSql} AT (VERSION => ${snapshotId})${orderSql}${limitSql}${offsetSql}`
66
+
67
+ // HTTP previews are bounded, so materialize them eagerly and release the
68
+ // runtime queue as soon as DuckDB has returned the small page.
69
+ const rows = input.limit === undefined ? runtime.streamRows(sql) : await runtime.query(sql)
70
+
71
+ // Step 3: DuckDB returns driver-native values. Normalize each projected
72
+ // column through the schema that was active at the resolved version.
73
+ yield* this.normalizeRows(rows, selectedColumns)
74
+ } finally {
75
+ await lease.release()
76
+ }
77
+ }
78
+
79
+ private async *normalizeRows(
80
+ rows: Iterable<Record<string, unknown>> | AsyncIterable<Record<string, unknown>>,
81
+ selectedColumns: readonly DatasetColumnDefinition[]
82
+ ): AsyncIterable<DatasetRow> {
83
+ for await (const row of rows) {
84
+ const output: Record<string, unknown> = {}
85
+ for (const column of selectedColumns) {
86
+ output[column.name] = normalizeReadValue(row[column.name], column)
87
+ }
88
+ yield output
89
+ }
90
+ }
91
+
92
+ private async resolveVersion(
93
+ runtime: DuckDbQueryRuntime,
94
+ tableRef: DatasetTableRef,
95
+ versionId?: string
96
+ ): Promise<DatasetVersion> {
97
+ if (versionId !== undefined) {
98
+ const snapshotId = parseVersionId(versionId)
99
+ const version = await this.snapshots.getVersionForTableRef(runtime, tableRef, snapshotId)
100
+ if (!version) {
101
+ this.throwNoCommittedVersion(tableRef.datasetId)
102
+ }
103
+
104
+ return version
105
+ }
106
+
107
+ const latestVersion = await this.snapshots.getLatestVersionForTableRef(runtime, tableRef)
108
+ if (!latestVersion) {
109
+ this.throwNoCommittedVersion(tableRef.datasetId)
110
+ }
111
+
112
+ return latestVersion
113
+ }
114
+
115
+ private resolveReadColumns(
116
+ datasetId: string,
117
+ schema: DatasetVersion["schema"],
118
+ columns?: readonly string[]
119
+ ): readonly DatasetColumnDefinition[] {
120
+ if (columns === undefined || columns.length === 0) {
121
+ return schema.columns
122
+ }
123
+
124
+ const columnsByName = new Map(schema.columns.map((column) => [column.name, column] as const))
125
+
126
+ return columns.map((columnName) => {
127
+ const column = columnsByName.get(columnName)
128
+ if (!column) {
129
+ throw new LakeStorageError(
130
+ `[SixbDuckLake] Dataset '${datasetId}' does not have column '${columnName}' at the requested version.`
131
+ )
132
+ }
133
+
134
+ return column
135
+ })
136
+ }
137
+
138
+ private throwNoCommittedVersion(datasetId: string): never {
139
+ throw new LakeStorageError(
140
+ `[SixbDuckLake] No committed version found for dataset '${datasetId}'.`
141
+ )
142
+ }
143
+ }