@gscdump/engine 0.33.10 → 0.35.2
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/THIRD-PARTY-LICENSES.md +1 -394
- package/dist/_chunks/libs/hyparquet-writer.mjs +1 -112
- package/dist/_chunks/libs/hyparquet.mjs +1 -35
- package/dist/_chunks/libs/icebird.d.mts +1 -98
- package/dist/_chunks/libs/icebird.mjs +51 -2894
- package/dist/_chunks/resolver.mjs +2 -2
- package/dist/_chunks/schema2.mjs +44 -26
- package/dist/_chunks/sink.d.mts +62 -359
- package/dist/adapters/duckdb-node.mjs +3 -3
- package/dist/iceberg/index.mjs +35 -335
- package/dist/sink-node.d.mts +2 -24
- package/dist/sink-node.mjs +2 -85
- package/package.json +4 -3
- package/dist/_chunks/libs/chokidar.d.mts +0 -1
- package/dist/_chunks/libs/db0.d.mts +0 -1
- package/dist/_chunks/libs/denque.d.mts +0 -1
- package/dist/_chunks/libs/ioredis.d.mts +0 -1
- package/dist/_chunks/libs/lru-cache.d.mts +0 -1
- package/dist/_chunks/libs/unstorage.d.mts +0 -120
package/dist/iceberg/index.mjs
CHANGED
|
@@ -1,85 +1,11 @@
|
|
|
1
1
|
import { TABLE_METADATA } from "../_chunks/schema.mjs";
|
|
2
2
|
import { engineErrors } from "../errors.mjs";
|
|
3
|
-
import { DEFAULT_PARTITION_KEY_ENCODING, ICEBERG_FIELD_ID_BASE, ICEBERG_PARTITION_COLUMNS, ICEBERG_PARTITION_SPEC, ICEBERG_SCHEMAS, ICEBERG_SCHEMAS_INT, ICEBERG_SCHEMAS_STRING, ICEBERG_TABLES, INT_SEARCH_TYPE, SEARCH_TYPE_INT, assertIcebergTable, icebergPartitionColumns, icebergSchemasFor, icebergTableSpec, isIcebergTable } from "../_chunks/schema2.mjs";
|
|
4
|
-
import {
|
|
3
|
+
import { DEFAULT_PARTITION_KEY_ENCODING, ICEBERG_FIELD_ID_BASE, ICEBERG_PARTITION_COLUMNS, ICEBERG_PARTITION_SPEC, ICEBERG_SCHEMAS, ICEBERG_SCHEMAS_INT, ICEBERG_SCHEMAS_STRING, ICEBERG_TABLES, INT_SEARCH_TYPE, SEARCH_TYPE_INT, assertIcebergTable, gscDataset, icebergPartitionColumns, icebergSchemasFor, icebergTableSpec, isIcebergTable } from "../_chunks/schema2.mjs";
|
|
4
|
+
import { icebergCreateTable, icebergManifests, restCatalogLoadTable } from "../_chunks/libs/icebird.mjs";
|
|
5
5
|
import { err, ok } from "gscdump/result";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return boxed.v;
|
|
10
|
-
}
|
|
11
|
-
function cachePut(cache, key, value, ttlMs, now) {
|
|
12
|
-
const boxed = {
|
|
13
|
-
v: value,
|
|
14
|
-
exp: now + ttlMs
|
|
15
|
-
};
|
|
16
|
-
const write = cache.storage.setItem(key, boxed, { ttl: Math.ceil(ttlMs / 1e3) }).catch(() => {});
|
|
17
|
-
if (cache.defer) {
|
|
18
|
-
cache.defer(write);
|
|
19
|
-
return Promise.resolve();
|
|
20
|
-
}
|
|
21
|
-
return write;
|
|
22
|
-
}
|
|
23
|
-
const SITE_ID_FIELD_INDEX = ICEBERG_PARTITION_SPEC.findIndex((f) => f.sourceColumn === "site_id" && f.transform === "identity");
|
|
24
|
-
const SEARCH_TYPE_FIELD_INDEX = ICEBERG_PARTITION_SPEC.findIndex((f) => f.sourceColumn === "search_type" && f.transform === "identity");
|
|
25
|
-
const DATE_MONTH_FIELD_INDEX = ICEBERG_PARTITION_SPEC.findIndex((f) => f.transform === "month");
|
|
26
|
-
function toUint8(bytes) {
|
|
27
|
-
if (bytes == null) return null;
|
|
28
|
-
return bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
29
|
-
}
|
|
30
|
-
function decodeString(bytes) {
|
|
31
|
-
const u = toUint8(bytes);
|
|
32
|
-
return u == null ? null : new TextDecoder().decode(u);
|
|
33
|
-
}
|
|
34
|
-
function decodeInt(bytes) {
|
|
35
|
-
const u = toUint8(bytes);
|
|
36
|
-
if (u == null) return null;
|
|
37
|
-
return new DataView(u.buffer, u.byteOffset, u.byteLength).getInt32(0, true);
|
|
38
|
-
}
|
|
39
|
-
function buildPartitionFilter(siteId, searchType, wantedMonths, encoding = "int") {
|
|
40
|
-
return (partitions) => {
|
|
41
|
-
const parts = partitions;
|
|
42
|
-
if (!parts || parts.length === 0) return true;
|
|
43
|
-
if (encoding === "string") {
|
|
44
|
-
const siteStr = String(siteId);
|
|
45
|
-
const searchStr = String(searchType);
|
|
46
|
-
const siteSummary = parts[SITE_ID_FIELD_INDEX];
|
|
47
|
-
if (siteSummary && (siteSummary.lower_bound != null || siteSummary.upper_bound != null)) {
|
|
48
|
-
const lo = decodeString(siteSummary.lower_bound);
|
|
49
|
-
const hi = decodeString(siteSummary.upper_bound);
|
|
50
|
-
if (lo != null && hi != null && (siteStr < lo || siteStr > hi)) return false;
|
|
51
|
-
}
|
|
52
|
-
const searchTypeSummary = parts[SEARCH_TYPE_FIELD_INDEX];
|
|
53
|
-
if (searchTypeSummary && (searchTypeSummary.lower_bound != null || searchTypeSummary.upper_bound != null)) {
|
|
54
|
-
const lo = decodeString(searchTypeSummary.lower_bound);
|
|
55
|
-
const hi = decodeString(searchTypeSummary.upper_bound);
|
|
56
|
-
if (lo != null && hi != null && (searchStr < lo || searchStr > hi)) return false;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
const monthSummary = parts[DATE_MONTH_FIELD_INDEX];
|
|
60
|
-
if (monthSummary && (monthSummary.lower_bound != null || monthSummary.upper_bound != null)) {
|
|
61
|
-
const lo = decodeInt(monthSummary.lower_bound);
|
|
62
|
-
const hi = decodeInt(monthSummary.upper_bound);
|
|
63
|
-
if (lo != null && hi != null) {
|
|
64
|
-
let anyInRange = false;
|
|
65
|
-
for (const wm of wantedMonths) if (wm >= lo && wm <= hi) {
|
|
66
|
-
anyInRange = true;
|
|
67
|
-
break;
|
|
68
|
-
}
|
|
69
|
-
if (!anyInRange) return false;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return true;
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
const ICEBERG_TYPE_MAP = {
|
|
76
|
-
STRING: "string",
|
|
77
|
-
INT: "int",
|
|
78
|
-
LONG: "long",
|
|
79
|
-
DOUBLE: "double",
|
|
80
|
-
DATE: "date"
|
|
81
|
-
};
|
|
82
|
-
function icebergSchemaFor(table, encoding = "int") {
|
|
6
|
+
import { ICEBERG_TYPE_MAP, connectIcebergCatalog, dropIcebergTables as dropIcebergTables$1, ensureIcebergNamespace, listIcebergTables, resolveIcebergDataFiles } from "@gscdump/lakehouse";
|
|
7
|
+
import { icebergAppendRetrying, isCommitRateLimited } from "@gscdump/lakehouse/unsafe-raw";
|
|
8
|
+
function icebergSchemaFor(table, encoding = DEFAULT_PARTITION_KEY_ENCODING) {
|
|
83
9
|
return {
|
|
84
10
|
"type": "struct",
|
|
85
11
|
"schema-id": 0,
|
|
@@ -91,7 +17,7 @@ function icebergSchemaFor(table, encoding = "int") {
|
|
|
91
17
|
}))
|
|
92
18
|
};
|
|
93
19
|
}
|
|
94
|
-
function icebergPartitionSpecFor(table, encoding =
|
|
20
|
+
function icebergPartitionSpecFor(table, encoding = DEFAULT_PARTITION_KEY_ENCODING) {
|
|
95
21
|
const fields = icebergSchemasFor(encoding)[table].columns;
|
|
96
22
|
const fieldId = (name) => {
|
|
97
23
|
const col = fields.find((c) => c.name === name);
|
|
@@ -108,7 +34,7 @@ function icebergPartitionSpecFor(table, encoding = "int") {
|
|
|
108
34
|
}))
|
|
109
35
|
};
|
|
110
36
|
}
|
|
111
|
-
function icebergSortOrderFor(table, encoding =
|
|
37
|
+
function icebergSortOrderFor(table, encoding = DEFAULT_PARTITION_KEY_ENCODING) {
|
|
112
38
|
const fields = icebergSchemasFor(encoding)[table].columns;
|
|
113
39
|
const fieldId = (name) => {
|
|
114
40
|
const col = fields.find((c) => c.name === name);
|
|
@@ -125,112 +51,7 @@ function icebergSortOrderFor(table, encoding = "int") {
|
|
|
125
51
|
}))
|
|
126
52
|
};
|
|
127
53
|
}
|
|
128
|
-
|
|
129
|
-
function catalogConfigKey(config) {
|
|
130
|
-
return `gsc-catalog-cfg\0${config.catalogUri}\0${config.warehouse}`;
|
|
131
|
-
}
|
|
132
|
-
async function connectIcebergCatalog(config, opts = {}) {
|
|
133
|
-
const now = (opts.clock ?? Date.now)();
|
|
134
|
-
const requestInit = { headers: { Authorization: `Bearer ${config.catalogToken}` } };
|
|
135
|
-
let catalog;
|
|
136
|
-
if (opts.cache) {
|
|
137
|
-
const cached = await cacheGet(opts.cache, catalogConfigKey(config), now);
|
|
138
|
-
if (cached) catalog = Object.freeze({
|
|
139
|
-
type: "rest",
|
|
140
|
-
url: cached.url,
|
|
141
|
-
prefix: cached.prefix,
|
|
142
|
-
defaults: cached.defaults,
|
|
143
|
-
overrides: cached.overrides,
|
|
144
|
-
requestInit
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
if (!catalog) {
|
|
148
|
-
catalog = await restCatalogConnect({
|
|
149
|
-
url: config.catalogUri,
|
|
150
|
-
warehouse: config.warehouse,
|
|
151
|
-
requestInit
|
|
152
|
-
});
|
|
153
|
-
if (opts.cache) {
|
|
154
|
-
const toCache = {
|
|
155
|
-
url: catalog.url,
|
|
156
|
-
prefix: catalog.prefix,
|
|
157
|
-
defaults: catalog.defaults,
|
|
158
|
-
overrides: catalog.overrides
|
|
159
|
-
};
|
|
160
|
-
await cachePut(opts.cache, catalogConfigKey(config), toCache, CATALOG_CONFIG_TTL_MS, now);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
const resolver = cachingResolver(s3SignedResolver({
|
|
164
|
-
accessKeyId: config.s3.accessKeyId,
|
|
165
|
-
secretAccessKey: config.s3.secretAccessKey,
|
|
166
|
-
region: config.s3.region ?? "auto",
|
|
167
|
-
endpoint: config.s3.endpoint,
|
|
168
|
-
pathStyle: true
|
|
169
|
-
}));
|
|
170
|
-
return {
|
|
171
|
-
catalog,
|
|
172
|
-
resolver,
|
|
173
|
-
namespace: config.namespace
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
const APPEND_ID_SUMMARY_KEY = "gscdump.append-id";
|
|
177
|
-
const APPEND_LANDED_SCAN_DEPTH = 25;
|
|
178
|
-
function isCommitRateLimited(err) {
|
|
179
|
-
if (err && typeof err === "object" && err.status === 429) return true;
|
|
180
|
-
const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
|
|
181
|
-
return msg.includes("429") || msg.includes("too many commits") || msg.includes("rate limit");
|
|
182
|
-
}
|
|
183
|
-
function defaultCommitSleep(ms) {
|
|
184
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
185
|
-
}
|
|
186
|
-
async function icebergAppendRetrying(args, options = {}) {
|
|
187
|
-
const maxAttempts = options.maxAttempts ?? 6;
|
|
188
|
-
const baseDelayMs = options.baseDelayMs ?? 1e3;
|
|
189
|
-
const maxDelayMs = options.maxDelayMs ?? 2e4;
|
|
190
|
-
const sleep = options.sleep ?? defaultCommitSleep;
|
|
191
|
-
const random = options.random ?? Math.random;
|
|
192
|
-
const appendId = options.appendId ?? await deriveAppendId(args);
|
|
193
|
-
const stampedArgs = {
|
|
194
|
-
...args,
|
|
195
|
-
snapshotProperties: {
|
|
196
|
-
...args.snapshotProperties,
|
|
197
|
-
[APPEND_ID_SUMMARY_KEY]: appendId
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
if (await appendAlreadyLanded(args, appendId).catch(() => false)) return;
|
|
201
|
-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
202
|
-
const err = await icebergAppend(stampedArgs).then(() => void 0, (e) => e);
|
|
203
|
-
if (err === void 0) return;
|
|
204
|
-
if (await appendAlreadyLanded(args, appendId).catch(() => false)) return;
|
|
205
|
-
if (!isCommitRateLimited(err) || attempt === maxAttempts - 1) throw err;
|
|
206
|
-
const ceiling = Math.min(maxDelayMs, baseDelayMs * 2 ** attempt);
|
|
207
|
-
await sleep(Math.floor(random() * ceiling));
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
async function deriveAppendId(args) {
|
|
211
|
-
const records = args.records ?? [];
|
|
212
|
-
if (records.length === 0) return globalThis.crypto.randomUUID();
|
|
213
|
-
const rowSig = (r) => Object.keys(r).sort().map((k) => `${k}=${String(r[k])}`).join("");
|
|
214
|
-
const body = records.map(rowSig).sort().join("");
|
|
215
|
-
const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(body));
|
|
216
|
-
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
217
|
-
}
|
|
218
|
-
async function appendAlreadyLanded(args, appendId) {
|
|
219
|
-
const a = args;
|
|
220
|
-
if (a.catalog?.type !== "rest" || a.namespace == null || a.table == null) return false;
|
|
221
|
-
const { metadata } = await restCatalogLoadTable(a.catalog, {
|
|
222
|
-
namespace: a.namespace,
|
|
223
|
-
table: a.table
|
|
224
|
-
});
|
|
225
|
-
const snapshots = metadata.snapshots ?? [];
|
|
226
|
-
const from = Math.max(0, snapshots.length - APPEND_LANDED_SCAN_DEPTH);
|
|
227
|
-
for (let i = snapshots.length - 1; i >= from; i--) if (snapshots[i]?.summary?.[APPEND_ID_SUMMARY_KEY] === appendId) return true;
|
|
228
|
-
return false;
|
|
229
|
-
}
|
|
230
|
-
async function ensureIcebergNamespace(conn) {
|
|
231
|
-
await restCatalogCreateNamespace(conn.catalog, { namespace: conn.namespace }).catch(() => {});
|
|
232
|
-
}
|
|
233
|
-
async function createIcebergTables(conn, tables = ICEBERG_TABLES, encoding = "int") {
|
|
54
|
+
async function createIcebergTables(conn, tables = ICEBERG_TABLES, encoding = DEFAULT_PARTITION_KEY_ENCODING) {
|
|
234
55
|
const results = [];
|
|
235
56
|
for (const table of tables) await icebergCreateTable({
|
|
236
57
|
catalog: conn.catalog,
|
|
@@ -248,153 +69,32 @@ async function createIcebergTables(conn, tables = ICEBERG_TABLES, encoding = "in
|
|
|
248
69
|
}));
|
|
249
70
|
return results;
|
|
250
71
|
}
|
|
251
|
-
async function
|
|
252
|
-
return (await
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const METADATA_TTL_MS = 1440 * 60 * 1e3;
|
|
257
|
-
const MAX_CACHED_METADATA_BYTES = 2 * 1024 * 1024;
|
|
258
|
-
function snapshotRefKey(namespace, table) {
|
|
259
|
-
return `gsc-snapref\0${namespace}\0${table}`;
|
|
260
|
-
}
|
|
261
|
-
function metadataRefKey(namespace, table, snapshotId) {
|
|
262
|
-
return `gsc-snapmeta\0${namespace}\0${table}\0${snapshotId}`;
|
|
263
|
-
}
|
|
264
|
-
function resolvedFilesKey(namespace, table, snapshotId, siteId, searchType, wantedMonths) {
|
|
265
|
-
return `gsc-files\0${namespace}\0${table}\0${snapshotId}\0${siteId}\0${searchType}\0${[...wantedMonths].sort((a, b) => a - b).join(",")}`;
|
|
266
|
-
}
|
|
267
|
-
function monthsInRange(range) {
|
|
268
|
-
const [sy, sm] = range.start.split("-").map(Number);
|
|
269
|
-
const [ey, em] = range.end.split("-").map(Number);
|
|
270
|
-
const out = [];
|
|
271
|
-
let y = sy;
|
|
272
|
-
let m = sm;
|
|
273
|
-
while (y < ey || y === ey && m <= em) {
|
|
274
|
-
out.push(`${y}-${String(m).padStart(2, "0")}`);
|
|
275
|
-
m++;
|
|
276
|
-
if (m > 12) {
|
|
277
|
-
m = 1;
|
|
278
|
-
y++;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
return out;
|
|
282
|
-
}
|
|
283
|
-
function monthsSinceEpoch(ym) {
|
|
284
|
-
const [y, m] = ym.split("-").map(Number);
|
|
285
|
-
return (y - 1970) * 12 + (m - 1);
|
|
286
|
-
}
|
|
287
|
-
function stripBucket(filePath) {
|
|
288
|
-
if (!filePath.startsWith("s3://")) return filePath;
|
|
289
|
-
const rest = filePath.slice(5);
|
|
290
|
-
const slash = rest.indexOf("/");
|
|
291
|
-
return slash >= 0 ? rest.slice(slash + 1) : rest;
|
|
292
|
-
}
|
|
293
|
-
async function loadSnapshotId(conn, opts, now) {
|
|
294
|
-
if (opts.cache) {
|
|
295
|
-
const cached = await cacheGet(opts.cache, snapshotRefKey(conn.namespace, opts.table), now);
|
|
296
|
-
if (cached !== void 0) return {
|
|
297
|
-
snapshotId: cached,
|
|
298
|
-
metadata: null
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
const { metadata } = await restCatalogLoadTable(conn.catalog, {
|
|
302
|
-
namespace: conn.namespace,
|
|
303
|
-
table: opts.table
|
|
304
|
-
});
|
|
305
|
-
const raw = metadata["current-snapshot-id"];
|
|
306
|
-
const snapshotId = raw == null ? null : String(raw);
|
|
307
|
-
if (opts.cache) {
|
|
308
|
-
await cachePut(opts.cache, snapshotRefKey(conn.namespace, opts.table), snapshotId, SNAPSHOT_REF_TTL_MS, now);
|
|
309
|
-
if (snapshotId != null) {
|
|
310
|
-
if (JSON.stringify(metadata).length <= MAX_CACHED_METADATA_BYTES) await cachePut(opts.cache, metadataRefKey(conn.namespace, opts.table, snapshotId), metadata, METADATA_TTL_MS, now);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
return {
|
|
314
|
-
snapshotId,
|
|
315
|
-
metadata
|
|
316
|
-
};
|
|
72
|
+
async function dropIcebergTables(conn, tables) {
|
|
73
|
+
return (await dropIcebergTables$1(conn, tables)).map((r) => ({
|
|
74
|
+
table: r.table,
|
|
75
|
+
outcome: r.ok ? ok(void 0) : err(engineErrors.icebergTableOpFailed("drop", r.table, r.error))
|
|
76
|
+
}));
|
|
317
77
|
}
|
|
318
78
|
async function listIcebergDataFiles(conn, opts) {
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const cached = await cacheGet(opts.cache, filesKey, now);
|
|
330
|
-
endCache?.({ hit: cached !== void 0 });
|
|
331
|
-
if (cached !== void 0) return cached;
|
|
332
|
-
}
|
|
333
|
-
if (!metadata && opts.cache) {
|
|
334
|
-
const cachedMeta = await cacheGet(opts.cache, metadataRefKey(conn.namespace, opts.table, snapshotId), now);
|
|
335
|
-
if (cachedMeta != null) metadata = cachedMeta;
|
|
336
|
-
}
|
|
337
|
-
if (!metadata) {
|
|
338
|
-
const reloaded = await loadSnapshotId(conn, {
|
|
339
|
-
...opts,
|
|
340
|
-
cache: void 0
|
|
341
|
-
}, now);
|
|
342
|
-
snapshotId = reloaded.snapshotId;
|
|
343
|
-
metadata = reloaded.metadata;
|
|
344
|
-
if (snapshotId == null || !metadata) return [];
|
|
345
|
-
}
|
|
346
|
-
const endWalk = profiler?.start("iceberg.walk");
|
|
347
|
-
const partitionFilter = buildPartitionFilter(opts.siteId, opts.searchType, wantedMonths, opts.encoding ?? "int");
|
|
348
|
-
const manifests = await icebergManifests({
|
|
349
|
-
metadata,
|
|
350
|
-
resolver: conn.resolver,
|
|
351
|
-
partitionFilter
|
|
352
|
-
});
|
|
353
|
-
const wantSite = String(opts.siteId);
|
|
354
|
-
const wantSearch = String(opts.searchType);
|
|
355
|
-
const out = [];
|
|
356
|
-
for (const m of manifests) for (const entry of m.entries) {
|
|
357
|
-
if (entry.status === 2) continue;
|
|
358
|
-
const df = entry.data_file;
|
|
359
|
-
if (df.content !== 0) continue;
|
|
360
|
-
const part = df.partition;
|
|
361
|
-
if (String(part.site_id) !== wantSite) continue;
|
|
362
|
-
if (String(part.search_type) !== wantSearch) continue;
|
|
363
|
-
const month = part.date_month;
|
|
364
|
-
if (typeof month !== "number" || !wantedMonths.has(month)) continue;
|
|
365
|
-
out.push({
|
|
366
|
-
filePath: df.file_path,
|
|
367
|
-
objectKey: stripBucket(df.file_path),
|
|
368
|
-
bytes: Number(df.file_size_in_bytes),
|
|
369
|
-
rowCount: Number(df.record_count)
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
endWalk?.({
|
|
373
|
-
manifests: manifests.length,
|
|
374
|
-
files: out.length
|
|
375
|
-
});
|
|
376
|
-
if (opts.cache) {
|
|
377
|
-
const freshKey = resolvedFilesKey(conn.namespace, opts.table, snapshotId, opts.siteId, opts.searchType, wantedMonths);
|
|
378
|
-
await cachePut(opts.cache, freshKey, out, RESOLVED_FILES_TTL_MS, now);
|
|
379
|
-
}
|
|
380
|
-
return out;
|
|
381
|
-
}
|
|
382
|
-
async function dropIcebergTables(conn, tables) {
|
|
383
|
-
const targets = tables ?? (await restCatalogListTables(conn.catalog, { namespace: conn.namespace })).map((t) => t.name);
|
|
384
|
-
const results = [];
|
|
385
|
-
for (const table of targets) await icebergDropTable({
|
|
386
|
-
catalog: conn.catalog,
|
|
79
|
+
const matches = (opts.encoding ?? DEFAULT_PARTITION_KEY_ENCODING) === "string" ? [{
|
|
80
|
+
field: "site_id",
|
|
81
|
+
value: opts.siteId,
|
|
82
|
+
encoding: "string"
|
|
83
|
+
}, {
|
|
84
|
+
field: "search_type",
|
|
85
|
+
value: opts.searchType,
|
|
86
|
+
encoding: "string"
|
|
87
|
+
}] : [];
|
|
88
|
+
return resolveIcebergDataFiles(conn, {
|
|
387
89
|
namespace: conn.namespace,
|
|
388
|
-
table,
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}));
|
|
397
|
-
return results;
|
|
90
|
+
table: opts.table,
|
|
91
|
+
partitionSpec: ICEBERG_PARTITION_SPEC,
|
|
92
|
+
matches,
|
|
93
|
+
range: opts.range,
|
|
94
|
+
cache: opts.cache,
|
|
95
|
+
clock: opts.clock,
|
|
96
|
+
profiler: opts.profiler
|
|
97
|
+
});
|
|
398
98
|
}
|
|
399
99
|
const DAY_MILLIS = 864e5;
|
|
400
100
|
const INT32_MIN = -2147483648;
|
|
@@ -426,7 +126,7 @@ function toIntPartitionSiteId(value) {
|
|
|
426
126
|
}
|
|
427
127
|
function dedupeByIdentity(table, records) {
|
|
428
128
|
if (records.length < 2) return records;
|
|
429
|
-
const key =
|
|
129
|
+
const key = gscDataset(table, "int").tableSpec.identityColumns;
|
|
430
130
|
const seen = /* @__PURE__ */ new Map();
|
|
431
131
|
for (const rec of records) {
|
|
432
132
|
const k = key.map((col) => `${rec[col] ?? ""}`).join("\0");
|
|
@@ -435,7 +135,7 @@ function dedupeByIdentity(table, records) {
|
|
|
435
135
|
return seen.size === records.length ? records : [...seen.values()];
|
|
436
136
|
}
|
|
437
137
|
function sortByClusterKey(table, records) {
|
|
438
|
-
const cols =
|
|
138
|
+
const cols = gscDataset(table, "int").tableSpec.clusterKey ?? [];
|
|
439
139
|
if (cols.length === 0 || records.length < 2) return records;
|
|
440
140
|
return records.slice().sort((a, b) => {
|
|
441
141
|
for (const col of cols) {
|
|
@@ -454,7 +154,7 @@ function sortByClusterKey(table, records) {
|
|
|
454
154
|
}
|
|
455
155
|
function toRecords(slice, rows, encoding) {
|
|
456
156
|
const siteVal = encoding === "int" ? toIntPartitionSiteId(slice.ctx.siteId) : slice.ctx.siteId ?? "";
|
|
457
|
-
const searchVal =
|
|
157
|
+
const searchVal = gscDataset(slice.table, encoding).def.dims.search_type.toPartitionValue(slice.searchType);
|
|
458
158
|
return rows.map((row) => {
|
|
459
159
|
const out = {};
|
|
460
160
|
for (const k in row) out[k] = coerceJsonSafe(row[k]);
|
|
@@ -466,7 +166,7 @@ function toRecords(slice, rows, encoding) {
|
|
|
466
166
|
}
|
|
467
167
|
function createIcebergAppendSink(options) {
|
|
468
168
|
let connection;
|
|
469
|
-
const encoding = options.encoding ??
|
|
169
|
+
const encoding = options.encoding ?? DEFAULT_PARTITION_KEY_ENCODING;
|
|
470
170
|
const buffers = /* @__PURE__ */ new Map();
|
|
471
171
|
function connect() {
|
|
472
172
|
connection ??= connectIcebergCatalog(options.catalog);
|
package/dist/sink-node.d.mts
CHANGED
|
@@ -1,27 +1,5 @@
|
|
|
1
1
|
import { Row } from "./_chunks/storage.mjs";
|
|
2
|
-
import { IcebergS3Config,
|
|
3
|
-
/** Full `LocalIcebergSink` options — extends the frozen contract options. */
|
|
4
|
-
interface LocalIcebergSinkFullOptions extends LocalIcebergSinkOptions {
|
|
5
|
-
/** S3 credentials for the warehouse. Defaults to the POC MinIO creds. */
|
|
6
|
-
s3?: IcebergS3Config;
|
|
7
|
-
/** Python interpreter. Defaults to `$GSCDUMP_ICEBERG_PYTHON` then `python3`. */
|
|
8
|
-
python?: string;
|
|
9
|
-
/** Override the writer-script path. Defaults to `scripts/iceberg-writer.py`. */
|
|
10
|
-
writerScript?: string;
|
|
11
|
-
}
|
|
12
|
-
/** Run the PyIceberg writer subprocess for one job, return its parsed result. */
|
|
13
|
-
interface LocalIcebergSink extends Sink {
|
|
14
|
-
/** The catalog namespace the 5 tables live under. */
|
|
15
|
-
readonly namespace: string;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Create a `LocalIcebergSink` pointed at a local Iceberg REST catalog.
|
|
19
|
-
*
|
|
20
|
-
* Requires the POC docker stack (`poc/iceberg/docker-compose.iceberg.yml`)
|
|
21
|
-
* running and a Python env with `pyiceberg` + `pyarrow` available. Tests that
|
|
22
|
-
* use this sink must skip when the stack is unreachable.
|
|
23
|
-
*/
|
|
24
|
-
declare function createLocalIcebergSink(options: LocalIcebergSinkFullOptions): LocalIcebergSink;
|
|
2
|
+
import { IcebergS3Config, Sink, SinkCloseResult, SliceOverwriteWriter } from "./_chunks/sink.mjs";
|
|
25
3
|
/** Connection details for the Iceberg REST catalog the writer targets. */
|
|
26
4
|
interface OverwriteWriterCatalogConfig {
|
|
27
5
|
/**
|
|
@@ -180,4 +158,4 @@ declare function deleteSiteFromShard(args: {
|
|
|
180
158
|
siteId: string | number;
|
|
181
159
|
tables: readonly string[];
|
|
182
160
|
}): Promise<DeleteSiteResult[]>;
|
|
183
|
-
export { type DeleteJob, type DeleteSiteResult, type IcebergOverwriteWriter, type IcebergOverwriteWriterOptions, type
|
|
161
|
+
export { type DeleteJob, type DeleteSiteResult, type IcebergOverwriteWriter, type IcebergOverwriteWriterOptions, type OverwriteBackend, type OverwriteJob, type OverwriteJobResult, type OverwriteWriterCatalogConfig, type PyIcebergJob, createIcebergOverwriteWriter, deleteSiteFromShard, httpBackend, overwriteWriterAsSink, subprocessBackend };
|
package/dist/sink-node.mjs
CHANGED
|
@@ -1,88 +1,5 @@
|
|
|
1
1
|
import { ICEBERG_SCHEMAS, assertIcebergTable } from "./_chunks/schema2.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import process from "node:process";
|
|
5
|
-
function resolvePyIcebergPython(override) {
|
|
6
|
-
return override ?? process.env["GSCDUMP_ICEBERG_PYTHON"] ?? "python3";
|
|
7
|
-
}
|
|
8
|
-
async function runPyIcebergWriter(options) {
|
|
9
|
-
const { execFile } = await import("node:child_process");
|
|
10
|
-
return new Promise((resolve, reject) => {
|
|
11
|
-
execFile(options.python, [options.script], { maxBuffer: 64 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
12
|
-
let parsed;
|
|
13
|
-
if (stdout.trim()) try {
|
|
14
|
-
parsed = JSON.parse(stdout);
|
|
15
|
-
} catch {}
|
|
16
|
-
if (parsed && !(err && options.rejectOnProcessError)) {
|
|
17
|
-
resolve(parsed);
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
if (err) {
|
|
21
|
-
if (options.processErrorAsParseFailure) {
|
|
22
|
-
reject(/* @__PURE__ */ new Error(`${options.label} produced no parseable output (${err.message})${stderr ? `: ${stderr}` : ""}`));
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
reject(/* @__PURE__ */ new Error(`${options.label} process failed (${err.message})${stderr ? `: ${stderr}` : ""}`));
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
reject(/* @__PURE__ */ new Error(`${options.label} produced no parseable output: ${stdout || stderr}`));
|
|
29
|
-
}).stdin?.end(JSON.stringify(options.job));
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
const POC_S3 = {
|
|
33
|
-
endpoint: "localhost:9100",
|
|
34
|
-
accessKeyId: "poc",
|
|
35
|
-
secretAccessKey: "pocpocpoc",
|
|
36
|
-
region: "us-east-1"
|
|
37
|
-
};
|
|
38
|
-
function resolveWriterScript(override) {
|
|
39
|
-
if (override) return override;
|
|
40
|
-
return join(dirname(fileURLToPath(import.meta.url)), "..", "..", "scripts", "iceberg-writer.py");
|
|
41
|
-
}
|
|
42
|
-
function createLocalIcebergSink(options) {
|
|
43
|
-
const s3 = options.s3 ?? POC_S3;
|
|
44
|
-
const python = resolvePyIcebergPython(options.python);
|
|
45
|
-
const script = resolveWriterScript(options.writerScript);
|
|
46
|
-
function buildJob(op, slice, rows) {
|
|
47
|
-
return {
|
|
48
|
-
op,
|
|
49
|
-
catalogUri: options.catalogUri,
|
|
50
|
-
namespace: options.namespace,
|
|
51
|
-
warehouse: options.warehouse,
|
|
52
|
-
s3,
|
|
53
|
-
table: slice.table,
|
|
54
|
-
spec: ICEBERG_SCHEMAS[slice.table],
|
|
55
|
-
siteId: slice.ctx.siteId ?? "",
|
|
56
|
-
searchType: slice.searchType,
|
|
57
|
-
date: slice.date,
|
|
58
|
-
rows
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
const touched = /* @__PURE__ */ new Set();
|
|
62
|
-
return {
|
|
63
|
-
namespace: options.namespace,
|
|
64
|
-
capabilities: { appendOnly: true },
|
|
65
|
-
async emit(slice, rows) {
|
|
66
|
-
if (rows.length === 0) return { rowCount: 0 };
|
|
67
|
-
const res = await runPyIcebergWriter({
|
|
68
|
-
python,
|
|
69
|
-
script,
|
|
70
|
-
job: buildJob("emit", slice, rows),
|
|
71
|
-
label: "LocalIcebergSink writer",
|
|
72
|
-
rejectOnProcessError: true
|
|
73
|
-
});
|
|
74
|
-
if (res.error) throw new Error(`LocalIcebergSink writer failed: ${res.error}`);
|
|
75
|
-
touched.add(slice.table);
|
|
76
|
-
return { rowCount: res.rowCount ?? 0 };
|
|
77
|
-
},
|
|
78
|
-
async close() {
|
|
79
|
-
return {
|
|
80
|
-
flushed: [...touched],
|
|
81
|
-
failed: []
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
}
|
|
2
|
+
import { resolvePyIcebergPython, runPyIcebergWriter } from "@gscdump/lakehouse";
|
|
86
3
|
function createIcebergOverwriteWriter(opts) {
|
|
87
4
|
const { catalog, backend } = opts;
|
|
88
5
|
async function overwriteSlice(slice, rows) {
|
|
@@ -181,4 +98,4 @@ async function deleteSiteFromShard(args) {
|
|
|
181
98
|
}
|
|
182
99
|
return results;
|
|
183
100
|
}
|
|
184
|
-
export { createIcebergOverwriteWriter,
|
|
101
|
+
export { createIcebergOverwriteWriter, deleteSiteFromShard, httpBackend, overwriteWriterAsSink, subprocessBackend };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gscdump/engine",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.35.2",
|
|
5
5
|
"description": "Append-only Parquet/DuckDB storage engine + planner + adapters for the gscdump pipeline. Node + edge runtimes; opt-in heavy peers.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -181,8 +181,9 @@
|
|
|
181
181
|
"dependencies": {
|
|
182
182
|
"drizzle-orm": "1.0.0-rc.3",
|
|
183
183
|
"proper-lockfile": "^4.1.2",
|
|
184
|
-
"@gscdump/contracts": "0.
|
|
185
|
-
"gscdump": "0.
|
|
184
|
+
"@gscdump/contracts": "0.35.2",
|
|
185
|
+
"@gscdump/lakehouse": "0.35.2",
|
|
186
|
+
"gscdump": "0.35.2"
|
|
186
187
|
},
|
|
187
188
|
"devDependencies": {
|
|
188
189
|
"@duckdb/duckdb-wasm": "^1.32.0",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { };
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { };
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { };
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { };
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { };
|