@sixb/ducklake 0.1.8 → 0.1.9
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 +59 -4
- package/dist/ducklake-storage.d.ts +1 -0
- package/dist/ducklake-storage.d.ts.map +1 -1
- package/dist/index.js +1817 -511
- package/dist/index.js.map +24 -14
- package/dist/internal/dataset-row-commit.d.ts +10 -0
- package/dist/internal/dataset-row-commit.d.ts.map +1 -1
- package/dist/internal/dataset-row-merge.d.ts.map +1 -1
- package/dist/internal/ducklake-dataset-catalog.d.ts +20 -3
- package/dist/internal/ducklake-dataset-catalog.d.ts.map +1 -1
- package/dist/internal/ducklake-dataset-table-ref.d.ts +13 -0
- package/dist/internal/ducklake-dataset-table-ref.d.ts.map +1 -1
- package/dist/internal/ducklake-maintenance.d.ts +21 -1
- package/dist/internal/ducklake-maintenance.d.ts.map +1 -1
- package/dist/internal/ducklake-snapshot-reader.d.ts +63 -16
- package/dist/internal/ducklake-snapshot-reader.d.ts.map +1 -1
- package/dist/internal/ducklake-sql-executor.d.ts +9 -1
- package/dist/internal/ducklake-sql-executor.d.ts.map +1 -1
- package/dist/internal/ducklake-version-transaction.d.ts +54 -0
- package/dist/internal/ducklake-version-transaction.d.ts.map +1 -0
- package/dist/internal/ducklake-write-coordinator.d.ts +30 -3
- package/dist/internal/ducklake-write-coordinator.d.ts.map +1 -1
- package/dist/internal/lineage-encoding.d.ts +8 -0
- package/dist/internal/lineage-encoding.d.ts.map +1 -0
- package/dist/internal/sql-lineage/analysis.d.ts +32 -0
- package/dist/internal/sql-lineage/analysis.d.ts.map +1 -0
- package/dist/internal/sql-lineage/expressions.d.ts +9 -0
- package/dist/internal/sql-lineage/expressions.d.ts.map +1 -0
- package/dist/internal/sql-lineage/plan-json.d.ts +34 -0
- package/dist/internal/sql-lineage/plan-json.d.ts.map +1 -0
- package/dist/internal/sql-lineage/plan-relation.d.ts +28 -0
- package/dist/internal/sql-lineage/plan-relation.d.ts.map +1 -0
- package/dist/internal/sql-lineage/plan-walker.d.ts +18 -0
- package/dist/internal/sql-lineage/plan-walker.d.ts.map +1 -0
- package/dist/internal/sql-lineage/rejection.d.ts +11 -0
- package/dist/internal/sql-lineage/rejection.d.ts.map +1 -0
- package/dist/internal/sql-lineage/statement-gate.d.ts +21 -0
- package/dist/internal/sql-lineage/statement-gate.d.ts.map +1 -0
- package/dist/internal/sql-lineage/table-functions.d.ts +6 -0
- package/dist/internal/sql-lineage/table-functions.d.ts.map +1 -0
- package/dist/internal/sql-transform-relations.d.ts +20 -5
- package/dist/internal/sql-transform-relations.d.ts.map +1 -1
- package/dist/internal/versions.d.ts +10 -1
- package/dist/internal/versions.d.ts.map +1 -1
- package/dist/types.d.ts +10 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/ducklake-storage.ts +9 -2
- package/src/internal/dataset-row-commit.ts +28 -2
- package/src/internal/dataset-row-merge.ts +4 -0
- package/src/internal/ducklake-dataset-catalog.ts +162 -49
- package/src/internal/ducklake-dataset-table-ref.ts +75 -17
- package/src/internal/ducklake-maintenance.ts +110 -48
- package/src/internal/ducklake-snapshot-reader.ts +352 -304
- package/src/internal/ducklake-sql-executor.ts +214 -14
- package/src/internal/ducklake-version-transaction.ts +224 -0
- package/src/internal/ducklake-write-coordinator.ts +226 -148
- package/src/internal/lineage-encoding.ts +136 -0
- package/src/internal/sql-lineage/analysis.ts +257 -0
- package/src/internal/sql-lineage/expressions.ts +208 -0
- package/src/internal/sql-lineage/plan-json.ts +130 -0
- package/src/internal/sql-lineage/plan-relation.ts +59 -0
- package/src/internal/sql-lineage/plan-walker.ts +727 -0
- package/src/internal/sql-lineage/rejection.ts +16 -0
- package/src/internal/sql-lineage/statement-gate.ts +164 -0
- package/src/internal/sql-lineage/table-functions.ts +11 -0
- package/src/internal/sql-transform-relations.ts +90 -40
- package/src/internal/versions.ts +37 -14
- package/src/types.ts +11 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import type { DatasetDefinition } from "@sixb/core"
|
|
1
|
+
import type { DatasetDefinition, DatasetSchema } from "@sixb/core"
|
|
2
2
|
import type {
|
|
3
3
|
DatasetCatalogState,
|
|
4
4
|
DatasetLatestVersionSummary,
|
|
5
|
+
DatasetProducer,
|
|
5
6
|
DatasetVersion,
|
|
7
|
+
DatasetVersionLineage,
|
|
6
8
|
DatasetVersionMode,
|
|
7
9
|
DatasetVersionRef,
|
|
8
10
|
LatestVersionsSince,
|
|
@@ -10,22 +12,34 @@ import type {
|
|
|
10
12
|
} from "@sixb/core/lake-storage"
|
|
11
13
|
import { LakeStorageError } from "@sixb/core/lake-storage"
|
|
12
14
|
import type { DuckLakeStorageOptions } from "../types"
|
|
13
|
-
import { getBigIntLike,
|
|
15
|
+
import { getBigIntLike, getDate, getOptionalString, getString } from "./duckdb-row"
|
|
14
16
|
import type { DuckDbQueryRuntime } from "./duckdb-runtime"
|
|
15
17
|
import type { DuckLakeConnectionManager } from "./ducklake-connection-manager"
|
|
16
18
|
import type { DuckLakeDatasetCatalog } from "./ducklake-dataset-catalog"
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
+
import {
|
|
20
|
+
type DatasetTableRef,
|
|
21
|
+
datasetTableIds,
|
|
22
|
+
resolveDatasetTableRef,
|
|
23
|
+
resolveDatasetTableRefAtCurrentSnapshot,
|
|
24
|
+
} from "./ducklake-dataset-table-ref"
|
|
25
|
+
import type { LatestVersionAtSnapshot } from "./ducklake-version-transaction"
|
|
26
|
+
import { decodeLineage } from "./lineage-encoding"
|
|
27
|
+
import {
|
|
28
|
+
decodeDatasetTableName,
|
|
29
|
+
encodeDatasetSequenceTableName,
|
|
30
|
+
encodeDatasetTableName,
|
|
31
|
+
} from "./names"
|
|
19
32
|
import { buildDuckLakeMetadataQuery, duckLakeMetadataTableName, quoteSqlString } from "./sql"
|
|
20
33
|
import {
|
|
34
|
+
changesTouchTables,
|
|
21
35
|
parseCommitMetadata,
|
|
22
|
-
|
|
36
|
+
parseTableDataChange,
|
|
23
37
|
parseVersionId,
|
|
24
38
|
type SixbCommitMetadata,
|
|
25
39
|
toVersionId,
|
|
26
40
|
} from "./versions"
|
|
27
41
|
|
|
28
|
-
interface DatasetSnapshotRow {
|
|
42
|
+
export interface DatasetSnapshotRow {
|
|
29
43
|
readonly snapshotId: string
|
|
30
44
|
readonly tableId: bigint
|
|
31
45
|
readonly createdAt: Date
|
|
@@ -38,8 +52,6 @@ interface DatasetSnapshotCandidateRow {
|
|
|
38
52
|
readonly snapshotId: string
|
|
39
53
|
readonly createdAt: Date
|
|
40
54
|
readonly changesMade: string
|
|
41
|
-
readonly hasFileChange: boolean
|
|
42
|
-
readonly hasFileDeleteChange: boolean
|
|
43
55
|
readonly metadata?: SixbCommitMetadata
|
|
44
56
|
}
|
|
45
57
|
|
|
@@ -50,31 +62,34 @@ interface FeedTable {
|
|
|
50
62
|
readonly startSnapshotId: bigint
|
|
51
63
|
}
|
|
52
64
|
|
|
53
|
-
interface MissingSnapshotCandidate {
|
|
54
|
-
readonly snapshotId: string
|
|
55
|
-
readonly missing: true
|
|
56
|
-
}
|
|
57
|
-
|
|
58
65
|
interface SnapshotCandidateQueryInput {
|
|
59
|
-
readonly
|
|
60
|
-
readonly tableId: bigint
|
|
66
|
+
readonly table: DatasetTableRef
|
|
61
67
|
readonly exactSnapshotId?: string
|
|
62
68
|
readonly beforeSnapshotId?: string
|
|
63
69
|
readonly limit: number
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
interface VisibleSnapshotRowsInput {
|
|
67
|
-
readonly
|
|
68
|
-
readonly tableId: bigint
|
|
73
|
+
readonly table: DatasetTableRef
|
|
69
74
|
readonly exactSnapshotId?: string
|
|
70
75
|
readonly beforeSnapshotId?: string
|
|
71
76
|
readonly visibleRowLimit?: number
|
|
72
77
|
}
|
|
73
78
|
|
|
79
|
+
/** A version reference, with the DuckLake table and snapshot it was resolved through. */
|
|
80
|
+
export interface DuckLakeTableVersionRef extends DatasetVersionRef {
|
|
81
|
+
readonly table: DatasetTableRef
|
|
82
|
+
readonly snapshot: DatasetSnapshotRow
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** What a commit needs of the version it builds on. */
|
|
74
86
|
export interface DuckLakeVersionSummary {
|
|
75
87
|
readonly datasetId: string
|
|
76
88
|
readonly versionId: string
|
|
89
|
+
readonly schema: DatasetSchema
|
|
90
|
+
readonly producer?: DatasetProducer
|
|
77
91
|
readonly inputs?: readonly DatasetVersionRef[]
|
|
92
|
+
readonly lineage?: DatasetVersionLineage
|
|
78
93
|
readonly rowCount?: number
|
|
79
94
|
readonly validatedPrimaryKeyColumns?: readonly string[]
|
|
80
95
|
}
|
|
@@ -84,19 +99,17 @@ interface CatalogScanSnapshot {
|
|
|
84
99
|
readonly snapshotId: string
|
|
85
100
|
readonly createdAt: Date
|
|
86
101
|
readonly changesMade: string
|
|
87
|
-
readonly metadata?: SixbCommitMetadata
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Per (snapshot, table) file-change flags merged into the catalog scan. */
|
|
91
|
-
interface FileChangeFlags {
|
|
92
|
-
readonly hasFileChange: boolean
|
|
93
|
-
readonly hasFileDeleteChange: boolean
|
|
94
102
|
}
|
|
95
103
|
|
|
96
104
|
const SNAPSHOT_ROW_BATCH_SIZE = 128
|
|
97
105
|
// One feed read covers at most this many commits; the next read continues from its cursor.
|
|
98
106
|
const FEED_PAGE_SIZE = 512
|
|
99
107
|
|
|
108
|
+
// A read of the latest version, or of the one before a version, usually finds it among the
|
|
109
|
+
// dataset's most recent snapshots. It first reads only a few of them: each carries the commit
|
|
110
|
+
// metadata of its version, whose lineage may be large.
|
|
111
|
+
const SHORT_READ_BATCH_SIZE = 8
|
|
112
|
+
|
|
100
113
|
// The bulk catalog scan shares one descending walk of recent snapshots across
|
|
101
114
|
// all requested datasets, so its cost is bounded by this window rather than by
|
|
102
115
|
// the dataset count. A dataset whose latest version is older than the window
|
|
@@ -113,8 +126,13 @@ const CATALOG_SNAPSHOT_SCAN_LIMIT = 512
|
|
|
113
126
|
*
|
|
114
127
|
* This class intentionally reads DuckLake metadata directly. The provider does
|
|
115
128
|
* not keep a Sixb side table for versions; a Sixb version is a DuckLake
|
|
116
|
-
* snapshot
|
|
117
|
-
* Sixb
|
|
129
|
+
* snapshot whose `changes_made` names the dataset table: one that changed its
|
|
130
|
+
* rows, or that altered the table with Sixb commit metadata naming the dataset,
|
|
131
|
+
* as a version that changes no rows does (see the write coordinator's
|
|
132
|
+
* `claimTable`).
|
|
133
|
+
*
|
|
134
|
+
* DuckLake names a table in `changes_made` for every change to its rows, inlined
|
|
135
|
+
* or written to files, so version reads never scan the catalog's file tables.
|
|
118
136
|
*/
|
|
119
137
|
export class DuckLakeSnapshotReader {
|
|
120
138
|
constructor(
|
|
@@ -189,11 +207,11 @@ export class DuckLakeSnapshotReader {
|
|
|
189
207
|
datasetIds: readonly string[]
|
|
190
208
|
): Promise<readonly DatasetCatalogState[]> {
|
|
191
209
|
const uniqueIds = [...new Set(datasetIds)]
|
|
192
|
-
const
|
|
193
|
-
const latestRowByDatasetId = await this.resolveLatestSnapshotRows(runtime,
|
|
210
|
+
const tablesByDatasetId = await this.resolveDatasetTables(runtime, uniqueIds)
|
|
211
|
+
const latestRowByDatasetId = await this.resolveLatestSnapshotRows(runtime, tablesByDatasetId)
|
|
194
212
|
|
|
195
213
|
return uniqueIds.map((datasetId) => {
|
|
196
|
-
if (!
|
|
214
|
+
if (!tablesByDatasetId.has(datasetId)) {
|
|
197
215
|
return { datasetId, materialized: false, latestVersion: null }
|
|
198
216
|
}
|
|
199
217
|
|
|
@@ -339,16 +357,10 @@ export class DuckLakeSnapshotReader {
|
|
|
339
357
|
`
|
|
340
358
|
)
|
|
341
359
|
)
|
|
342
|
-
const fileFlags = await this.queryFileChangeFlags(
|
|
343
|
-
runtime,
|
|
344
|
-
snapshots.map((snapshot) => snapshot.snapshotId),
|
|
345
|
-
tables.map((table) => table.tableId)
|
|
346
|
-
)
|
|
347
360
|
const changesBySnapshotId = new Map(
|
|
348
361
|
changeRows.map((row) => [String(getBigIntLike(row, "snapshot_id")), row])
|
|
349
362
|
)
|
|
350
363
|
const tablesByTableId = new Map(tables.map((table) => [table.tableId.toString(), table]))
|
|
351
|
-
const tablesByDatasetId = new Map(tables.map((table) => [table.datasetId, table]))
|
|
352
364
|
|
|
353
365
|
for (const snapshot of snapshots) {
|
|
354
366
|
const changes = changesBySnapshotId.get(snapshot.snapshotId)
|
|
@@ -358,29 +370,21 @@ export class DuckLakeSnapshotReader {
|
|
|
358
370
|
const changesMade = getString(changes, "changes_made")
|
|
359
371
|
const metadata = parseCommitMetadata(changes.commit_extra_info)
|
|
360
372
|
|
|
361
|
-
// Only tables this snapshot names
|
|
373
|
+
// Only the tables this snapshot names can have a version in it.
|
|
362
374
|
const touched = new Set<FeedTable>()
|
|
363
375
|
for (const change of changesMade.split(",")) {
|
|
364
376
|
const table = tablesByTableId.get(change.split(":")[1] ?? "")
|
|
365
377
|
if (table) touched.add(table)
|
|
366
378
|
}
|
|
367
|
-
for (const table of tables) {
|
|
368
|
-
if (fileFlags.has(fileChangeKey(snapshot.snapshotId, table.tableId))) touched.add(table)
|
|
369
|
-
}
|
|
370
|
-
const tagged = metadata === undefined ? undefined : tablesByDatasetId.get(metadata.datasetId)
|
|
371
|
-
if (tagged) touched.add(tagged)
|
|
372
379
|
|
|
373
380
|
for (const table of touched) {
|
|
374
381
|
if (BigInt(snapshot.snapshotId) < table.startSnapshotId) {
|
|
375
382
|
continue
|
|
376
383
|
}
|
|
377
|
-
const flags = fileFlags.get(fileChangeKey(snapshot.snapshotId, table.tableId))
|
|
378
384
|
const row = this.candidateToSnapshotRow(table.datasetId, table.tableId, {
|
|
379
385
|
snapshotId: snapshot.snapshotId,
|
|
380
386
|
createdAt: snapshot.createdAt,
|
|
381
387
|
changesMade,
|
|
382
|
-
hasFileChange: flags?.hasFileChange ?? false,
|
|
383
|
-
hasFileDeleteChange: flags?.hasFileDeleteChange ?? false,
|
|
384
388
|
...(metadata !== undefined ? { metadata } : {}),
|
|
385
389
|
})
|
|
386
390
|
if (row && row.mode !== "schema") {
|
|
@@ -392,6 +396,33 @@ export class DuckLakeSnapshotReader {
|
|
|
392
396
|
return latest
|
|
393
397
|
}
|
|
394
398
|
|
|
399
|
+
/**
|
|
400
|
+
* The snapshot of every dataset's latest version, however old, from one shared walk down the
|
|
401
|
+
* whole history: maintenance must never expire one.
|
|
402
|
+
*/
|
|
403
|
+
async latestVersionSnapshotIds(runtime: DuckDbQueryRuntime): Promise<readonly string[]> {
|
|
404
|
+
const tables = await runtime.query(`
|
|
405
|
+
SELECT table_name
|
|
406
|
+
FROM ${duckLakeMetadataTableName(this.options, "ducklake_table")}
|
|
407
|
+
WHERE end_snapshot IS NULL
|
|
408
|
+
`)
|
|
409
|
+
const datasetIds = tables.flatMap((table) => {
|
|
410
|
+
const datasetId = decodeDatasetTableName(getString(table, "table_name"))
|
|
411
|
+
return datasetId === null ? [] : [datasetId]
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
if (datasetIds.length === 0) {
|
|
415
|
+
return []
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const latest = await this.resolveLatestSnapshotRows(
|
|
419
|
+
runtime,
|
|
420
|
+
await this.resolveDatasetTables(runtime, datasetIds),
|
|
421
|
+
Number.POSITIVE_INFINITY
|
|
422
|
+
)
|
|
423
|
+
return [...latest.values()].map((row) => row.snapshotId)
|
|
424
|
+
}
|
|
425
|
+
|
|
395
426
|
private snapshotRowToSummary(
|
|
396
427
|
datasetId: string,
|
|
397
428
|
row: DatasetSnapshotRow
|
|
@@ -405,19 +436,21 @@ export class DuckLakeSnapshotReader {
|
|
|
405
436
|
}
|
|
406
437
|
}
|
|
407
438
|
|
|
408
|
-
private async
|
|
439
|
+
private async resolveDatasetTables(
|
|
409
440
|
runtime: DuckDbQueryRuntime,
|
|
410
441
|
datasetIds: readonly string[]
|
|
411
|
-
): Promise<Map<string,
|
|
442
|
+
): Promise<Map<string, DatasetTableRef>> {
|
|
412
443
|
// Encoded table names are a collision-free bijection with dataset ids, so a
|
|
413
|
-
// single `ducklake_table` read maps every requested id to its current
|
|
444
|
+
// single `ducklake_table` read maps every requested id to its current tables.
|
|
414
445
|
const datasetIdByTableName = new Map<string, string>()
|
|
446
|
+
const datasetIdBySequenceTableName = new Map<string, string>()
|
|
415
447
|
for (const datasetId of datasetIds) {
|
|
416
448
|
datasetIdByTableName.set(encodeDatasetTableName(datasetId), datasetId)
|
|
449
|
+
datasetIdBySequenceTableName.set(encodeDatasetSequenceTableName(datasetId), datasetId)
|
|
417
450
|
}
|
|
418
451
|
|
|
419
452
|
const ducklakeTable = duckLakeMetadataTableName(this.options, "ducklake_table")
|
|
420
|
-
const tableNameList = [...datasetIdByTableName.keys()]
|
|
453
|
+
const tableNameList = [...datasetIdByTableName.keys(), ...datasetIdBySequenceTableName.keys()]
|
|
421
454
|
.map((name) => quoteSqlString(name))
|
|
422
455
|
.join(", ")
|
|
423
456
|
const rows = await runtime.query(`
|
|
@@ -427,15 +460,31 @@ export class DuckLakeSnapshotReader {
|
|
|
427
460
|
AND end_snapshot IS NULL
|
|
428
461
|
`)
|
|
429
462
|
|
|
430
|
-
const
|
|
463
|
+
const tablesByDatasetId = new Map<string, DatasetTableRef>()
|
|
431
464
|
for (const row of rows) {
|
|
432
|
-
const
|
|
465
|
+
const tableName = getString(row, "table_name")
|
|
466
|
+
const datasetId = datasetIdByTableName.get(tableName)
|
|
433
467
|
if (datasetId !== undefined) {
|
|
434
|
-
|
|
468
|
+
tablesByDatasetId.set(datasetId, {
|
|
469
|
+
datasetId,
|
|
470
|
+
tableName,
|
|
471
|
+
tableId: getBigIntLike(row, "table_id"),
|
|
472
|
+
})
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
for (const row of rows) {
|
|
477
|
+
const datasetId = datasetIdBySequenceTableName.get(getString(row, "table_name"))
|
|
478
|
+
const table = datasetId === undefined ? undefined : tablesByDatasetId.get(datasetId)
|
|
479
|
+
if (datasetId !== undefined && table !== undefined) {
|
|
480
|
+
tablesByDatasetId.set(datasetId, {
|
|
481
|
+
...table,
|
|
482
|
+
sequenceTableId: getBigIntLike(row, "table_id"),
|
|
483
|
+
})
|
|
435
484
|
}
|
|
436
485
|
}
|
|
437
486
|
|
|
438
|
-
return
|
|
487
|
+
return tablesByDatasetId
|
|
439
488
|
}
|
|
440
489
|
|
|
441
490
|
/**
|
|
@@ -447,19 +496,19 @@ export class DuckLakeSnapshotReader {
|
|
|
447
496
|
*/
|
|
448
497
|
private async resolveLatestSnapshotRows(
|
|
449
498
|
runtime: DuckDbQueryRuntime,
|
|
450
|
-
|
|
499
|
+
tablesByDatasetId: ReadonlyMap<string, DatasetTableRef>,
|
|
500
|
+
scanLimit = CATALOG_SNAPSHOT_SCAN_LIMIT
|
|
451
501
|
): Promise<Map<string, DatasetSnapshotRow>> {
|
|
452
502
|
const result = new Map<string, DatasetSnapshotRow>()
|
|
453
|
-
if (
|
|
503
|
+
if (tablesByDatasetId.size === 0) {
|
|
454
504
|
return result
|
|
455
505
|
}
|
|
456
506
|
|
|
457
|
-
const unresolved = new Map<string,
|
|
458
|
-
const tableIds = [...new Set(tableIdsByDatasetId.values())]
|
|
507
|
+
const unresolved = new Map<string, DatasetTableRef>(tablesByDatasetId)
|
|
459
508
|
|
|
460
509
|
let beforeSnapshotId: string | undefined
|
|
461
510
|
let scanned = 0
|
|
462
|
-
while (unresolved.size > 0 && scanned <
|
|
511
|
+
while (unresolved.size > 0 && scanned < scanLimit) {
|
|
463
512
|
const candidates = await this.queryCatalogSnapshotBatch(
|
|
464
513
|
runtime,
|
|
465
514
|
beforeSnapshotId,
|
|
@@ -469,23 +518,27 @@ export class DuckLakeSnapshotReader {
|
|
|
469
518
|
break
|
|
470
519
|
}
|
|
471
520
|
|
|
472
|
-
const
|
|
521
|
+
const tables = [...unresolved.values()]
|
|
522
|
+
const metadata = await this.readCommitMetadata(
|
|
473
523
|
runtime,
|
|
474
|
-
candidates
|
|
475
|
-
|
|
524
|
+
candidates
|
|
525
|
+
.filter((candidate) =>
|
|
526
|
+
tables.some((table) =>
|
|
527
|
+
changesTouchTables(candidate.changesMade, datasetTableIds(table))
|
|
528
|
+
)
|
|
529
|
+
)
|
|
530
|
+
.map((candidate) => candidate.snapshotId)
|
|
476
531
|
)
|
|
477
532
|
|
|
478
533
|
for (const candidate of candidates) {
|
|
479
534
|
scanned += 1
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
const row = this.candidateToSnapshotRow(datasetId, tableId, {
|
|
535
|
+
const candidateMetadata = metadata.get(candidate.snapshotId)
|
|
536
|
+
for (const [datasetId, table] of [...unresolved]) {
|
|
537
|
+
const row = this.candidateToSnapshotRow(datasetId, table.tableId, {
|
|
483
538
|
snapshotId: candidate.snapshotId,
|
|
484
539
|
createdAt: candidate.createdAt,
|
|
485
540
|
changesMade: candidate.changesMade,
|
|
486
|
-
|
|
487
|
-
hasFileDeleteChange: flags?.hasFileDeleteChange ?? false,
|
|
488
|
-
...(candidate.metadata !== undefined ? { metadata: candidate.metadata } : {}),
|
|
541
|
+
...(candidateMetadata !== undefined ? { metadata: candidateMetadata } : {}),
|
|
489
542
|
})
|
|
490
543
|
if (row) {
|
|
491
544
|
result.set(datasetId, row)
|
|
@@ -528,8 +581,7 @@ export class DuckLakeSnapshotReader {
|
|
|
528
581
|
SELECT
|
|
529
582
|
snapshot.snapshot_id,
|
|
530
583
|
snapshot.snapshot_time,
|
|
531
|
-
changes.changes_made
|
|
532
|
-
changes.commit_extra_info
|
|
584
|
+
changes.changes_made
|
|
533
585
|
FROM ${ducklakeSnapshot} snapshot
|
|
534
586
|
JOIN ${ducklakeSnapshotChanges} changes ON changes.snapshot_id = snapshot.snapshot_id
|
|
535
587
|
${whereSql}
|
|
@@ -537,70 +589,11 @@ export class DuckLakeSnapshotReader {
|
|
|
537
589
|
LIMIT ${Math.max(0, Math.trunc(limit))}
|
|
538
590
|
`)
|
|
539
591
|
|
|
540
|
-
return rows.map((row) => {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
changesMade: getString(row, "changes_made"),
|
|
546
|
-
...(metadata !== undefined ? { metadata } : {}),
|
|
547
|
-
}
|
|
548
|
-
})
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
private async queryFileChangeFlags(
|
|
552
|
-
runtime: DuckDbQueryRuntime,
|
|
553
|
-
snapshotIds: readonly string[],
|
|
554
|
-
tableIds: readonly bigint[]
|
|
555
|
-
): Promise<Map<string, FileChangeFlags>> {
|
|
556
|
-
const flags = new Map<string, FileChangeFlags>()
|
|
557
|
-
if (snapshotIds.length === 0 || tableIds.length === 0) {
|
|
558
|
-
return flags
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
for (const snapshotId of snapshotIds) {
|
|
562
|
-
assertDuckLakeSnapshotId(snapshotId)
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
const snapshotIdList = snapshotIds.join(", ")
|
|
566
|
-
const tableIdList = tableIds.map((tableId) => tableId.toString()).join(", ")
|
|
567
|
-
const ducklakeDataFile = duckLakeMetadataTableName(this.options, "ducklake_data_file")
|
|
568
|
-
const ducklakeDeleteFile = duckLakeMetadataTableName(this.options, "ducklake_delete_file")
|
|
569
|
-
|
|
570
|
-
// Large tables keep their changes in data/delete files instead of inline
|
|
571
|
-
// `changes_made`, so merge file-level changes for the scanned snapshots.
|
|
572
|
-
const rows = await runtime.query(`
|
|
573
|
-
WITH file_changes AS (
|
|
574
|
-
SELECT begin_snapshot AS snapshot_id, table_id, false AS is_delete
|
|
575
|
-
FROM ${ducklakeDataFile}
|
|
576
|
-
WHERE table_id IN (${tableIdList}) AND begin_snapshot IN (${snapshotIdList})
|
|
577
|
-
UNION ALL
|
|
578
|
-
SELECT end_snapshot, table_id, true
|
|
579
|
-
FROM ${ducklakeDataFile}
|
|
580
|
-
WHERE table_id IN (${tableIdList}) AND end_snapshot IN (${snapshotIdList})
|
|
581
|
-
UNION ALL
|
|
582
|
-
SELECT begin_snapshot, table_id, true
|
|
583
|
-
FROM ${ducklakeDeleteFile}
|
|
584
|
-
WHERE table_id IN (${tableIdList}) AND begin_snapshot IN (${snapshotIdList})
|
|
585
|
-
UNION ALL
|
|
586
|
-
SELECT end_snapshot, table_id, true
|
|
587
|
-
FROM ${ducklakeDeleteFile}
|
|
588
|
-
WHERE table_id IN (${tableIdList}) AND end_snapshot IN (${snapshotIdList})
|
|
589
|
-
)
|
|
590
|
-
SELECT snapshot_id, table_id, bool_or(is_delete) AS has_delete
|
|
591
|
-
FROM file_changes
|
|
592
|
-
GROUP BY snapshot_id, table_id
|
|
593
|
-
`)
|
|
594
|
-
|
|
595
|
-
for (const row of rows) {
|
|
596
|
-
const key = fileChangeKey(
|
|
597
|
-
String(getBigIntLike(row, "snapshot_id")),
|
|
598
|
-
getBigIntLike(row, "table_id")
|
|
599
|
-
)
|
|
600
|
-
flags.set(key, { hasFileChange: true, hasFileDeleteChange: getBoolean(row, "has_delete") })
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
return flags
|
|
592
|
+
return rows.map((row) => ({
|
|
593
|
+
snapshotId: String(getBigIntLike(row, "snapshot_id")),
|
|
594
|
+
createdAt: getDate(row, "snapshot_time"),
|
|
595
|
+
changesMade: getString(row, "changes_made"),
|
|
596
|
+
}))
|
|
604
597
|
}
|
|
605
598
|
|
|
606
599
|
private assertNoMetadataConflict(
|
|
@@ -626,9 +619,14 @@ export class DuckLakeSnapshotReader {
|
|
|
626
619
|
async getLatestVersionRefForDefinition(
|
|
627
620
|
runtime: DuckDbQueryRuntime,
|
|
628
621
|
dataset: DatasetDefinition
|
|
629
|
-
): Promise<
|
|
630
|
-
const
|
|
631
|
-
|
|
622
|
+
): Promise<DuckLakeTableVersionRef | null> {
|
|
623
|
+
const tableRef = await resolveDatasetTableRef(this.options, runtime, dataset.id)
|
|
624
|
+
if (!tableRef) {
|
|
625
|
+
return null
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const row = await this.getLatestSnapshotRowForTableRef(runtime, tableRef)
|
|
629
|
+
return row ? tableVersionRef(tableRef, row) : null
|
|
632
630
|
}
|
|
633
631
|
|
|
634
632
|
async getLatestVersionSummaryForDefinition(
|
|
@@ -636,19 +634,49 @@ export class DuckLakeSnapshotReader {
|
|
|
636
634
|
dataset: DatasetDefinition
|
|
637
635
|
): Promise<DuckLakeVersionSummary | null> {
|
|
638
636
|
const tableRef = await resolveDatasetTableRef(this.options, runtime, dataset.id)
|
|
639
|
-
|
|
640
|
-
|
|
637
|
+
return tableRef ? this.getLatestVersionSummaryForTableRef(runtime, tableRef) : null
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* The latest version a commit builds on, with the lake's snapshot it was read at. Its first
|
|
642
|
+
* read fixes the snapshot of a transaction it runs in (see `beginTransactionOnLatestVersion`).
|
|
643
|
+
*/
|
|
644
|
+
async getLatestVersionSummaryAtCurrentSnapshot(
|
|
645
|
+
runtime: DuckDbQueryRuntime,
|
|
646
|
+
dataset: DatasetDefinition
|
|
647
|
+
): Promise<LatestVersionAtSnapshot<DuckLakeVersionSummary>> {
|
|
648
|
+
const { snapshotId, table } = await resolveDatasetTableRefAtCurrentSnapshot(
|
|
649
|
+
this.options,
|
|
650
|
+
runtime,
|
|
651
|
+
dataset.id
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
return {
|
|
655
|
+
snapshotId,
|
|
656
|
+
latestVersion: table ? await this.getLatestVersionSummaryForTableRef(runtime, table) : null,
|
|
641
657
|
}
|
|
658
|
+
}
|
|
642
659
|
|
|
660
|
+
private async getLatestVersionSummaryForTableRef(
|
|
661
|
+
runtime: DuckDbQueryRuntime,
|
|
662
|
+
tableRef: DatasetTableRef
|
|
663
|
+
): Promise<DuckLakeVersionSummary | null> {
|
|
643
664
|
const row = await this.getLatestSnapshotRowForTableRef(runtime, tableRef)
|
|
644
665
|
if (!row) {
|
|
645
666
|
return null
|
|
646
667
|
}
|
|
647
668
|
|
|
669
|
+
const [schema] = await this.datasets.getDatasetSchemasAtSnapshots(runtime, [
|
|
670
|
+
{ table: tableRef, snapshotId: row.snapshotId },
|
|
671
|
+
])
|
|
672
|
+
|
|
648
673
|
return {
|
|
649
674
|
datasetId: tableRef.datasetId,
|
|
650
675
|
versionId: toVersionId(row.snapshotId),
|
|
676
|
+
schema,
|
|
677
|
+
producer: row.metadata?.producer,
|
|
651
678
|
inputs: row.metadata?.inputs,
|
|
679
|
+
...storedLineage(tableRef.datasetId, row),
|
|
652
680
|
...(row.metadata?.rowCount !== undefined ? { rowCount: row.metadata.rowCount } : {}),
|
|
653
681
|
...(row.metadata?.validatedPrimaryKeyColumns !== undefined
|
|
654
682
|
? { validatedPrimaryKeyColumns: row.metadata.validatedPrimaryKeyColumns }
|
|
@@ -675,14 +703,14 @@ export class DuckLakeSnapshotReader {
|
|
|
675
703
|
const match = await this.getSnapshotRowForTableRef(runtime, tableRef, snapshotId, {
|
|
676
704
|
includeParent: true,
|
|
677
705
|
})
|
|
678
|
-
return match ? this.
|
|
706
|
+
return match ? (await this.snapshotsToVersions(runtime, tableRef, [match]))[0] : null
|
|
679
707
|
}
|
|
680
708
|
|
|
681
709
|
async getVersionRefForSnapshot(
|
|
682
710
|
runtime: DuckDbQueryRuntime,
|
|
683
711
|
dataset: DatasetDefinition,
|
|
684
712
|
snapshotId: string
|
|
685
|
-
): Promise<
|
|
713
|
+
): Promise<DuckLakeTableVersionRef | null> {
|
|
686
714
|
assertDuckLakeSnapshotId(snapshotId)
|
|
687
715
|
|
|
688
716
|
const tableRef = await resolveDatasetTableRef(this.options, runtime, dataset.id)
|
|
@@ -693,7 +721,23 @@ export class DuckLakeSnapshotReader {
|
|
|
693
721
|
const row = await this.getSnapshotRowForTableRef(runtime, tableRef, snapshotId, {
|
|
694
722
|
includeParent: false,
|
|
695
723
|
})
|
|
696
|
-
return row ?
|
|
724
|
+
return row ? tableVersionRef(tableRef, row) : null
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* The versions resolved refs point to, with their schemas from one catalog read. Refs resolve
|
|
729
|
+
* without their parent, so these versions carry no `parentVersionId`.
|
|
730
|
+
*/
|
|
731
|
+
async getVersionsForRefs(
|
|
732
|
+
runtime: DuckDbQueryRuntime,
|
|
733
|
+
refs: readonly DuckLakeTableVersionRef[]
|
|
734
|
+
): Promise<DatasetVersion[]> {
|
|
735
|
+
const schemas = await this.datasets.getDatasetSchemasAtSnapshots(
|
|
736
|
+
runtime,
|
|
737
|
+
refs.map((ref) => ({ table: ref.table, snapshotId: ref.snapshot.snapshotId }))
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
return refs.map((ref, index) => snapshotToVersion(ref.table, ref.snapshot, schemas[index]))
|
|
697
741
|
}
|
|
698
742
|
|
|
699
743
|
async listVersionsForDefinition(
|
|
@@ -711,13 +755,7 @@ export class DuckLakeSnapshotReader {
|
|
|
711
755
|
limit?: number
|
|
712
756
|
): Promise<readonly DatasetVersion[]> {
|
|
713
757
|
const limitedRows = await this.getSnapshotRowsForTableRef(runtime, tableRef, limit)
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
for (const row of limitedRows) {
|
|
717
|
-
versions.push(await this.snapshotToVersion(runtime, tableRef, row))
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
return versions
|
|
758
|
+
return this.snapshotsToVersions(runtime, tableRef, limitedRows)
|
|
721
759
|
}
|
|
722
760
|
|
|
723
761
|
async getLatestVersionForTableRef(
|
|
@@ -733,8 +771,7 @@ export class DuckLakeSnapshotReader {
|
|
|
733
771
|
tableRef: DatasetTableRef
|
|
734
772
|
): Promise<DatasetSnapshotRow | null> {
|
|
735
773
|
const [row] = await this.collectVisibleSnapshotRows(runtime, {
|
|
736
|
-
|
|
737
|
-
tableId: tableRef.tableId,
|
|
774
|
+
table: tableRef,
|
|
738
775
|
visibleRowLimit: 1,
|
|
739
776
|
})
|
|
740
777
|
return row ?? null
|
|
@@ -749,8 +786,7 @@ export class DuckLakeSnapshotReader {
|
|
|
749
786
|
assertDuckLakeSnapshotId(snapshotId)
|
|
750
787
|
|
|
751
788
|
const [row] = await this.collectVisibleSnapshotRows(runtime, {
|
|
752
|
-
|
|
753
|
-
tableId: tableRef.tableId,
|
|
789
|
+
table: tableRef,
|
|
754
790
|
exactSnapshotId: snapshotId,
|
|
755
791
|
visibleRowLimit: 1,
|
|
756
792
|
})
|
|
@@ -766,8 +802,7 @@ export class DuckLakeSnapshotReader {
|
|
|
766
802
|
}
|
|
767
803
|
|
|
768
804
|
const [parent] = await this.collectVisibleSnapshotRows(runtime, {
|
|
769
|
-
|
|
770
|
-
tableId: tableRef.tableId,
|
|
805
|
+
table: tableRef,
|
|
771
806
|
beforeSnapshotId: snapshotId,
|
|
772
807
|
visibleRowLimit: 1,
|
|
773
808
|
})
|
|
@@ -786,8 +821,7 @@ export class DuckLakeSnapshotReader {
|
|
|
786
821
|
}
|
|
787
822
|
|
|
788
823
|
const rows = await this.collectVisibleSnapshotRows(runtime, {
|
|
789
|
-
|
|
790
|
-
tableId: tableRef.tableId,
|
|
824
|
+
table: tableRef,
|
|
791
825
|
visibleRowLimit: visibleLimit === undefined ? undefined : visibleLimit + 1,
|
|
792
826
|
})
|
|
793
827
|
const rowsWithParents = this.withParentSnapshotIds(rows)
|
|
@@ -804,24 +838,29 @@ export class DuckLakeSnapshotReader {
|
|
|
804
838
|
|
|
805
839
|
const snapshots: DatasetSnapshotRow[] = []
|
|
806
840
|
let beforeSnapshotId = input.beforeSnapshotId
|
|
841
|
+
let shortRead =
|
|
842
|
+
input.exactSnapshotId !== undefined ||
|
|
843
|
+
(input.visibleRowLimit !== undefined && input.visibleRowLimit <= 2)
|
|
807
844
|
|
|
808
845
|
while (input.visibleRowLimit === undefined || snapshots.length < input.visibleRowLimit) {
|
|
846
|
+
const batchSize = shortRead ? SHORT_READ_BATCH_SIZE : SNAPSHOT_ROW_BATCH_SIZE
|
|
809
847
|
const candidates = await this.querySnapshotCandidates(runtime, {
|
|
810
|
-
|
|
811
|
-
tableId: input.tableId,
|
|
848
|
+
table: input.table,
|
|
812
849
|
exactSnapshotId: input.exactSnapshotId,
|
|
813
850
|
beforeSnapshotId,
|
|
814
|
-
limit: input.exactSnapshotId === undefined ?
|
|
851
|
+
limit: input.exactSnapshotId === undefined ? batchSize : 1,
|
|
815
852
|
})
|
|
853
|
+
shortRead = false
|
|
816
854
|
if (candidates.length === 0) {
|
|
817
855
|
break
|
|
818
856
|
}
|
|
819
857
|
|
|
820
858
|
for (const candidate of candidates) {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
859
|
+
const snapshot = this.candidateToSnapshotRow(
|
|
860
|
+
input.table.datasetId,
|
|
861
|
+
input.table.tableId,
|
|
862
|
+
candidate
|
|
863
|
+
)
|
|
825
864
|
if (snapshot) {
|
|
826
865
|
snapshots.push(snapshot)
|
|
827
866
|
}
|
|
@@ -831,7 +870,7 @@ export class DuckLakeSnapshotReader {
|
|
|
831
870
|
}
|
|
832
871
|
}
|
|
833
872
|
|
|
834
|
-
if (input.exactSnapshotId !== undefined || candidates.length <
|
|
873
|
+
if (input.exactSnapshotId !== undefined || candidates.length < batchSize) {
|
|
835
874
|
break
|
|
836
875
|
}
|
|
837
876
|
|
|
@@ -842,20 +881,20 @@ export class DuckLakeSnapshotReader {
|
|
|
842
881
|
}
|
|
843
882
|
|
|
844
883
|
/**
|
|
845
|
-
* Hydrates, newest first, the snapshots
|
|
846
|
-
* that
|
|
847
|
-
*
|
|
848
|
-
* candidateToSnapshotRow decides visibility. Only this table's snapshots are read, so the cost
|
|
849
|
-
* follows the dataset's own history rather than every commit in the catalog.
|
|
884
|
+
* Hydrates, newest first, the snapshots whose changes name one of the dataset's tables: every
|
|
885
|
+
* snapshot that may hold one of its versions. The text match is a superset;
|
|
886
|
+
* candidateToSnapshotRow decides visibility.
|
|
850
887
|
*
|
|
851
|
-
*
|
|
852
|
-
*
|
|
888
|
+
* The query walks the catalog's snapshot ids downwards, from `beforeSnapshotId` or the head,
|
|
889
|
+
* and stops at the limit or at the table's creation. A lookup therefore reads the snapshots
|
|
890
|
+
* between where it starts and the versions it returns: a few for an active dataset, at most the
|
|
891
|
+
* table's lifetime for one written long ago. It never reads the file tables, whose replaced
|
|
892
|
+
* files stay until cleanup.
|
|
853
893
|
*/
|
|
854
894
|
private async querySnapshotCandidates(
|
|
855
895
|
runtime: DuckDbQueryRuntime,
|
|
856
896
|
input: SnapshotCandidateQueryInput
|
|
857
|
-
): Promise<readonly
|
|
858
|
-
const tableId = input.tableId.toString()
|
|
897
|
+
): Promise<readonly DatasetSnapshotCandidateRow[]> {
|
|
859
898
|
const limit = Math.max(0, Math.trunc(input.limit))
|
|
860
899
|
if (limit === 0) {
|
|
861
900
|
return []
|
|
@@ -872,130 +911,102 @@ export class DuckLakeSnapshotReader {
|
|
|
872
911
|
// A dataset cannot have a version before its table exists. Use the earliest table record:
|
|
873
912
|
// a rename may create a newer record with the same table id and must not hide older
|
|
874
913
|
// versions.
|
|
875
|
-
const
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
UNION ALL
|
|
889
|
-
SELECT begin_snapshot
|
|
890
|
-
FROM ${table("ducklake_delete_file")} WHERE table_id = ${tableId}
|
|
891
|
-
UNION ALL
|
|
892
|
-
SELECT end_snapshot
|
|
893
|
-
FROM ${table("ducklake_delete_file")}
|
|
894
|
-
WHERE table_id = ${tableId} AND end_snapshot IS NOT NULL
|
|
895
|
-
UNION ALL
|
|
896
|
-
SELECT snapshot_id
|
|
897
|
-
FROM ${table("ducklake_snapshot_changes")}
|
|
898
|
-
WHERE strpos(',' || changes_made || ',', ${quoteSqlString(`:${tableId},`)}) > 0
|
|
899
|
-
OR strpos(
|
|
900
|
-
commit_extra_info,
|
|
901
|
-
${quoteSqlString(`"datasetId":${JSON.stringify(input.datasetId)}`)}
|
|
902
|
-
) > 0
|
|
903
|
-
) touched
|
|
904
|
-
WHERE snapshot_id >= ${tableStart}
|
|
905
|
-
${input.beforeSnapshotId === undefined ? "" : `AND snapshot_id < ${input.beforeSnapshotId}`}
|
|
906
|
-
ORDER BY snapshot_id DESC
|
|
907
|
-
LIMIT ${limit}`
|
|
914
|
+
const bounds = [
|
|
915
|
+
`changes.snapshot_id >= (
|
|
916
|
+
SELECT min(begin_snapshot)
|
|
917
|
+
FROM ${table("ducklake_table")}
|
|
918
|
+
WHERE table_id = ${input.table.tableId}
|
|
919
|
+
)`,
|
|
920
|
+
]
|
|
921
|
+
if (input.exactSnapshotId !== undefined) {
|
|
922
|
+
bounds.push(`changes.snapshot_id = ${input.exactSnapshotId}`)
|
|
923
|
+
}
|
|
924
|
+
if (input.beforeSnapshotId !== undefined) {
|
|
925
|
+
bounds.push(`changes.snapshot_id < ${input.beforeSnapshotId}`)
|
|
926
|
+
}
|
|
908
927
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
candidate_snapshots AS (
|
|
914
|
-
SELECT
|
|
915
|
-
snapshot.snapshot_id,
|
|
916
|
-
snapshot.snapshot_time,
|
|
917
|
-
changes.changes_made,
|
|
918
|
-
changes.commit_extra_info
|
|
919
|
-
FROM ${table("ducklake_snapshot")} snapshot
|
|
920
|
-
JOIN ${table("ducklake_snapshot_changes")} changes
|
|
921
|
-
ON changes.snapshot_id = snapshot.snapshot_id
|
|
922
|
-
WHERE snapshot.snapshot_id IN (SELECT snapshot_id FROM candidate_ids)
|
|
923
|
-
),
|
|
924
|
-
file_changes AS (
|
|
925
|
-
-- File metadata tells whether this table changed in each candidate snapshot.
|
|
926
|
-
SELECT begin_snapshot AS snapshot_id, false AS is_delete_change
|
|
927
|
-
FROM ${table("ducklake_data_file")}
|
|
928
|
-
WHERE table_id = ${tableId}
|
|
929
|
-
AND begin_snapshot IN (SELECT snapshot_id FROM candidate_ids)
|
|
930
|
-
UNION ALL
|
|
931
|
-
SELECT end_snapshot AS snapshot_id, true AS is_delete_change
|
|
932
|
-
FROM ${table("ducklake_data_file")}
|
|
933
|
-
WHERE table_id = ${tableId}
|
|
934
|
-
AND end_snapshot IN (SELECT snapshot_id FROM candidate_ids)
|
|
935
|
-
UNION ALL
|
|
936
|
-
SELECT begin_snapshot AS snapshot_id, true AS is_delete_change
|
|
937
|
-
FROM ${table("ducklake_delete_file")}
|
|
938
|
-
WHERE table_id = ${tableId}
|
|
939
|
-
AND begin_snapshot IN (SELECT snapshot_id FROM candidate_ids)
|
|
940
|
-
UNION ALL
|
|
941
|
-
SELECT end_snapshot AS snapshot_id, true AS is_delete_change
|
|
942
|
-
FROM ${table("ducklake_delete_file")}
|
|
943
|
-
WHERE table_id = ${tableId}
|
|
944
|
-
AND end_snapshot IN (SELECT snapshot_id FROM candidate_ids)
|
|
945
|
-
),
|
|
946
|
-
file_changes_by_snapshot AS (
|
|
947
|
-
-- Collapse file-level changes into one row per snapshot.
|
|
948
|
-
SELECT
|
|
949
|
-
snapshot_id,
|
|
950
|
-
count(*) > 0 AS has_file_change,
|
|
951
|
-
count(*) FILTER (WHERE is_delete_change) > 0 AS has_file_delete_change
|
|
952
|
-
FROM file_changes
|
|
953
|
-
GROUP BY snapshot_id
|
|
928
|
+
const namesDatasetTable = datasetTableIds(input.table)
|
|
929
|
+
.map(
|
|
930
|
+
(tableId) =>
|
|
931
|
+
`strpos(',' || changes.changes_made || ',', ${quoteSqlString(`:${tableId},`)}) > 0`
|
|
954
932
|
)
|
|
933
|
+
.join(" OR ")
|
|
934
|
+
|
|
935
|
+
// Bounds and order follow the snapshot-id primary key, so PostgreSQL reads it backwards
|
|
936
|
+
// and stops at the limit; the text match only filters the rows it passes.
|
|
937
|
+
return `
|
|
955
938
|
SELECT
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
ON file_changes.snapshot_id = candidate_ids.snapshot_id
|
|
968
|
-
ORDER BY candidate_ids.snapshot_id DESC
|
|
939
|
+
changes.snapshot_id,
|
|
940
|
+
snapshot.snapshot_time,
|
|
941
|
+
changes.changes_made,
|
|
942
|
+
changes.commit_extra_info
|
|
943
|
+
FROM ${table("ducklake_snapshot_changes")} changes
|
|
944
|
+
JOIN ${table("ducklake_snapshot")} snapshot
|
|
945
|
+
ON snapshot.snapshot_id = changes.snapshot_id
|
|
946
|
+
WHERE ${bounds.join(" AND ")}
|
|
947
|
+
AND (${namesDatasetTable})
|
|
948
|
+
ORDER BY changes.snapshot_id DESC
|
|
949
|
+
LIMIT ${limit}
|
|
969
950
|
`
|
|
970
951
|
})
|
|
971
952
|
)
|
|
972
953
|
|
|
973
954
|
return rows.map((row) => {
|
|
974
|
-
const snapshotId = String(getBigIntLike(row, "snapshot_id"))
|
|
975
|
-
// Expired snapshots keep their file records; they are no longer versions.
|
|
976
|
-
if (row.snapshot_time === null || row.snapshot_time === undefined) {
|
|
977
|
-
return { snapshotId, missing: true } as const
|
|
978
|
-
}
|
|
979
955
|
const metadata = parseCommitMetadata(row.commit_extra_info)
|
|
956
|
+
|
|
980
957
|
return {
|
|
981
|
-
snapshotId,
|
|
958
|
+
snapshotId: String(getBigIntLike(row, "snapshot_id")),
|
|
982
959
|
createdAt: getDate(row, "snapshot_time"),
|
|
983
960
|
changesMade: getString(row, "changes_made"),
|
|
984
|
-
hasFileChange: getBoolean(row, "has_file_change"),
|
|
985
|
-
hasFileDeleteChange: getBoolean(row, "has_file_delete_change"),
|
|
986
961
|
...(metadata !== undefined ? { metadata } : {}),
|
|
987
962
|
}
|
|
988
963
|
})
|
|
989
964
|
}
|
|
990
965
|
|
|
966
|
+
/**
|
|
967
|
+
* Sixb commit metadata of the snapshots that changed a scanned dataset. Each one carries the
|
|
968
|
+
* lineage of its version, which can be large: scans read it only for the snapshots they keep,
|
|
969
|
+
* never for the other datasets' snapshots they page past.
|
|
970
|
+
*/
|
|
971
|
+
private async readCommitMetadata(
|
|
972
|
+
runtime: DuckDbQueryRuntime,
|
|
973
|
+
snapshotIds: readonly string[]
|
|
974
|
+
): Promise<Map<string, SixbCommitMetadata>> {
|
|
975
|
+
const metadata = new Map<string, SixbCommitMetadata>()
|
|
976
|
+
if (snapshotIds.length === 0) {
|
|
977
|
+
return metadata
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
for (const snapshotId of snapshotIds) {
|
|
981
|
+
assertDuckLakeSnapshotId(snapshotId)
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const ducklakeSnapshotChanges = duckLakeMetadataTableName(
|
|
985
|
+
this.options,
|
|
986
|
+
"ducklake_snapshot_changes"
|
|
987
|
+
)
|
|
988
|
+
const rows = await runtime.query(`
|
|
989
|
+
SELECT snapshot_id, commit_extra_info
|
|
990
|
+
FROM ${ducklakeSnapshotChanges}
|
|
991
|
+
WHERE snapshot_id IN (${snapshotIds.join(", ")})
|
|
992
|
+
`)
|
|
993
|
+
|
|
994
|
+
for (const row of rows) {
|
|
995
|
+
const parsed = parseCommitMetadata(getOptionalString(row, "commit_extra_info"))
|
|
996
|
+
if (parsed !== undefined) {
|
|
997
|
+
metadata.set(String(getBigIntLike(row, "snapshot_id")), parsed)
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
return metadata
|
|
1002
|
+
}
|
|
1003
|
+
|
|
991
1004
|
private candidateToSnapshotRow(
|
|
992
1005
|
datasetId: string,
|
|
993
1006
|
tableId: bigint,
|
|
994
1007
|
candidate: DatasetSnapshotCandidateRow
|
|
995
1008
|
): DatasetSnapshotRow | null {
|
|
996
|
-
const
|
|
997
|
-
const hasDataChange = inlineChange.hasDataChange || candidate.hasFileChange
|
|
998
|
-
const hasDeleteChange = inlineChange.hasDeleteChange || candidate.hasFileDeleteChange
|
|
1009
|
+
const { hasDataChange, hasDeleteChange } = parseTableDataChange(candidate.changesMade, tableId)
|
|
999
1010
|
|
|
1000
1011
|
// Metadata-only snapshots are common for table comments, schema changes,
|
|
1001
1012
|
// and other catalog operations. Treat them as dataset versions only when
|
|
@@ -1029,36 +1040,17 @@ export class DuckLakeSnapshotReader {
|
|
|
1029
1040
|
}
|
|
1030
1041
|
}
|
|
1031
1042
|
|
|
1032
|
-
|
|
1043
|
+
/** One catalog read for the schemas of every listed version, not one per version. */
|
|
1044
|
+
private async snapshotsToVersions(
|
|
1033
1045
|
runtime: DuckDbQueryRuntime,
|
|
1034
1046
|
tableRef: DatasetTableRef,
|
|
1035
|
-
|
|
1036
|
-
): Promise<DatasetVersion> {
|
|
1037
|
-
const
|
|
1047
|
+
rows: readonly DatasetSnapshotRow[]
|
|
1048
|
+
): Promise<DatasetVersion[]> {
|
|
1049
|
+
const schemas = await this.datasets.getDatasetSchemasAtSnapshots(
|
|
1038
1050
|
runtime,
|
|
1039
|
-
tableRef.
|
|
1040
|
-
tableRef.tableName,
|
|
1041
|
-
tableRef.tableId,
|
|
1042
|
-
row.snapshotId
|
|
1051
|
+
rows.map((row) => ({ table: tableRef, snapshotId: row.snapshotId }))
|
|
1043
1052
|
)
|
|
1044
|
-
|
|
1045
|
-
// DuckLake gives us version id, timestamp, and time travel. Sixb metadata
|
|
1046
|
-
// fills in caller intent such as append vs snapshot and producer lineage.
|
|
1047
|
-
// Version listing is a metadata path, so it only carries row counts already
|
|
1048
|
-
// stored in Sixb commit metadata and never counts historical table contents.
|
|
1049
|
-
|
|
1050
|
-
return {
|
|
1051
|
-
datasetId: tableRef.datasetId,
|
|
1052
|
-
versionId: toVersionId(row.snapshotId),
|
|
1053
|
-
parentVersionId:
|
|
1054
|
-
row.parentSnapshotId === undefined ? undefined : toVersionId(row.parentSnapshotId),
|
|
1055
|
-
mode: row.mode,
|
|
1056
|
-
createdAt: row.createdAt,
|
|
1057
|
-
schema: schemaAtSnapshot,
|
|
1058
|
-
producer: row.metadata?.producer,
|
|
1059
|
-
inputs: row.metadata?.inputs,
|
|
1060
|
-
...(row.metadata?.rowCount !== undefined ? { rowCount: row.metadata.rowCount } : {}),
|
|
1061
|
-
}
|
|
1053
|
+
return rows.map((row, index) => snapshotToVersion(tableRef, row, schemas[index]))
|
|
1062
1054
|
}
|
|
1063
1055
|
|
|
1064
1056
|
private withParentSnapshotIds(
|
|
@@ -1076,12 +1068,68 @@ export class DuckLakeSnapshotReader {
|
|
|
1076
1068
|
}
|
|
1077
1069
|
}
|
|
1078
1070
|
|
|
1071
|
+
function snapshotToVersion(
|
|
1072
|
+
tableRef: DatasetTableRef,
|
|
1073
|
+
row: DatasetSnapshotRow,
|
|
1074
|
+
schema: DatasetVersion["schema"]
|
|
1075
|
+
): DatasetVersion {
|
|
1076
|
+
// DuckLake gives us version id, timestamp, and time travel. Sixb metadata
|
|
1077
|
+
// fills in caller intent such as append vs snapshot and producer lineage.
|
|
1078
|
+
// Version listing is a metadata path, so it only carries row counts already
|
|
1079
|
+
// stored in Sixb commit metadata and never counts historical table contents.
|
|
1080
|
+
|
|
1081
|
+
return {
|
|
1082
|
+
datasetId: tableRef.datasetId,
|
|
1083
|
+
versionId: toVersionId(row.snapshotId),
|
|
1084
|
+
parentVersionId:
|
|
1085
|
+
row.parentSnapshotId === undefined ? undefined : toVersionId(row.parentSnapshotId),
|
|
1086
|
+
mode: row.mode,
|
|
1087
|
+
createdAt: row.createdAt,
|
|
1088
|
+
schema,
|
|
1089
|
+
producer: row.metadata?.producer,
|
|
1090
|
+
inputs: row.metadata?.inputs,
|
|
1091
|
+
...storedLineage(tableRef.datasetId, row),
|
|
1092
|
+
...(row.metadata?.rowCount !== undefined ? { rowCount: row.metadata.rowCount } : {}),
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* A data snapshot without Sixb metadata was written outside Sixb, by any DuckLake client. It may
|
|
1098
|
+
* keep rows of its parent whatever change it made, and nothing says where its own rows come from:
|
|
1099
|
+
* its lineage is unknown.
|
|
1100
|
+
*/
|
|
1101
|
+
function storedLineage(
|
|
1102
|
+
datasetId: string,
|
|
1103
|
+
row: DatasetSnapshotRow
|
|
1104
|
+
): Pick<DatasetVersion, "lineage"> {
|
|
1105
|
+
const metadata = row.metadata
|
|
1106
|
+
if (metadata === undefined) {
|
|
1107
|
+
return {
|
|
1108
|
+
lineage: {
|
|
1109
|
+
unknown: `version '${toVersionId(row.snapshotId)}' of '${datasetId}' was written outside Sixb`,
|
|
1110
|
+
},
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
return metadata.lineage === undefined
|
|
1115
|
+
? {}
|
|
1116
|
+
: { lineage: decodeLineage(metadata.datasetId, metadata.lineage) }
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function tableVersionRef(
|
|
1120
|
+
table: DatasetTableRef,
|
|
1121
|
+
snapshot: DatasetSnapshotRow
|
|
1122
|
+
): DuckLakeTableVersionRef {
|
|
1123
|
+
return {
|
|
1124
|
+
datasetId: table.datasetId,
|
|
1125
|
+
versionId: toVersionId(snapshot.snapshotId),
|
|
1126
|
+
table,
|
|
1127
|
+
snapshot,
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1079
1131
|
function assertDuckLakeSnapshotId(snapshotId: string): void {
|
|
1080
1132
|
if (!/^\d+$/.test(snapshotId)) {
|
|
1081
1133
|
throw new LakeStorageError(`[SixbDuckLake] Invalid DuckLake snapshot id '${snapshotId}'.`)
|
|
1082
1134
|
}
|
|
1083
1135
|
}
|
|
1084
|
-
|
|
1085
|
-
function fileChangeKey(snapshotId: string, tableId: bigint): string {
|
|
1086
|
-
return `${snapshotId}|${tableId}`
|
|
1087
|
-
}
|