@gscdump/engine 4.2.1 → 4.2.3

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.
@@ -29,37 +29,36 @@ function toIntPartitionSiteId(value) {
29
29
  if (siteId < INT32_MIN || siteId > INT32_MAX) throw new TypeError(`toRecords: int partition site_id must fit Iceberg INT, got '${String(value)}'`);
30
30
  return siteId;
31
31
  }
32
- function dedupeByIdentity(table, records) {
33
- if (records.length < 2) return records;
34
- const key = gscDataset(table, "int").tableSpec.identityColumns;
35
- const seen = /* @__PURE__ */ new Map();
36
- for (const rec of records) {
37
- let identity = "";
38
- for (let index = 0; index < key.length; index++) {
39
- if (index > 0) identity += "\0";
40
- identity += `${rec[key[index]] ?? ""}`;
41
- }
42
- seen.set(identity, rec);
43
- }
44
- return seen.size === records.length ? records : [...seen.values()];
32
+ function compareIdentityValue(av, bv) {
33
+ if (av === bv) return 0;
34
+ if (typeof av === "number" && typeof bv === "number") return av - bv;
35
+ const as = `${av ?? ""}`;
36
+ const bs = `${bv ?? ""}`;
37
+ if (as === bs) return 0;
38
+ return as < bs ? -1 : 1;
45
39
  }
46
- function sortByClusterKey(table, records) {
47
- const cols = gscDataset(table, "int").tableSpec.clusterKey ?? [];
48
- if (cols.length === 0 || records.length < 2) return records;
49
- return records.slice().sort((a, b) => {
50
- for (const col of cols) {
51
- const av = a[col];
52
- const bv = b[col];
53
- if (av === bv) continue;
54
- if (av == null) return -1;
55
- if (bv == null) return 1;
56
- if (typeof av === "number" && typeof bv === "number") return av - bv;
57
- const as = String(av);
58
- const bs = String(bv);
59
- if (as !== bs) return as < bs ? -1 : 1;
40
+ function clusterAndDedupe(table, records) {
41
+ if (records.length < 2) return records;
42
+ const spec = gscDataset(table, "int").tableSpec;
43
+ const cluster = spec.clusterKey ?? [];
44
+ for (const col of cluster) if (!spec.identityColumns.includes(col)) throw new TypeError(`clusterAndDedupe: clusterKey column '${col}' is not an identity column of '${table}'. Dedupe would silently key on cluster plus identity.`);
45
+ const order = [...cluster, ...spec.identityColumns.filter((col) => !cluster.includes(col))];
46
+ const compare = (a, b) => {
47
+ for (const col of order) {
48
+ const c = compareIdentityValue(a[col], b[col]);
49
+ if (c !== 0) return c;
60
50
  }
61
51
  return 0;
62
- });
52
+ };
53
+ records.sort(compare);
54
+ let write = 0;
55
+ for (let read = 0; read < records.length; read++) {
56
+ const rec = records[read];
57
+ if (read + 1 < records.length && compare(rec, records[read + 1]) === 0) continue;
58
+ records[write++] = rec;
59
+ }
60
+ records.length = write;
61
+ return records;
63
62
  }
64
63
  function toRecords(slice, rows, encoding) {
65
64
  const siteVal = encoding === "int" ? toIntPartitionSiteId(slice.ctx.siteId) : slice.ctx.siteId ?? "";
@@ -115,7 +114,16 @@ function createIcebergAppendSink(options) {
115
114
  }
116
115
  for (const [table, records] of buffers) {
117
116
  if (records.length === 0) continue;
118
- const deduped = sortByClusterKey(table, dedupeByIdentity(table, records));
117
+ let deduped;
118
+ try {
119
+ deduped = clusterAndDedupe(table, records);
120
+ } catch (cause) {
121
+ failed.push({
122
+ table,
123
+ error: engineErrors.sinkTableFlushFailed(table, cause)
124
+ });
125
+ continue;
126
+ }
119
127
  await icebergAppendRetrying({
120
128
  catalog: conn.catalog,
121
129
  namespace: conn.namespace,
package/dist/ingest.mjs CHANGED
@@ -242,14 +242,20 @@ const STORED_KEY_COLUMNS = Object.fromEntries(Object.keys(TABLE_DIMS).map((table
242
242
  function sumByStoredKey(table, rows) {
243
243
  const keyColumns = STORED_KEY_COLUMNS[table];
244
244
  const grouped = /* @__PURE__ */ new Map();
245
+ const owned = /* @__PURE__ */ new Set();
245
246
  for (const row of rows) {
246
247
  const r = row;
247
248
  const key = JSON.stringify(keyColumns.map((col) => r[col] ?? null));
248
- const prior = grouped.get(key);
249
+ let prior = grouped.get(key);
249
250
  if (!prior) {
250
- grouped.set(key, { ...r });
251
+ grouped.set(key, r);
251
252
  continue;
252
253
  }
254
+ if (!owned.has(prior)) {
255
+ prior = { ...prior };
256
+ owned.add(prior);
257
+ grouped.set(key, prior);
258
+ }
253
259
  for (const col of Object.keys(r)) if (ADDITIVE_COLUMN_RE.test(col)) prior[col] = Number(prior[col] ?? 0) + Number(r[col] ?? 0);
254
260
  }
255
261
  return [...grouped.values()];
@@ -299,7 +305,14 @@ function createRowAccumulator(options = {}) {
299
305
  },
300
306
  drain() {
301
307
  const out = /* @__PURE__ */ new Map();
302
- for (const [table, byDate] of buckets) out.set(table, new Map([...byDate].map(([date, sourceRows]) => [date, sumByStoredKey(table, sourceRows.values())])));
308
+ for (const [table, byDate] of buckets) {
309
+ const summed = /* @__PURE__ */ new Map();
310
+ for (const [date, sourceRows] of byDate) {
311
+ summed.set(date, sumByStoredKey(table, sourceRows.values()));
312
+ byDate.delete(date);
313
+ }
314
+ out.set(table, summed);
315
+ }
303
316
  buckets = /* @__PURE__ */ new Map();
304
317
  latestDate.clear();
305
318
  total = 0;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/engine",
3
3
  "type": "module",
4
- "version": "4.2.1",
4
+ "version": "4.2.3",
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",
@@ -189,10 +189,10 @@
189
189
  }
190
190
  },
191
191
  "dependencies": {
192
- "@gscdump/contracts": "^4.2.1",
193
- "@gscdump/lakehouse": "^4.2.1",
192
+ "@gscdump/contracts": "^4.2.3",
193
+ "@gscdump/lakehouse": "^4.2.3",
194
194
  "drizzle-orm": "1.0.0-rc.4",
195
- "gscdump": "^4.2.1",
195
+ "gscdump": "^4.2.3",
196
196
  "pathe": "^2.0.3",
197
197
  "proper-lockfile": "^4.1.2"
198
198
  },