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