@sixb/ducklake 0.1.7 → 0.1.8
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 +1 -1
- package/dist/ducklake-storage.d.ts +2 -1
- package/dist/ducklake-storage.d.ts.map +1 -1
- package/dist/index.js +253 -76
- package/dist/index.js.map +5 -5
- package/dist/internal/ducklake-snapshot-reader.d.ts +26 -1
- package/dist/internal/ducklake-snapshot-reader.d.ts.map +1 -1
- package/dist/internal/sql.d.ts +11 -2
- package/dist/internal/sql.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/ducklake-storage.ts +8 -0
- package/src/internal/ducklake-snapshot-reader.ts +331 -84
- package/src/internal/sql.ts +35 -6
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DatasetDefinition } from "@sixb/core";
|
|
2
|
-
import type { DatasetCatalogState, DatasetVersion, DatasetVersionRef } from "@sixb/core/lake-storage";
|
|
2
|
+
import type { DatasetCatalogState, DatasetVersion, DatasetVersionRef, LatestVersionsSince, ListLatestVersionsSinceInput } from "@sixb/core/lake-storage";
|
|
3
3
|
import type { DuckLakeStorageOptions } from "../types";
|
|
4
4
|
import type { DuckDbQueryRuntime } from "./duckdb-runtime";
|
|
5
5
|
import type { DuckLakeConnectionManager } from "./ducklake-connection-manager";
|
|
@@ -41,6 +41,21 @@ export declare class DuckLakeSnapshotReader {
|
|
|
41
41
|
*/
|
|
42
42
|
listDatasetCatalogState(datasetIds: readonly string[]): Promise<readonly DatasetCatalogState[]>;
|
|
43
43
|
private collectDatasetCatalogState;
|
|
44
|
+
/**
|
|
45
|
+
* Change feed over DuckLake's snapshot table, which is an append-only commit log: snapshot ids
|
|
46
|
+
* only increase, and a commit becomes visible only after every lower id has (concurrent commits
|
|
47
|
+
* that pick the same id conflict, and the loser retries with the next one). The cursor is the
|
|
48
|
+
* last snapshot id read. Every query is bounded by a snapshot-id range, so an idle read is one
|
|
49
|
+
* indexed query that returns the cursor's own snapshot, whatever the history length.
|
|
50
|
+
*/
|
|
51
|
+
listLatestVersionsSince(input: ListLatestVersionsSinceInput): Promise<LatestVersionsSince | null>;
|
|
52
|
+
private queryHeadSnapshotId;
|
|
53
|
+
/** The cursor's snapshot and up to one feed page of snapshots after it, oldest first. */
|
|
54
|
+
private querySnapshotsFrom;
|
|
55
|
+
/** Current dataset tables, with the first snapshot each table id existed in. */
|
|
56
|
+
private resolveFeedTables;
|
|
57
|
+
/** Folds a page of snapshots, oldest first, into each dataset's newest data version. */
|
|
58
|
+
private collectFeedPage;
|
|
44
59
|
private snapshotRowToSummary;
|
|
45
60
|
private resolveDatasetTableIds;
|
|
46
61
|
/**
|
|
@@ -67,6 +82,16 @@ export declare class DuckLakeSnapshotReader {
|
|
|
67
82
|
private getSnapshotRowForTableRef;
|
|
68
83
|
private getSnapshotRowsForTableRef;
|
|
69
84
|
private collectVisibleSnapshotRows;
|
|
85
|
+
/**
|
|
86
|
+
* Hydrates, newest first, the snapshots that may hold a version of one dataset table: those
|
|
87
|
+
* that added or removed its data or delete files, that DuckLake recorded as changing it inline,
|
|
88
|
+
* or whose Sixb commit metadata names the dataset. The text matches are a superset;
|
|
89
|
+
* candidateToSnapshotRow decides visibility. Only this table's snapshots are read, so the cost
|
|
90
|
+
* follows the dataset's own history rather than every commit in the catalog.
|
|
91
|
+
*
|
|
92
|
+
* Returns one row per candidate id, including ids whose snapshot no longer exists, so the caller
|
|
93
|
+
* can tell a short batch from the end of the history.
|
|
94
|
+
*/
|
|
70
95
|
private querySnapshotCandidates;
|
|
71
96
|
private candidateToSnapshotRow;
|
|
72
97
|
private snapshotToVersion;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ducklake-snapshot-reader.d.ts","sourceRoot":"","sources":["../../src/internal/ducklake-snapshot-reader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AACnD,OAAO,KAAK,EACV,mBAAmB,EAEnB,cAAc,EAEd,iBAAiB,
|
|
1
|
+
{"version":3,"file":"ducklake-snapshot-reader.d.ts","sourceRoot":"","sources":["../../src/internal/ducklake-snapshot-reader.ts"],"names":[],"mappings":"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"}
|
package/dist/internal/sql.d.ts
CHANGED
|
@@ -19,10 +19,19 @@ export declare function buildSetPostgresPoolSql(options: Pick<DuckLakeStorageOpt
|
|
|
19
19
|
* Render a fully-qualified DuckLake metadata table path.
|
|
20
20
|
*
|
|
21
21
|
* PostgreSQL catalogs expose DuckLake metadata tables under the configured
|
|
22
|
-
* metadata schema
|
|
23
|
-
* DuckDB's default `main` schema.
|
|
22
|
+
* metadata schema (PostgreSQL's `public` by default) inside the attached
|
|
23
|
+
* metadata catalog. Local catalogs use DuckDB's default `main` schema.
|
|
24
24
|
*/
|
|
25
25
|
export declare function duckLakeMetadataTableName(options: Pick<DuckLakeStorageOptions, "alias" | "catalog">, tableName: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Render a metadata query that runs where the catalog lives.
|
|
28
|
+
*
|
|
29
|
+
* DuckDB's PostgreSQL scanner pushes comparisons and constant IN lists down to PostgreSQL, but not
|
|
30
|
+
* string matching, so a query that filters on text would copy the whole column into DuckDB. On a
|
|
31
|
+
* PostgreSQL catalog the query therefore runs server-side through `postgres_query`; elsewhere the
|
|
32
|
+
* catalog is local and DuckDB runs it. `build` must produce SQL that both dialects accept.
|
|
33
|
+
*/
|
|
34
|
+
export declare function buildDuckLakeMetadataQuery(options: Pick<DuckLakeStorageOptions, "alias" | "catalog">, build: (table: (tableName: string) => string) => string): string;
|
|
26
35
|
/**
|
|
27
36
|
* Quote a SQL identifier after validating it is already identifier-shaped.
|
|
28
37
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../../src/internal/sql.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,mBAAmB,EACnB,sBAAsB,EACtB,sBAAsB,EAEtB,8BAA8B,EAG/B,MAAM,UAAU,CAAA;AAIjB;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,sBAAsB,EAAE,OAAO,CAAC,GAAG,MAAM,CAEpF;AAUD;;;;;GAKG;AACH,wBAAgB,qCAAqC,CACnD,OAAO,EAAE,IAAI,CAAC,sBAAsB,EAAE,OAAO,GAAG,cAAc,CAAC,GAC9D,MAAM,CAOR;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,GACpD,SAAS,MAAM,EAAE,CAInB;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,IAAI,CAAC,sBAAsB,EAAE,OAAO,GAAG,SAAS,CAAC,EAC1D,SAAS,EAAE,MAAM,GAChB,MAAM,
|
|
1
|
+
{"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../../src/internal/sql.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,mBAAmB,EACnB,sBAAsB,EACtB,sBAAsB,EAEtB,8BAA8B,EAG/B,MAAM,UAAU,CAAA;AAIjB;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,sBAAsB,EAAE,OAAO,CAAC,GAAG,MAAM,CAEpF;AAUD;;;;;GAKG;AACH,wBAAgB,qCAAqC,CACnD,OAAO,EAAE,IAAI,CAAC,sBAAsB,EAAE,OAAO,GAAG,cAAc,CAAC,GAC9D,MAAM,CAOR;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,GACpD,SAAS,MAAM,EAAE,CAInB;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,IAAI,CAAC,sBAAsB,EAAE,OAAO,GAAG,SAAS,CAAC,EAC1D,SAAS,EAAE,MAAM,GAChB,MAAM,CAIR;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,IAAI,CAAC,sBAAsB,EAAE,OAAO,GAAG,SAAS,CAAC,EAC1D,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,KAAK,MAAM,GACtD,MAAM,CAaR;AAQD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAM1D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAEpE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,IAAI,CAAC,sBAAsB,EAAE,OAAO,CAAC,EAC9C,SAAS,EAAE,MAAM,GAChB,MAAM,CAER;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,sBAAsB,GAAG,MAAM,CAWlE;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,8BAA8B,GAAG,MAAM,CAuBxF;AAkBD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,sBAAsB,GAAG,SAAS,MAAM,EAAE,CAcrF;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,mBAAmB,GAAG,MAAM,CAIxE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,sBAAsB,GAAG,MAAM,CA+BtE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sixb/ducklake",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "DuckDB/DuckLake lake storage provider for Sixb",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sixb",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"typecheck": "tsc --noEmit",
|
|
37
37
|
"test": "bun test",
|
|
38
38
|
"test:e2e": "bun run test:e2e:local && bun run test:e2e:remote",
|
|
39
|
-
"test:e2e:local": "bun test ./tests/ducklake-reader.e2e.ts ./tests/ducklake-changes.e2e.ts ./tests/ducklake-contract.e2e.ts ./tests/ducklake-file-refs.e2e.ts ./tests/ducklake-catalog-state.e2e.ts ./tests/ducklake-catalogs.e2e.ts ./tests/ducklake-concurrency.e2e.ts ./tests/ducklake-datasets.e2e.ts ./tests/ducklake-driver.e2e.ts ./tests/ducklake-maintenance.e2e.ts ./tests/ducklake-merges.e2e.ts ./tests/ducklake-sql-transforms.e2e.ts ./tests/ducklake-versions.e2e.ts ./tests/ducklake-writes.e2e.ts ./tests/ducklake-sqlite-writes.e2e.ts",
|
|
39
|
+
"test:e2e:local": "bun test ./tests/ducklake-reader.e2e.ts ./tests/ducklake-changes.e2e.ts ./tests/ducklake-contract.e2e.ts ./tests/ducklake-file-refs.e2e.ts ./tests/ducklake-catalog-state.e2e.ts ./tests/ducklake-catalogs.e2e.ts ./tests/ducklake-concurrency.e2e.ts ./tests/ducklake-datasets.e2e.ts ./tests/ducklake-driver.e2e.ts ./tests/ducklake-maintenance.e2e.ts ./tests/ducklake-merges.e2e.ts ./tests/ducklake-sql-transforms.e2e.ts ./tests/ducklake-versions.e2e.ts ./tests/ducklake-long-history.e2e.ts ./tests/ducklake-writes.e2e.ts ./tests/ducklake-sqlite-writes.e2e.ts",
|
|
40
40
|
"test:e2e:remote": "bun test --preload ./tests/setup.ts ./tests/ducklake-remote.e2e.ts",
|
|
41
41
|
"prepack": "bun run build"
|
|
42
42
|
},
|
|
@@ -44,10 +44,10 @@
|
|
|
44
44
|
"@duckdb/node-api": "^1.5.2-r.1"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"@sixb/core": "^0.1.
|
|
47
|
+
"@sixb/core": "^0.1.17"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@sixb/core": "0.1.
|
|
50
|
+
"@sixb/core": "0.1.17",
|
|
51
51
|
"@types/bun": "latest",
|
|
52
52
|
"typescript": "^5.7.0"
|
|
53
53
|
},
|
package/src/ducklake-storage.ts
CHANGED
|
@@ -7,6 +7,8 @@ import type {
|
|
|
7
7
|
DatasetVersion,
|
|
8
8
|
LakeMergeSession,
|
|
9
9
|
LakeWriteSession,
|
|
10
|
+
LatestVersionsSince,
|
|
11
|
+
ListLatestVersionsSinceInput,
|
|
10
12
|
ReadDatasetChangesInput,
|
|
11
13
|
ReadDatasetRowsInput,
|
|
12
14
|
} from "@sixb/core/lake-storage"
|
|
@@ -127,6 +129,12 @@ export class DuckLakeStorage implements LakeStorageWithSql<"duckdb"> {
|
|
|
127
129
|
return this.snapshotReader.getVersion(datasetId, versionId)
|
|
128
130
|
}
|
|
129
131
|
|
|
132
|
+
async listLatestVersionsSince(
|
|
133
|
+
input: ListLatestVersionsSinceInput
|
|
134
|
+
): Promise<LatestVersionsSince | null> {
|
|
135
|
+
return this.snapshotReader.listLatestVersionsSince(input)
|
|
136
|
+
}
|
|
137
|
+
|
|
130
138
|
readRows(input: ReadDatasetRowsInput): AsyncIterable<DatasetRow> {
|
|
131
139
|
return this.rows.readRows(input)
|
|
132
140
|
}
|
|
@@ -5,6 +5,8 @@ import type {
|
|
|
5
5
|
DatasetVersion,
|
|
6
6
|
DatasetVersionMode,
|
|
7
7
|
DatasetVersionRef,
|
|
8
|
+
LatestVersionsSince,
|
|
9
|
+
ListLatestVersionsSinceInput,
|
|
8
10
|
} from "@sixb/core/lake-storage"
|
|
9
11
|
import { LakeStorageError } from "@sixb/core/lake-storage"
|
|
10
12
|
import type { DuckLakeStorageOptions } from "../types"
|
|
@@ -13,8 +15,8 @@ import type { DuckDbQueryRuntime } from "./duckdb-runtime"
|
|
|
13
15
|
import type { DuckLakeConnectionManager } from "./ducklake-connection-manager"
|
|
14
16
|
import type { DuckLakeDatasetCatalog } from "./ducklake-dataset-catalog"
|
|
15
17
|
import { type DatasetTableRef, resolveDatasetTableRef } from "./ducklake-dataset-table-ref"
|
|
16
|
-
import { encodeDatasetTableName } from "./names"
|
|
17
|
-
import { duckLakeMetadataTableName, quoteSqlString } from "./sql"
|
|
18
|
+
import { decodeDatasetTableName, encodeDatasetTableName } from "./names"
|
|
19
|
+
import { buildDuckLakeMetadataQuery, duckLakeMetadataTableName, quoteSqlString } from "./sql"
|
|
18
20
|
import {
|
|
19
21
|
parseCommitMetadata,
|
|
20
22
|
parseInlineDataChange,
|
|
@@ -41,11 +43,24 @@ interface DatasetSnapshotCandidateRow {
|
|
|
41
43
|
readonly metadata?: SixbCommitMetadata
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
interface FeedTable {
|
|
47
|
+
readonly datasetId: string
|
|
48
|
+
readonly tableId: bigint
|
|
49
|
+
/** A dataset cannot have a version before its table exists. */
|
|
50
|
+
readonly startSnapshotId: bigint
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface MissingSnapshotCandidate {
|
|
54
|
+
readonly snapshotId: string
|
|
55
|
+
readonly missing: true
|
|
56
|
+
}
|
|
57
|
+
|
|
44
58
|
interface SnapshotCandidateQueryInput {
|
|
59
|
+
readonly datasetId: string
|
|
45
60
|
readonly tableId: bigint
|
|
46
61
|
readonly exactSnapshotId?: string
|
|
47
62
|
readonly beforeSnapshotId?: string
|
|
48
|
-
readonly limit
|
|
63
|
+
readonly limit: number
|
|
49
64
|
}
|
|
50
65
|
|
|
51
66
|
interface VisibleSnapshotRowsInput {
|
|
@@ -79,6 +94,8 @@ interface FileChangeFlags {
|
|
|
79
94
|
}
|
|
80
95
|
|
|
81
96
|
const SNAPSHOT_ROW_BATCH_SIZE = 128
|
|
97
|
+
// One feed read covers at most this many commits; the next read continues from its cursor.
|
|
98
|
+
const FEED_PAGE_SIZE = 512
|
|
82
99
|
|
|
83
100
|
// The bulk catalog scan shares one descending walk of recent snapshots across
|
|
84
101
|
// all requested datasets, so its cost is bounded by this window rather than by
|
|
@@ -189,6 +206,192 @@ export class DuckLakeSnapshotReader {
|
|
|
189
206
|
})
|
|
190
207
|
}
|
|
191
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Change feed over DuckLake's snapshot table, which is an append-only commit log: snapshot ids
|
|
211
|
+
* only increase, and a commit becomes visible only after every lower id has (concurrent commits
|
|
212
|
+
* that pick the same id conflict, and the loser retries with the next one). The cursor is the
|
|
213
|
+
* last snapshot id read. Every query is bounded by a snapshot-id range, so an idle read is one
|
|
214
|
+
* indexed query that returns the cursor's own snapshot, whatever the history length.
|
|
215
|
+
*/
|
|
216
|
+
async listLatestVersionsSince(
|
|
217
|
+
input: ListLatestVersionsSinceInput
|
|
218
|
+
): Promise<LatestVersionsSince | null> {
|
|
219
|
+
this.connections.assertOpen()
|
|
220
|
+
|
|
221
|
+
return this.connections.withAttachedRuntime(async (runtime) => {
|
|
222
|
+
if (input.cursor === null) {
|
|
223
|
+
return { cursor: await this.queryHeadSnapshotId(runtime), versions: [] }
|
|
224
|
+
}
|
|
225
|
+
if (!/^\d+$/.test(input.cursor)) {
|
|
226
|
+
return null
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const snapshots = await this.querySnapshotsFrom(runtime, input.cursor)
|
|
230
|
+
// The cursor's own snapshot proves the cursor is still a position in this lake's history.
|
|
231
|
+
if (snapshots[0]?.snapshotId !== input.cursor) {
|
|
232
|
+
return null
|
|
233
|
+
}
|
|
234
|
+
const newSnapshots = snapshots.slice(1)
|
|
235
|
+
if (newSnapshots.length === 0) {
|
|
236
|
+
return { cursor: input.cursor, versions: [] }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const tables = await this.resolveFeedTables(runtime, input.datasetIds)
|
|
240
|
+
const latest = await this.collectFeedPage(runtime, tables, newSnapshots)
|
|
241
|
+
return {
|
|
242
|
+
cursor: newSnapshots[newSnapshots.length - 1]?.snapshotId ?? input.cursor,
|
|
243
|
+
versions: [...latest].map(([datasetId, row]) => this.snapshotRowToSummary(datasetId, row)),
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private async queryHeadSnapshotId(runtime: DuckDbQueryRuntime): Promise<string> {
|
|
249
|
+
const [row] = await runtime.query(
|
|
250
|
+
buildDuckLakeMetadataQuery(
|
|
251
|
+
this.options,
|
|
252
|
+
(table) => `SELECT max(snapshot_id) AS snapshot_id FROM ${table("ducklake_snapshot")}`
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
if (row?.snapshot_id === null || row?.snapshot_id === undefined) {
|
|
256
|
+
throw new LakeStorageError("[SixbDuckLake] DuckLake catalog has no snapshots.")
|
|
257
|
+
}
|
|
258
|
+
return String(getBigIntLike(row, "snapshot_id"))
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** The cursor's snapshot and up to one feed page of snapshots after it, oldest first. */
|
|
262
|
+
private async querySnapshotsFrom(
|
|
263
|
+
runtime: DuckDbQueryRuntime,
|
|
264
|
+
cursor: string
|
|
265
|
+
): Promise<readonly { readonly snapshotId: string; readonly createdAt: Date }[]> {
|
|
266
|
+
const rows = await runtime.query(
|
|
267
|
+
buildDuckLakeMetadataQuery(
|
|
268
|
+
this.options,
|
|
269
|
+
(table) => `
|
|
270
|
+
SELECT snapshot_id, snapshot_time
|
|
271
|
+
FROM ${table("ducklake_snapshot")}
|
|
272
|
+
WHERE snapshot_id >= ${cursor}
|
|
273
|
+
ORDER BY snapshot_id
|
|
274
|
+
LIMIT ${FEED_PAGE_SIZE + 1}
|
|
275
|
+
`
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
return rows.map((row) => ({
|
|
279
|
+
snapshotId: String(getBigIntLike(row, "snapshot_id")),
|
|
280
|
+
createdAt: getDate(row, "snapshot_time"),
|
|
281
|
+
}))
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Current dataset tables, with the first snapshot each table id existed in. */
|
|
285
|
+
private async resolveFeedTables(
|
|
286
|
+
runtime: DuckDbQueryRuntime,
|
|
287
|
+
datasetIds: readonly string[] | undefined
|
|
288
|
+
): Promise<readonly FeedTable[]> {
|
|
289
|
+
const requested = datasetIds === undefined ? null : new Set(datasetIds)
|
|
290
|
+
const rows = await runtime.query(`
|
|
291
|
+
SELECT table_id, table_name, begin_snapshot, end_snapshot
|
|
292
|
+
FROM ${duckLakeMetadataTableName(this.options, "ducklake_table")}
|
|
293
|
+
`)
|
|
294
|
+
|
|
295
|
+
const startByTableId = new Map<bigint, bigint>()
|
|
296
|
+
const current: { datasetId: string; tableId: bigint }[] = []
|
|
297
|
+
for (const row of rows) {
|
|
298
|
+
const tableId = getBigIntLike(row, "table_id")
|
|
299
|
+
const begin = getBigIntLike(row, "begin_snapshot")
|
|
300
|
+
const start = startByTableId.get(tableId)
|
|
301
|
+
if (start === undefined || begin < start) {
|
|
302
|
+
startByTableId.set(tableId, begin)
|
|
303
|
+
}
|
|
304
|
+
if (row.end_snapshot !== null && row.end_snapshot !== undefined) {
|
|
305
|
+
continue
|
|
306
|
+
}
|
|
307
|
+
const datasetId = decodeDatasetTableName(getString(row, "table_name"))
|
|
308
|
+
if (datasetId !== null && (requested === null || requested.has(datasetId))) {
|
|
309
|
+
current.push({ datasetId, tableId })
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return current.map((table) => ({
|
|
314
|
+
...table,
|
|
315
|
+
startSnapshotId: startByTableId.get(table.tableId) ?? 0n,
|
|
316
|
+
}))
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Folds a page of snapshots, oldest first, into each dataset's newest data version. */
|
|
320
|
+
private async collectFeedPage(
|
|
321
|
+
runtime: DuckDbQueryRuntime,
|
|
322
|
+
tables: readonly FeedTable[],
|
|
323
|
+
snapshots: readonly { readonly snapshotId: string; readonly createdAt: Date }[]
|
|
324
|
+
): Promise<Map<string, DatasetSnapshotRow>> {
|
|
325
|
+
const latest = new Map<string, DatasetSnapshotRow>()
|
|
326
|
+
const first = snapshots[0]?.snapshotId
|
|
327
|
+
const last = snapshots[snapshots.length - 1]?.snapshotId
|
|
328
|
+
if (first === undefined || last === undefined || tables.length === 0) {
|
|
329
|
+
return latest
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const changeRows = await runtime.query(
|
|
333
|
+
buildDuckLakeMetadataQuery(
|
|
334
|
+
this.options,
|
|
335
|
+
(table) => `
|
|
336
|
+
SELECT snapshot_id, changes_made, commit_extra_info
|
|
337
|
+
FROM ${table("ducklake_snapshot_changes")}
|
|
338
|
+
WHERE snapshot_id >= ${first} AND snapshot_id <= ${last}
|
|
339
|
+
`
|
|
340
|
+
)
|
|
341
|
+
)
|
|
342
|
+
const fileFlags = await this.queryFileChangeFlags(
|
|
343
|
+
runtime,
|
|
344
|
+
snapshots.map((snapshot) => snapshot.snapshotId),
|
|
345
|
+
tables.map((table) => table.tableId)
|
|
346
|
+
)
|
|
347
|
+
const changesBySnapshotId = new Map(
|
|
348
|
+
changeRows.map((row) => [String(getBigIntLike(row, "snapshot_id")), row])
|
|
349
|
+
)
|
|
350
|
+
const tablesByTableId = new Map(tables.map((table) => [table.tableId.toString(), table]))
|
|
351
|
+
const tablesByDatasetId = new Map(tables.map((table) => [table.datasetId, table]))
|
|
352
|
+
|
|
353
|
+
for (const snapshot of snapshots) {
|
|
354
|
+
const changes = changesBySnapshotId.get(snapshot.snapshotId)
|
|
355
|
+
if (changes === undefined) {
|
|
356
|
+
continue
|
|
357
|
+
}
|
|
358
|
+
const changesMade = getString(changes, "changes_made")
|
|
359
|
+
const metadata = parseCommitMetadata(changes.commit_extra_info)
|
|
360
|
+
|
|
361
|
+
// Only tables this snapshot names, touches through files, or tags can change visibility.
|
|
362
|
+
const touched = new Set<FeedTable>()
|
|
363
|
+
for (const change of changesMade.split(",")) {
|
|
364
|
+
const table = tablesByTableId.get(change.split(":")[1] ?? "")
|
|
365
|
+
if (table) touched.add(table)
|
|
366
|
+
}
|
|
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
|
+
|
|
373
|
+
for (const table of touched) {
|
|
374
|
+
if (BigInt(snapshot.snapshotId) < table.startSnapshotId) {
|
|
375
|
+
continue
|
|
376
|
+
}
|
|
377
|
+
const flags = fileFlags.get(fileChangeKey(snapshot.snapshotId, table.tableId))
|
|
378
|
+
const row = this.candidateToSnapshotRow(table.datasetId, table.tableId, {
|
|
379
|
+
snapshotId: snapshot.snapshotId,
|
|
380
|
+
createdAt: snapshot.createdAt,
|
|
381
|
+
changesMade,
|
|
382
|
+
hasFileChange: flags?.hasFileChange ?? false,
|
|
383
|
+
hasFileDeleteChange: flags?.hasFileDeleteChange ?? false,
|
|
384
|
+
...(metadata !== undefined ? { metadata } : {}),
|
|
385
|
+
})
|
|
386
|
+
if (row && row.mode !== "schema") {
|
|
387
|
+
latest.set(table.datasetId, row)
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return latest
|
|
393
|
+
}
|
|
394
|
+
|
|
192
395
|
private snapshotRowToSummary(
|
|
193
396
|
datasetId: string,
|
|
194
397
|
row: DatasetSnapshotRow
|
|
@@ -604,6 +807,7 @@ export class DuckLakeSnapshotReader {
|
|
|
604
807
|
|
|
605
808
|
while (input.visibleRowLimit === undefined || snapshots.length < input.visibleRowLimit) {
|
|
606
809
|
const candidates = await this.querySnapshotCandidates(runtime, {
|
|
810
|
+
datasetId: input.datasetId,
|
|
607
811
|
tableId: input.tableId,
|
|
608
812
|
exactSnapshotId: input.exactSnapshotId,
|
|
609
813
|
beforeSnapshotId,
|
|
@@ -614,6 +818,9 @@ export class DuckLakeSnapshotReader {
|
|
|
614
818
|
}
|
|
615
819
|
|
|
616
820
|
for (const candidate of candidates) {
|
|
821
|
+
if ("missing" in candidate) {
|
|
822
|
+
continue
|
|
823
|
+
}
|
|
617
824
|
const snapshot = this.candidateToSnapshotRow(input.datasetId, input.tableId, candidate)
|
|
618
825
|
if (snapshot) {
|
|
619
826
|
snapshots.push(snapshot)
|
|
@@ -634,104 +841,144 @@ export class DuckLakeSnapshotReader {
|
|
|
634
841
|
return snapshots
|
|
635
842
|
}
|
|
636
843
|
|
|
844
|
+
/**
|
|
845
|
+
* Hydrates, newest first, the snapshots that may hold a version of one dataset table: those
|
|
846
|
+
* that added or removed its data or delete files, that DuckLake recorded as changing it inline,
|
|
847
|
+
* or whose Sixb commit metadata names the dataset. The text matches are a superset;
|
|
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.
|
|
850
|
+
*
|
|
851
|
+
* Returns one row per candidate id, including ids whose snapshot no longer exists, so the caller
|
|
852
|
+
* can tell a short batch from the end of the history.
|
|
853
|
+
*/
|
|
637
854
|
private async querySnapshotCandidates(
|
|
638
855
|
runtime: DuckDbQueryRuntime,
|
|
639
856
|
input: SnapshotCandidateQueryInput
|
|
640
|
-
): Promise<readonly DatasetSnapshotCandidateRow[]> {
|
|
641
|
-
|
|
857
|
+
): Promise<readonly (DatasetSnapshotCandidateRow | MissingSnapshotCandidate)[]> {
|
|
858
|
+
const tableId = input.tableId.toString()
|
|
859
|
+
const limit = Math.max(0, Math.trunc(input.limit))
|
|
860
|
+
if (limit === 0) {
|
|
642
861
|
return []
|
|
643
862
|
}
|
|
644
|
-
|
|
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
|
-
]
|
|
651
863
|
if (input.exactSnapshotId !== undefined) {
|
|
652
864
|
assertDuckLakeSnapshotId(input.exactSnapshotId)
|
|
653
|
-
where.push(`snapshot.snapshot_id = ${input.exactSnapshotId}`)
|
|
654
865
|
}
|
|
655
866
|
if (input.beforeSnapshotId !== undefined) {
|
|
656
867
|
assertDuckLakeSnapshotId(input.beforeSnapshotId)
|
|
657
|
-
where.push(`snapshot.snapshot_id < ${input.beforeSnapshotId}`)
|
|
658
868
|
}
|
|
659
869
|
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
870
|
+
const rows = await runtime.query(
|
|
871
|
+
buildDuckLakeMetadataQuery(this.options, (table) => {
|
|
872
|
+
// A dataset cannot have a version before its table exists. Use the earliest table record:
|
|
873
|
+
// a rename may create a newer record with the same table id and must not hide older
|
|
874
|
+
// versions.
|
|
875
|
+
const tableStart = `(SELECT min(begin_snapshot) FROM ${table("ducklake_table")} WHERE table_id = ${tableId})`
|
|
876
|
+
const candidateIds =
|
|
877
|
+
input.exactSnapshotId !== undefined
|
|
878
|
+
? `SELECT CAST(${input.exactSnapshotId} AS BIGINT) AS snapshot_id
|
|
879
|
+
WHERE ${input.exactSnapshotId} >= ${tableStart}`
|
|
880
|
+
: `SELECT DISTINCT snapshot_id
|
|
881
|
+
FROM (
|
|
882
|
+
SELECT begin_snapshot AS snapshot_id
|
|
883
|
+
FROM ${table("ducklake_data_file")} WHERE table_id = ${tableId}
|
|
884
|
+
UNION ALL
|
|
885
|
+
SELECT end_snapshot
|
|
886
|
+
FROM ${table("ducklake_data_file")}
|
|
887
|
+
WHERE table_id = ${tableId} AND end_snapshot IS NOT NULL
|
|
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}`
|
|
908
|
+
|
|
909
|
+
return `
|
|
910
|
+
WITH candidate_ids AS (
|
|
911
|
+
${candidateIds}
|
|
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
|
|
954
|
+
)
|
|
955
|
+
SELECT
|
|
956
|
+
-- Keep metadata-only candidates; Sixb filters them with commit_extra_info.
|
|
957
|
+
candidate_ids.snapshot_id,
|
|
958
|
+
candidate.snapshot_time,
|
|
959
|
+
candidate.changes_made,
|
|
960
|
+
candidate.commit_extra_info,
|
|
961
|
+
coalesce(file_changes.has_file_change, false) AS has_file_change,
|
|
962
|
+
coalesce(file_changes.has_file_delete_change, false) AS has_file_delete_change
|
|
963
|
+
FROM candidate_ids
|
|
964
|
+
LEFT JOIN candidate_snapshots candidate
|
|
965
|
+
ON candidate.snapshot_id = candidate_ids.snapshot_id
|
|
966
|
+
LEFT JOIN file_changes_by_snapshot file_changes
|
|
967
|
+
ON file_changes.snapshot_id = candidate_ids.snapshot_id
|
|
968
|
+
ORDER BY candidate_ids.snapshot_id DESC
|
|
969
|
+
`
|
|
970
|
+
})
|
|
667
971
|
)
|
|
668
|
-
const ducklakeDataFile = duckLakeMetadataTableName(this.options, "ducklake_data_file")
|
|
669
|
-
const ducklakeDeleteFile = duckLakeMetadataTableName(this.options, "ducklake_delete_file")
|
|
670
|
-
|
|
671
|
-
const rows = await runtime.query(`
|
|
672
|
-
WITH candidate_snapshots AS (
|
|
673
|
-
-- Recent DuckLake snapshots to inspect; Sixb visibility is filtered later.
|
|
674
|
-
SELECT
|
|
675
|
-
snapshot.snapshot_id,
|
|
676
|
-
snapshot.snapshot_time,
|
|
677
|
-
changes.changes_made,
|
|
678
|
-
changes.commit_extra_info
|
|
679
|
-
FROM ${ducklakeSnapshot} snapshot
|
|
680
|
-
JOIN ${ducklakeSnapshotChanges} changes
|
|
681
|
-
ON changes.snapshot_id = snapshot.snapshot_id
|
|
682
|
-
${whereSql}
|
|
683
|
-
ORDER BY snapshot.snapshot_id DESC
|
|
684
|
-
${limitSql}
|
|
685
|
-
),
|
|
686
|
-
file_changes AS (
|
|
687
|
-
-- File metadata tells whether this table changed in each candidate snapshot.
|
|
688
|
-
SELECT begin_snapshot AS snapshot_id, false AS is_delete_change
|
|
689
|
-
FROM ${ducklakeDataFile}
|
|
690
|
-
WHERE table_id = ${input.tableId}
|
|
691
|
-
AND begin_snapshot IN (SELECT snapshot_id FROM candidate_snapshots)
|
|
692
|
-
UNION ALL
|
|
693
|
-
SELECT end_snapshot AS snapshot_id, true AS is_delete_change
|
|
694
|
-
FROM ${ducklakeDataFile}
|
|
695
|
-
WHERE table_id = ${input.tableId}
|
|
696
|
-
AND end_snapshot IN (SELECT snapshot_id FROM candidate_snapshots)
|
|
697
|
-
UNION ALL
|
|
698
|
-
SELECT begin_snapshot AS snapshot_id, true AS is_delete_change
|
|
699
|
-
FROM ${ducklakeDeleteFile}
|
|
700
|
-
WHERE table_id = ${input.tableId}
|
|
701
|
-
AND begin_snapshot IN (SELECT snapshot_id FROM candidate_snapshots)
|
|
702
|
-
UNION ALL
|
|
703
|
-
SELECT end_snapshot AS snapshot_id, true AS is_delete_change
|
|
704
|
-
FROM ${ducklakeDeleteFile}
|
|
705
|
-
WHERE table_id = ${input.tableId}
|
|
706
|
-
AND end_snapshot IN (SELECT snapshot_id FROM candidate_snapshots)
|
|
707
|
-
),
|
|
708
|
-
file_changes_by_snapshot AS (
|
|
709
|
-
-- Collapse file-level changes into one row per snapshot.
|
|
710
|
-
SELECT
|
|
711
|
-
snapshot_id,
|
|
712
|
-
count(*) > 0 AS has_file_change,
|
|
713
|
-
count(*) FILTER (WHERE is_delete_change) > 0 AS has_file_delete_change
|
|
714
|
-
FROM file_changes
|
|
715
|
-
GROUP BY snapshot_id
|
|
716
|
-
)
|
|
717
|
-
SELECT
|
|
718
|
-
-- Keep metadata-only candidates; Sixb filters them with commit_extra_info.
|
|
719
|
-
candidate.snapshot_id,
|
|
720
|
-
candidate.snapshot_time,
|
|
721
|
-
candidate.changes_made,
|
|
722
|
-
candidate.commit_extra_info,
|
|
723
|
-
coalesce(file_changes.has_file_change, false) AS has_file_change,
|
|
724
|
-
coalesce(file_changes.has_file_delete_change, false) AS has_file_delete_change
|
|
725
|
-
FROM candidate_snapshots candidate
|
|
726
|
-
LEFT JOIN file_changes_by_snapshot file_changes
|
|
727
|
-
ON file_changes.snapshot_id = candidate.snapshot_id
|
|
728
|
-
ORDER BY candidate.snapshot_id DESC
|
|
729
|
-
`)
|
|
730
972
|
|
|
731
973
|
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
|
+
}
|
|
732
979
|
const metadata = parseCommitMetadata(row.commit_extra_info)
|
|
733
980
|
return {
|
|
734
|
-
snapshotId
|
|
981
|
+
snapshotId,
|
|
735
982
|
createdAt: getDate(row, "snapshot_time"),
|
|
736
983
|
changesMade: getString(row, "changes_made"),
|
|
737
984
|
hasFileChange: getBoolean(row, "has_file_change"),
|