@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,826 @@
|
|
|
1
|
+
import type { DatasetDefinition } from "@sixb/core"
|
|
2
|
+
import type {
|
|
3
|
+
DatasetCatalogState,
|
|
4
|
+
DatasetLatestVersionSummary,
|
|
5
|
+
DatasetVersion,
|
|
6
|
+
DatasetVersionMode,
|
|
7
|
+
DatasetVersionRef,
|
|
8
|
+
} from "@sixb/core/lake-storage"
|
|
9
|
+
import { LakeStorageError } from "@sixb/core/lake-storage"
|
|
10
|
+
import type { DuckLakeStorageOptions } from "../types"
|
|
11
|
+
import { getBigIntLike, getBoolean, getDate, getOptionalString, getString } from "./duckdb-row"
|
|
12
|
+
import type { DuckDbQueryRuntime } from "./duckdb-runtime"
|
|
13
|
+
import type { DuckLakeConnectionManager } from "./ducklake-connection-manager"
|
|
14
|
+
import type { DuckLakeDatasetCatalog } from "./ducklake-dataset-catalog"
|
|
15
|
+
import { type DatasetTableRef, resolveDatasetTableRef } from "./ducklake-dataset-table-ref"
|
|
16
|
+
import { encodeDatasetTableName } from "./names"
|
|
17
|
+
import { duckLakeMetadataTableName, quoteSqlString } from "./sql"
|
|
18
|
+
import {
|
|
19
|
+
parseCommitMetadata,
|
|
20
|
+
parseInlineDataChange,
|
|
21
|
+
parseVersionId,
|
|
22
|
+
type SixbCommitMetadata,
|
|
23
|
+
toVersionId,
|
|
24
|
+
} from "./versions"
|
|
25
|
+
|
|
26
|
+
interface DatasetSnapshotRow {
|
|
27
|
+
readonly snapshotId: string
|
|
28
|
+
readonly tableId: bigint
|
|
29
|
+
readonly createdAt: Date
|
|
30
|
+
readonly mode: DatasetVersionMode
|
|
31
|
+
readonly parentSnapshotId?: string
|
|
32
|
+
readonly metadata?: SixbCommitMetadata
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface DatasetSnapshotCandidateRow {
|
|
36
|
+
readonly snapshotId: string
|
|
37
|
+
readonly createdAt: Date
|
|
38
|
+
readonly changesMade: string
|
|
39
|
+
readonly hasFileChange: boolean
|
|
40
|
+
readonly hasFileDeleteChange: boolean
|
|
41
|
+
readonly metadata?: SixbCommitMetadata
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface SnapshotCandidateQueryInput {
|
|
45
|
+
readonly tableId: bigint
|
|
46
|
+
readonly exactSnapshotId?: string
|
|
47
|
+
readonly beforeSnapshotId?: string
|
|
48
|
+
readonly limit?: number
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface VisibleSnapshotRowsInput {
|
|
52
|
+
readonly datasetId: string
|
|
53
|
+
readonly tableId: bigint
|
|
54
|
+
readonly exactSnapshotId?: string
|
|
55
|
+
readonly beforeSnapshotId?: string
|
|
56
|
+
readonly visibleRowLimit?: number
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface DuckLakeVersionSummary {
|
|
60
|
+
readonly datasetId: string
|
|
61
|
+
readonly versionId: string
|
|
62
|
+
readonly rowCount?: number
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** One snapshot row from the shared catalog scan, before per-dataset filtering. */
|
|
66
|
+
interface CatalogScanSnapshot {
|
|
67
|
+
readonly snapshotId: string
|
|
68
|
+
readonly createdAt: Date
|
|
69
|
+
readonly changesMade: string
|
|
70
|
+
readonly metadata?: SixbCommitMetadata
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Per (snapshot, table) file-change flags merged into the catalog scan. */
|
|
74
|
+
interface FileChangeFlags {
|
|
75
|
+
readonly hasFileChange: boolean
|
|
76
|
+
readonly hasFileDeleteChange: boolean
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const SNAPSHOT_ROW_BATCH_SIZE = 128
|
|
80
|
+
|
|
81
|
+
// The bulk catalog scan shares one descending walk of recent snapshots across
|
|
82
|
+
// all requested datasets, so its cost is bounded by this window rather than by
|
|
83
|
+
// the dataset count. A dataset whose latest version is older than the window
|
|
84
|
+
// reports a null latest version, which is acceptable for a catalog summary; the
|
|
85
|
+
// detail routes still hydrate exact history.
|
|
86
|
+
const CATALOG_SNAPSHOT_SCAN_LIMIT = 512
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Reconstructs Sixb DatasetVersion objects from DuckLake snapshots.
|
|
90
|
+
*
|
|
91
|
+
* DuckLake remains the source of truth for version ids, commit times, and
|
|
92
|
+
* historical reads. Sixb commit metadata only hydrates fields DuckLake does
|
|
93
|
+
* not know about, such as producer info and declared inputs.
|
|
94
|
+
*
|
|
95
|
+
* This class intentionally reads DuckLake metadata directly. The provider does
|
|
96
|
+
* not keep a Sixb side table for versions; a Sixb version is a DuckLake
|
|
97
|
+
* snapshot that either changed the dataset table or was explicitly tagged with
|
|
98
|
+
* Sixb dataset metadata.
|
|
99
|
+
*/
|
|
100
|
+
export class DuckLakeSnapshotReader {
|
|
101
|
+
constructor(
|
|
102
|
+
private readonly options: DuckLakeStorageOptions,
|
|
103
|
+
private readonly connections: DuckLakeConnectionManager,
|
|
104
|
+
private readonly datasets: DuckLakeDatasetCatalog
|
|
105
|
+
) {}
|
|
106
|
+
|
|
107
|
+
async listVersions(datasetId: string, limit?: number): Promise<readonly DatasetVersion[]> {
|
|
108
|
+
this.connections.assertOpen()
|
|
109
|
+
|
|
110
|
+
return this.connections.withAttachedRuntime(async (runtime) => {
|
|
111
|
+
const tableRef = await resolveDatasetTableRef(this.options, runtime, datasetId)
|
|
112
|
+
if (!tableRef) {
|
|
113
|
+
return []
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return this.listVersionsForTableRef(runtime, tableRef, limit)
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async getLatestVersion(datasetId: string): Promise<DatasetVersion | null> {
|
|
121
|
+
this.connections.assertOpen()
|
|
122
|
+
|
|
123
|
+
return this.connections.withAttachedRuntime(async (runtime) => {
|
|
124
|
+
const tableRef = await resolveDatasetTableRef(this.options, runtime, datasetId)
|
|
125
|
+
if (!tableRef) {
|
|
126
|
+
return null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return this.getLatestVersionForTableRef(runtime, tableRef)
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async getVersion(datasetId: string, versionId: string): Promise<DatasetVersion | null> {
|
|
134
|
+
const snapshotId = parseVersionId(versionId)
|
|
135
|
+
this.connections.assertOpen()
|
|
136
|
+
|
|
137
|
+
return this.connections.withAttachedRuntime(async (runtime) => {
|
|
138
|
+
const tableRef = await resolveDatasetTableRef(this.options, runtime, datasetId)
|
|
139
|
+
if (!tableRef) {
|
|
140
|
+
return null
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return this.getVersionForTableRef(runtime, tableRef, snapshotId)
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Bulk catalog-summary read used by the dataset list view.
|
|
149
|
+
*
|
|
150
|
+
* Resolves materialized state and a lightweight latest-version summary for
|
|
151
|
+
* many datasets with a bounded number of metadata queries, never one snapshot
|
|
152
|
+
* scan per dataset and never a count(*) over dataset contents.
|
|
153
|
+
*/
|
|
154
|
+
async listDatasetCatalogState(
|
|
155
|
+
datasetIds: readonly string[]
|
|
156
|
+
): Promise<readonly DatasetCatalogState[]> {
|
|
157
|
+
this.connections.assertOpen()
|
|
158
|
+
|
|
159
|
+
if (datasetIds.length === 0) {
|
|
160
|
+
return []
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return this.connections.withAttachedRuntime((runtime) =>
|
|
164
|
+
this.collectDatasetCatalogState(runtime, datasetIds)
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private async collectDatasetCatalogState(
|
|
169
|
+
runtime: DuckDbQueryRuntime,
|
|
170
|
+
datasetIds: readonly string[]
|
|
171
|
+
): Promise<readonly DatasetCatalogState[]> {
|
|
172
|
+
const uniqueIds = [...new Set(datasetIds)]
|
|
173
|
+
const tableIdsByDatasetId = await this.resolveDatasetTableIds(runtime, uniqueIds)
|
|
174
|
+
const latestRowByDatasetId = await this.resolveLatestSnapshotRows(runtime, tableIdsByDatasetId)
|
|
175
|
+
|
|
176
|
+
return uniqueIds.map((datasetId) => {
|
|
177
|
+
if (!tableIdsByDatasetId.has(datasetId)) {
|
|
178
|
+
return { datasetId, materialized: false, latestVersion: null }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const row = latestRowByDatasetId.get(datasetId)
|
|
182
|
+
return {
|
|
183
|
+
datasetId,
|
|
184
|
+
materialized: true,
|
|
185
|
+
latestVersion: row ? this.snapshotRowToSummary(datasetId, row) : null,
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private snapshotRowToSummary(
|
|
191
|
+
datasetId: string,
|
|
192
|
+
row: DatasetSnapshotRow
|
|
193
|
+
): DatasetLatestVersionSummary {
|
|
194
|
+
return {
|
|
195
|
+
datasetId,
|
|
196
|
+
versionId: toVersionId(row.snapshotId),
|
|
197
|
+
mode: row.mode,
|
|
198
|
+
createdAt: row.createdAt,
|
|
199
|
+
...(row.metadata?.rowCount !== undefined ? { rowCount: row.metadata.rowCount } : {}),
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private async resolveDatasetTableIds(
|
|
204
|
+
runtime: DuckDbQueryRuntime,
|
|
205
|
+
datasetIds: readonly string[]
|
|
206
|
+
): Promise<Map<string, bigint>> {
|
|
207
|
+
// Encoded table names are a collision-free bijection with dataset ids, so a
|
|
208
|
+
// single `ducklake_table` read maps every requested id to its current table.
|
|
209
|
+
const datasetIdByTableName = new Map<string, string>()
|
|
210
|
+
for (const datasetId of datasetIds) {
|
|
211
|
+
datasetIdByTableName.set(encodeDatasetTableName(datasetId), datasetId)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const ducklakeTable = duckLakeMetadataTableName(this.options, "ducklake_table")
|
|
215
|
+
const tableNameList = [...datasetIdByTableName.keys()]
|
|
216
|
+
.map((name) => quoteSqlString(name))
|
|
217
|
+
.join(", ")
|
|
218
|
+
const rows = await runtime.query(`
|
|
219
|
+
SELECT table_id, table_name
|
|
220
|
+
FROM ${ducklakeTable}
|
|
221
|
+
WHERE table_name IN (${tableNameList})
|
|
222
|
+
AND end_snapshot IS NULL
|
|
223
|
+
`)
|
|
224
|
+
|
|
225
|
+
const tableIdsByDatasetId = new Map<string, bigint>()
|
|
226
|
+
for (const row of rows) {
|
|
227
|
+
const datasetId = datasetIdByTableName.get(getString(row, "table_name"))
|
|
228
|
+
if (datasetId !== undefined) {
|
|
229
|
+
tableIdsByDatasetId.set(datasetId, getBigIntLike(row, "table_id"))
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return tableIdsByDatasetId
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Resolve each dataset's latest snapshot row with one shared descending walk.
|
|
238
|
+
*
|
|
239
|
+
* The walk reuses {@link candidateToSnapshotRow} so Sixb visibility, mode
|
|
240
|
+
* derivation, and the loud conflict rule match exact version hydration. Cost
|
|
241
|
+
* is bounded by the snapshot window, not by the number of datasets.
|
|
242
|
+
*/
|
|
243
|
+
private async resolveLatestSnapshotRows(
|
|
244
|
+
runtime: DuckDbQueryRuntime,
|
|
245
|
+
tableIdsByDatasetId: ReadonlyMap<string, bigint>
|
|
246
|
+
): Promise<Map<string, DatasetSnapshotRow>> {
|
|
247
|
+
const result = new Map<string, DatasetSnapshotRow>()
|
|
248
|
+
if (tableIdsByDatasetId.size === 0) {
|
|
249
|
+
return result
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const unresolved = new Map<string, bigint>(tableIdsByDatasetId)
|
|
253
|
+
const tableIds = [...new Set(tableIdsByDatasetId.values())]
|
|
254
|
+
|
|
255
|
+
let beforeSnapshotId: string | undefined
|
|
256
|
+
let scanned = 0
|
|
257
|
+
while (unresolved.size > 0 && scanned < CATALOG_SNAPSHOT_SCAN_LIMIT) {
|
|
258
|
+
const candidates = await this.queryCatalogSnapshotBatch(
|
|
259
|
+
runtime,
|
|
260
|
+
beforeSnapshotId,
|
|
261
|
+
SNAPSHOT_ROW_BATCH_SIZE
|
|
262
|
+
)
|
|
263
|
+
if (candidates.length === 0) {
|
|
264
|
+
break
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const fileFlags = await this.queryFileChangeFlags(
|
|
268
|
+
runtime,
|
|
269
|
+
candidates.map((candidate) => candidate.snapshotId),
|
|
270
|
+
tableIds
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
for (const candidate of candidates) {
|
|
274
|
+
scanned += 1
|
|
275
|
+
for (const [datasetId, tableId] of [...unresolved]) {
|
|
276
|
+
const flags = fileFlags.get(fileChangeKey(candidate.snapshotId, tableId))
|
|
277
|
+
const row = this.candidateToSnapshotRow(datasetId, tableId, {
|
|
278
|
+
snapshotId: candidate.snapshotId,
|
|
279
|
+
createdAt: candidate.createdAt,
|
|
280
|
+
changesMade: candidate.changesMade,
|
|
281
|
+
hasFileChange: flags?.hasFileChange ?? false,
|
|
282
|
+
hasFileDeleteChange: flags?.hasFileDeleteChange ?? false,
|
|
283
|
+
...(candidate.metadata !== undefined ? { metadata: candidate.metadata } : {}),
|
|
284
|
+
})
|
|
285
|
+
if (row) {
|
|
286
|
+
result.set(datasetId, row)
|
|
287
|
+
unresolved.delete(datasetId)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (unresolved.size === 0) {
|
|
292
|
+
break
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (candidates.length < SNAPSHOT_ROW_BATCH_SIZE) {
|
|
297
|
+
break
|
|
298
|
+
}
|
|
299
|
+
beforeSnapshotId = candidates[candidates.length - 1]?.snapshotId
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return result
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private async queryCatalogSnapshotBatch(
|
|
306
|
+
runtime: DuckDbQueryRuntime,
|
|
307
|
+
beforeSnapshotId: string | undefined,
|
|
308
|
+
limit: number
|
|
309
|
+
): Promise<readonly CatalogScanSnapshot[]> {
|
|
310
|
+
const ducklakeSnapshot = duckLakeMetadataTableName(this.options, "ducklake_snapshot")
|
|
311
|
+
const ducklakeSnapshotChanges = duckLakeMetadataTableName(
|
|
312
|
+
this.options,
|
|
313
|
+
"ducklake_snapshot_changes"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
let whereSql = ""
|
|
317
|
+
if (beforeSnapshotId !== undefined) {
|
|
318
|
+
assertDuckLakeSnapshotId(beforeSnapshotId)
|
|
319
|
+
whereSql = `WHERE snapshot.snapshot_id < ${beforeSnapshotId}`
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const rows = await runtime.query(`
|
|
323
|
+
SELECT
|
|
324
|
+
snapshot.snapshot_id,
|
|
325
|
+
snapshot.snapshot_time,
|
|
326
|
+
changes.changes_made,
|
|
327
|
+
changes.commit_extra_info
|
|
328
|
+
FROM ${ducklakeSnapshot} snapshot
|
|
329
|
+
JOIN ${ducklakeSnapshotChanges} changes ON changes.snapshot_id = snapshot.snapshot_id
|
|
330
|
+
${whereSql}
|
|
331
|
+
ORDER BY snapshot.snapshot_id DESC
|
|
332
|
+
LIMIT ${Math.max(0, Math.trunc(limit))}
|
|
333
|
+
`)
|
|
334
|
+
|
|
335
|
+
return rows.map((row) => {
|
|
336
|
+
const metadata = parseCommitMetadata(getOptionalString(row, "commit_extra_info"))
|
|
337
|
+
return {
|
|
338
|
+
snapshotId: String(getBigIntLike(row, "snapshot_id")),
|
|
339
|
+
createdAt: getDate(row, "snapshot_time"),
|
|
340
|
+
changesMade: getString(row, "changes_made"),
|
|
341
|
+
...(metadata !== undefined ? { metadata } : {}),
|
|
342
|
+
}
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private async queryFileChangeFlags(
|
|
347
|
+
runtime: DuckDbQueryRuntime,
|
|
348
|
+
snapshotIds: readonly string[],
|
|
349
|
+
tableIds: readonly bigint[]
|
|
350
|
+
): Promise<Map<string, FileChangeFlags>> {
|
|
351
|
+
const flags = new Map<string, FileChangeFlags>()
|
|
352
|
+
if (snapshotIds.length === 0 || tableIds.length === 0) {
|
|
353
|
+
return flags
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
for (const snapshotId of snapshotIds) {
|
|
357
|
+
assertDuckLakeSnapshotId(snapshotId)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const snapshotIdList = snapshotIds.join(", ")
|
|
361
|
+
const tableIdList = tableIds.map((tableId) => tableId.toString()).join(", ")
|
|
362
|
+
const ducklakeDataFile = duckLakeMetadataTableName(this.options, "ducklake_data_file")
|
|
363
|
+
const ducklakeDeleteFile = duckLakeMetadataTableName(this.options, "ducklake_delete_file")
|
|
364
|
+
|
|
365
|
+
// Large tables keep their changes in data/delete files instead of inline
|
|
366
|
+
// `changes_made`, so merge file-level changes for the scanned snapshots.
|
|
367
|
+
const rows = await runtime.query(`
|
|
368
|
+
WITH file_changes AS (
|
|
369
|
+
SELECT begin_snapshot AS snapshot_id, table_id, false AS is_delete
|
|
370
|
+
FROM ${ducklakeDataFile}
|
|
371
|
+
WHERE table_id IN (${tableIdList}) AND begin_snapshot IN (${snapshotIdList})
|
|
372
|
+
UNION ALL
|
|
373
|
+
SELECT end_snapshot, table_id, true
|
|
374
|
+
FROM ${ducklakeDataFile}
|
|
375
|
+
WHERE table_id IN (${tableIdList}) AND end_snapshot IN (${snapshotIdList})
|
|
376
|
+
UNION ALL
|
|
377
|
+
SELECT begin_snapshot, table_id, true
|
|
378
|
+
FROM ${ducklakeDeleteFile}
|
|
379
|
+
WHERE table_id IN (${tableIdList}) AND begin_snapshot IN (${snapshotIdList})
|
|
380
|
+
UNION ALL
|
|
381
|
+
SELECT end_snapshot, table_id, true
|
|
382
|
+
FROM ${ducklakeDeleteFile}
|
|
383
|
+
WHERE table_id IN (${tableIdList}) AND end_snapshot IN (${snapshotIdList})
|
|
384
|
+
)
|
|
385
|
+
SELECT snapshot_id, table_id, bool_or(is_delete) AS has_delete
|
|
386
|
+
FROM file_changes
|
|
387
|
+
GROUP BY snapshot_id, table_id
|
|
388
|
+
`)
|
|
389
|
+
|
|
390
|
+
for (const row of rows) {
|
|
391
|
+
const key = fileChangeKey(
|
|
392
|
+
String(getBigIntLike(row, "snapshot_id")),
|
|
393
|
+
getBigIntLike(row, "table_id")
|
|
394
|
+
)
|
|
395
|
+
flags.set(key, { hasFileChange: true, hasFileDeleteChange: getBoolean(row, "has_delete") })
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return flags
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private assertNoMetadataConflict(
|
|
402
|
+
snapshotId: string,
|
|
403
|
+
datasetId: string,
|
|
404
|
+
metadata: SixbCommitMetadata | undefined
|
|
405
|
+
): void {
|
|
406
|
+
if (metadata !== undefined && metadata.datasetId !== datasetId) {
|
|
407
|
+
throw new LakeStorageError(
|
|
408
|
+
`[SixbDuckLake] DuckLake snapshot '${snapshotId}' changed dataset '${datasetId}' but Sixb commit metadata references dataset '${metadata.datasetId}'.`
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async getLatestVersionForDefinition(
|
|
414
|
+
runtime: DuckDbQueryRuntime,
|
|
415
|
+
dataset: DatasetDefinition
|
|
416
|
+
): Promise<DatasetVersion | null> {
|
|
417
|
+
const tableRef = await resolveDatasetTableRef(this.options, runtime, dataset.id)
|
|
418
|
+
return tableRef ? this.getLatestVersionForTableRef(runtime, tableRef) : null
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async getLatestVersionRefForDefinition(
|
|
422
|
+
runtime: DuckDbQueryRuntime,
|
|
423
|
+
dataset: DatasetDefinition
|
|
424
|
+
): Promise<DatasetVersionRef | null> {
|
|
425
|
+
const summary = await this.getLatestVersionSummaryForDefinition(runtime, dataset)
|
|
426
|
+
return summary ? { datasetId: summary.datasetId, versionId: summary.versionId } : null
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async getLatestVersionSummaryForDefinition(
|
|
430
|
+
runtime: DuckDbQueryRuntime,
|
|
431
|
+
dataset: DatasetDefinition
|
|
432
|
+
): Promise<DuckLakeVersionSummary | null> {
|
|
433
|
+
const tableRef = await resolveDatasetTableRef(this.options, runtime, dataset.id)
|
|
434
|
+
if (!tableRef) {
|
|
435
|
+
return null
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const row = await this.getLatestSnapshotRowForTableRef(runtime, tableRef)
|
|
439
|
+
if (!row) {
|
|
440
|
+
return null
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
datasetId: tableRef.datasetId,
|
|
445
|
+
versionId: toVersionId(row.snapshotId),
|
|
446
|
+
...(row.metadata?.rowCount !== undefined ? { rowCount: row.metadata.rowCount } : {}),
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async getVersionForSnapshot(
|
|
451
|
+
runtime: DuckDbQueryRuntime,
|
|
452
|
+
dataset: DatasetDefinition,
|
|
453
|
+
snapshotId: string
|
|
454
|
+
): Promise<DatasetVersion | null> {
|
|
455
|
+
assertDuckLakeSnapshotId(snapshotId)
|
|
456
|
+
|
|
457
|
+
const tableRef = await resolveDatasetTableRef(this.options, runtime, dataset.id)
|
|
458
|
+
return tableRef ? this.getVersionForTableRef(runtime, tableRef, snapshotId) : null
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async getVersionForTableRef(
|
|
462
|
+
runtime: DuckDbQueryRuntime,
|
|
463
|
+
tableRef: DatasetTableRef,
|
|
464
|
+
snapshotId: string
|
|
465
|
+
): Promise<DatasetVersion | null> {
|
|
466
|
+
const match = await this.getSnapshotRowForTableRef(runtime, tableRef, snapshotId, {
|
|
467
|
+
includeParent: true,
|
|
468
|
+
})
|
|
469
|
+
return match ? this.snapshotToVersion(runtime, tableRef, match) : null
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async getVersionRefForSnapshot(
|
|
473
|
+
runtime: DuckDbQueryRuntime,
|
|
474
|
+
dataset: DatasetDefinition,
|
|
475
|
+
snapshotId: string
|
|
476
|
+
): Promise<DatasetVersionRef | null> {
|
|
477
|
+
assertDuckLakeSnapshotId(snapshotId)
|
|
478
|
+
|
|
479
|
+
const tableRef = await resolveDatasetTableRef(this.options, runtime, dataset.id)
|
|
480
|
+
if (!tableRef) {
|
|
481
|
+
return null
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const row = await this.getSnapshotRowForTableRef(runtime, tableRef, snapshotId, {
|
|
485
|
+
includeParent: false,
|
|
486
|
+
})
|
|
487
|
+
return row ? { datasetId: tableRef.datasetId, versionId: toVersionId(row.snapshotId) } : null
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async listVersionsForDefinition(
|
|
491
|
+
runtime: DuckDbQueryRuntime,
|
|
492
|
+
dataset: DatasetDefinition,
|
|
493
|
+
limit?: number
|
|
494
|
+
): Promise<readonly DatasetVersion[]> {
|
|
495
|
+
const tableRef = await resolveDatasetTableRef(this.options, runtime, dataset.id)
|
|
496
|
+
return tableRef ? this.listVersionsForTableRef(runtime, tableRef, limit) : []
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
private async listVersionsForTableRef(
|
|
500
|
+
runtime: DuckDbQueryRuntime,
|
|
501
|
+
tableRef: DatasetTableRef,
|
|
502
|
+
limit?: number
|
|
503
|
+
): Promise<readonly DatasetVersion[]> {
|
|
504
|
+
const limitedRows = await this.getSnapshotRowsForTableRef(runtime, tableRef, limit)
|
|
505
|
+
const versions: DatasetVersion[] = []
|
|
506
|
+
|
|
507
|
+
for (const row of limitedRows) {
|
|
508
|
+
versions.push(await this.snapshotToVersion(runtime, tableRef, row))
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return versions
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async getLatestVersionForTableRef(
|
|
515
|
+
runtime: DuckDbQueryRuntime,
|
|
516
|
+
tableRef: DatasetTableRef
|
|
517
|
+
): Promise<DatasetVersion | null> {
|
|
518
|
+
const [latest] = await this.listVersionsForTableRef(runtime, tableRef, 1)
|
|
519
|
+
return latest ?? null
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private async getLatestSnapshotRowForTableRef(
|
|
523
|
+
runtime: DuckDbQueryRuntime,
|
|
524
|
+
tableRef: DatasetTableRef
|
|
525
|
+
): Promise<DatasetSnapshotRow | null> {
|
|
526
|
+
const [row] = await this.collectVisibleSnapshotRows(runtime, {
|
|
527
|
+
datasetId: tableRef.datasetId,
|
|
528
|
+
tableId: tableRef.tableId,
|
|
529
|
+
visibleRowLimit: 1,
|
|
530
|
+
})
|
|
531
|
+
return row ?? null
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
private async getSnapshotRowForTableRef(
|
|
535
|
+
runtime: DuckDbQueryRuntime,
|
|
536
|
+
tableRef: DatasetTableRef,
|
|
537
|
+
snapshotId: string,
|
|
538
|
+
options: { readonly includeParent: boolean }
|
|
539
|
+
): Promise<DatasetSnapshotRow | null> {
|
|
540
|
+
assertDuckLakeSnapshotId(snapshotId)
|
|
541
|
+
|
|
542
|
+
const [row] = await this.collectVisibleSnapshotRows(runtime, {
|
|
543
|
+
datasetId: tableRef.datasetId,
|
|
544
|
+
tableId: tableRef.tableId,
|
|
545
|
+
exactSnapshotId: snapshotId,
|
|
546
|
+
visibleRowLimit: 1,
|
|
547
|
+
})
|
|
548
|
+
if (!row) {
|
|
549
|
+
return null
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (!options.includeParent || (row.mode !== "append" && row.mode !== "schema")) {
|
|
553
|
+
return row
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const [parent] = await this.collectVisibleSnapshotRows(runtime, {
|
|
557
|
+
datasetId: tableRef.datasetId,
|
|
558
|
+
tableId: tableRef.tableId,
|
|
559
|
+
beforeSnapshotId: snapshotId,
|
|
560
|
+
visibleRowLimit: 1,
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
return parent ? { ...row, parentSnapshotId: parent.snapshotId } : row
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
private async getSnapshotRowsForTableRef(
|
|
567
|
+
runtime: DuckDbQueryRuntime,
|
|
568
|
+
tableRef: DatasetTableRef,
|
|
569
|
+
limit?: number
|
|
570
|
+
): Promise<readonly DatasetSnapshotRow[]> {
|
|
571
|
+
const visibleLimit = limit === undefined ? undefined : Math.max(0, limit)
|
|
572
|
+
if (visibleLimit === 0) {
|
|
573
|
+
return []
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const rows = await this.collectVisibleSnapshotRows(runtime, {
|
|
577
|
+
datasetId: tableRef.datasetId,
|
|
578
|
+
tableId: tableRef.tableId,
|
|
579
|
+
visibleRowLimit: visibleLimit === undefined ? undefined : visibleLimit + 1,
|
|
580
|
+
})
|
|
581
|
+
const rowsWithParents = this.withParentSnapshotIds(rows)
|
|
582
|
+
return visibleLimit === undefined ? rowsWithParents : rowsWithParents.slice(0, visibleLimit)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
private async collectVisibleSnapshotRows(
|
|
586
|
+
runtime: DuckDbQueryRuntime,
|
|
587
|
+
input: VisibleSnapshotRowsInput
|
|
588
|
+
): Promise<readonly DatasetSnapshotRow[]> {
|
|
589
|
+
if (input.visibleRowLimit !== undefined && input.visibleRowLimit <= 0) {
|
|
590
|
+
return []
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const snapshots: DatasetSnapshotRow[] = []
|
|
594
|
+
let beforeSnapshotId = input.beforeSnapshotId
|
|
595
|
+
|
|
596
|
+
while (input.visibleRowLimit === undefined || snapshots.length < input.visibleRowLimit) {
|
|
597
|
+
const candidates = await this.querySnapshotCandidates(runtime, {
|
|
598
|
+
tableId: input.tableId,
|
|
599
|
+
exactSnapshotId: input.exactSnapshotId,
|
|
600
|
+
beforeSnapshotId,
|
|
601
|
+
limit: input.exactSnapshotId === undefined ? SNAPSHOT_ROW_BATCH_SIZE : 1,
|
|
602
|
+
})
|
|
603
|
+
if (candidates.length === 0) {
|
|
604
|
+
break
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
for (const candidate of candidates) {
|
|
608
|
+
const snapshot = this.candidateToSnapshotRow(input.datasetId, input.tableId, candidate)
|
|
609
|
+
if (snapshot) {
|
|
610
|
+
snapshots.push(snapshot)
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (input.visibleRowLimit !== undefined && snapshots.length >= input.visibleRowLimit) {
|
|
614
|
+
break
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (input.exactSnapshotId !== undefined || candidates.length < SNAPSHOT_ROW_BATCH_SIZE) {
|
|
619
|
+
break
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
beforeSnapshotId = candidates[candidates.length - 1]?.snapshotId
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return snapshots
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
private async querySnapshotCandidates(
|
|
629
|
+
runtime: DuckDbQueryRuntime,
|
|
630
|
+
input: SnapshotCandidateQueryInput
|
|
631
|
+
): Promise<readonly DatasetSnapshotCandidateRow[]> {
|
|
632
|
+
if (input.limit !== undefined && input.limit <= 0) {
|
|
633
|
+
return []
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const where: string[] = []
|
|
637
|
+
if (input.exactSnapshotId !== undefined) {
|
|
638
|
+
assertDuckLakeSnapshotId(input.exactSnapshotId)
|
|
639
|
+
where.push(`snapshot.snapshot_id = ${input.exactSnapshotId}`)
|
|
640
|
+
}
|
|
641
|
+
if (input.beforeSnapshotId !== undefined) {
|
|
642
|
+
assertDuckLakeSnapshotId(input.beforeSnapshotId)
|
|
643
|
+
where.push(`snapshot.snapshot_id < ${input.beforeSnapshotId}`)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const whereSql = where.length === 0 ? "" : `WHERE ${where.join(" AND ")}`
|
|
647
|
+
const limitSql =
|
|
648
|
+
input.limit === undefined ? "" : `LIMIT ${Math.max(0, Math.trunc(input.limit))}`
|
|
649
|
+
const ducklakeSnapshot = duckLakeMetadataTableName(this.options, "ducklake_snapshot")
|
|
650
|
+
const ducklakeSnapshotChanges = duckLakeMetadataTableName(
|
|
651
|
+
this.options,
|
|
652
|
+
"ducklake_snapshot_changes"
|
|
653
|
+
)
|
|
654
|
+
const ducklakeDataFile = duckLakeMetadataTableName(this.options, "ducklake_data_file")
|
|
655
|
+
const ducklakeDeleteFile = duckLakeMetadataTableName(this.options, "ducklake_delete_file")
|
|
656
|
+
|
|
657
|
+
const rows = await runtime.query(`
|
|
658
|
+
WITH candidate_snapshots AS (
|
|
659
|
+
-- Recent DuckLake snapshots to inspect; Sixb visibility is filtered later.
|
|
660
|
+
SELECT
|
|
661
|
+
snapshot.snapshot_id,
|
|
662
|
+
snapshot.snapshot_time,
|
|
663
|
+
changes.changes_made,
|
|
664
|
+
changes.commit_extra_info
|
|
665
|
+
FROM ${ducklakeSnapshot} snapshot
|
|
666
|
+
JOIN ${ducklakeSnapshotChanges} changes
|
|
667
|
+
ON changes.snapshot_id = snapshot.snapshot_id
|
|
668
|
+
${whereSql}
|
|
669
|
+
ORDER BY snapshot.snapshot_id DESC
|
|
670
|
+
${limitSql}
|
|
671
|
+
),
|
|
672
|
+
file_changes AS (
|
|
673
|
+
-- File metadata tells whether this table changed in each candidate snapshot.
|
|
674
|
+
SELECT begin_snapshot AS snapshot_id, false AS is_delete_change
|
|
675
|
+
FROM ${ducklakeDataFile}
|
|
676
|
+
WHERE table_id = ${input.tableId}
|
|
677
|
+
AND begin_snapshot IN (SELECT snapshot_id FROM candidate_snapshots)
|
|
678
|
+
UNION ALL
|
|
679
|
+
SELECT end_snapshot AS snapshot_id, true AS is_delete_change
|
|
680
|
+
FROM ${ducklakeDataFile}
|
|
681
|
+
WHERE table_id = ${input.tableId}
|
|
682
|
+
AND end_snapshot IN (SELECT snapshot_id FROM candidate_snapshots)
|
|
683
|
+
UNION ALL
|
|
684
|
+
SELECT begin_snapshot AS snapshot_id, true AS is_delete_change
|
|
685
|
+
FROM ${ducklakeDeleteFile}
|
|
686
|
+
WHERE table_id = ${input.tableId}
|
|
687
|
+
AND begin_snapshot IN (SELECT snapshot_id FROM candidate_snapshots)
|
|
688
|
+
UNION ALL
|
|
689
|
+
SELECT end_snapshot AS snapshot_id, true AS is_delete_change
|
|
690
|
+
FROM ${ducklakeDeleteFile}
|
|
691
|
+
WHERE table_id = ${input.tableId}
|
|
692
|
+
AND end_snapshot IN (SELECT snapshot_id FROM candidate_snapshots)
|
|
693
|
+
),
|
|
694
|
+
file_changes_by_snapshot AS (
|
|
695
|
+
-- Collapse file-level changes into one row per snapshot.
|
|
696
|
+
SELECT
|
|
697
|
+
snapshot_id,
|
|
698
|
+
count(*) > 0 AS has_file_change,
|
|
699
|
+
count(*) FILTER (WHERE is_delete_change) > 0 AS has_file_delete_change
|
|
700
|
+
FROM file_changes
|
|
701
|
+
GROUP BY snapshot_id
|
|
702
|
+
)
|
|
703
|
+
SELECT
|
|
704
|
+
-- Keep metadata-only candidates; Sixb filters them with commit_extra_info.
|
|
705
|
+
candidate.snapshot_id,
|
|
706
|
+
candidate.snapshot_time,
|
|
707
|
+
candidate.changes_made,
|
|
708
|
+
candidate.commit_extra_info,
|
|
709
|
+
coalesce(file_changes.has_file_change, false) AS has_file_change,
|
|
710
|
+
coalesce(file_changes.has_file_delete_change, false) AS has_file_delete_change
|
|
711
|
+
FROM candidate_snapshots candidate
|
|
712
|
+
LEFT JOIN file_changes_by_snapshot file_changes
|
|
713
|
+
ON file_changes.snapshot_id = candidate.snapshot_id
|
|
714
|
+
ORDER BY candidate.snapshot_id DESC
|
|
715
|
+
`)
|
|
716
|
+
|
|
717
|
+
return rows.map((row) => {
|
|
718
|
+
const metadata = parseCommitMetadata(row.commit_extra_info)
|
|
719
|
+
return {
|
|
720
|
+
snapshotId: String(getBigIntLike(row, "snapshot_id")),
|
|
721
|
+
createdAt: getDate(row, "snapshot_time"),
|
|
722
|
+
changesMade: getString(row, "changes_made"),
|
|
723
|
+
hasFileChange: getBoolean(row, "has_file_change"),
|
|
724
|
+
hasFileDeleteChange: getBoolean(row, "has_file_delete_change"),
|
|
725
|
+
...(metadata !== undefined ? { metadata } : {}),
|
|
726
|
+
}
|
|
727
|
+
})
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
private candidateToSnapshotRow(
|
|
731
|
+
datasetId: string,
|
|
732
|
+
tableId: bigint,
|
|
733
|
+
candidate: DatasetSnapshotCandidateRow
|
|
734
|
+
): DatasetSnapshotRow | null {
|
|
735
|
+
const inlineChange = parseInlineDataChange(candidate.changesMade, tableId)
|
|
736
|
+
const hasDataChange = inlineChange.hasDataChange || candidate.hasFileChange
|
|
737
|
+
const hasDeleteChange = inlineChange.hasDeleteChange || candidate.hasFileDeleteChange
|
|
738
|
+
|
|
739
|
+
// Metadata-only snapshots are common for table comments, schema changes,
|
|
740
|
+
// and other catalog operations. Treat them as dataset versions only when
|
|
741
|
+
// their Sixb metadata names this dataset.
|
|
742
|
+
if (!hasDataChange) {
|
|
743
|
+
const metadata = candidate.metadata
|
|
744
|
+
if (!metadata || metadata.datasetId !== datasetId) {
|
|
745
|
+
return null
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
return {
|
|
749
|
+
snapshotId: candidate.snapshotId,
|
|
750
|
+
tableId,
|
|
751
|
+
createdAt: candidate.createdAt,
|
|
752
|
+
mode: metadata.mode ?? "schema",
|
|
753
|
+
metadata,
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// A real data-change snapshot belongs to this dataset because DuckLake's
|
|
758
|
+
// change metadata touched this table id. If Sixb metadata is present but
|
|
759
|
+
// points elsewhere, fail loudly rather than hydrating the wrong lineage.
|
|
760
|
+
this.assertNoMetadataConflict(candidate.snapshotId, datasetId, candidate.metadata)
|
|
761
|
+
|
|
762
|
+
return {
|
|
763
|
+
snapshotId: candidate.snapshotId,
|
|
764
|
+
tableId,
|
|
765
|
+
createdAt: candidate.createdAt,
|
|
766
|
+
mode: candidate.metadata?.mode ?? (hasDeleteChange ? "snapshot" : "append"),
|
|
767
|
+
...(candidate.metadata !== undefined ? { metadata: candidate.metadata } : {}),
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
private async snapshotToVersion(
|
|
772
|
+
runtime: DuckDbQueryRuntime,
|
|
773
|
+
tableRef: DatasetTableRef,
|
|
774
|
+
row: DatasetSnapshotRow
|
|
775
|
+
): Promise<DatasetVersion> {
|
|
776
|
+
const schemaAtSnapshot = await this.datasets.getDatasetSchemaAtSnapshot(
|
|
777
|
+
runtime,
|
|
778
|
+
tableRef.datasetId,
|
|
779
|
+
tableRef.tableName,
|
|
780
|
+
tableRef.tableId,
|
|
781
|
+
row.snapshotId
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
// DuckLake gives us version id, timestamp, and time travel. Sixb metadata
|
|
785
|
+
// fills in caller intent such as append vs snapshot and producer lineage.
|
|
786
|
+
// Version listing is a metadata path, so it only carries row counts already
|
|
787
|
+
// stored in Sixb commit metadata and never counts historical table contents.
|
|
788
|
+
|
|
789
|
+
return {
|
|
790
|
+
datasetId: tableRef.datasetId,
|
|
791
|
+
versionId: toVersionId(row.snapshotId),
|
|
792
|
+
parentVersionId:
|
|
793
|
+
row.parentSnapshotId === undefined ? undefined : toVersionId(row.parentSnapshotId),
|
|
794
|
+
mode: row.mode,
|
|
795
|
+
createdAt: row.createdAt,
|
|
796
|
+
schema: schemaAtSnapshot,
|
|
797
|
+
producer: row.metadata?.producer,
|
|
798
|
+
inputs: row.metadata?.inputs,
|
|
799
|
+
...(row.metadata?.rowCount !== undefined ? { rowCount: row.metadata.rowCount } : {}),
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
private withParentSnapshotIds(
|
|
804
|
+
snapshots: readonly DatasetSnapshotRow[]
|
|
805
|
+
): readonly DatasetSnapshotRow[] {
|
|
806
|
+
return snapshots.map((snapshot, index) => {
|
|
807
|
+
// Parent ids track versions that reuse the previous dataset state.
|
|
808
|
+
// Snapshot versions replace the rows, so time travel stands on the snapshot id.
|
|
809
|
+
const parentSnapshotId =
|
|
810
|
+
snapshot.mode === "append" || snapshot.mode === "schema"
|
|
811
|
+
? snapshots[index + 1]?.snapshotId
|
|
812
|
+
: undefined
|
|
813
|
+
return parentSnapshotId === undefined ? snapshot : { ...snapshot, parentSnapshotId }
|
|
814
|
+
})
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function assertDuckLakeSnapshotId(snapshotId: string): void {
|
|
819
|
+
if (!/^\d+$/.test(snapshotId)) {
|
|
820
|
+
throw new LakeStorageError(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`)
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function fileChangeKey(snapshotId: string, tableId: bigint): string {
|
|
825
|
+
return `${snapshotId}|${tableId}`
|
|
826
|
+
}
|