@sixb/ducklake 0.1.5 → 0.1.7
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/README.md +24 -26
- package/dist/ducklake-storage.d.ts +3 -1
- package/dist/ducklake-storage.d.ts.map +1 -1
- package/dist/index.js +1100 -661
- package/dist/index.js.map +22 -19
- package/dist/internal/duckdb-reader.d.ts +28 -0
- package/dist/internal/duckdb-reader.d.ts.map +1 -0
- package/dist/internal/duckdb-runtime.d.ts +5 -3
- package/dist/internal/duckdb-runtime.d.ts.map +1 -1
- package/dist/internal/ducklake-change-reader.d.ts +13 -0
- package/dist/internal/ducklake-change-reader.d.ts.map +1 -0
- package/dist/internal/ducklake-connection-manager.d.ts +19 -3
- package/dist/internal/ducklake-connection-manager.d.ts.map +1 -1
- package/dist/internal/ducklake-read-gate.d.ts +11 -0
- package/dist/internal/ducklake-read-gate.d.ts.map +1 -0
- package/dist/internal/ducklake-row-reader.d.ts.map +1 -1
- package/dist/internal/ducklake-snapshot-reader.d.ts.map +1 -1
- package/dist/internal/ducklake-write-coordinator.d.ts +2 -1
- package/dist/internal/ducklake-write-coordinator.d.ts.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/ducklake-storage.ts +13 -0
- package/src/internal/duckdb-reader.ts +129 -0
- package/src/internal/duckdb-runtime.ts +75 -51
- package/src/internal/ducklake-change-reader.ts +157 -0
- package/src/internal/ducklake-connection-manager.ts +117 -33
- package/src/internal/ducklake-maintenance.ts +1 -1
- package/src/internal/ducklake-read-gate.ts +58 -0
- package/src/internal/ducklake-row-reader.ts +84 -63
- package/src/internal/ducklake-snapshot-reader.ts +7 -2
- package/src/internal/ducklake-write-coordinator.ts +56 -15
- package/src/internal/ducklake-write-session.ts +1 -0
- package/src/types.ts +2 -0
|
@@ -2,6 +2,7 @@ import type { DatasetColumnDefinition, DatasetRow } from "@sixb/core"
|
|
|
2
2
|
import type { DatasetVersion, ReadDatasetRowsInput } from "@sixb/core/lake-storage"
|
|
3
3
|
import { LakeStorageError } from "@sixb/core/lake-storage"
|
|
4
4
|
import type { DuckLakeStorageOptions } from "../types"
|
|
5
|
+
import type { DuckDbReader } from "./duckdb-reader"
|
|
5
6
|
import { getBigIntLike } from "./duckdb-row"
|
|
6
7
|
import type { DuckDbQueryRuntime } from "./duckdb-runtime"
|
|
7
8
|
import type { DuckLakeConnectionManager } from "./ducklake-connection-manager"
|
|
@@ -11,10 +12,7 @@ import { normalizeReadValue } from "./schema"
|
|
|
11
12
|
import { qualifiedTableName, quoteIdentifier } from "./sql"
|
|
12
13
|
import { parseVersionId } from "./versions"
|
|
13
14
|
|
|
14
|
-
//
|
|
15
|
-
// bounding converted-row memory, this lets a JavaScript pipeline feed one DuckLake dataset into
|
|
16
|
-
// another: the destination appender can use the runtime after each page instead of waiting forever
|
|
17
|
-
// behind its own still-open source stream.
|
|
15
|
+
// Bounded fallback for catalogs whose metadata transactions cannot stay open during a read.
|
|
18
16
|
const READ_PAGE_ROWS = 5_000
|
|
19
17
|
const PHYSICAL_ROW_ID_ALIAS = "__sixb_physical_row_id"
|
|
20
18
|
|
|
@@ -33,78 +31,88 @@ export class DuckLakeRowReader {
|
|
|
33
31
|
) {}
|
|
34
32
|
|
|
35
33
|
async *readRows(input: ReadDatasetRowsInput): AsyncIterable<DatasetRow> {
|
|
36
|
-
this.connections.
|
|
37
|
-
|
|
38
|
-
// Keep the attachment lease across pages so local catalogs do not pay DETACH/ATTACH for every
|
|
39
|
-
// page. Individual query() calls still release the DuckDB queue before rows are yielded.
|
|
40
|
-
const lease = await this.connections.acquireAttachedRuntime()
|
|
34
|
+
const read = await this.connections.acquireRead(input.signal)
|
|
35
|
+
let reader: DuckDbReader | null = null
|
|
41
36
|
try {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
37
|
+
const prepared = await this.connections.withAttachedRuntime(async (runtime) => {
|
|
38
|
+
read.signal.throwIfAborted()
|
|
39
|
+
const tableRef = await resolveDatasetTableRef(this.options, runtime, input.datasetId)
|
|
40
|
+
if (!tableRef) {
|
|
41
|
+
throw new LakeStorageError(`[SixbDuckLake] Unknown dataset '${input.datasetId}'.`)
|
|
42
|
+
}
|
|
43
|
+
// Resolve latest once, before yielding any rows. Every query names the same snapshot.
|
|
44
|
+
const version = await this.resolveVersion(runtime, tableRef, input.versionId)
|
|
45
|
+
const columns = this.resolveReadColumns(tableRef.datasetId, version.schema, input.columns)
|
|
46
|
+
const names = new Set(version.schema.columns.map((column) => column.name.toLowerCase()))
|
|
47
|
+
const physicalOrder = !names.has("rowid") && !names.has(PHYSICAL_ROW_ID_ALIAS)
|
|
48
|
+
const relation = `${qualifiedTableName(this.options, tableRef.tableName)} AT (VERSION => ${parseVersionId(version.versionId)})`
|
|
49
|
+
const columnsSql = columns.map((column) => quoteIdentifier(column.name)).join(", ")
|
|
50
|
+
const offset = Math.max(0, Math.trunc(input.offset ?? 0))
|
|
51
|
+
const limit = input.limit === undefined ? undefined : Math.max(0, Math.trunc(input.limit))
|
|
52
|
+
const sql = `SELECT ${columnsSql} FROM ${relation}${physicalOrder ? " ORDER BY rowid" : ""}${limit === undefined ? "" : ` LIMIT ${limit}`}${offset > 0 ? ` OFFSET ${offset}` : ""}`
|
|
53
|
+
const cursor =
|
|
54
|
+
limit === undefined || limit > READ_PAGE_ROWS
|
|
55
|
+
? await this.connections.openReader(runtime, sql, read.signal, read.release)
|
|
56
|
+
: null
|
|
57
|
+
const query = { columnsSql, relation, physicalOrder }
|
|
58
|
+
const firstPage =
|
|
59
|
+
cursor || limit === 0
|
|
60
|
+
? []
|
|
61
|
+
: await runtime.query(
|
|
62
|
+
pageSql(query, Math.min(limit ?? READ_PAGE_ROWS, READ_PAGE_ROWS), offset, null)
|
|
63
|
+
)
|
|
64
|
+
return { columns, ...query, offset, limit, cursor, firstPage }
|
|
65
|
+
})
|
|
66
|
+
reader = prepared.cursor
|
|
67
|
+
// A paused paged reader owns no native query. Cancellation can release its read lease without
|
|
68
|
+
// waiting for the consumer to call next()/return(); native readers release after cleanup.
|
|
69
|
+
if (!reader) read.signal.addEventListener("abort", read.release, { once: true })
|
|
70
|
+
read.signal.throwIfAborted()
|
|
71
|
+
if (reader) {
|
|
72
|
+
yield* this.normalizeRows(reader.rows(), prepared.columns, read.signal)
|
|
73
|
+
return
|
|
48
74
|
}
|
|
49
|
-
const version = await this.resolveVersion(runtime, tableRef, input.versionId)
|
|
50
|
-
const selectedColumns = this.resolveReadColumns(
|
|
51
|
-
tableRef.datasetId,
|
|
52
|
-
version.schema,
|
|
53
|
-
input.columns
|
|
54
|
-
)
|
|
55
|
-
const snapshotId = parseVersionId(version.versionId)
|
|
56
|
-
const columnsSql = selectedColumns.map((column) => quoteIdentifier(column.name)).join(", ")
|
|
57
|
-
const tableSql = qualifiedTableName(this.options, tableRef.tableName)
|
|
58
|
-
// A user column named `rowid` shadows DuckDB's virtual row id. In that edge case the
|
|
59
|
-
// connection-level invariant still preserves insertion order; otherwise make it explicit.
|
|
60
|
-
const schemaNames = new Set(version.schema.columns.map((column) => column.name.toLowerCase()))
|
|
61
|
-
const canUsePhysicalRowId =
|
|
62
|
-
!schemaNames.has("rowid") && !schemaNames.has(PHYSICAL_ROW_ID_ALIAS.toLowerCase())
|
|
63
|
-
const orderSql = canUsePhysicalRowId ? " ORDER BY rowid" : ""
|
|
64
|
-
const baseSql = `SELECT ${columnsSql} FROM ${tableSql} AT (VERSION => ${snapshotId})`
|
|
65
|
-
const requestedOffset = Math.max(0, Math.trunc(input.offset ?? 0))
|
|
66
|
-
let remaining = input.limit === undefined ? undefined : Math.max(0, Math.trunc(input.limit))
|
|
67
|
-
if (remaining === 0) return
|
|
68
75
|
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
let
|
|
73
|
-
let
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
76
|
+
// Each bounded query releases both the runtime queue and attachment lease before yielding.
|
|
77
|
+
// SQLite metadata locks and PostgreSQL pool slots are released before consumer operations.
|
|
78
|
+
let offset = prepared.offset
|
|
79
|
+
let remaining = prepared.limit
|
|
80
|
+
let cursor: bigint | null = null
|
|
81
|
+
let page: readonly Record<string, unknown>[] | undefined = prepared.firstPage
|
|
82
|
+
while (remaining !== 0) {
|
|
83
|
+
read.signal.throwIfAborted()
|
|
84
|
+
const pageSize = Math.min(remaining ?? READ_PAGE_ROWS, READ_PAGE_ROWS)
|
|
85
|
+
const rows: readonly Record<string, unknown>[] =
|
|
86
|
+
page ??
|
|
87
|
+
(await this.connections.withAttachedRuntime((runtime) =>
|
|
88
|
+
runtime.query(pageSql(prepared, pageSize, offset, cursor))
|
|
89
|
+
))
|
|
90
|
+
page = undefined
|
|
91
|
+
read.signal.throwIfAborted()
|
|
92
|
+
yield* this.normalizeRows(rows, prepared.columns, read.signal)
|
|
93
|
+
if (remaining !== undefined) remaining -= rows.length
|
|
94
|
+
if (rows.length < pageSize) return
|
|
95
|
+
if (prepared.physicalOrder)
|
|
96
|
+
cursor = getBigIntLike(rows[rows.length - 1]!, PHYSICAL_ROW_ID_ALIAS)
|
|
88
97
|
offset += rows.length
|
|
89
|
-
if (remaining !== undefined) {
|
|
90
|
-
remaining -= rows.length
|
|
91
|
-
if (remaining === 0) return
|
|
92
|
-
}
|
|
93
|
-
if (canUsePhysicalRowId) {
|
|
94
|
-
physicalCursor = getBigIntLike(rows[rows.length - 1]!, PHYSICAL_ROW_ID_ALIAS)
|
|
95
|
-
}
|
|
96
|
-
if (rows.length < pageRows) return
|
|
97
98
|
}
|
|
98
99
|
} finally {
|
|
99
|
-
|
|
100
|
+
read.signal.removeEventListener("abort", read.release)
|
|
101
|
+
try {
|
|
102
|
+
await reader?.close()
|
|
103
|
+
} finally {
|
|
104
|
+
read.release()
|
|
105
|
+
}
|
|
100
106
|
}
|
|
101
107
|
}
|
|
102
108
|
|
|
103
109
|
private async *normalizeRows(
|
|
104
110
|
rows: Iterable<Record<string, unknown>> | AsyncIterable<Record<string, unknown>>,
|
|
105
|
-
selectedColumns: readonly DatasetColumnDefinition[]
|
|
111
|
+
selectedColumns: readonly DatasetColumnDefinition[],
|
|
112
|
+
signal: AbortSignal
|
|
106
113
|
): AsyncIterable<DatasetRow> {
|
|
107
114
|
for await (const row of rows) {
|
|
115
|
+
signal.throwIfAborted()
|
|
108
116
|
const output: Record<string, unknown> = {}
|
|
109
117
|
for (const column of selectedColumns) {
|
|
110
118
|
output[column.name] = normalizeReadValue(row[column.name], column)
|
|
@@ -165,3 +173,16 @@ export class DuckLakeRowReader {
|
|
|
165
173
|
)
|
|
166
174
|
}
|
|
167
175
|
}
|
|
176
|
+
|
|
177
|
+
function pageSql(
|
|
178
|
+
read: { readonly columnsSql: string; readonly relation: string; readonly physicalOrder: boolean },
|
|
179
|
+
limit: number,
|
|
180
|
+
offset: number,
|
|
181
|
+
cursor: bigint | null
|
|
182
|
+
): string {
|
|
183
|
+
return read.physicalOrder
|
|
184
|
+
? `SELECT rowid AS ${quoteIdentifier(PHYSICAL_ROW_ID_ALIAS)}, ${read.columnsSql}
|
|
185
|
+
FROM ${read.relation} ${cursor === null ? "" : `WHERE rowid > ${cursor}`}
|
|
186
|
+
ORDER BY rowid LIMIT ${limit} ${cursor === null && offset > 0 ? `OFFSET ${offset}` : ""}`
|
|
187
|
+
: `SELECT ${read.columnsSql} FROM ${read.relation} LIMIT ${limit} OFFSET ${offset}`
|
|
188
|
+
}
|
|
@@ -642,7 +642,12 @@ export class DuckLakeSnapshotReader {
|
|
|
642
642
|
return []
|
|
643
643
|
}
|
|
644
644
|
|
|
645
|
-
|
|
645
|
+
// A dataset cannot have a version before its table exists. Use the earliest table record:
|
|
646
|
+
// a rename may create a newer record with the same table id and must not hide older versions.
|
|
647
|
+
const ducklakeTable = duckLakeMetadataTableName(this.options, "ducklake_table")
|
|
648
|
+
const where = [
|
|
649
|
+
`snapshot.snapshot_id >= (SELECT min(begin_snapshot) FROM ${ducklakeTable} WHERE table_id = ${input.tableId})`,
|
|
650
|
+
]
|
|
646
651
|
if (input.exactSnapshotId !== undefined) {
|
|
647
652
|
assertDuckLakeSnapshotId(input.exactSnapshotId)
|
|
648
653
|
where.push(`snapshot.snapshot_id = ${input.exactSnapshotId}`)
|
|
@@ -652,7 +657,7 @@ export class DuckLakeSnapshotReader {
|
|
|
652
657
|
where.push(`snapshot.snapshot_id < ${input.beforeSnapshotId}`)
|
|
653
658
|
}
|
|
654
659
|
|
|
655
|
-
const whereSql =
|
|
660
|
+
const whereSql = `WHERE ${where.join(" AND ")}`
|
|
656
661
|
const limitSql =
|
|
657
662
|
input.limit === undefined ? "" : `LIMIT ${Math.max(0, Math.trunc(input.limit))}`
|
|
658
663
|
const ducklakeSnapshot = duckLakeMetadataTableName(this.options, "ducklake_snapshot")
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
LakeStorageError,
|
|
19
19
|
retryDatasetMergeCommit,
|
|
20
20
|
} from "@sixb/core/lake-storage"
|
|
21
|
-
import type { DuckLakeStorageOptions } from "../types"
|
|
21
|
+
import type { DuckLakeCatalogOptions, DuckLakeStorageOptions } from "../types"
|
|
22
22
|
import { localCatalogCoordinationKey } from "./catalog-key"
|
|
23
23
|
import {
|
|
24
24
|
type ApplyDatasetRowsResult,
|
|
@@ -330,13 +330,12 @@ export class DuckLakeWriteCoordinator {
|
|
|
330
330
|
// which snapshot belongs to this transaction.
|
|
331
331
|
const previousWriteSnapshotId = await this.lastCommittedSnapshotId(input.runtime)
|
|
332
332
|
|
|
333
|
-
await input.runtime.run("BEGIN TRANSACTION")
|
|
334
333
|
let committed = false
|
|
335
334
|
try {
|
|
336
335
|
// Guarded Sixb commits need compare-and-swap semantics. DuckLake can
|
|
337
336
|
// automatically retry non-conflicting commits, so guarded transactions
|
|
338
|
-
// disable retries and
|
|
339
|
-
const latestVersion = await this.
|
|
337
|
+
// disable retries and validate a head belonging to the native write snapshot.
|
|
338
|
+
const latestVersion = await this.beginVersionTransaction(input)
|
|
340
339
|
if (input.expectedLatestVersionId !== undefined) {
|
|
341
340
|
this.assertExpectedLatestVersion({
|
|
342
341
|
datasetId: input.dataset.id,
|
|
@@ -527,15 +526,16 @@ export class DuckLakeWriteCoordinator {
|
|
|
527
526
|
private async ensureNoOpHasSnapshot(
|
|
528
527
|
input: DuckLakeCommitVersionOutcomeRuntimeInput,
|
|
529
528
|
changeResult: ApplyDatasetRowsResult,
|
|
530
|
-
knownLatestVersion: DuckLakeVersionSummary | null
|
|
529
|
+
knownLatestVersion: DuckLakeVersionSummary | null | undefined
|
|
531
530
|
): Promise<void> {
|
|
532
531
|
if (changeResult.dataChangeExpected) {
|
|
533
532
|
return
|
|
534
533
|
}
|
|
535
534
|
|
|
536
535
|
const latestVersion =
|
|
537
|
-
knownLatestVersion
|
|
538
|
-
|
|
536
|
+
knownLatestVersion === undefined
|
|
537
|
+
? await this.snapshots.getLatestVersionSummaryForDefinition(input.runtime, input.dataset)
|
|
538
|
+
: knownLatestVersion
|
|
539
539
|
if (
|
|
540
540
|
latestVersion
|
|
541
541
|
? input.mode === "merge" || !hasDatasetInputChanges(latestVersion.inputs, input.inputs)
|
|
@@ -582,14 +582,49 @@ export class DuckLakeWriteCoordinator {
|
|
|
582
582
|
}
|
|
583
583
|
}
|
|
584
584
|
|
|
585
|
-
private async
|
|
585
|
+
private async beginVersionTransaction(
|
|
586
586
|
input: DuckLakeCommitVersionOutcomeRuntimeInput
|
|
587
|
-
): Promise<DuckLakeVersionSummary | null> {
|
|
588
|
-
if (
|
|
589
|
-
|
|
587
|
+
): Promise<DuckLakeVersionSummary | null | undefined> {
|
|
588
|
+
if (!isSqliteCatalog(this.options.catalog)) {
|
|
589
|
+
await input.runtime.run("BEGIN TRANSACTION")
|
|
590
|
+
if (input.expectedLatestVersionId === undefined && input.mode !== "append") {
|
|
591
|
+
return undefined
|
|
592
|
+
}
|
|
593
|
+
return this.snapshots.getLatestVersionSummaryForDefinition(input.runtime, input.dataset)
|
|
590
594
|
}
|
|
591
595
|
|
|
592
|
-
|
|
596
|
+
// Direct metadata reads enlist a second SQLite transaction on the outer DuckDB connection.
|
|
597
|
+
// Its read lock blocks DuckLake's own metadata connection at COMMIT. Read in autocommit,
|
|
598
|
+
// then pin the native DuckLake snapshot and verify nothing changed in between. The summary
|
|
599
|
+
// is now valid for this write transaction, including null (no previous version).
|
|
600
|
+
// Only preparation can repeat: staged mutations and COMMIT are never replayed here.
|
|
601
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
602
|
+
input.signal?.throwIfAborted()
|
|
603
|
+
const snapshotId = await this.currentSnapshotId(input.runtime)
|
|
604
|
+
const latestVersion = await this.snapshots.getLatestVersionSummaryForDefinition(
|
|
605
|
+
input.runtime,
|
|
606
|
+
input.dataset
|
|
607
|
+
)
|
|
608
|
+
await input.runtime.run("BEGIN TRANSACTION")
|
|
609
|
+
if ((await this.currentSnapshotId(input.runtime)) === snapshotId) {
|
|
610
|
+
return latestVersion
|
|
611
|
+
}
|
|
612
|
+
await input.runtime.run("ROLLBACK")
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
throw new LakeConcurrencyError(
|
|
616
|
+
`[SixbDuckLake] Catalog changed during three write preparations for dataset '${input.dataset.id}'. Retry the write.`
|
|
617
|
+
)
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
private async currentSnapshotId(runtime: DuckDbQueryRuntime): Promise<string> {
|
|
621
|
+
const [row] = await runtime.query(
|
|
622
|
+
`SELECT id FROM ${quoteIdentifier(duckLakeAlias(this.options))}.current_snapshot()`
|
|
623
|
+
)
|
|
624
|
+
if (row === undefined) {
|
|
625
|
+
throw new LakeStorageError("[SixbDuckLake] DuckLake did not return a current snapshot.")
|
|
626
|
+
}
|
|
627
|
+
return String(getBigIntLike(row, "id"))
|
|
593
628
|
}
|
|
594
629
|
|
|
595
630
|
private async lastCommittedSnapshotId(runtime: DuckDbQueryRuntime): Promise<string | null> {
|
|
@@ -681,15 +716,21 @@ export class DuckLakeWriteCoordinator {
|
|
|
681
716
|
}
|
|
682
717
|
}
|
|
683
718
|
|
|
719
|
+
function isSqliteCatalog(catalog: DuckLakeCatalogOptions): boolean {
|
|
720
|
+
return (
|
|
721
|
+
catalog.type === "sqlite" ||
|
|
722
|
+
(catalog.type === "custom" && catalog.uri.toLowerCase().startsWith("sqlite:"))
|
|
723
|
+
)
|
|
724
|
+
}
|
|
725
|
+
|
|
684
726
|
function commitRowCount(
|
|
685
727
|
input: Pick<DuckLakeCommitVersionOutcomeInput, "expectedLatestVersionId" | "mode">,
|
|
686
728
|
changeResult: ApplyDatasetRowsResult,
|
|
687
|
-
latestVersion: DuckLakeVersionSummary | null
|
|
729
|
+
latestVersion: DuckLakeVersionSummary | null | undefined
|
|
688
730
|
): CommitRowCount {
|
|
689
731
|
// Unguarded append commits can be retried by DuckLake after this transaction
|
|
690
732
|
// read the previous latest version. In that case previousRowCount +
|
|
691
|
-
// sourceRowCount can underreport the committed snapshot, so
|
|
692
|
-
// and let hydration count the exact committed snapshot instead.
|
|
733
|
+
// sourceRowCount can underreport the committed snapshot, so leave the count unknown.
|
|
693
734
|
if (input.mode === "append" && input.expectedLatestVersionId === undefined) {
|
|
694
735
|
return { kind: "unknown" }
|
|
695
736
|
}
|
package/src/types.ts
CHANGED
|
@@ -151,6 +151,8 @@ export interface DuckLakeStorageOptions {
|
|
|
151
151
|
readonly alias?: string
|
|
152
152
|
readonly duckdb?: DuckDbRuntimeOptions
|
|
153
153
|
readonly postgresPool?: DuckLakePostgresPoolOptions
|
|
154
|
+
/** Maximum native streaming readers per provider; extra reads use pages. Default: 4. */
|
|
155
|
+
readonly maxStreamingReads?: number
|
|
154
156
|
readonly secrets?: readonly DuckDbSecretOptions[]
|
|
155
157
|
/** Runs after DuckLake/catalog extensions load and before ATTACH. */
|
|
156
158
|
readonly setupSql?: readonly string[]
|