@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 type { DatasetColumnDefinition, DatasetSchema } from "@sixb/core"
|
|
2
|
+
import { LakeStorageError } from "@sixb/core/lake-storage"
|
|
3
|
+
import { duckDbTypeToDatasetColumnType } from "./schema"
|
|
4
|
+
|
|
5
|
+
export interface DuckLakeCatalogColumn {
|
|
6
|
+
readonly tableName: string
|
|
7
|
+
readonly columnId: bigint
|
|
8
|
+
readonly columnOrder: bigint
|
|
9
|
+
readonly columnName: string
|
|
10
|
+
readonly columnType: string
|
|
11
|
+
readonly nullsAllowed: boolean
|
|
12
|
+
readonly parentColumnId?: bigint
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const FILE_REF_STRUCT_CHILDREN = [
|
|
16
|
+
{ name: "blobId", type: "string" },
|
|
17
|
+
{ name: "digest", type: "string" },
|
|
18
|
+
{ name: "sizeBytes", type: "int64" },
|
|
19
|
+
{ name: "fileName", type: "string" },
|
|
20
|
+
{ name: "mediaType", type: "string" },
|
|
21
|
+
{ name: "logicalPath", type: "string" },
|
|
22
|
+
] satisfies readonly { readonly name: string; readonly type: DatasetColumnDefinition["type"] }[]
|
|
23
|
+
|
|
24
|
+
export function duckLakeCatalogColumnsToDatasetSchema(
|
|
25
|
+
tableName: string,
|
|
26
|
+
rows: readonly DuckLakeCatalogColumn[]
|
|
27
|
+
): DatasetSchema {
|
|
28
|
+
const childrenByParentId = new Map<bigint, DuckLakeCatalogColumn[]>()
|
|
29
|
+
const topLevelColumns: DuckLakeCatalogColumn[] = []
|
|
30
|
+
|
|
31
|
+
for (const row of rows) {
|
|
32
|
+
if (row.parentColumnId === undefined) {
|
|
33
|
+
topLevelColumns.push(row)
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const children = childrenByParentId.get(row.parentColumnId) ?? []
|
|
38
|
+
children.push(row)
|
|
39
|
+
childrenByParentId.set(row.parentColumnId, children)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
columns: topLevelColumns
|
|
44
|
+
.sort(compareDuckLakeCatalogColumnOrder)
|
|
45
|
+
.map((column) => duckLakeCatalogColumnToDefinition(tableName, column, childrenByParentId)),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function duckLakeCatalogColumnToDefinition(
|
|
50
|
+
tableName: string,
|
|
51
|
+
column: DuckLakeCatalogColumn,
|
|
52
|
+
childrenByParentId: ReadonlyMap<bigint, readonly DuckLakeCatalogColumn[]>
|
|
53
|
+
): DatasetColumnDefinition {
|
|
54
|
+
const children = [...(childrenByParentId.get(column.columnId) ?? [])].sort(
|
|
55
|
+
compareDuckLakeCatalogColumnOrder
|
|
56
|
+
)
|
|
57
|
+
return {
|
|
58
|
+
name: column.columnName,
|
|
59
|
+
type: duckLakeCatalogColumnTypeToDatasetType(tableName, column, children),
|
|
60
|
+
...(column.nullsAllowed ? { nullable: true } : {}),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function duckLakeCatalogColumnTypeToDatasetType(
|
|
65
|
+
tableName: string,
|
|
66
|
+
column: DuckLakeCatalogColumn,
|
|
67
|
+
children: readonly DuckLakeCatalogColumn[]
|
|
68
|
+
): DatasetColumnDefinition["type"] {
|
|
69
|
+
if (children.length === 0 && column.columnType.toLowerCase() !== "struct") {
|
|
70
|
+
return duckDbTypeToDatasetColumnType(column.columnType)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (isFileRefStruct(children)) {
|
|
74
|
+
return "fileRef"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
throw new LakeStorageError(
|
|
78
|
+
`[SixbDuckLake] DuckDB column type '${formatDuckLakeCatalogColumnType(
|
|
79
|
+
column,
|
|
80
|
+
children
|
|
81
|
+
)}' in dataset table '${tableName}' cannot be mapped to a Sixb dataset column type.`
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function compareDuckLakeCatalogColumnOrder(
|
|
86
|
+
left: DuckLakeCatalogColumn,
|
|
87
|
+
right: DuckLakeCatalogColumn
|
|
88
|
+
): number {
|
|
89
|
+
return Number(left.columnOrder - right.columnOrder)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isFileRefStruct(children: readonly DuckLakeCatalogColumn[]): boolean {
|
|
93
|
+
if (children.length !== FILE_REF_STRUCT_CHILDREN.length) {
|
|
94
|
+
return false
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return FILE_REF_STRUCT_CHILDREN.every((expected, index) => {
|
|
98
|
+
const child = children[index]
|
|
99
|
+
return (
|
|
100
|
+
child !== undefined &&
|
|
101
|
+
child.columnName === expected.name &&
|
|
102
|
+
duckDbTypeToDatasetColumnType(child.columnType) === expected.type
|
|
103
|
+
)
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function formatDuckLakeCatalogColumnType(
|
|
108
|
+
column: DuckLakeCatalogColumn,
|
|
109
|
+
children: readonly DuckLakeCatalogColumn[]
|
|
110
|
+
): string {
|
|
111
|
+
if (children.length === 0) {
|
|
112
|
+
return column.columnType
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return `STRUCT(${children.map((child) => `${child.columnName} ${child.columnType}`).join(", ")})`
|
|
116
|
+
}
|
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import { LakeStorageError } from "@sixb/core/lake-storage"
|
|
2
|
+
import type { DuckLakeStorageOptions } from "../types"
|
|
3
|
+
import { localCatalogCoordinationKey } from "./catalog-key"
|
|
4
|
+
import {
|
|
5
|
+
createDuckDbRuntime,
|
|
6
|
+
type DuckDbExclusiveRuntime,
|
|
7
|
+
type DuckDbQueryRuntime,
|
|
8
|
+
type DuckDbRuntime,
|
|
9
|
+
installDuckLakeExtensions,
|
|
10
|
+
setupDuckLake,
|
|
11
|
+
} from "./duckdb-runtime"
|
|
12
|
+
import {
|
|
13
|
+
buildAttachSql,
|
|
14
|
+
buildConfigurePostgresMetadataPoolSql,
|
|
15
|
+
duckLakeAlias,
|
|
16
|
+
quoteIdentifier,
|
|
17
|
+
} from "./sql"
|
|
18
|
+
|
|
19
|
+
export interface DuckLakeAttachedRuntimeLease {
|
|
20
|
+
readonly runtime: DuckDbRuntime
|
|
21
|
+
release(): Promise<void>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Owns DuckDB/DuckLake runtime lifecycle for DuckLakeStorage.
|
|
26
|
+
*
|
|
27
|
+
* A storage instance owns one DuckDB runtime and at most one DuckLake
|
|
28
|
+
* attachment. The runtime starts unattached so construction and non-lake setup
|
|
29
|
+
* do not immediately open DuckLake metadata connections; reads and commits
|
|
30
|
+
* attach lazily through `attachedRuntime()`.
|
|
31
|
+
*/
|
|
32
|
+
export class DuckLakeConnectionManager {
|
|
33
|
+
private runtimePromise: Promise<DuckDbRuntime> | undefined
|
|
34
|
+
private installExtensionsPromise: Promise<void> | undefined
|
|
35
|
+
private attachPromise: Promise<void> | undefined
|
|
36
|
+
private refreshPromise: Promise<void> | undefined
|
|
37
|
+
// Local catalogs need DETACH/ATTACH to refresh metadata visibility. Keep
|
|
38
|
+
// those attachment changes outside whole read operations so a read cannot
|
|
39
|
+
// observe the DuckLake alias disappearing between its metadata queries.
|
|
40
|
+
private localAttachmentLock: Promise<void> = Promise.resolve()
|
|
41
|
+
private observedLocalCatalogGeneration: number
|
|
42
|
+
private attached = false
|
|
43
|
+
private closed = false
|
|
44
|
+
private runtimePoisoned = false
|
|
45
|
+
|
|
46
|
+
constructor(private readonly options: DuckLakeStorageOptions) {
|
|
47
|
+
this.observedLocalCatalogGeneration = currentLocalCatalogGeneration(options)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
assertOpen(): void {
|
|
51
|
+
if (this.closed) {
|
|
52
|
+
throw new LakeStorageError("[SixbDuckLake] DuckLakeStorage is closed.")
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async runtime(): Promise<DuckDbRuntime> {
|
|
57
|
+
this.assertOpen()
|
|
58
|
+
|
|
59
|
+
if (this.runtimePromise === undefined) {
|
|
60
|
+
const runtimePromise = this.createRuntime().catch((error) => {
|
|
61
|
+
if (this.runtimePromise === runtimePromise) {
|
|
62
|
+
this.runtimePromise = undefined
|
|
63
|
+
}
|
|
64
|
+
throw error
|
|
65
|
+
})
|
|
66
|
+
this.runtimePromise = runtimePromise
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return this.runtimePromise
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async attachedRuntime(): Promise<DuckDbRuntime> {
|
|
73
|
+
const runtime = await this.runtime()
|
|
74
|
+
await this.ensureAttached(runtime)
|
|
75
|
+
return runtime
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async withAttachedRuntime<T>(run: (runtime: DuckDbRuntime) => Promise<T>): Promise<T> {
|
|
79
|
+
const lease = await this.acquireAttachedRuntime()
|
|
80
|
+
try {
|
|
81
|
+
return await run(lease.runtime)
|
|
82
|
+
} finally {
|
|
83
|
+
await lease.release()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async acquireAttachedRuntime(): Promise<DuckLakeAttachedRuntimeLease> {
|
|
88
|
+
this.assertOpen()
|
|
89
|
+
|
|
90
|
+
// The lease is intentionally wider than one DuckDB queue operation. It
|
|
91
|
+
// covers the full Sixb read/commit boundary for local catalogs while
|
|
92
|
+
// remaining a no-op for PostgreSQL catalogs.
|
|
93
|
+
const releaseLock = await this.acquireLocalAttachmentLock()
|
|
94
|
+
let released = false
|
|
95
|
+
const release = async () => {
|
|
96
|
+
if (released) {
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
released = true
|
|
101
|
+
try {
|
|
102
|
+
if (this.runtimePoisoned) {
|
|
103
|
+
this.runtimePoisoned = false
|
|
104
|
+
await this.discardRuntimeUnlocked()
|
|
105
|
+
} else {
|
|
106
|
+
await this.detachDuckDbCatalogAfterLease()
|
|
107
|
+
}
|
|
108
|
+
} finally {
|
|
109
|
+
releaseLock()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
await this.refreshForExternalChangesUnlocked()
|
|
115
|
+
const runtime = await this.attachedRuntime()
|
|
116
|
+
return { runtime, release }
|
|
117
|
+
} catch (error) {
|
|
118
|
+
await release()
|
|
119
|
+
throw error
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async withExclusiveAttached<T>(run: (runtime: DuckDbExclusiveRuntime) => Promise<T>): Promise<T> {
|
|
124
|
+
const lease = await this.acquireAttachedRuntime()
|
|
125
|
+
try {
|
|
126
|
+
return await lease.runtime.withExclusive(run)
|
|
127
|
+
} finally {
|
|
128
|
+
await lease.release()
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async stagingRuntime(): Promise<DuckDbRuntime> {
|
|
133
|
+
const runtime = await this.runtime()
|
|
134
|
+
|
|
135
|
+
if (this.options.catalog.type !== "postgres") {
|
|
136
|
+
// Local catalogs can keep stale attached metadata when another provider
|
|
137
|
+
// commits while this write session is open. Stage rows unattached so the
|
|
138
|
+
// later exclusive commit attaches fresh without losing temp tables.
|
|
139
|
+
await this.detachRuntime()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return runtime
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Create the one runtime owned by this manager. Setup loads extensions,
|
|
147
|
+
* secrets, setup SQL, and pre-attach Postgres pool settings, but deliberately
|
|
148
|
+
* leaves DuckLake unattached until `attachedRuntime()` is needed.
|
|
149
|
+
*/
|
|
150
|
+
async createRuntime(): Promise<DuckDbRuntime> {
|
|
151
|
+
this.assertOpen()
|
|
152
|
+
|
|
153
|
+
const runtime = await createDuckDbRuntime(this.options.duckdb)
|
|
154
|
+
try {
|
|
155
|
+
await this.ensureExtensionsInstalled(runtime)
|
|
156
|
+
await setupDuckLake(runtime, this.options, {
|
|
157
|
+
attach: false,
|
|
158
|
+
installExtensions: false,
|
|
159
|
+
})
|
|
160
|
+
// Projection checkpoints use physical offsets. Reapply this after caller setup SQL so it
|
|
161
|
+
// cannot weaken immutable snapshot ordering, including after runtime recreation.
|
|
162
|
+
await runtime.run("SET preserve_insertion_order = true")
|
|
163
|
+
return runtime
|
|
164
|
+
} catch (error) {
|
|
165
|
+
await this.closeRuntime(runtime, { detach: false })
|
|
166
|
+
throw error
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async resetRuntime(): Promise<void> {
|
|
171
|
+
const releaseLock = await this.acquireLocalAttachmentLock()
|
|
172
|
+
try {
|
|
173
|
+
await this.resetRuntimeUnlocked()
|
|
174
|
+
} finally {
|
|
175
|
+
releaseLock()
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private async resetRuntimeUnlocked(): Promise<void> {
|
|
180
|
+
const runtimePromise = this.runtimePromise
|
|
181
|
+
if (runtimePromise === undefined) {
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (this.attachPromise !== undefined) {
|
|
186
|
+
await this.attachPromise
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!this.attached) {
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let refreshedPromise: Promise<DuckDbRuntime>
|
|
194
|
+
refreshedPromise = runtimePromise
|
|
195
|
+
.then(async (runtime) => {
|
|
196
|
+
try {
|
|
197
|
+
await this.refreshAttachedRuntime(runtime)
|
|
198
|
+
this.attached = true
|
|
199
|
+
return runtime
|
|
200
|
+
} catch (error) {
|
|
201
|
+
this.attached = false
|
|
202
|
+
await this.closeRuntime(runtime, { detach: false })
|
|
203
|
+
throw error
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
.catch((error) => {
|
|
207
|
+
if (this.runtimePromise === refreshedPromise) {
|
|
208
|
+
this.runtimePromise = undefined
|
|
209
|
+
}
|
|
210
|
+
throw error
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
this.attached = false
|
|
214
|
+
this.runtimePromise = refreshedPromise
|
|
215
|
+
await refreshedPromise
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async refreshForExternalChanges(): Promise<void> {
|
|
219
|
+
if (this.options.catalog.type === "postgres") {
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!this.needsLocalCatalogRefresh()) {
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (this.refreshPromise === undefined) {
|
|
228
|
+
let refreshPromise: Promise<void>
|
|
229
|
+
refreshPromise = this.withLocalAttachmentLock(() =>
|
|
230
|
+
this.refreshForExternalChangesUnlocked()
|
|
231
|
+
).finally(() => {
|
|
232
|
+
if (this.refreshPromise === refreshPromise) {
|
|
233
|
+
this.refreshPromise = undefined
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
this.refreshPromise = refreshPromise
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
await this.refreshPromise
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
markLocalCatalogChanged(): void {
|
|
243
|
+
if (this.options.catalog.type === "postgres") {
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
this.observedLocalCatalogGeneration = incrementLocalCatalogGeneration(this.options)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async detachLocalCatalogAfterCommit(runtime: DuckDbQueryRuntime): Promise<void> {
|
|
251
|
+
if (this.options.catalog.type === "postgres") {
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
await runtime.run(`DETACH ${quoteIdentifier(duckLakeAlias(this.options))}`)
|
|
257
|
+
this.attached = false
|
|
258
|
+
} catch {
|
|
259
|
+
// The commit already succeeded. Keep the original write result and let a
|
|
260
|
+
// later refresh/close retry detaching this local catalog if needed.
|
|
261
|
+
} finally {
|
|
262
|
+
this.markLocalCatalogChanged()
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private async refreshForExternalChangesUnlocked(): Promise<void> {
|
|
267
|
+
const generation = currentLocalCatalogGeneration(this.options)
|
|
268
|
+
if (this.observedLocalCatalogGeneration === generation) {
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
await this.resetRuntimeUnlocked()
|
|
273
|
+
|
|
274
|
+
this.observedLocalCatalogGeneration = generation
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private needsLocalCatalogRefresh(): boolean {
|
|
278
|
+
return this.observedLocalCatalogGeneration !== currentLocalCatalogGeneration(this.options)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async close(): Promise<void> {
|
|
282
|
+
if (this.closed) {
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
this.closed = true
|
|
287
|
+
|
|
288
|
+
const runtimePromise = this.runtimePromise
|
|
289
|
+
this.runtimePromise = undefined
|
|
290
|
+
await this.closeRuntimePromise(runtimePromise, {
|
|
291
|
+
// For Postgres catalogs, the DuckDB Postgres extension owns the attached
|
|
292
|
+
// pool. Explicit DETACH can briefly create or retain another backend;
|
|
293
|
+
// closing the DuckDB runtime is the tighter shutdown path.
|
|
294
|
+
detach: this.attached && this.options.catalog.type !== "postgres",
|
|
295
|
+
})
|
|
296
|
+
this.attached = false
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private async ensureAttached(runtime: DuckDbRuntime): Promise<void> {
|
|
300
|
+
this.assertOpen()
|
|
301
|
+
|
|
302
|
+
if (this.attached) {
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (this.attachPromise === undefined) {
|
|
307
|
+
let attachPromise: Promise<void>
|
|
308
|
+
attachPromise = this.attachRuntime(runtime)
|
|
309
|
+
.then(() => {
|
|
310
|
+
this.attached = true
|
|
311
|
+
})
|
|
312
|
+
.catch(async (error) => {
|
|
313
|
+
if (this.attachPromise === attachPromise) {
|
|
314
|
+
this.runtimePromise = undefined
|
|
315
|
+
this.attached = false
|
|
316
|
+
}
|
|
317
|
+
await this.closeRuntime(runtime, { detach: false })
|
|
318
|
+
throw error
|
|
319
|
+
})
|
|
320
|
+
.finally(() => {
|
|
321
|
+
if (this.attachPromise === attachPromise) {
|
|
322
|
+
this.attachPromise = undefined
|
|
323
|
+
}
|
|
324
|
+
})
|
|
325
|
+
this.attachPromise = attachPromise
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
await this.attachPromise
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private async ensureExtensionsInstalled(runtime: DuckDbRuntime): Promise<void> {
|
|
332
|
+
if (this.installExtensionsPromise === undefined) {
|
|
333
|
+
const installExtensionsPromise = installDuckLakeExtensions(runtime, this.options).catch(
|
|
334
|
+
(error) => {
|
|
335
|
+
if (this.installExtensionsPromise === installExtensionsPromise) {
|
|
336
|
+
this.installExtensionsPromise = undefined
|
|
337
|
+
}
|
|
338
|
+
throw error
|
|
339
|
+
}
|
|
340
|
+
)
|
|
341
|
+
this.installExtensionsPromise = installExtensionsPromise
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
await this.installExtensionsPromise
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private async attachRuntime(runtime: DuckDbRuntime): Promise<void> {
|
|
348
|
+
await runtime.runStatements([
|
|
349
|
+
buildAttachSql(this.options),
|
|
350
|
+
...(this.options.catalog.type === "postgres"
|
|
351
|
+
? [buildConfigurePostgresMetadataPoolSql(this.options)]
|
|
352
|
+
: []),
|
|
353
|
+
])
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private async refreshAttachedRuntime(runtime: DuckDbRuntime): Promise<void> {
|
|
357
|
+
await runtime.runStatements([
|
|
358
|
+
`DETACH ${quoteIdentifier(duckLakeAlias(this.options))}`,
|
|
359
|
+
buildAttachSql(this.options),
|
|
360
|
+
...(this.options.catalog.type === "postgres"
|
|
361
|
+
? [buildConfigurePostgresMetadataPoolSql(this.options)]
|
|
362
|
+
: []),
|
|
363
|
+
])
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private async detachRuntime(): Promise<void> {
|
|
367
|
+
await this.withLocalAttachmentLock(() => this.detachRuntimeUnlocked())
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Mark the active runtime as unusable for reuse. The next lease release
|
|
372
|
+
* discards the connection so session state that could not be cleaned up
|
|
373
|
+
* (such as a SET pragma whose RESET failed) cannot reach a later operation.
|
|
374
|
+
*/
|
|
375
|
+
poisonRuntime(): void {
|
|
376
|
+
this.runtimePoisoned = true
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private async detachDuckDbCatalogAfterLease(): Promise<void> {
|
|
380
|
+
if (this.options.catalog.type !== "duckdb" || !this.attached) {
|
|
381
|
+
return
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
await this.detachRuntimeUnlocked()
|
|
386
|
+
} catch {
|
|
387
|
+
// DuckDB-backed DuckLake catalogs are single-client local catalogs. If
|
|
388
|
+
// cleanup cannot detach, discard this runtime so the next operation
|
|
389
|
+
// starts from an unattached connection instead of keeping stale metadata.
|
|
390
|
+
await this.discardRuntimeUnlocked()
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Close and forget the current runtime so the next `runtime()` builds a fresh
|
|
395
|
+
// connection with default session state. Unlike `resetRuntimeUnlocked()`,
|
|
396
|
+
// which DETACH/ATTACHes the same connection, this drops the connection
|
|
397
|
+
// entirely -- the only way to clear a leaked session pragma.
|
|
398
|
+
private async discardRuntimeUnlocked(): Promise<void> {
|
|
399
|
+
const runtimePromise = this.runtimePromise
|
|
400
|
+
this.runtimePromise = undefined
|
|
401
|
+
this.attached = false
|
|
402
|
+
await this.closeRuntimePromise(runtimePromise, { detach: false }).catch(() => {})
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private async detachRuntimeUnlocked(): Promise<void> {
|
|
406
|
+
if (this.attachPromise !== undefined) {
|
|
407
|
+
await this.attachPromise
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (!this.attached) {
|
|
411
|
+
return
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const runtime = await this.runtime()
|
|
415
|
+
await runtime.run(`DETACH ${quoteIdentifier(duckLakeAlias(this.options))}`)
|
|
416
|
+
this.attached = false
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private async withLocalAttachmentLock<T>(run: () => Promise<T>): Promise<T> {
|
|
420
|
+
const releaseLock = await this.acquireLocalAttachmentLock()
|
|
421
|
+
try {
|
|
422
|
+
return await run()
|
|
423
|
+
} finally {
|
|
424
|
+
releaseLock()
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private async acquireLocalAttachmentLock(): Promise<() => void> {
|
|
429
|
+
if (this.options.catalog.type === "postgres") {
|
|
430
|
+
return () => {}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const previous = this.localAttachmentLock.catch(() => {})
|
|
434
|
+
let release!: () => void
|
|
435
|
+
const current = new Promise<void>((resolve) => {
|
|
436
|
+
release = resolve
|
|
437
|
+
})
|
|
438
|
+
this.localAttachmentLock = previous.then(() => current)
|
|
439
|
+
|
|
440
|
+
await previous
|
|
441
|
+
|
|
442
|
+
let released = false
|
|
443
|
+
return () => {
|
|
444
|
+
if (released) {
|
|
445
|
+
return
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
released = true
|
|
449
|
+
release()
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
private async closeRuntimePromise(
|
|
454
|
+
runtimePromise: Promise<DuckDbRuntime> | undefined,
|
|
455
|
+
options: { readonly detach: boolean }
|
|
456
|
+
): Promise<void> {
|
|
457
|
+
if (runtimePromise === undefined) {
|
|
458
|
+
return
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
let runtime: DuckDbRuntime
|
|
462
|
+
try {
|
|
463
|
+
runtime = await runtimePromise
|
|
464
|
+
} catch {
|
|
465
|
+
// createRuntime/attachRuntime already close partially-initialized
|
|
466
|
+
// runtimes before rejecting. Swallowing here keeps close/reset
|
|
467
|
+
// idempotent while preserving the original operation error.
|
|
468
|
+
return
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
await this.closeRuntime(runtime, options)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
private async closeRuntime(
|
|
475
|
+
runtime: DuckDbRuntime,
|
|
476
|
+
options: { readonly detach: boolean }
|
|
477
|
+
): Promise<void> {
|
|
478
|
+
if (options.detach) {
|
|
479
|
+
try {
|
|
480
|
+
await runtime.run(`DETACH ${quoteIdentifier(duckLakeAlias(this.options))}`)
|
|
481
|
+
} catch {
|
|
482
|
+
// The runtime may already be detached, partially initialized, or in an
|
|
483
|
+
// error state. Closing the native connection is still required.
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
await runtime.close()
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Local catalog tests commonly create multiple DuckLakeStorage instances in one
|
|
492
|
+
// process. DuckLake/Postgres handles cross-connection visibility itself; local
|
|
493
|
+
// file catalogs need this in-process signal so peers refresh after Sixb writes.
|
|
494
|
+
const localCatalogGenerations = new Map<string, number>()
|
|
495
|
+
|
|
496
|
+
function currentLocalCatalogGeneration(options: DuckLakeStorageOptions): number {
|
|
497
|
+
const key = localCatalogCoordinationKey(options)
|
|
498
|
+
return key === undefined ? 0 : (localCatalogGenerations.get(key) ?? 0)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function incrementLocalCatalogGeneration(options: DuckLakeStorageOptions): number {
|
|
502
|
+
const key = localCatalogCoordinationKey(options)
|
|
503
|
+
if (key === undefined) {
|
|
504
|
+
return 0
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const nextGeneration = (localCatalogGenerations.get(key) ?? 0) + 1
|
|
508
|
+
localCatalogGenerations.set(key, nextGeneration)
|
|
509
|
+
return nextGeneration
|
|
510
|
+
}
|