@gscdump/lakehouse 0.38.1 → 0.39.0
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/dist/_chunks/catalog.mjs +26 -17
- package/dist/index.mjs +7 -3
- package/dist/unsafe-raw.d.mts +2 -0
- package/dist/unsafe-raw.mjs +64 -36
- package/package.json +1 -1
package/dist/_chunks/catalog.mjs
CHANGED
|
@@ -45,9 +45,10 @@ function toUint8(bytes) {
|
|
|
45
45
|
if (bytes == null) return null;
|
|
46
46
|
return bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
47
47
|
}
|
|
48
|
+
const UTF8_DECODER = new TextDecoder();
|
|
48
49
|
function decodeString(bytes) {
|
|
49
50
|
const u = toUint8(bytes);
|
|
50
|
-
return u == null ? null :
|
|
51
|
+
return u == null ? null : UTF8_DECODER.decode(u);
|
|
51
52
|
}
|
|
52
53
|
function decodeMonthInt(bytes) {
|
|
53
54
|
const u = toUint8(bytes);
|
|
@@ -57,32 +58,40 @@ function decodeMonthInt(bytes) {
|
|
|
57
58
|
function buildManifestPartitionFilter(partitionSpec, matches, wantedMonths) {
|
|
58
59
|
const fieldIndex = (name) => partitionSpec.findIndex((f) => f.name === name || f.sourceColumn === name);
|
|
59
60
|
const monthFieldIndex = partitionSpec.findIndex((f) => f.transform === "month");
|
|
61
|
+
const stringMatches = matches.flatMap((match) => {
|
|
62
|
+
if (match.encoding !== "string") return [];
|
|
63
|
+
const index = fieldIndex(match.field);
|
|
64
|
+
return index < 0 ? [] : [{
|
|
65
|
+
index,
|
|
66
|
+
value: String(match.value)
|
|
67
|
+
}];
|
|
68
|
+
});
|
|
69
|
+
const wantedMonthValues = wantedMonths ? [...wantedMonths].filter(Number.isFinite).sort((a, b) => a - b) : [];
|
|
70
|
+
const hasWantedMonthInRange = (lo, hi) => {
|
|
71
|
+
let left = 0;
|
|
72
|
+
let right = wantedMonthValues.length;
|
|
73
|
+
while (left < right) {
|
|
74
|
+
const middle = left + right >>> 1;
|
|
75
|
+
if (wantedMonthValues[middle] < lo) left = middle + 1;
|
|
76
|
+
else right = middle;
|
|
77
|
+
}
|
|
78
|
+
return left < wantedMonthValues.length && wantedMonthValues[left] <= hi;
|
|
79
|
+
};
|
|
60
80
|
return (partitions) => {
|
|
61
81
|
if (!partitions || partitions.length === 0) return true;
|
|
62
|
-
for (const match of
|
|
63
|
-
|
|
64
|
-
const idx = fieldIndex(match.field);
|
|
65
|
-
if (idx < 0) continue;
|
|
66
|
-
const summary = partitions[idx];
|
|
82
|
+
for (const match of stringMatches) {
|
|
83
|
+
const summary = partitions[match.index];
|
|
67
84
|
if (!summary || summary.lower_bound == null && summary.upper_bound == null) continue;
|
|
68
85
|
const lo = decodeString(summary.lower_bound);
|
|
69
86
|
const hi = decodeString(summary.upper_bound);
|
|
70
|
-
|
|
71
|
-
if (lo != null && hi != null && (wantStr < lo || wantStr > hi)) return false;
|
|
87
|
+
if (lo != null && hi != null && (match.value < lo || match.value > hi)) return false;
|
|
72
88
|
}
|
|
73
|
-
if (
|
|
89
|
+
if (wantedMonthValues.length > 0 && monthFieldIndex >= 0) {
|
|
74
90
|
const monthSummary = partitions[monthFieldIndex];
|
|
75
91
|
if (monthSummary && (monthSummary.lower_bound != null || monthSummary.upper_bound != null)) {
|
|
76
92
|
const lo = decodeMonthInt(monthSummary.lower_bound);
|
|
77
93
|
const hi = decodeMonthInt(monthSummary.upper_bound);
|
|
78
|
-
if (lo != null && hi != null)
|
|
79
|
-
let anyInRange = false;
|
|
80
|
-
for (const wm of wantedMonths) if (wm >= lo && wm <= hi) {
|
|
81
|
-
anyInRange = true;
|
|
82
|
-
break;
|
|
83
|
-
}
|
|
84
|
-
if (!anyInRange) return false;
|
|
85
|
-
}
|
|
94
|
+
if (lo != null && hi != null && !hasWantedMonthInRange(lo, hi)) return false;
|
|
86
95
|
}
|
|
87
96
|
}
|
|
88
97
|
return true;
|
package/dist/index.mjs
CHANGED
|
@@ -122,6 +122,7 @@ function buildRowProcessor(def, tableSpec) {
|
|
|
122
122
|
const idNames = identityColumnNames(def.identity);
|
|
123
123
|
const idEncodings = identityEncodings(def.identity);
|
|
124
124
|
const dimNames = def.dims ? Object.keys(def.dims) : [];
|
|
125
|
+
const identityColumns = tableSpec.identityColumns;
|
|
125
126
|
function guard(row) {
|
|
126
127
|
const out = {};
|
|
127
128
|
for (const name of idNames) {
|
|
@@ -142,11 +143,14 @@ function buildRowProcessor(def, tableSpec) {
|
|
|
142
143
|
}
|
|
143
144
|
function dedupe(rows) {
|
|
144
145
|
if (rows.length < 2) return rows;
|
|
145
|
-
const keyCols = tableSpec.identityColumns;
|
|
146
146
|
const seen = /* @__PURE__ */ new Map();
|
|
147
147
|
for (const rec of rows) {
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
let identity = "";
|
|
149
|
+
for (let index = 0; index < identityColumns.length; index++) {
|
|
150
|
+
if (index > 0) identity += "\0";
|
|
151
|
+
identity += `${rec[identityColumns[index]] ?? ""}`;
|
|
152
|
+
}
|
|
153
|
+
seen.set(identity, rec);
|
|
150
154
|
}
|
|
151
155
|
return seen.size === rows.length ? rows : [...seen.values()];
|
|
152
156
|
}
|
package/dist/unsafe-raw.d.mts
CHANGED
|
@@ -50,6 +50,8 @@ interface SweepUncommittedOrphansOptions {
|
|
|
50
50
|
dryRun?: boolean;
|
|
51
51
|
/** Injectable clock (ms epoch). Defaults to `Date.now`. */
|
|
52
52
|
now?: () => number;
|
|
53
|
+
/** Maximum catalog/object-store reads held in flight. Default 4, max 32. */
|
|
54
|
+
ioConcurrency?: number;
|
|
53
55
|
}
|
|
54
56
|
interface SweepUncommittedOrphansResult {
|
|
55
57
|
/** Tables whose retained snapshots were successfully walked. */
|
package/dist/unsafe-raw.mjs
CHANGED
|
@@ -2,7 +2,30 @@ import { icebergAppendRetrying, isCommitRateLimited } from "./_chunks/catalog.mj
|
|
|
2
2
|
import { icebergAppend, icebergCreateTable, icebergDropTable, icebergManifests, restCatalogConnect, restCatalogCreateNamespace, restCatalogListTables, restCatalogLoadTable, s3SignedResolver } from "./_chunks/libs/icebird.mjs";
|
|
3
3
|
const DEFAULT_GRACE_HOURS = 48;
|
|
4
4
|
const DEFAULT_MAX_DELETES = 500;
|
|
5
|
+
const DEFAULT_IO_CONCURRENCY = 4;
|
|
6
|
+
const MAX_IO_CONCURRENCY = 32;
|
|
5
7
|
const HOUR_MS = 3600 * 1e3;
|
|
8
|
+
function createIoLimiter(requested) {
|
|
9
|
+
const concurrency = typeof requested === "number" && Number.isFinite(requested) ? Math.max(1, Math.min(MAX_IO_CONCURRENCY, Math.floor(requested))) : DEFAULT_IO_CONCURRENCY;
|
|
10
|
+
let active = 0;
|
|
11
|
+
const waiters = [];
|
|
12
|
+
return async (operation) => {
|
|
13
|
+
if (active >= concurrency) await new Promise((resolve) => waiters.push(resolve));
|
|
14
|
+
active++;
|
|
15
|
+
try {
|
|
16
|
+
return await operation();
|
|
17
|
+
} finally {
|
|
18
|
+
active--;
|
|
19
|
+
waiters.shift()?.();
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
async function settleOrThrow(promises) {
|
|
24
|
+
const settled = await Promise.allSettled(promises);
|
|
25
|
+
const failure = settled.find((result) => result.status === "rejected");
|
|
26
|
+
if (failure) throw failure.reason;
|
|
27
|
+
return settled.map((result) => result.value);
|
|
28
|
+
}
|
|
6
29
|
function splitBucketKey(filePath) {
|
|
7
30
|
if (!filePath.startsWith("s3://")) return null;
|
|
8
31
|
const rest = filePath.slice(5);
|
|
@@ -13,11 +36,11 @@ function splitBucketKey(filePath) {
|
|
|
13
36
|
key: rest.slice(slash + 1)
|
|
14
37
|
};
|
|
15
38
|
}
|
|
16
|
-
async function scanTable(conn, table, bucket) {
|
|
17
|
-
const { metadata } = await restCatalogLoadTable(conn.catalog, {
|
|
39
|
+
async function scanTable(conn, table, bucket, io) {
|
|
40
|
+
const { metadata } = await io(() => restCatalogLoadTable(conn.catalog, {
|
|
18
41
|
namespace: conn.namespace,
|
|
19
42
|
table
|
|
20
|
-
});
|
|
43
|
+
}));
|
|
21
44
|
const location = splitBucketKey(String(metadata.location ?? ""));
|
|
22
45
|
if (!location || location.bucket !== bucket) return {
|
|
23
46
|
dataPrefix: null,
|
|
@@ -25,34 +48,31 @@ async function scanTable(conn, table, bucket) {
|
|
|
25
48
|
};
|
|
26
49
|
const dataPrefix = `${location.key.replace(/\/+$/, "")}/data/`;
|
|
27
50
|
const liveKeys = /* @__PURE__ */ new Set();
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const parsed = splitBucketKey(filePath);
|
|
40
|
-
if (parsed && parsed.bucket === bucket) liveKeys.add(parsed.key);
|
|
41
|
-
}
|
|
51
|
+
const manifestsBySnapshot = await settleOrThrow((metadata.snapshots ?? []).map((snapshot) => io(() => icebergManifests({
|
|
52
|
+
metadata,
|
|
53
|
+
resolver: conn.resolver,
|
|
54
|
+
snapshotId: snapshot["snapshot-id"]
|
|
55
|
+
}))));
|
|
56
|
+
for (const manifests of manifestsBySnapshot) for (const manifest of manifests) for (const entry of manifest.entries) {
|
|
57
|
+
if (entry.status === 2) continue;
|
|
58
|
+
const filePath = entry.data_file?.file_path;
|
|
59
|
+
if (!filePath) continue;
|
|
60
|
+
const parsed = splitBucketKey(filePath);
|
|
61
|
+
if (parsed && parsed.bucket === bucket) liveKeys.add(parsed.key);
|
|
42
62
|
}
|
|
43
63
|
return {
|
|
44
64
|
dataPrefix,
|
|
45
65
|
liveKeys
|
|
46
66
|
};
|
|
47
67
|
}
|
|
48
|
-
async function listAllUnderPrefix(s3, prefix) {
|
|
68
|
+
async function listAllUnderPrefix(s3, prefix, io) {
|
|
49
69
|
const out = [];
|
|
50
70
|
let cursor;
|
|
51
71
|
do {
|
|
52
|
-
const page = await s3.list({
|
|
72
|
+
const page = await io(() => s3.list({
|
|
53
73
|
prefix,
|
|
54
74
|
cursor
|
|
55
|
-
});
|
|
75
|
+
}));
|
|
56
76
|
out.push(...page.objects);
|
|
57
77
|
cursor = page.truncated ? page.cursor : void 0;
|
|
58
78
|
} while (cursor);
|
|
@@ -64,7 +84,8 @@ async function sweepUncommittedOrphans(opts) {
|
|
|
64
84
|
const maxDeletes = opts.maxDeletes ?? DEFAULT_MAX_DELETES;
|
|
65
85
|
const dryRun = opts.dryRun ?? false;
|
|
66
86
|
const graceCutoff = (opts.now ?? Date.now)() - graceHours * HOUR_MS;
|
|
67
|
-
const
|
|
87
|
+
const io = createIoLimiter(opts.ioConcurrency);
|
|
88
|
+
const tables = opts.tables ?? (await io(() => restCatalogListTables(conn.catalog, { namespace: conn.namespace }))).map((t) => t.name);
|
|
68
89
|
if (tables.length === 0) return {
|
|
69
90
|
scannedTables: [],
|
|
70
91
|
skippedTables: [],
|
|
@@ -75,26 +96,33 @@ async function sweepUncommittedOrphans(opts) {
|
|
|
75
96
|
cappedAtLimit: false,
|
|
76
97
|
reason: "zero-tables"
|
|
77
98
|
};
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
scannedTables.push(table);
|
|
89
|
-
liveFileCount += liveKeys.size;
|
|
90
|
-
const listed = await listAllUnderPrefix(s3, dataPrefix);
|
|
99
|
+
const tableResults = await settleOrThrow(tables.map(async (table) => {
|
|
100
|
+
const { dataPrefix, liveKeys } = await scanTable(conn, table, bucket, io);
|
|
101
|
+
if (!dataPrefix) return {
|
|
102
|
+
table,
|
|
103
|
+
skipped: true,
|
|
104
|
+
liveFileCount: 0,
|
|
105
|
+
candidates: []
|
|
106
|
+
};
|
|
107
|
+
const listed = await listAllUnderPrefix(s3, dataPrefix, io);
|
|
108
|
+
const tableCandidates = [];
|
|
91
109
|
for (const obj of listed) {
|
|
92
110
|
if (!obj.key.startsWith(dataPrefix)) continue;
|
|
93
111
|
if (liveKeys.has(obj.key)) continue;
|
|
94
112
|
if (obj.uploaded.getTime() > graceCutoff) continue;
|
|
95
|
-
|
|
113
|
+
tableCandidates.push(obj);
|
|
96
114
|
}
|
|
97
|
-
|
|
115
|
+
return {
|
|
116
|
+
table,
|
|
117
|
+
skipped: false,
|
|
118
|
+
liveFileCount: liveKeys.size,
|
|
119
|
+
candidates: tableCandidates
|
|
120
|
+
};
|
|
121
|
+
}));
|
|
122
|
+
const scannedTables = tableResults.filter((result) => !result.skipped).map((result) => result.table);
|
|
123
|
+
const skippedTables = tableResults.filter((result) => result.skipped).map((result) => result.table);
|
|
124
|
+
const liveFileCount = tableResults.reduce((sum, result) => sum + result.liveFileCount, 0);
|
|
125
|
+
const candidates = tableResults.flatMap((result) => result.candidates);
|
|
98
126
|
const candidateCount = candidates.length;
|
|
99
127
|
const toDelete = candidates.slice(0, maxDeletes);
|
|
100
128
|
const cappedAtLimit = candidateCount > maxDeletes;
|
package/package.json
CHANGED