@secondlayer/subgraphs 3.12.0 → 3.14.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.
Files changed (36) hide show
  1. package/dist/src/index.d.ts +54 -1
  2. package/dist/src/index.js +952 -199
  3. package/dist/src/index.js.map +12 -11
  4. package/dist/src/runtime/block-processor.d.ts +36 -1
  5. package/dist/src/runtime/block-processor.js +568 -86
  6. package/dist/src/runtime/block-processor.js.map +9 -8
  7. package/dist/src/runtime/catchup.d.ts +7 -0
  8. package/dist/src/runtime/catchup.js +587 -132
  9. package/dist/src/runtime/catchup.js.map +10 -9
  10. package/dist/src/runtime/context.d.ts +65 -3
  11. package/dist/src/runtime/context.js +390 -8
  12. package/dist/src/runtime/context.js.map +6 -4
  13. package/dist/src/runtime/processor.js +665 -248
  14. package/dist/src/runtime/processor.js.map +13 -13
  15. package/dist/src/runtime/reindex.d.ts +7 -0
  16. package/dist/src/runtime/reindex.js +618 -198
  17. package/dist/src/runtime/reindex.js.map +10 -10
  18. package/dist/src/runtime/reorg.d.ts +7 -0
  19. package/dist/src/runtime/reorg.js +588 -87
  20. package/dist/src/runtime/reorg.js.map +10 -9
  21. package/dist/src/runtime/replay.js.map +2 -2
  22. package/dist/src/runtime/runner.d.ts +70 -2
  23. package/dist/src/runtime/runner.js +56 -58
  24. package/dist/src/runtime/runner.js.map +3 -3
  25. package/dist/src/runtime/source-matcher.d.ts +2 -0
  26. package/dist/src/runtime/source-matcher.js.map +2 -2
  27. package/dist/src/schema/index.d.ts +7 -0
  28. package/dist/src/schema/index.js +20 -2
  29. package/dist/src/schema/index.js.map +4 -4
  30. package/dist/src/service.js +665 -248
  31. package/dist/src/service.js.map +13 -13
  32. package/dist/src/types.d.ts +7 -0
  33. package/dist/src/validate.d.ts +7 -0
  34. package/dist/src/validate.js +3 -2
  35. package/dist/src/validate.js.map +3 -3
  36. package/package.json +2 -2
package/dist/src/index.js CHANGED
@@ -62,7 +62,8 @@ var SubgraphFilterSchema = z.object({
62
62
  topic: z.string().optional(),
63
63
  lockedAddress: z.string().optional(),
64
64
  abi: z.record(z.string(), z.any()).optional(),
65
- trait: z.string().optional()
65
+ trait: z.string().optional(),
66
+ prints: z.record(z.string(), z.record(z.string(), ColumnTypeSchema)).optional()
66
67
  }).strict();
67
68
  var SubgraphDefinitionSchema = z.object({
68
69
  name: SubgraphNameSchema,
@@ -82,6 +83,134 @@ function validateSubgraphDefinition(def) {
82
83
  import { logger } from "@secondlayer/shared/logger";
83
84
  import { formatUnits } from "@secondlayer/stacks/utils";
84
85
  import { sql } from "kysely";
86
+
87
+ // src/schema/generator.ts
88
+ import { createHash } from "node:crypto";
89
+
90
+ // src/schema/utils.ts
91
+ import { pgSchemaName } from "@secondlayer/shared/db/queries/subgraphs";
92
+
93
+ // src/schema/generator.ts
94
+ var TYPE_MAP = {
95
+ text: "TEXT",
96
+ uint: "NUMERIC",
97
+ int: "NUMERIC",
98
+ principal: "TEXT",
99
+ boolean: "BOOLEAN",
100
+ timestamp: "TIMESTAMPTZ",
101
+ jsonb: "JSONB"
102
+ };
103
+ function escapeLiteralDefault(value) {
104
+ if (value === null || value === undefined)
105
+ return "NULL";
106
+ if (typeof value === "number" || typeof value === "bigint")
107
+ return String(value);
108
+ if (typeof value === "boolean")
109
+ return value ? "TRUE" : "FALSE";
110
+ return `'${String(value).replace(/'/g, "''")}'`;
111
+ }
112
+ function tableNeedsTrgm(tableDef) {
113
+ return Object.values(tableDef.columns).some((col) => col.search);
114
+ }
115
+ function emitTableDDL(schemaName, tableName, tableDef) {
116
+ const qualifiedName = `${schemaName}.${tableName}`;
117
+ const statements = [];
118
+ const columnDefs = [
119
+ "_id BIGSERIAL PRIMARY KEY",
120
+ "_block_height BIGINT NOT NULL",
121
+ "_tx_id TEXT NOT NULL",
122
+ "_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()"
123
+ ];
124
+ for (const [colName, col] of Object.entries(tableDef.columns)) {
125
+ const sqlType = TYPE_MAP[col.type];
126
+ const nullable = col.nullable ? "" : " NOT NULL";
127
+ let colDef = `${colName} ${sqlType}${nullable}`;
128
+ if (col.default !== undefined) {
129
+ colDef += ` DEFAULT ${escapeLiteralDefault(col.default)}`;
130
+ }
131
+ if (col.type === "uint") {
132
+ colDef += ` CHECK (${colName} >= 0)`;
133
+ }
134
+ columnDefs.push(colDef);
135
+ }
136
+ statements.push(`CREATE TABLE IF NOT EXISTS ${qualifiedName} (
137
+ ${columnDefs.join(`,
138
+ `)}
139
+ )`);
140
+ statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_block_height ON ${qualifiedName} (_block_height)`);
141
+ statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_tx_id ON ${qualifiedName} (_tx_id)`);
142
+ for (const [colName, col] of Object.entries(tableDef.columns)) {
143
+ if (col.indexed) {
144
+ statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName} ON ${qualifiedName} (${colName})`);
145
+ }
146
+ }
147
+ for (const [colName, col] of Object.entries(tableDef.columns)) {
148
+ if (col.search) {
149
+ statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName}_trgm ON ${qualifiedName} USING gin (${colName} gin_trgm_ops)`);
150
+ }
151
+ }
152
+ if (tableDef.indexes) {
153
+ for (let i = 0;i < tableDef.indexes.length; i++) {
154
+ const cols = tableDef.indexes[i];
155
+ const idxName = `idx_${schemaName}_${tableName}_composite_${i}`;
156
+ statements.push(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${qualifiedName} (${cols.join(", ")})`);
157
+ }
158
+ }
159
+ if (tableDef.uniqueKeys) {
160
+ for (let i = 0;i < tableDef.uniqueKeys.length; i++) {
161
+ const cols = tableDef.uniqueKeys[i];
162
+ const constraintName = `uq_${schemaName}_${tableName}_${cols.join("_")}`;
163
+ statements.push(`ALTER TABLE ${qualifiedName} ADD CONSTRAINT ${constraintName} UNIQUE (${cols.join(", ")})`);
164
+ }
165
+ }
166
+ return statements;
167
+ }
168
+ function emitJournalDDL(schemaName) {
169
+ return [
170
+ `CREATE TABLE IF NOT EXISTS ${schemaName}._journal (
171
+ _jid BIGSERIAL PRIMARY KEY,
172
+ block_height BIGINT NOT NULL,
173
+ table_name TEXT NOT NULL,
174
+ row_key JSONB NOT NULL,
175
+ prev_row JSONB,
176
+ _created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
177
+ )`,
178
+ `CREATE INDEX IF NOT EXISTS idx_${schemaName}_journal_height ON ${schemaName}._journal (block_height)`
179
+ ];
180
+ }
181
+ function emitForeignKeyDDL(schemaName, tableName, tableDef) {
182
+ return (tableDef.relations ?? []).map((rel) => {
183
+ const constraintName = `fk_${schemaName}_${tableName}_${rel.name}`;
184
+ return `ALTER TABLE ${schemaName}.${tableName} ADD CONSTRAINT ${constraintName} ` + `FOREIGN KEY (${rel.fields.join(", ")}) ` + `REFERENCES ${schemaName}.${rel.references} (${rel.referencedColumns.join(", ")})`;
185
+ });
186
+ }
187
+ function generateSubgraphSQL(def, schemaNameOverride) {
188
+ const schemaName = schemaNameOverride ?? pgSchemaName(def.name);
189
+ const statements = [];
190
+ const needsTrgm = Object.values(def.schema).some((table) => Object.values(table.columns).some((col) => col.search));
191
+ if (needsTrgm) {
192
+ statements.push("CREATE EXTENSION IF NOT EXISTS pg_trgm");
193
+ }
194
+ statements.push(`CREATE SCHEMA IF NOT EXISTS ${schemaName}`);
195
+ for (const [tableName, tableDef] of Object.entries(def.schema)) {
196
+ statements.push(...emitTableDDL(schemaName, tableName, tableDef));
197
+ }
198
+ statements.push(...emitJournalDDL(schemaName));
199
+ for (const [tableName, tableDef] of Object.entries(def.schema)) {
200
+ statements.push(...emitForeignKeyDDL(schemaName, tableName, tableDef));
201
+ }
202
+ const hashInput = JSON.stringify({
203
+ name: def.name,
204
+ schema: def.schema,
205
+ sources: def.sources
206
+ }, (_key, value) => typeof value === "bigint" ? value.toString() : value);
207
+ const hash = createHash("sha256").update(hashInput).digest("hex");
208
+ return { statements, hash };
209
+ }
210
+
211
+ // src/runtime/context.ts
212
+ var JOURNAL_RETENTION_BLOCKS = 300;
213
+ var journalEnsured = new Set;
85
214
  function validateColumnName(name) {
86
215
  if (!/^[a-z_][a-z0-9_]*$/i.test(name)) {
87
216
  throw new Error(`Invalid column name: ${name}`);
@@ -96,13 +225,15 @@ class SubgraphContext {
96
225
  subgraphSchema;
97
226
  ops = [];
98
227
  byo;
99
- constructor(db, pgSchemaName, subgraphSchema, block, tx, byo = false) {
228
+ journal;
229
+ constructor(db, pgSchemaName2, subgraphSchema, block, tx, byo = false, journal = false) {
100
230
  this.db = db;
101
- this.pgSchemaName = pgSchemaName;
231
+ this.pgSchemaName = pgSchemaName2;
102
232
  this.subgraphSchema = subgraphSchema;
103
233
  this.block = block;
104
234
  this._tx = tx;
105
235
  this.byo = byo;
236
+ this.journal = journal;
106
237
  }
107
238
  get tx() {
108
239
  return this._tx;
@@ -158,6 +289,43 @@ class SubgraphContext {
158
289
  this.validateTable(table);
159
290
  this.ops.push({ kind: "delete", table, data: where });
160
291
  }
292
+ increment(table, key, deltas) {
293
+ this.validateTable(table);
294
+ const tableDef = this.subgraphSchema[table];
295
+ const keyColumns = Object.keys(key);
296
+ const hasUniqueConstraint = tableDef?.uniqueKeys?.some((uk) => uk.length === keyColumns.length && uk.every((c) => keyColumns.includes(c)));
297
+ if (!hasUniqueConstraint) {
298
+ throw new Error(`increment("${table}") requires a uniqueKeys constraint on [${keyColumns.join(", ")}]`);
299
+ }
300
+ for (const [col, v] of Object.entries(deltas)) {
301
+ validateColumnName(col);
302
+ if (keyColumns.includes(col)) {
303
+ throw new Error(`increment("${table}"): "${col}" is a key column`);
304
+ }
305
+ if (typeof v !== "bigint" && typeof v !== "number") {
306
+ throw new Error(`increment("${table}"): delta for "${col}" must be bigint or number`);
307
+ }
308
+ }
309
+ this.ops.push({
310
+ kind: "increment",
311
+ table,
312
+ data: {
313
+ ...key,
314
+ _block_height: this.block.height,
315
+ _tx_id: this._tx.txId,
316
+ _upsert_keys: keyColumns
317
+ },
318
+ set: { ...deltas }
319
+ });
320
+ }
321
+ opsCheckpoint() {
322
+ return this.ops.length;
323
+ }
324
+ rollbackTo(checkpoint) {
325
+ if (checkpoint < 0 || checkpoint > this.ops.length)
326
+ return;
327
+ this.ops.length = checkpoint;
328
+ }
161
329
  patch(table, where, set) {
162
330
  this.update(table, where, set);
163
331
  }
@@ -179,7 +347,7 @@ class SubgraphContext {
179
347
  const query = `SELECT * FROM ${qualifiedTable} WHERE ${clause} LIMIT 1`;
180
348
  const { rows } = await sql.raw(query).execute(this.db);
181
349
  const row = rows[0] ?? null;
182
- return row ? this.coerceRow(table, row) : null;
350
+ return this.overlayOne(table, where, row ? this.coerceRow(table, row) : null);
183
351
  }
184
352
  async findMany(table, where) {
185
353
  this.validateTable(table);
@@ -187,7 +355,85 @@ class SubgraphContext {
187
355
  const { clause } = buildWhereClause(where);
188
356
  const query = `SELECT * FROM ${qualifiedTable} WHERE ${clause}`;
189
357
  const { rows } = await sql.raw(query).execute(this.db);
190
- return rows.map((r) => this.coerceRow(table, r));
358
+ const dbRows = rows.map((r) => this.coerceRow(table, r));
359
+ return this.overlayMany(table, where, dbRows);
360
+ }
361
+ overlayOne(table, where, dbRow) {
362
+ let row = dbRow;
363
+ for (const op of this.ops) {
364
+ if (op.table !== table)
365
+ continue;
366
+ row = this.applyOpToRow(op, row, where);
367
+ }
368
+ return row;
369
+ }
370
+ overlayMany(table, where, dbRows) {
371
+ let result = [...dbRows];
372
+ for (const op of this.ops) {
373
+ if (op.table !== table)
374
+ continue;
375
+ if (op.kind === "update") {
376
+ result = result.map((r) => rowMatches(r, op.data) ? { ...r, ...op.set ?? {} } : r);
377
+ } else if (op.kind === "delete") {
378
+ result = result.filter((r) => !rowMatches(r, op.data));
379
+ } else {
380
+ const upsertKeys = op.data._upsert_keys;
381
+ const clean = stripControlKeys(op.data);
382
+ const idx = upsertKeys ? result.findIndex((r) => upsertKeys.every((k) => valEq(r[k], clean[k]))) : -1;
383
+ if (idx >= 0) {
384
+ result[idx] = this.applyOpToRow(op, result[idx], where) ?? result[idx];
385
+ } else {
386
+ const created = this.applyOpToRow(op, null, where);
387
+ if (created)
388
+ result.push(created);
389
+ }
390
+ }
391
+ }
392
+ return result;
393
+ }
394
+ applyOpToRow(op, row, where) {
395
+ const upsertKeys = op.data._upsert_keys;
396
+ const clean = stripControlKeys(op.data);
397
+ switch (op.kind) {
398
+ case "insert": {
399
+ if (row) {
400
+ if (upsertKeys?.every((k) => valEq(row[k], clean[k]))) {
401
+ const merged = { ...row };
402
+ for (const [k, v] of Object.entries(clean)) {
403
+ if (!upsertKeys.includes(k) && !k.startsWith("_"))
404
+ merged[k] = v;
405
+ }
406
+ return merged;
407
+ }
408
+ return row;
409
+ }
410
+ return rowMatches(clean, where) ? { ...clean } : null;
411
+ }
412
+ case "increment": {
413
+ const deltas = op.set ?? {};
414
+ if (row) {
415
+ if (upsertKeys.every((k) => valEq(row[k], clean[k]))) {
416
+ const merged = { ...row };
417
+ for (const [col, d] of Object.entries(deltas)) {
418
+ merged[col] = toBigIntOr0(merged[col]) + toBigIntOr0(d);
419
+ }
420
+ return merged;
421
+ }
422
+ return row;
423
+ }
424
+ if (!rowMatches(clean, where))
425
+ return null;
426
+ const created = { ...clean };
427
+ for (const [col, d] of Object.entries(deltas)) {
428
+ created[col] = toBigIntOr0(d);
429
+ }
430
+ return created;
431
+ }
432
+ case "update":
433
+ return row && rowMatches(row, op.data) ? { ...row, ...op.set ?? {} } : row;
434
+ case "delete":
435
+ return row && rowMatches(row, op.data) ? null : row;
436
+ }
191
437
  }
192
438
  async count(table, where) {
193
439
  this.validateTable(table);
@@ -248,6 +494,7 @@ class SubgraphContext {
248
494
  async flush() {
249
495
  if (this.ops.length === 0)
250
496
  return { count: 0, writes: [] };
497
+ await this.ensureJournalTable();
251
498
  const opsToFlush = [...this.ops];
252
499
  this.ops.length = 0;
253
500
  const statements = this.buildStatements(opsToFlush);
@@ -265,12 +512,12 @@ class SubgraphContext {
265
512
  const writes = opsToFlush.map((op, rowIndex) => {
266
513
  const blockHeight = op.data._block_height ?? this.block.height;
267
514
  const txId = op.data._tx_id ?? this._tx.txId;
268
- const baseRow = op.kind === "update" ? { ...op.data, ...op.set ?? {} } : { ...op.data };
515
+ const baseRow = op.kind === "update" || op.kind === "increment" ? { ...op.data, ...op.set ?? {} } : { ...op.data };
269
516
  baseRow._upsert_keys = undefined;
270
517
  baseRow._upsert_fallback_keys = undefined;
271
518
  baseRow._upsert_fallback_set = undefined;
272
519
  return {
273
- op: op.kind,
520
+ op: op.kind === "increment" ? "update" : op.kind,
274
521
  table: op.table,
275
522
  row: jsonSafe(baseRow),
276
523
  pk: { blockHeight, txId, rowIndex }
@@ -295,6 +542,35 @@ class SubgraphContext {
295
542
  const batchKey = `${op.table}:${[...cols].sort().join(",")}:${upsertKeys ? [...upsertKeys].sort().join(",") : ""}`;
296
543
  return { data, cols, vals, upsertKeys, batchKey };
297
544
  }
545
+ async ensureJournalTable() {
546
+ if (!this.journal || journalEnsured.has(this.pgSchemaName))
547
+ return;
548
+ const { rows } = await sql.raw(`SELECT to_regclass('"${this.pgSchemaName}"."_journal"') AS r`).execute(this.db);
549
+ if (rows[0]?.r) {
550
+ journalEnsured.add(this.pgSchemaName);
551
+ return;
552
+ }
553
+ for (const stmt of emitJournalDDL(this.pgSchemaName)) {
554
+ await sql.raw(stmt).execute(this.db);
555
+ }
556
+ }
557
+ columnSqlType(table, col) {
558
+ const def = this.subgraphSchema[table]?.columns?.[col];
559
+ return def ? TYPE_MAP[def.type] : undefined;
560
+ }
561
+ journalCaptureSQL(table, keyCols, keyLiteralRows) {
562
+ const cast = (col, expr) => {
563
+ const t = this.columnSqlType(table, col);
564
+ return t ? `CAST(${expr} AS ${t})` : expr;
565
+ };
566
+ const keyObj = keyCols.map((k) => `'${k}', ${cast(k, `v."${k}"`)}`).join(", ");
567
+ const joinCond = keyCols.map((k) => `t."${k}" = ${cast(k, `v."${k}"`)}`).join(" AND ");
568
+ const valuesList = keyLiteralRows.map((r) => `(${r.join(", ")})`).join(", ");
569
+ return `INSERT INTO "${this.pgSchemaName}"."_journal" ("block_height", "table_name", "row_key", "prev_row") ` + `SELECT ${this.block.height}, '${table}', jsonb_build_object(${keyObj}), to_jsonb(t.*) ` + `FROM (VALUES ${valuesList}) AS v(${keyCols.map((k) => `"${k}"`).join(", ")}) ` + `LEFT JOIN "${this.pgSchemaName}"."${table}" t ON ${joinCond}`;
570
+ }
571
+ journalCaptureByWhereSQL(table, clause) {
572
+ return `INSERT INTO "${this.pgSchemaName}"."_journal" ("block_height", "table_name", "row_key", "prev_row") ` + `SELECT ${this.block.height}, '${table}', jsonb_build_object('_id', t."_id"), to_jsonb(t.*) ` + `FROM "${this.pgSchemaName}"."${table}" t WHERE ${clause}`;
573
+ }
298
574
  buildStatements(ops) {
299
575
  const statements = [];
300
576
  if (this.byo) {
@@ -308,6 +584,38 @@ class SubgraphContext {
308
584
  }
309
585
  let currentBatch = null;
310
586
  let currentBatchKey = "";
587
+ let incBatch = null;
588
+ let incBatchKey = "";
589
+ const flushIncrementBatch = () => {
590
+ if (!incBatch)
591
+ return;
592
+ const batch = incBatch;
593
+ const qualifiedTable = `"${this.pgSchemaName}"."${batch.table}"`;
594
+ const cols = [
595
+ ...batch.keyCols,
596
+ ...batch.deltaCols,
597
+ "_block_height",
598
+ "_tx_id",
599
+ "_created_at"
600
+ ];
601
+ const valuesList = Array.from(batch.rows.values()).map((r) => {
602
+ const vals = [
603
+ ...batch.keyCols.map((k) => escapeLiteral(r.keys[k])),
604
+ ...batch.deltaCols.map((c) => String(r.deltas[c] ?? 0n)),
605
+ escapeLiteral(r.meta.blockHeight),
606
+ escapeLiteral(r.meta.txId),
607
+ "NOW()"
608
+ ];
609
+ return `(${vals.join(", ")})`;
610
+ }).join(", ");
611
+ const setClauses = batch.deltaCols.map((c) => `"${c}" = COALESCE("${batch.table}"."${c}", 0) + EXCLUDED."${c}"`);
612
+ if (this.journal) {
613
+ statements.push(this.journalCaptureSQL(batch.table, batch.keyCols, Array.from(batch.rows.values()).map((r) => batch.keyCols.map((k) => escapeLiteral(r.keys[k])))));
614
+ }
615
+ statements.push(`INSERT INTO ${qualifiedTable} (${cols.map((c) => `"${c}"`).join(", ")}) VALUES ${valuesList} ` + `ON CONFLICT (${batch.keyCols.map((k) => `"${k}"`).join(", ")}) DO UPDATE SET ${setClauses.join(", ")}`);
616
+ incBatch = null;
617
+ incBatchKey = "";
618
+ };
311
619
  const flushInsertBatch = () => {
312
620
  if (!currentBatch)
313
621
  return;
@@ -329,6 +637,11 @@ class SubgraphContext {
329
637
  }
330
638
  const valuesList = rows.map((r) => `(${r.join(", ")})`).join(", ");
331
639
  let stmt = `INSERT INTO ${qualifiedTable} (${colList}) VALUES ${valuesList}`;
640
+ if (this.journal && batch.upsertKeys && batch.upsertKeys.length > 0) {
641
+ const uKeys = batch.upsertKeys;
642
+ const keyIndices = uKeys.map((k) => batch.cols.indexOf(k));
643
+ statements.push(this.journalCaptureSQL(batch.table, uKeys, rows.map((r) => keyIndices.map((ki) => r[ki]))));
644
+ }
332
645
  if (batch.upsertKeys && batch.upsertKeys.length > 0) {
333
646
  const batchKeys = batch.upsertKeys;
334
647
  const updateCols = batch.cols.filter((c) => !batchKeys.includes(c) && !c.startsWith("_"));
@@ -346,6 +659,7 @@ class SubgraphContext {
346
659
  for (const op of ops) {
347
660
  const qualifiedTable = `"${this.pgSchemaName}"."${op.table}"`;
348
661
  if (op.kind === "insert") {
662
+ flushIncrementBatch();
349
663
  const { cols, vals, upsertKeys, batchKey } = this.prepareInsert(op);
350
664
  if (batchKey === currentBatchKey && currentBatch) {
351
665
  currentBatch.rows.push(vals);
@@ -354,22 +668,60 @@ class SubgraphContext {
354
668
  currentBatch = { table: op.table, cols, rows: [vals], upsertKeys };
355
669
  currentBatchKey = batchKey;
356
670
  }
671
+ } else if (op.kind === "increment") {
672
+ flushInsertBatch();
673
+ const keyCols = [...op.data._upsert_keys].sort();
674
+ const deltaCols = Object.keys(op.set ?? {}).sort();
675
+ const batchKey = `inc:${op.table}:${keyCols.join(",")}:${deltaCols.join(",")}`;
676
+ if (batchKey !== incBatchKey || !incBatch) {
677
+ flushIncrementBatch();
678
+ incBatch = { table: op.table, keyCols, deltaCols, rows: new Map };
679
+ incBatchKey = batchKey;
680
+ }
681
+ const clean = stripControlKeys(op.data);
682
+ const keySig = keyCols.map((k) => escapeLiteral(clean[k])).join("\x00");
683
+ const existing = incBatch.rows.get(keySig);
684
+ if (existing) {
685
+ for (const c of deltaCols) {
686
+ existing.deltas[c] = (existing.deltas[c] ?? 0n) + toBigIntOr0(op.set?.[c]);
687
+ }
688
+ } else {
689
+ const deltas = {};
690
+ for (const c of deltaCols)
691
+ deltas[c] = toBigIntOr0(op.set?.[c]);
692
+ incBatch.rows.set(keySig, {
693
+ keys: clean,
694
+ deltas,
695
+ meta: {
696
+ blockHeight: op.data._block_height ?? this.block.height,
697
+ txId: op.data._tx_id ?? this._tx.txId
698
+ }
699
+ });
700
+ }
357
701
  } else {
358
702
  flushInsertBatch();
703
+ flushIncrementBatch();
359
704
  if (op.kind === "update") {
360
705
  const setEntries = Object.entries(op.set ?? {});
361
706
  for (const [k] of setEntries)
362
707
  validateColumnName(k);
363
708
  const setClauses = setEntries.map(([k, v]) => `"${k}" = ${escapeLiteral(v)}`);
364
709
  const { clause } = buildWhereClause(op.data);
710
+ if (this.journal) {
711
+ statements.push(this.journalCaptureByWhereSQL(op.table, clause));
712
+ }
365
713
  statements.push(`UPDATE ${qualifiedTable} SET ${setClauses.join(", ")} WHERE ${clause}`);
366
714
  } else if (op.kind === "delete") {
367
715
  const { clause } = buildWhereClause(op.data);
716
+ if (this.journal) {
717
+ statements.push(this.journalCaptureByWhereSQL(op.table, clause));
718
+ }
368
719
  statements.push(`DELETE FROM ${qualifiedTable} WHERE ${clause}`);
369
720
  }
370
721
  }
371
722
  }
372
723
  flushInsertBatch();
724
+ flushIncrementBatch();
373
725
  return statements;
374
726
  }
375
727
  validateTable(table) {
@@ -378,6 +730,36 @@ class SubgraphContext {
378
730
  }
379
731
  }
380
732
  }
733
+ function stripControlKeys(data) {
734
+ const {
735
+ _upsert_keys: _a,
736
+ _upsert_fallback_keys: _b,
737
+ _upsert_fallback_set: _c,
738
+ ...clean
739
+ } = data;
740
+ return clean;
741
+ }
742
+ function valEq(a, b) {
743
+ if (a === b)
744
+ return true;
745
+ if (a == null || b == null)
746
+ return false;
747
+ return String(a) === String(b);
748
+ }
749
+ function rowMatches(row, where) {
750
+ return Object.entries(where).every(([k, v]) => valEq(row[k], v));
751
+ }
752
+ function toBigIntOr0(v) {
753
+ if (typeof v === "bigint")
754
+ return v;
755
+ if (v == null)
756
+ return 0n;
757
+ try {
758
+ return BigInt(String(v));
759
+ } catch {
760
+ return 0n;
761
+ }
762
+ }
381
763
  function jsonSafe(row) {
382
764
  const out = {};
383
765
  for (const [k, v] of Object.entries(row)) {
@@ -696,7 +1078,25 @@ async function runHandlers(subgraph, matched, ctx, opts) {
696
1078
  filterLookup.set(name, filter);
697
1079
  }
698
1080
  }
1081
+ const units = [];
699
1082
  for (const { tx, events, sourceName } of matched) {
1083
+ if (events.length === 0) {
1084
+ units.push({ tx, sourceName, event: null });
1085
+ } else {
1086
+ for (const event of events)
1087
+ units.push({ tx, sourceName, event });
1088
+ }
1089
+ }
1090
+ units.sort((a, b) => (a.tx.tx_index ?? 0) - (b.tx.tx_index ?? 0) || (a.event?.event_index ?? -1) - (b.event?.event_index ?? -1));
1091
+ for (const { tx, event, sourceName } of units) {
1092
+ if (errors >= threshold) {
1093
+ logger2.error("Subgraph error threshold reached, skipping remaining events", {
1094
+ subgraph: subgraph.name,
1095
+ errors,
1096
+ threshold
1097
+ });
1098
+ return { processed, errors };
1099
+ }
700
1100
  const handler = subgraph.handlers[sourceName] ?? subgraph.handlers["*"] ?? null;
701
1101
  if (!handler) {
702
1102
  logger2.warn("No handler found for source", {
@@ -715,9 +1115,29 @@ async function runHandlers(subgraph, matched, ctx, opts) {
715
1115
  functionName: tx.function_name ?? null
716
1116
  });
717
1117
  const filter = filterLookup.get(sourceName);
718
- if (events.length === 0) {
719
- try {
720
- const payload = filter ? buildEventPayload(filter, tx, null) : {
1118
+ const checkpoint = ctx.opsCheckpoint();
1119
+ try {
1120
+ let payload;
1121
+ if (event === null) {
1122
+ payload = filter ? buildEventPayload(filter, tx, null) : {
1123
+ tx: {
1124
+ txId: tx.tx_id,
1125
+ sender: tx.sender,
1126
+ type: tx.type,
1127
+ status: tx.status,
1128
+ contractId: tx.contract_id,
1129
+ functionName: tx.function_name
1130
+ }
1131
+ };
1132
+ } else if (filter) {
1133
+ payload = buildEventPayload(filter, tx, event);
1134
+ } else {
1135
+ const decoded = decodeEventData(event.data);
1136
+ payload = {
1137
+ ...decoded,
1138
+ _eventId: event.id,
1139
+ _eventType: event.type,
1140
+ _eventIndex: event.event_index,
721
1141
  tx: {
722
1142
  txId: tx.tx_id,
723
1143
  sender: tx.sender,
@@ -727,62 +1147,22 @@ async function runHandlers(subgraph, matched, ctx, opts) {
727
1147
  functionName: tx.function_name
728
1148
  }
729
1149
  };
730
- await handler(payload, ctx);
731
- processed++;
732
- } catch (err) {
733
- errors++;
734
- logger2.error("Subgraph handler error", {
735
- subgraph: subgraph.name,
736
- sourceName,
737
- txId: tx.tx_id,
738
- error: getErrorMessage(err)
739
- });
740
- }
741
- continue;
742
- }
743
- for (const event of events) {
744
- if (errors >= threshold) {
745
- logger2.error("Subgraph error threshold reached, skipping remaining events", {
746
- subgraph: subgraph.name,
747
- errors,
748
- threshold
749
- });
750
- return { processed, errors };
751
1150
  }
752
- try {
753
- const payload = filter ? buildEventPayload(filter, tx, event) : (() => {
754
- const decoded = decodeEventData(event.data);
755
- return {
756
- ...decoded,
757
- _eventId: event.id,
758
- _eventType: event.type,
759
- _eventIndex: event.event_index,
760
- tx: {
761
- txId: tx.tx_id,
762
- sender: tx.sender,
763
- type: tx.type,
764
- status: tx.status,
765
- contractId: tx.contract_id,
766
- functionName: tx.function_name
767
- }
768
- };
769
- })();
770
- if (filter?.type === "print_event" && filter.topic && payload.topic !== filter.topic) {
771
- continue;
772
- }
773
- await handler(payload, ctx);
774
- processed++;
775
- } catch (err) {
776
- errors++;
777
- logger2.error("Subgraph handler error", {
778
- subgraph: subgraph.name,
779
- sourceName,
780
- txId: tx.tx_id,
781
- eventId: event.id,
782
- eventType: event.type,
783
- error: getErrorMessage(err)
784
- });
1151
+ if (event !== null && filter?.type === "print_event" && filter.topic && payload.topic !== filter.topic) {
1152
+ continue;
785
1153
  }
1154
+ await handler(payload, ctx);
1155
+ processed++;
1156
+ } catch (err) {
1157
+ ctx.rollbackTo(checkpoint);
1158
+ errors++;
1159
+ logger2.error("Subgraph handler error", {
1160
+ subgraph: subgraph.name,
1161
+ sourceName,
1162
+ txId: tx.tx_id,
1163
+ ...event !== null ? { eventId: event.id, eventType: event.type } : {},
1164
+ error: getErrorMessage(err)
1165
+ });
786
1166
  }
787
1167
  }
788
1168
  return { processed, errors };
@@ -1030,6 +1410,7 @@ function matchSources(sources, transactions, events, traitContracts = new Map) {
1030
1410
  // src/runtime/block-processor.ts
1031
1411
  import { getTargetDb } from "@secondlayer/shared/db";
1032
1412
  import { resolveTraitContractIds } from "@secondlayer/shared/db/queries/contracts";
1413
+ import { advanceOperationCursor } from "@secondlayer/shared/db/queries/subgraph-operations";
1033
1414
  import {
1034
1415
  isByoSubgraph,
1035
1416
  recordSubgraphProcessed,
@@ -1039,9 +1420,6 @@ import {
1039
1420
  import { logger as logger5 } from "@secondlayer/shared/logger";
1040
1421
  import { sql as sql3 } from "kysely";
1041
1422
 
1042
- // src/schema/utils.ts
1043
- import { pgSchemaName } from "@secondlayer/shared/db/queries/subgraphs";
1044
-
1045
1423
  // src/runtime/block-source.ts
1046
1424
  import { getSourceDb } from "@secondlayer/shared/db";
1047
1425
  import { IndexHttpClient } from "@secondlayer/shared/index-http";
@@ -1387,7 +1765,7 @@ function resolveBlockSource(subgraph) {
1387
1765
  }
1388
1766
 
1389
1767
  // src/runtime/outbox-emit.ts
1390
- import { createHash } from "node:crypto";
1768
+ import { createHash as createHash2 } from "node:crypto";
1391
1769
  import { logger as logger4 } from "@secondlayer/shared/logger";
1392
1770
  var loggedKillSwitch = false;
1393
1771
  var OP_VERB = {
@@ -1400,7 +1778,7 @@ function isEmitOutboxEnabled() {
1400
1778
  }
1401
1779
  function dedupKey(subgraphName, tableName, blockHeight, txId, rowIndex, row) {
1402
1780
  const canonical = `${subgraphName}:${tableName}:${blockHeight}:${txId}:${rowIndex}:${stableStringify(row)}`;
1403
- return createHash("sha256").update(canonical).digest("hex").slice(0, 32);
1781
+ return createHash2("sha256").update(canonical).digest("hex").slice(0, 32);
1404
1782
  }
1405
1783
  function stableStringify(obj) {
1406
1784
  const keys = Object.keys(obj).sort();
@@ -1652,6 +2030,47 @@ async function resolveTraitContracts(subgraph, blockHeight, db) {
1652
2030
  }
1653
2031
  return resolved;
1654
2032
  }
2033
+
2034
+ class CursorRaceLostError extends Error {
2035
+ constructor(operationId, height) {
2036
+ super(`op ${operationId} lost cursor race at block ${height}`);
2037
+ this.name = "CursorRaceLostError";
2038
+ }
2039
+ }
2040
+ function opCursorMode(opts) {
2041
+ const ap = opts?.atomicProgress;
2042
+ return ap && "operationId" in ap ? ap : undefined;
2043
+ }
2044
+ function statusMode(opts) {
2045
+ const ap = opts?.atomicProgress;
2046
+ return ap && "status" in ap ? ap : undefined;
2047
+ }
2048
+ var BLOCK_RETRY_DELAYS_MS = [500, 2000, 5000];
2049
+ function journalEnabled(opts) {
2050
+ return !opts?.skipProgressUpdate;
2051
+ }
2052
+ async function processBlockWithRetry(subgraph, subgraphName, blockHeight, opts, retryDelaysMs = BLOCK_RETRY_DELAYS_MS) {
2053
+ let lastError;
2054
+ for (let attempt = 0;attempt <= retryDelaysMs.length; attempt++) {
2055
+ try {
2056
+ return await processBlock(subgraph, subgraphName, blockHeight, opts);
2057
+ } catch (err) {
2058
+ lastError = err;
2059
+ const delay = retryDelaysMs[attempt];
2060
+ if (delay === undefined)
2061
+ break;
2062
+ logger5.warn("Block processing failed, retrying", {
2063
+ subgraph: subgraphName,
2064
+ blockHeight,
2065
+ attempt: attempt + 1,
2066
+ retryInMs: delay,
2067
+ error: err instanceof Error ? err.message : String(err)
2068
+ });
2069
+ await new Promise((r) => setTimeout(r, delay));
2070
+ }
2071
+ }
2072
+ throw lastError;
2073
+ }
1655
2074
  async function processBlock(subgraph, subgraphName, blockHeight, opts) {
1656
2075
  const targetDb = getTargetDb();
1657
2076
  const blockStart = performance.now();
@@ -1719,10 +2138,24 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
1719
2138
  }
1720
2139
  };
1721
2140
  if (route.byo) {
2141
+ if (statusMode(opts)) {
2142
+ const row = await targetDb.selectFrom("subgraphs").select("last_processed_block").where("name", "=", subgraphName).executeTakeFirst();
2143
+ if (row && Number(row.last_processed_block) >= blockHeight) {
2144
+ result.skipped = true;
2145
+ return result;
2146
+ }
2147
+ } else if (opCursorMode(opts)) {
2148
+ const om = opCursorMode(opts);
2149
+ const row = await targetDb.selectFrom("subgraph_operations").select("cursor_block").where("id", "=", om.operationId).executeTakeFirst();
2150
+ if (row?.cursor_block != null && Number(row.cursor_block) >= blockHeight) {
2151
+ result.skipped = true;
2152
+ return result;
2153
+ }
2154
+ }
1722
2155
  let runResult = { processed: 0, errors: 0 };
1723
2156
  let manifest;
1724
2157
  await route.dataDb.transaction().execute(async (tx) => {
1725
- const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx, true);
2158
+ const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx, true, journalEnabled(opts));
1726
2159
  const handlerStart = performance.now();
1727
2160
  runResult = await runHandlers(subgraph, matched, ctx);
1728
2161
  handlerMs = performance.now() - handlerStart;
@@ -1738,26 +2171,71 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
1738
2171
  if (manifest && manifest.count > 0) {
1739
2172
  await emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, block.height);
1740
2173
  }
2174
+ const byoSm = statusMode(opts);
2175
+ const byoOm = opCursorMode(opts);
2176
+ if (byoSm && manifest && manifest.count > 0) {
2177
+ await updateSubgraphStatus(tx, subgraphName, byoSm.status, blockHeight);
2178
+ } else if (byoOm && manifest && manifest.count > 0) {
2179
+ await advanceOperationCursor(tx, byoOm.operationId, blockHeight);
2180
+ }
1741
2181
  await applyProgress(tx, runResult);
1742
2182
  });
1743
2183
  } else {
1744
- await targetDb.transaction().execute(async (tx) => {
1745
- const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx);
1746
- const handlerStart = performance.now();
1747
- const runResult = await runHandlers(subgraph, matched, ctx);
1748
- handlerMs = performance.now() - handlerStart;
1749
- result.processed = runResult.processed;
1750
- result.errors = runResult.errors;
1751
- if (ctx.pendingOps > 0) {
1752
- const flushStart = performance.now();
1753
- const manifest = await ctx.flush();
1754
- if (manifest.count > 0) {
1755
- await emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, block.height);
2184
+ try {
2185
+ await targetDb.transaction().execute(async (tx) => {
2186
+ const opMode = opCursorMode(opts);
2187
+ if (statusMode(opts)) {
2188
+ const row = await tx.selectFrom("subgraphs").select("last_processed_block").where("name", "=", subgraphName).executeTakeFirst();
2189
+ if (row && Number(row.last_processed_block) >= blockHeight) {
2190
+ result.skipped = true;
2191
+ return;
2192
+ }
2193
+ } else if (opMode) {
2194
+ const row = await tx.selectFrom("subgraph_operations").select("cursor_block").where("id", "=", opMode.operationId).executeTakeFirst();
2195
+ if (row?.cursor_block != null && Number(row.cursor_block) >= blockHeight) {
2196
+ result.skipped = true;
2197
+ return;
2198
+ }
1756
2199
  }
1757
- flushMs = performance.now() - flushStart;
2200
+ const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx, false, journalEnabled(opts));
2201
+ const handlerStart = performance.now();
2202
+ const runResult = await runHandlers(subgraph, matched, ctx);
2203
+ handlerMs = performance.now() - handlerStart;
2204
+ result.processed = runResult.processed;
2205
+ result.errors = runResult.errors;
2206
+ let flushedWrites = false;
2207
+ if (ctx.pendingOps > 0) {
2208
+ const flushStart = performance.now();
2209
+ const manifest = await ctx.flush();
2210
+ flushedWrites = manifest.count > 0;
2211
+ if (manifest.count > 0) {
2212
+ await emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, block.height);
2213
+ }
2214
+ flushMs = performance.now() - flushStart;
2215
+ }
2216
+ const sm = statusMode(opts);
2217
+ if (sm && flushedWrites) {
2218
+ await updateSubgraphStatus(tx, subgraphName, sm.status, blockHeight);
2219
+ } else if (opMode && flushedWrites) {
2220
+ const advanced = await advanceOperationCursor(tx, opMode.operationId, blockHeight);
2221
+ if (!advanced) {
2222
+ throw new CursorRaceLostError(opMode.operationId, blockHeight);
2223
+ }
2224
+ }
2225
+ await applyProgress(tx, runResult);
2226
+ });
2227
+ } catch (err) {
2228
+ if (err instanceof CursorRaceLostError) {
2229
+ logger5.warn("cursor race lost — block already covered", {
2230
+ subgraph: subgraphName,
2231
+ blockHeight,
2232
+ error: err.message
2233
+ });
2234
+ result.skipped = true;
2235
+ return result;
1758
2236
  }
1759
- await applyProgress(tx, runResult);
1760
- });
2237
+ throw err;
2238
+ }
1761
2239
  }
1762
2240
  const totalMs = performance.now() - blockStart;
1763
2241
  result.timing = {
@@ -1785,6 +2263,9 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
1785
2263
  error: err instanceof Error ? err.message : String(err)
1786
2264
  });
1787
2265
  }
2266
+ if (journalEnabled(opts)) {
2267
+ await sql3.raw(`DELETE FROM "${schemaName}"."_journal" WHERE "block_height" < ${blockHeight - JOURNAL_RETENTION_BLOCKS}`).execute(route.dataDb).catch(() => {});
2268
+ }
1788
2269
  }
1789
2270
  return result;
1790
2271
  }
@@ -1859,119 +2340,19 @@ import {
1859
2340
  recordGapBatch,
1860
2341
  resolveGaps
1861
2342
  } from "@secondlayer/shared/db/queries/subgraph-gaps";
1862
- import { updateOperationProcessedEvents } from "@secondlayer/shared/db/queries/subgraph-operations";
2343
+ import {
2344
+ advanceOperationCursor as advanceOperationCursor2,
2345
+ updateOperationProcessedEvents
2346
+ } from "@secondlayer/shared/db/queries/subgraph-operations";
1863
2347
  import {
1864
2348
  recordSubgraphProcessed as recordSubgraphProcessed2,
1865
2349
  updateSubgraphStatus as updateSubgraphStatus2
1866
2350
  } from "@secondlayer/shared/db/queries/subgraphs";
1867
2351
  import { logger as logger6 } from "@secondlayer/shared/logger";
1868
-
1869
- // src/schema/generator.ts
1870
- import { createHash as createHash2 } from "node:crypto";
1871
- var TYPE_MAP = {
1872
- text: "TEXT",
1873
- uint: "NUMERIC",
1874
- int: "NUMERIC",
1875
- principal: "TEXT",
1876
- boolean: "BOOLEAN",
1877
- timestamp: "TIMESTAMPTZ",
1878
- jsonb: "JSONB"
1879
- };
1880
- function escapeLiteralDefault(value) {
1881
- if (value === null || value === undefined)
1882
- return "NULL";
1883
- if (typeof value === "number" || typeof value === "bigint")
1884
- return String(value);
1885
- if (typeof value === "boolean")
1886
- return value ? "TRUE" : "FALSE";
1887
- return `'${String(value).replace(/'/g, "''")}'`;
1888
- }
1889
- function tableNeedsTrgm(tableDef) {
1890
- return Object.values(tableDef.columns).some((col) => col.search);
1891
- }
1892
- function emitTableDDL(schemaName, tableName, tableDef) {
1893
- const qualifiedName = `${schemaName}.${tableName}`;
1894
- const statements = [];
1895
- const columnDefs = [
1896
- "_id BIGSERIAL PRIMARY KEY",
1897
- "_block_height BIGINT NOT NULL",
1898
- "_tx_id TEXT NOT NULL",
1899
- "_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()"
1900
- ];
1901
- for (const [colName, col] of Object.entries(tableDef.columns)) {
1902
- const sqlType = TYPE_MAP[col.type];
1903
- const nullable = col.nullable ? "" : " NOT NULL";
1904
- let colDef = `${colName} ${sqlType}${nullable}`;
1905
- if (col.default !== undefined) {
1906
- colDef += ` DEFAULT ${escapeLiteralDefault(col.default)}`;
1907
- }
1908
- columnDefs.push(colDef);
1909
- }
1910
- statements.push(`CREATE TABLE IF NOT EXISTS ${qualifiedName} (
1911
- ${columnDefs.join(`,
1912
- `)}
1913
- )`);
1914
- statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_block_height ON ${qualifiedName} (_block_height)`);
1915
- statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_tx_id ON ${qualifiedName} (_tx_id)`);
1916
- for (const [colName, col] of Object.entries(tableDef.columns)) {
1917
- if (col.indexed) {
1918
- statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName} ON ${qualifiedName} (${colName})`);
1919
- }
1920
- }
1921
- for (const [colName, col] of Object.entries(tableDef.columns)) {
1922
- if (col.search) {
1923
- statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName}_trgm ON ${qualifiedName} USING gin (${colName} gin_trgm_ops)`);
1924
- }
1925
- }
1926
- if (tableDef.indexes) {
1927
- for (let i = 0;i < tableDef.indexes.length; i++) {
1928
- const cols = tableDef.indexes[i];
1929
- const idxName = `idx_${schemaName}_${tableName}_composite_${i}`;
1930
- statements.push(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${qualifiedName} (${cols.join(", ")})`);
1931
- }
1932
- }
1933
- if (tableDef.uniqueKeys) {
1934
- for (let i = 0;i < tableDef.uniqueKeys.length; i++) {
1935
- const cols = tableDef.uniqueKeys[i];
1936
- const constraintName = `uq_${schemaName}_${tableName}_${cols.join("_")}`;
1937
- statements.push(`ALTER TABLE ${qualifiedName} ADD CONSTRAINT ${constraintName} UNIQUE (${cols.join(", ")})`);
1938
- }
1939
- }
1940
- return statements;
1941
- }
1942
- function emitForeignKeyDDL(schemaName, tableName, tableDef) {
1943
- return (tableDef.relations ?? []).map((rel) => {
1944
- const constraintName = `fk_${schemaName}_${tableName}_${rel.name}`;
1945
- return `ALTER TABLE ${schemaName}.${tableName} ADD CONSTRAINT ${constraintName} ` + `FOREIGN KEY (${rel.fields.join(", ")}) ` + `REFERENCES ${schemaName}.${rel.references} (${rel.referencedColumns.join(", ")})`;
1946
- });
1947
- }
1948
- function generateSubgraphSQL(def, schemaNameOverride) {
1949
- const schemaName = schemaNameOverride ?? pgSchemaName(def.name);
1950
- const statements = [];
1951
- const needsTrgm = Object.values(def.schema).some((table) => Object.values(table.columns).some((col) => col.search));
1952
- if (needsTrgm) {
1953
- statements.push("CREATE EXTENSION IF NOT EXISTS pg_trgm");
1954
- }
1955
- statements.push(`CREATE SCHEMA IF NOT EXISTS ${schemaName}`);
1956
- for (const [tableName, tableDef] of Object.entries(def.schema)) {
1957
- statements.push(...emitTableDDL(schemaName, tableName, tableDef));
1958
- }
1959
- for (const [tableName, tableDef] of Object.entries(def.schema)) {
1960
- statements.push(...emitForeignKeyDDL(schemaName, tableName, tableDef));
1961
- }
1962
- const hashInput = JSON.stringify({
1963
- name: def.name,
1964
- schema: def.schema,
1965
- sources: def.sources
1966
- }, (_key, value) => typeof value === "bigint" ? value.toString() : value);
1967
- const hash = createHash2("sha256").update(hashInput).digest("hex");
1968
- return { statements, hash };
1969
- }
1970
-
1971
- // src/runtime/reindex.ts
1972
2352
  var LOG_INTERVAL = 1000;
1973
2353
  var HEALTH_FLUSH_INTERVAL = 1000;
1974
2354
  var PROGRESS_FLUSH_INTERVAL_MS = 5000;
2355
+ var EMPTY_BATCH_HALT_THRESHOLD = 3;
1975
2356
  var STANDARD_REINDEX_BATCH_CONFIG = {
1976
2357
  defaultBatchSize: 500,
1977
2358
  minBatchSize: 100,
@@ -2035,6 +2416,7 @@ async function processBlockRange(def, opts) {
2035
2416
  const totalBlocks = toBlock - fromBlock + 1;
2036
2417
  const stats = new StatsAccumulator(subgraphName, opts.isCatchup);
2037
2418
  let blocksProcessed = 0;
2419
+ let blocksSkippedByCursor = 0;
2038
2420
  let totalEventsProcessed = 0;
2039
2421
  let totalErrors = 0;
2040
2422
  let pendingEventsProcessed = 0;
@@ -2047,6 +2429,7 @@ async function processBlockRange(def, opts) {
2047
2429
  let batchSize = batchConfig.defaultBatchSize;
2048
2430
  let currentHeight = fromBlock;
2049
2431
  let aborted = false;
2432
+ let consecutiveEmptyBatches = 0;
2050
2433
  const sparse = Boolean(source.nextDataHeight && canSparseScan(def));
2051
2434
  const flushHealth = async () => {
2052
2435
  if (pendingEventsProcessed === 0 && pendingErrors === 0)
@@ -2058,6 +2441,13 @@ async function processBlockRange(def, opts) {
2058
2441
  lastHealthFlushBlock = blocksProcessed;
2059
2442
  lastHealthFlushAt = Date.now();
2060
2443
  };
2444
+ const haltRange = async (errorMsg, height) => {
2445
+ pendingErrors++;
2446
+ pendingLastError = errorMsg;
2447
+ await flushHealth().catch(() => {});
2448
+ await updateSubgraphStatus2(targetDb, subgraphName, "error").catch(() => {});
2449
+ throw new Error(`${subgraphName}: halted at block ${height}: ${errorMsg}`);
2450
+ };
2061
2451
  let nextBatchEnd = Math.min(currentHeight + batchSize - 1, toBlock);
2062
2452
  let nextBatchPromise = source.loadBlockRange(currentHeight, nextBatchEnd);
2063
2453
  while (currentHeight <= toBlock) {
@@ -2072,6 +2462,14 @@ async function processBlockRange(def, opts) {
2072
2462
  }
2073
2463
  const batch = await nextBatchPromise;
2074
2464
  const batchEnd = nextBatchEnd;
2465
+ if (batch.size === 0 && batchEnd >= currentHeight) {
2466
+ consecutiveEmptyBatches++;
2467
+ if (consecutiveEmptyBatches >= EMPTY_BATCH_HALT_THRESHOLD) {
2468
+ await haltRange(`block source returned ${consecutiveEmptyBatches} consecutive empty batches (ending ${currentHeight}..${batchEnd}) — source degraded`, currentHeight);
2469
+ }
2470
+ } else {
2471
+ consecutiveEmptyBatches = 0;
2472
+ }
2075
2473
  const nextStart = batchEnd + 1;
2076
2474
  if (nextStart <= toBlock) {
2077
2475
  nextBatchEnd = Math.min(nextStart + batchSize - 1, toBlock);
@@ -2079,28 +2477,40 @@ async function processBlockRange(def, opts) {
2079
2477
  }
2080
2478
  const batchFailedBlocks = [];
2081
2479
  let batchMatched = 0;
2480
+ const opCursor = status === "active" && opts.operationId ? { operationId: opts.operationId } : undefined;
2481
+ const atomicProgress = status === "reindexing" ? { status } : opCursor;
2082
2482
  for (let height = currentHeight;height <= batchEnd; height++) {
2083
- const blockData = batch.get(height);
2483
+ let blockData = batch.get(height);
2084
2484
  if (!blockData) {
2485
+ blockData = (await source.loadBlockRange(height, height)).get(height);
2486
+ }
2487
+ if (!blockData) {
2488
+ if (status === "reindexing") {
2489
+ const errorMsg = `block ${height} missing from source — halting reindex (cursor stays at ${height - 1})`;
2490
+ await haltRange(errorMsg, height);
2491
+ }
2085
2492
  batchFailedBlocks.push({ height, reason: "block_missing" });
2086
2493
  blocksProcessed++;
2087
2494
  continue;
2088
2495
  }
2089
2496
  let result;
2090
2497
  try {
2091
- result = await processBlock(def, subgraphName, height, {
2498
+ result = await processBlockWithRetry(def, subgraphName, height, {
2092
2499
  skipProgressUpdate: true,
2500
+ atomicProgress,
2093
2501
  preloaded: blockData
2094
2502
  });
2095
2503
  } catch (err) {
2096
- const errorMsg = err instanceof Error ? err.message : String(err);
2097
- logger6.error("Block processing error", {
2504
+ const errorMsg = getErrorMessage2(err);
2505
+ logger6.error("Block processing failed persistently", {
2098
2506
  subgraph: subgraphName,
2099
2507
  blockHeight: height,
2100
2508
  error: errorMsg
2101
2509
  });
2510
+ if (status === "reindexing") {
2511
+ await haltRange(`block ${height} failed persistently: ${errorMsg}`, height);
2512
+ }
2102
2513
  batchFailedBlocks.push({ height, reason: "processing_error" });
2103
- await updateSubgraphStatus2(targetDb, subgraphName, status, height).catch(() => {});
2104
2514
  blocksProcessed++;
2105
2515
  totalErrors++;
2106
2516
  pendingErrors++;
@@ -2108,6 +2518,8 @@ async function processBlockRange(def, opts) {
2108
2518
  continue;
2109
2519
  }
2110
2520
  blocksProcessed++;
2521
+ if (result.skipped)
2522
+ blocksSkippedByCursor++;
2111
2523
  batchMatched += result.matched;
2112
2524
  totalEventsProcessed += result.processed;
2113
2525
  totalErrors += result.errors;
@@ -2125,7 +2537,11 @@ async function processBlockRange(def, opts) {
2125
2537
  const now = Date.now();
2126
2538
  const shouldFlushProgress = blocksProcessed % 100 === 0 || now - lastProgressFlushAt >= PROGRESS_FLUSH_INTERVAL_MS;
2127
2539
  if (shouldFlushProgress) {
2128
- await updateSubgraphStatus2(targetDb, subgraphName, status, height);
2540
+ if (opCursor) {
2541
+ await advanceOperationCursor2(targetDb, opCursor.operationId, height);
2542
+ } else {
2543
+ await updateSubgraphStatus2(targetDb, subgraphName, status, height);
2544
+ }
2129
2545
  if (opts.operationId) {
2130
2546
  await updateOperationProcessedEvents(targetDb, opts.operationId, totalEventsProcessed).catch(() => {});
2131
2547
  }
@@ -2137,7 +2553,8 @@ async function processBlockRange(def, opts) {
2137
2553
  processed: blocksProcessed,
2138
2554
  total: totalBlocks,
2139
2555
  currentBlock: height,
2140
- pct: Math.round(blocksProcessed / totalBlocks * 100)
2556
+ pct: Math.round(blocksProcessed / totalBlocks * 100),
2557
+ ...blocksSkippedByCursor > 0 ? { skippedByCursor: blocksSkippedByCursor } : {}
2141
2558
  });
2142
2559
  }
2143
2560
  }
@@ -2163,7 +2580,11 @@ async function processBlockRange(def, opts) {
2163
2580
  if (jumpTo > batchEnd + 1) {
2164
2581
  const skipped = Math.min(jumpTo, toBlock + 1) - (batchEnd + 1);
2165
2582
  blocksProcessed += skipped;
2166
- await updateSubgraphStatus2(targetDb, subgraphName, status, jumpTo - 1);
2583
+ if (opCursor) {
2584
+ await advanceOperationCursor2(targetDb, opCursor.operationId, jumpTo - 1);
2585
+ } else {
2586
+ await updateSubgraphStatus2(targetDb, subgraphName, status, jumpTo - 1);
2587
+ }
2167
2588
  logger6.info("Sparse skip", {
2168
2589
  subgraph: subgraphName,
2169
2590
  from: batchEnd + 1,
@@ -2405,6 +2826,336 @@ async function backfillSubgraph(def, opts) {
2405
2826
  function defineSubgraph(def) {
2406
2827
  return def;
2407
2828
  }
2829
+ // src/print-schema.ts
2830
+ import {
2831
+ deserializeCV as deserializeCV3
2832
+ } from "@secondlayer/stacks/clarity";
2833
+ function camelizeDataKey(str) {
2834
+ return str.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
2835
+ }
2836
+ function cvToTree(cv) {
2837
+ switch (cv.type) {
2838
+ case "uint":
2839
+ case "int":
2840
+ return { kind: cv.type };
2841
+ case "true":
2842
+ case "false":
2843
+ return { kind: "bool" };
2844
+ case "address":
2845
+ case "contract":
2846
+ return { kind: "principal" };
2847
+ case "buffer":
2848
+ return { kind: "buffer", len: cv.value.length / 2 };
2849
+ case "ascii":
2850
+ return { kind: "ascii", len: cv.value.length };
2851
+ case "utf8":
2852
+ return { kind: "utf8", len: new TextEncoder().encode(cv.value).length };
2853
+ case "none":
2854
+ return { kind: "optional", inner: null };
2855
+ case "some":
2856
+ return { kind: "optional", inner: cvToTree(cv.value) };
2857
+ case "ok":
2858
+ return { kind: "response", ok: cvToTree(cv.value), err: null };
2859
+ case "err":
2860
+ return { kind: "response", ok: null, err: cvToTree(cv.value) };
2861
+ case "list": {
2862
+ let inner = null;
2863
+ for (const el of cv.value) {
2864
+ const t = cvToTree(el);
2865
+ inner = inner ? unify(inner, t) : t;
2866
+ }
2867
+ return { kind: "list", inner };
2868
+ }
2869
+ case "tuple": {
2870
+ const entries = new Map;
2871
+ for (const [k, v] of Object.entries(cv.value)) {
2872
+ entries.set(k, { tree: cvToTree(v), present: 1 });
2873
+ }
2874
+ return { kind: "tuple", count: 1, entries };
2875
+ }
2876
+ }
2877
+ }
2878
+ function unifyNullable(a, b) {
2879
+ if (!a)
2880
+ return b;
2881
+ if (!b)
2882
+ return a;
2883
+ return unify(a, b);
2884
+ }
2885
+ var UNION_KIND_ORDER = {
2886
+ uint: 0,
2887
+ int: 1,
2888
+ bool: 2,
2889
+ principal: 3,
2890
+ buffer: 4,
2891
+ ascii: 5,
2892
+ utf8: 6,
2893
+ list: 7,
2894
+ tuple: 8,
2895
+ response: 9
2896
+ };
2897
+ function makeUnion(members) {
2898
+ const sorted = [...members].sort((a, b) => (UNION_KIND_ORDER[a.kind] ?? 99) - (UNION_KIND_ORDER[b.kind] ?? 99));
2899
+ return { kind: "union", members: sorted };
2900
+ }
2901
+ function unify(a, b) {
2902
+ if (a.kind === "optional" || b.kind === "optional") {
2903
+ const ai = a.kind === "optional" ? a.inner : a;
2904
+ const bi = b.kind === "optional" ? b.inner : b;
2905
+ return { kind: "optional", inner: unifyNullable(ai, bi) };
2906
+ }
2907
+ if (a.kind === "union")
2908
+ return unionAdd(a.members, b);
2909
+ if (b.kind === "union")
2910
+ return unionAdd(b.members, a);
2911
+ if (a.kind !== b.kind)
2912
+ return makeUnion([a, b]);
2913
+ switch (a.kind) {
2914
+ case "uint":
2915
+ case "int":
2916
+ case "bool":
2917
+ case "principal":
2918
+ return a;
2919
+ case "buffer":
2920
+ case "ascii":
2921
+ case "utf8":
2922
+ return { kind: a.kind, len: Math.max(a.len, b.len) };
2923
+ case "list":
2924
+ return {
2925
+ kind: "list",
2926
+ inner: unifyNullable(a.inner, b.inner)
2927
+ };
2928
+ case "response": {
2929
+ const rb = b;
2930
+ return {
2931
+ kind: "response",
2932
+ ok: unifyNullable(a.ok, rb.ok),
2933
+ err: unifyNullable(a.err, rb.err)
2934
+ };
2935
+ }
2936
+ case "tuple": {
2937
+ const tb = b;
2938
+ const entries = new Map([...a.entries].map(([k, e]) => [k, { ...e }]));
2939
+ for (const [k, e] of tb.entries) {
2940
+ const existing = entries.get(k);
2941
+ entries.set(k, existing ? {
2942
+ tree: unify(existing.tree, e.tree),
2943
+ present: existing.present + e.present
2944
+ } : { ...e });
2945
+ }
2946
+ return { kind: "tuple", count: a.count + tb.count, entries };
2947
+ }
2948
+ }
2949
+ }
2950
+ function unionAdd(members, t) {
2951
+ if (t.kind === "union") {
2952
+ let acc = { kind: "union", members };
2953
+ for (const m of t.members)
2954
+ acc = unify(acc, m);
2955
+ return acc;
2956
+ }
2957
+ const next = [...members];
2958
+ for (let i = 0;i < next.length; i++) {
2959
+ const member = next[i];
2960
+ if (!member)
2961
+ continue;
2962
+ const merged = unify(member, t);
2963
+ if (merged.kind !== "union") {
2964
+ next[i] = merged;
2965
+ return makeUnion(next);
2966
+ }
2967
+ }
2968
+ next.push(t);
2969
+ return makeUnion(next);
2970
+ }
2971
+ function wrapOptional(t) {
2972
+ return t.kind === "optional" ? t : { kind: "optional", inner: t };
2973
+ }
2974
+ function renderClarity(t) {
2975
+ if (!t)
2976
+ return "?";
2977
+ switch (t.kind) {
2978
+ case "uint":
2979
+ case "int":
2980
+ case "bool":
2981
+ case "principal":
2982
+ return t.kind;
2983
+ case "buffer":
2984
+ return `(buff ${t.len})`;
2985
+ case "ascii":
2986
+ return `(string-ascii ${t.len})`;
2987
+ case "utf8":
2988
+ return `(string-utf8 ${t.len})`;
2989
+ case "optional":
2990
+ return `(optional ${renderClarity(t.inner)})`;
2991
+ case "list":
2992
+ return `(list ${renderClarity(t.inner)})`;
2993
+ case "response":
2994
+ return `(response ${renderClarity(t.ok)} ${renderClarity(t.err)})`;
2995
+ case "tuple": {
2996
+ const parts = [...t.entries].map(([k, e]) => {
2997
+ const tree = e.present < t.count ? wrapOptional(e.tree) : e.tree;
2998
+ return `(${k} ${renderClarity(tree)})`;
2999
+ });
3000
+ return `(tuple ${parts.join(" ")})`;
3001
+ }
3002
+ case "union":
3003
+ return t.members.map(renderClarity).join(" | ");
3004
+ }
3005
+ }
3006
+ function renderTs(t) {
3007
+ if (!t)
3008
+ return "unknown";
3009
+ switch (t.kind) {
3010
+ case "uint":
3011
+ case "int":
3012
+ return "bigint";
3013
+ case "bool":
3014
+ return "boolean";
3015
+ case "principal":
3016
+ case "buffer":
3017
+ case "ascii":
3018
+ case "utf8":
3019
+ return "string";
3020
+ case "optional":
3021
+ return t.inner ? `${renderTs(t.inner)} | null` : "unknown | null";
3022
+ case "list": {
3023
+ const inner = renderTs(t.inner);
3024
+ return inner.includes(" | ") ? `(${inner})[]` : `${inner}[]`;
3025
+ }
3026
+ case "response": {
3027
+ const sides = [...new Set([renderTs(t.ok), renderTs(t.err)])];
3028
+ return sides.join(" | ");
3029
+ }
3030
+ case "tuple": {
3031
+ const parts = [...t.entries].map(([k, e]) => {
3032
+ const opt = e.present < t.count ? "?" : "";
3033
+ return `${camelizeDataKey(k)}${opt}: ${renderTs(e.tree)}`;
3034
+ });
3035
+ return `{ ${parts.join("; ")} }`;
3036
+ }
3037
+ case "union":
3038
+ return [...new Set(t.members.map((m) => renderTs(m)))].join(" | ");
3039
+ }
3040
+ }
3041
+ function toColumnType(t) {
3042
+ if (!t)
3043
+ return "jsonb";
3044
+ switch (t.kind) {
3045
+ case "uint":
3046
+ return "uint";
3047
+ case "int":
3048
+ return "int";
3049
+ case "bool":
3050
+ return "boolean";
3051
+ case "principal":
3052
+ return "principal";
3053
+ case "buffer":
3054
+ case "ascii":
3055
+ case "utf8":
3056
+ return "text";
3057
+ case "list":
3058
+ case "tuple":
3059
+ return "jsonb";
3060
+ case "optional":
3061
+ return toColumnType(t.inner);
3062
+ case "response":
3063
+ return t.ok ? toColumnType(t.ok) : "jsonb";
3064
+ case "union":
3065
+ return "jsonb";
3066
+ }
3067
+ }
3068
+ var MAX_DECODED_PER_TOPIC_NEWEST = 75;
3069
+ var MAX_DECODED_PER_TOPIC_OLDEST = 25;
3070
+ function inferPrintTopics(samples) {
3071
+ const groups = new Map;
3072
+ for (const s of samples) {
3073
+ const group = groups.get(s.topic);
3074
+ if (group)
3075
+ group.push(s);
3076
+ else
3077
+ groups.set(s.topic, [s]);
3078
+ }
3079
+ const out = [];
3080
+ for (const [topic, rows] of groups) {
3081
+ let first = Number.POSITIVE_INFINITY;
3082
+ let last = Number.NEGATIVE_INFINITY;
3083
+ for (const r of rows) {
3084
+ if (r.blockHeight < first)
3085
+ first = r.blockHeight;
3086
+ if (r.blockHeight > last)
3087
+ last = r.blockHeight;
3088
+ }
3089
+ const withHex = [...rows].filter((r) => r.rawHex !== null).sort((a, b) => b.blockHeight - a.blockHeight);
3090
+ const budget = MAX_DECODED_PER_TOPIC_NEWEST + MAX_DECODED_PER_TOPIC_OLDEST;
3091
+ const picked = withHex.length <= budget ? withHex : [
3092
+ ...withHex.slice(0, MAX_DECODED_PER_TOPIC_NEWEST),
3093
+ ...withHex.slice(-MAX_DECODED_PER_TOPIC_OLDEST)
3094
+ ];
3095
+ const tuples = [];
3096
+ let decoded = 0;
3097
+ for (const p of picked) {
3098
+ try {
3099
+ const cv = deserializeCV3(p.rawHex);
3100
+ decoded++;
3101
+ if (cv.type === "tuple")
3102
+ tuples.push(cv);
3103
+ } catch {}
3104
+ }
3105
+ const nonTuple = decoded > 0 && tuples.length === 0;
3106
+ const fields = [];
3107
+ if (!nonTuple) {
3108
+ const stats = new Map;
3109
+ for (const t of tuples) {
3110
+ for (const [key2, value] of Object.entries(t.value)) {
3111
+ if (key2 === "topic")
3112
+ continue;
3113
+ const tree = cvToTree(value);
3114
+ const existing = stats.get(key2);
3115
+ if (existing) {
3116
+ existing.present++;
3117
+ existing.tree = unify(existing.tree, tree);
3118
+ if (value.type === "none")
3119
+ existing.noneCount++;
3120
+ if (value.type === "none" || value.type === "some") {
3121
+ existing.optionalSeen = true;
3122
+ }
3123
+ } else {
3124
+ stats.set(key2, {
3125
+ tree,
3126
+ present: 1,
3127
+ noneCount: value.type === "none" ? 1 : 0,
3128
+ optionalSeen: value.type === "none" || value.type === "some"
3129
+ });
3130
+ }
3131
+ }
3132
+ }
3133
+ for (const [name, st] of [...stats].sort(([a], [b]) => a.localeCompare(b))) {
3134
+ const field = {
3135
+ name,
3136
+ camel_name: camelizeDataKey(name),
3137
+ clarity_type: renderClarity(st.tree),
3138
+ ts_type: renderTs(st.tree),
3139
+ column_type: toColumnType(st.tree),
3140
+ always_present: st.present === tuples.length
3141
+ };
3142
+ if (st.optionalSeen) {
3143
+ field.optional_some_rate = (st.present - st.noneCount) / st.present;
3144
+ }
3145
+ fields.push(field);
3146
+ }
3147
+ }
3148
+ out.push({
3149
+ topic,
3150
+ count: rows.length,
3151
+ first_height: first,
3152
+ last_height: last,
3153
+ non_tuple: nonTuple,
3154
+ fields
3155
+ });
3156
+ }
3157
+ return out.sort((a, b) => b.count - a.count);
3158
+ }
2408
3159
  // src/schema/prisma.ts
2409
3160
  var PRISMA_TYPE = {
2410
3161
  uint: { type: "Decimal", db: "@db.Numeric" },
@@ -3251,6 +4002,7 @@ export {
3251
4002
  renderDeployPlan,
3252
4003
  reindexSubgraph,
3253
4004
  pgSchemaName,
4005
+ inferPrintTopics,
3254
4006
  hasBreakingChanges,
3255
4007
  generateSubgraphSQL,
3256
4008
  generatePrismaSchema,
@@ -3261,10 +4013,11 @@ export {
3261
4013
  deploySchema,
3262
4014
  defineSubgraph,
3263
4015
  canSparseScan,
4016
+ camelizeDataKey,
3264
4017
  backfillSubgraph,
3265
4018
  INDEX_CODEGEN_TABLES,
3266
4019
  ByoBreakingChangeError
3267
4020
  };
3268
4021
 
3269
- //# debugId=415665ECDA2A436C64756E2164756E21
4022
+ //# debugId=4F9187B8B213FB0764756E2164756E21
3270
4023
  //# sourceMappingURL=index.js.map