@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.
@@ -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 { cachingResolver, icebergAppend, icebergCreateTable, icebergDropTable, icebergManifests, restCatalogConnect, restCatalogCreateNamespace, restCatalogListTables, restCatalogLoadTable, s3SignedResolver } from "../_chunks/libs/icebird.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, 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
- async function cacheGet(cache, key, now) {
7
- const boxed = await cache.storage.getItem(key).catch(() => null);
8
- if (!boxed || typeof boxed.exp !== "number" || boxed.exp <= now) return void 0;
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 = "int") {
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 = "int") {
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
- const CATALOG_CONFIG_TTL_MS = 3600 * 1e3;
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 listIcebergTables(conn) {
252
- return (await restCatalogListTables(conn.catalog, { namespace: conn.namespace })).map((t) => t.name).sort();
253
- }
254
- const SNAPSHOT_REF_TTL_MS = 1800 * 1e3;
255
- const RESOLVED_FILES_TTL_MS = 1440 * 60 * 1e3;
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 profiler = opts.profiler;
320
- const now = (opts.clock ?? Date.now)();
321
- const wantedMonths = new Set(monthsInRange(opts.range).map(monthsSinceEpoch));
322
- const endSnapshot = profiler?.start("iceberg.snapshot");
323
- let { snapshotId, metadata } = await loadSnapshotId(conn, opts, now);
324
- endSnapshot?.({ cached: metadata == null && snapshotId != null });
325
- if (snapshotId == null) return [];
326
- const filesKey = resolvedFilesKey(conn.namespace, opts.table, snapshotId, opts.siteId, opts.searchType, wantedMonths);
327
- if (opts.cache) {
328
- const endCache = profiler?.start("iceberg.cache");
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
- purgeRequested: true
390
- }).then(() => results.push({
391
- table,
392
- outcome: ok(void 0)
393
- }), (e) => results.push({
394
- table,
395
- outcome: err(engineErrors.icebergTableOpFailed("drop", table, e))
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 = ICEBERG_SCHEMAS[table].identityColumns;
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 = TABLE_METADATA[table].clusterKey;
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 = encoding === "int" ? SEARCH_TYPE_INT[slice.searchType] : slice.searchType;
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 ?? "int";
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);
@@ -1,27 +1,5 @@
1
1
  import { Row } from "./_chunks/storage.mjs";
2
- import { IcebergS3Config, LocalIcebergSinkOptions, Sink, SinkCloseResult, SliceOverwriteWriter } from "./_chunks/sink.mjs";
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 LocalIcebergSink, type LocalIcebergSinkFullOptions, type LocalIcebergSinkOptions, type OverwriteBackend, type OverwriteJob, type OverwriteJobResult, type OverwriteWriterCatalogConfig, type PyIcebergJob, createIcebergOverwriteWriter, createLocalIcebergSink, deleteSiteFromShard, httpBackend, overwriteWriterAsSink, subprocessBackend };
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 };
@@ -1,88 +1,5 @@
1
1
  import { ICEBERG_SCHEMAS, assertIcebergTable } from "./_chunks/schema2.mjs";
2
- import { dirname, join } from "node:path";
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, createLocalIcebergSink, deleteSiteFromShard, httpBackend, overwriteWriterAsSink, subprocessBackend };
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.33.10",
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.33.10",
185
- "gscdump": "0.33.10"
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 { };