@secondlayer/subgraphs 3.11.0 → 3.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/index.d.ts +36 -1
- package/dist/src/index.js +542 -180
- package/dist/src/index.js.map +12 -12
- package/dist/src/runtime/block-processor.d.ts +34 -1
- package/dist/src/runtime/block-processor.js +501 -72
- package/dist/src/runtime/block-processor.js.map +9 -8
- package/dist/src/runtime/catchup.d.ts +10 -0
- package/dist/src/runtime/catchup.js +520 -118
- package/dist/src/runtime/catchup.js.map +10 -9
- package/dist/src/runtime/context.d.ts +65 -3
- package/dist/src/runtime/context.js +390 -8
- package/dist/src/runtime/context.js.map +6 -4
- package/dist/src/runtime/processor.js +590 -230
- package/dist/src/runtime/processor.js.map +13 -13
- package/dist/src/runtime/reindex.d.ts +14 -0
- package/dist/src/runtime/reindex.js +538 -180
- package/dist/src/runtime/reindex.js.map +10 -10
- package/dist/src/runtime/reorg.d.ts +10 -0
- package/dist/src/runtime/reorg.js +521 -73
- package/dist/src/runtime/reorg.js.map +10 -9
- package/dist/src/runtime/replay.js.map +2 -2
- package/dist/src/runtime/runner.d.ts +73 -2
- package/dist/src/runtime/runner.js +56 -58
- package/dist/src/runtime/runner.js.map +3 -3
- package/dist/src/runtime/source-matcher.d.ts +2 -0
- package/dist/src/runtime/source-matcher.js.map +2 -2
- package/dist/src/schema/index.d.ts +10 -0
- package/dist/src/schema/index.js +19 -1
- package/dist/src/schema/index.js.map +5 -5
- package/dist/src/service.js +590 -230
- package/dist/src/service.js.map +13 -13
- package/dist/src/types.d.ts +10 -0
- package/dist/src/validate.d.ts +10 -0
- package/dist/src/validate.js +2 -1
- package/dist/src/validate.js.map +3 -3
- package/package.json +2 -2
|
@@ -5,6 +5,134 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
5
5
|
import { logger } from "@secondlayer/shared/logger";
|
|
6
6
|
import { formatUnits } from "@secondlayer/stacks/utils";
|
|
7
7
|
import { sql } from "kysely";
|
|
8
|
+
|
|
9
|
+
// src/schema/generator.ts
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
|
|
12
|
+
// src/schema/utils.ts
|
|
13
|
+
import { pgSchemaName } from "@secondlayer/shared/db/queries/subgraphs";
|
|
14
|
+
|
|
15
|
+
// src/schema/generator.ts
|
|
16
|
+
var TYPE_MAP = {
|
|
17
|
+
text: "TEXT",
|
|
18
|
+
uint: "NUMERIC",
|
|
19
|
+
int: "NUMERIC",
|
|
20
|
+
principal: "TEXT",
|
|
21
|
+
boolean: "BOOLEAN",
|
|
22
|
+
timestamp: "TIMESTAMPTZ",
|
|
23
|
+
jsonb: "JSONB"
|
|
24
|
+
};
|
|
25
|
+
function escapeLiteralDefault(value) {
|
|
26
|
+
if (value === null || value === undefined)
|
|
27
|
+
return "NULL";
|
|
28
|
+
if (typeof value === "number" || typeof value === "bigint")
|
|
29
|
+
return String(value);
|
|
30
|
+
if (typeof value === "boolean")
|
|
31
|
+
return value ? "TRUE" : "FALSE";
|
|
32
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
33
|
+
}
|
|
34
|
+
function tableNeedsTrgm(tableDef) {
|
|
35
|
+
return Object.values(tableDef.columns).some((col) => col.search);
|
|
36
|
+
}
|
|
37
|
+
function emitTableDDL(schemaName, tableName, tableDef) {
|
|
38
|
+
const qualifiedName = `${schemaName}.${tableName}`;
|
|
39
|
+
const statements = [];
|
|
40
|
+
const columnDefs = [
|
|
41
|
+
"_id BIGSERIAL PRIMARY KEY",
|
|
42
|
+
"_block_height BIGINT NOT NULL",
|
|
43
|
+
"_tx_id TEXT NOT NULL",
|
|
44
|
+
"_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()"
|
|
45
|
+
];
|
|
46
|
+
for (const [colName, col] of Object.entries(tableDef.columns)) {
|
|
47
|
+
const sqlType = TYPE_MAP[col.type];
|
|
48
|
+
const nullable = col.nullable ? "" : " NOT NULL";
|
|
49
|
+
let colDef = `${colName} ${sqlType}${nullable}`;
|
|
50
|
+
if (col.default !== undefined) {
|
|
51
|
+
colDef += ` DEFAULT ${escapeLiteralDefault(col.default)}`;
|
|
52
|
+
}
|
|
53
|
+
if (col.type === "uint") {
|
|
54
|
+
colDef += ` CHECK (${colName} >= 0)`;
|
|
55
|
+
}
|
|
56
|
+
columnDefs.push(colDef);
|
|
57
|
+
}
|
|
58
|
+
statements.push(`CREATE TABLE IF NOT EXISTS ${qualifiedName} (
|
|
59
|
+
${columnDefs.join(`,
|
|
60
|
+
`)}
|
|
61
|
+
)`);
|
|
62
|
+
statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_block_height ON ${qualifiedName} (_block_height)`);
|
|
63
|
+
statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_tx_id ON ${qualifiedName} (_tx_id)`);
|
|
64
|
+
for (const [colName, col] of Object.entries(tableDef.columns)) {
|
|
65
|
+
if (col.indexed) {
|
|
66
|
+
statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName} ON ${qualifiedName} (${colName})`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (const [colName, col] of Object.entries(tableDef.columns)) {
|
|
70
|
+
if (col.search) {
|
|
71
|
+
statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName}_trgm ON ${qualifiedName} USING gin (${colName} gin_trgm_ops)`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (tableDef.indexes) {
|
|
75
|
+
for (let i = 0;i < tableDef.indexes.length; i++) {
|
|
76
|
+
const cols = tableDef.indexes[i];
|
|
77
|
+
const idxName = `idx_${schemaName}_${tableName}_composite_${i}`;
|
|
78
|
+
statements.push(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${qualifiedName} (${cols.join(", ")})`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (tableDef.uniqueKeys) {
|
|
82
|
+
for (let i = 0;i < tableDef.uniqueKeys.length; i++) {
|
|
83
|
+
const cols = tableDef.uniqueKeys[i];
|
|
84
|
+
const constraintName = `uq_${schemaName}_${tableName}_${cols.join("_")}`;
|
|
85
|
+
statements.push(`ALTER TABLE ${qualifiedName} ADD CONSTRAINT ${constraintName} UNIQUE (${cols.join(", ")})`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return statements;
|
|
89
|
+
}
|
|
90
|
+
function emitJournalDDL(schemaName) {
|
|
91
|
+
return [
|
|
92
|
+
`CREATE TABLE IF NOT EXISTS ${schemaName}._journal (
|
|
93
|
+
_jid BIGSERIAL PRIMARY KEY,
|
|
94
|
+
block_height BIGINT NOT NULL,
|
|
95
|
+
table_name TEXT NOT NULL,
|
|
96
|
+
row_key JSONB NOT NULL,
|
|
97
|
+
prev_row JSONB,
|
|
98
|
+
_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
99
|
+
)`,
|
|
100
|
+
`CREATE INDEX IF NOT EXISTS idx_${schemaName}_journal_height ON ${schemaName}._journal (block_height)`
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
function emitForeignKeyDDL(schemaName, tableName, tableDef) {
|
|
104
|
+
return (tableDef.relations ?? []).map((rel) => {
|
|
105
|
+
const constraintName = `fk_${schemaName}_${tableName}_${rel.name}`;
|
|
106
|
+
return `ALTER TABLE ${schemaName}.${tableName} ADD CONSTRAINT ${constraintName} ` + `FOREIGN KEY (${rel.fields.join(", ")}) ` + `REFERENCES ${schemaName}.${rel.references} (${rel.referencedColumns.join(", ")})`;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function generateSubgraphSQL(def, schemaNameOverride) {
|
|
110
|
+
const schemaName = schemaNameOverride ?? pgSchemaName(def.name);
|
|
111
|
+
const statements = [];
|
|
112
|
+
const needsTrgm = Object.values(def.schema).some((table) => Object.values(table.columns).some((col) => col.search));
|
|
113
|
+
if (needsTrgm) {
|
|
114
|
+
statements.push("CREATE EXTENSION IF NOT EXISTS pg_trgm");
|
|
115
|
+
}
|
|
116
|
+
statements.push(`CREATE SCHEMA IF NOT EXISTS ${schemaName}`);
|
|
117
|
+
for (const [tableName, tableDef] of Object.entries(def.schema)) {
|
|
118
|
+
statements.push(...emitTableDDL(schemaName, tableName, tableDef));
|
|
119
|
+
}
|
|
120
|
+
statements.push(...emitJournalDDL(schemaName));
|
|
121
|
+
for (const [tableName, tableDef] of Object.entries(def.schema)) {
|
|
122
|
+
statements.push(...emitForeignKeyDDL(schemaName, tableName, tableDef));
|
|
123
|
+
}
|
|
124
|
+
const hashInput = JSON.stringify({
|
|
125
|
+
name: def.name,
|
|
126
|
+
schema: def.schema,
|
|
127
|
+
sources: def.sources
|
|
128
|
+
}, (_key, value) => typeof value === "bigint" ? value.toString() : value);
|
|
129
|
+
const hash = createHash("sha256").update(hashInput).digest("hex");
|
|
130
|
+
return { statements, hash };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/runtime/context.ts
|
|
134
|
+
var JOURNAL_RETENTION_BLOCKS = 300;
|
|
135
|
+
var journalEnsured = new Set;
|
|
8
136
|
function validateColumnName(name) {
|
|
9
137
|
if (!/^[a-z_][a-z0-9_]*$/i.test(name)) {
|
|
10
138
|
throw new Error(`Invalid column name: ${name}`);
|
|
@@ -19,13 +147,15 @@ class SubgraphContext {
|
|
|
19
147
|
subgraphSchema;
|
|
20
148
|
ops = [];
|
|
21
149
|
byo;
|
|
22
|
-
|
|
150
|
+
journal;
|
|
151
|
+
constructor(db, pgSchemaName2, subgraphSchema, block, tx, byo = false, journal = false) {
|
|
23
152
|
this.db = db;
|
|
24
|
-
this.pgSchemaName =
|
|
153
|
+
this.pgSchemaName = pgSchemaName2;
|
|
25
154
|
this.subgraphSchema = subgraphSchema;
|
|
26
155
|
this.block = block;
|
|
27
156
|
this._tx = tx;
|
|
28
157
|
this.byo = byo;
|
|
158
|
+
this.journal = journal;
|
|
29
159
|
}
|
|
30
160
|
get tx() {
|
|
31
161
|
return this._tx;
|
|
@@ -81,6 +211,43 @@ class SubgraphContext {
|
|
|
81
211
|
this.validateTable(table);
|
|
82
212
|
this.ops.push({ kind: "delete", table, data: where });
|
|
83
213
|
}
|
|
214
|
+
increment(table, key, deltas) {
|
|
215
|
+
this.validateTable(table);
|
|
216
|
+
const tableDef = this.subgraphSchema[table];
|
|
217
|
+
const keyColumns = Object.keys(key);
|
|
218
|
+
const hasUniqueConstraint = tableDef?.uniqueKeys?.some((uk) => uk.length === keyColumns.length && uk.every((c) => keyColumns.includes(c)));
|
|
219
|
+
if (!hasUniqueConstraint) {
|
|
220
|
+
throw new Error(`increment("${table}") requires a uniqueKeys constraint on [${keyColumns.join(", ")}]`);
|
|
221
|
+
}
|
|
222
|
+
for (const [col, v] of Object.entries(deltas)) {
|
|
223
|
+
validateColumnName(col);
|
|
224
|
+
if (keyColumns.includes(col)) {
|
|
225
|
+
throw new Error(`increment("${table}"): "${col}" is a key column`);
|
|
226
|
+
}
|
|
227
|
+
if (typeof v !== "bigint" && typeof v !== "number") {
|
|
228
|
+
throw new Error(`increment("${table}"): delta for "${col}" must be bigint or number`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
this.ops.push({
|
|
232
|
+
kind: "increment",
|
|
233
|
+
table,
|
|
234
|
+
data: {
|
|
235
|
+
...key,
|
|
236
|
+
_block_height: this.block.height,
|
|
237
|
+
_tx_id: this._tx.txId,
|
|
238
|
+
_upsert_keys: keyColumns
|
|
239
|
+
},
|
|
240
|
+
set: { ...deltas }
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
opsCheckpoint() {
|
|
244
|
+
return this.ops.length;
|
|
245
|
+
}
|
|
246
|
+
rollbackTo(checkpoint) {
|
|
247
|
+
if (checkpoint < 0 || checkpoint > this.ops.length)
|
|
248
|
+
return;
|
|
249
|
+
this.ops.length = checkpoint;
|
|
250
|
+
}
|
|
84
251
|
patch(table, where, set) {
|
|
85
252
|
this.update(table, where, set);
|
|
86
253
|
}
|
|
@@ -102,7 +269,7 @@ class SubgraphContext {
|
|
|
102
269
|
const query = `SELECT * FROM ${qualifiedTable} WHERE ${clause} LIMIT 1`;
|
|
103
270
|
const { rows } = await sql.raw(query).execute(this.db);
|
|
104
271
|
const row = rows[0] ?? null;
|
|
105
|
-
return row ? this.coerceRow(table, row) : null;
|
|
272
|
+
return this.overlayOne(table, where, row ? this.coerceRow(table, row) : null);
|
|
106
273
|
}
|
|
107
274
|
async findMany(table, where) {
|
|
108
275
|
this.validateTable(table);
|
|
@@ -110,7 +277,85 @@ class SubgraphContext {
|
|
|
110
277
|
const { clause } = buildWhereClause(where);
|
|
111
278
|
const query = `SELECT * FROM ${qualifiedTable} WHERE ${clause}`;
|
|
112
279
|
const { rows } = await sql.raw(query).execute(this.db);
|
|
113
|
-
|
|
280
|
+
const dbRows = rows.map((r) => this.coerceRow(table, r));
|
|
281
|
+
return this.overlayMany(table, where, dbRows);
|
|
282
|
+
}
|
|
283
|
+
overlayOne(table, where, dbRow) {
|
|
284
|
+
let row = dbRow;
|
|
285
|
+
for (const op of this.ops) {
|
|
286
|
+
if (op.table !== table)
|
|
287
|
+
continue;
|
|
288
|
+
row = this.applyOpToRow(op, row, where);
|
|
289
|
+
}
|
|
290
|
+
return row;
|
|
291
|
+
}
|
|
292
|
+
overlayMany(table, where, dbRows) {
|
|
293
|
+
let result = [...dbRows];
|
|
294
|
+
for (const op of this.ops) {
|
|
295
|
+
if (op.table !== table)
|
|
296
|
+
continue;
|
|
297
|
+
if (op.kind === "update") {
|
|
298
|
+
result = result.map((r) => rowMatches(r, op.data) ? { ...r, ...op.set ?? {} } : r);
|
|
299
|
+
} else if (op.kind === "delete") {
|
|
300
|
+
result = result.filter((r) => !rowMatches(r, op.data));
|
|
301
|
+
} else {
|
|
302
|
+
const upsertKeys = op.data._upsert_keys;
|
|
303
|
+
const clean = stripControlKeys(op.data);
|
|
304
|
+
const idx = upsertKeys ? result.findIndex((r) => upsertKeys.every((k) => valEq(r[k], clean[k]))) : -1;
|
|
305
|
+
if (idx >= 0) {
|
|
306
|
+
result[idx] = this.applyOpToRow(op, result[idx], where) ?? result[idx];
|
|
307
|
+
} else {
|
|
308
|
+
const created = this.applyOpToRow(op, null, where);
|
|
309
|
+
if (created)
|
|
310
|
+
result.push(created);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return result;
|
|
315
|
+
}
|
|
316
|
+
applyOpToRow(op, row, where) {
|
|
317
|
+
const upsertKeys = op.data._upsert_keys;
|
|
318
|
+
const clean = stripControlKeys(op.data);
|
|
319
|
+
switch (op.kind) {
|
|
320
|
+
case "insert": {
|
|
321
|
+
if (row) {
|
|
322
|
+
if (upsertKeys?.every((k) => valEq(row[k], clean[k]))) {
|
|
323
|
+
const merged = { ...row };
|
|
324
|
+
for (const [k, v] of Object.entries(clean)) {
|
|
325
|
+
if (!upsertKeys.includes(k) && !k.startsWith("_"))
|
|
326
|
+
merged[k] = v;
|
|
327
|
+
}
|
|
328
|
+
return merged;
|
|
329
|
+
}
|
|
330
|
+
return row;
|
|
331
|
+
}
|
|
332
|
+
return rowMatches(clean, where) ? { ...clean } : null;
|
|
333
|
+
}
|
|
334
|
+
case "increment": {
|
|
335
|
+
const deltas = op.set ?? {};
|
|
336
|
+
if (row) {
|
|
337
|
+
if (upsertKeys.every((k) => valEq(row[k], clean[k]))) {
|
|
338
|
+
const merged = { ...row };
|
|
339
|
+
for (const [col, d] of Object.entries(deltas)) {
|
|
340
|
+
merged[col] = toBigIntOr0(merged[col]) + toBigIntOr0(d);
|
|
341
|
+
}
|
|
342
|
+
return merged;
|
|
343
|
+
}
|
|
344
|
+
return row;
|
|
345
|
+
}
|
|
346
|
+
if (!rowMatches(clean, where))
|
|
347
|
+
return null;
|
|
348
|
+
const created = { ...clean };
|
|
349
|
+
for (const [col, d] of Object.entries(deltas)) {
|
|
350
|
+
created[col] = toBigIntOr0(d);
|
|
351
|
+
}
|
|
352
|
+
return created;
|
|
353
|
+
}
|
|
354
|
+
case "update":
|
|
355
|
+
return row && rowMatches(row, op.data) ? { ...row, ...op.set ?? {} } : row;
|
|
356
|
+
case "delete":
|
|
357
|
+
return row && rowMatches(row, op.data) ? null : row;
|
|
358
|
+
}
|
|
114
359
|
}
|
|
115
360
|
async count(table, where) {
|
|
116
361
|
this.validateTable(table);
|
|
@@ -171,6 +416,7 @@ class SubgraphContext {
|
|
|
171
416
|
async flush() {
|
|
172
417
|
if (this.ops.length === 0)
|
|
173
418
|
return { count: 0, writes: [] };
|
|
419
|
+
await this.ensureJournalTable();
|
|
174
420
|
const opsToFlush = [...this.ops];
|
|
175
421
|
this.ops.length = 0;
|
|
176
422
|
const statements = this.buildStatements(opsToFlush);
|
|
@@ -188,12 +434,12 @@ class SubgraphContext {
|
|
|
188
434
|
const writes = opsToFlush.map((op, rowIndex) => {
|
|
189
435
|
const blockHeight = op.data._block_height ?? this.block.height;
|
|
190
436
|
const txId = op.data._tx_id ?? this._tx.txId;
|
|
191
|
-
const baseRow = op.kind === "update" ? { ...op.data, ...op.set ?? {} } : { ...op.data };
|
|
437
|
+
const baseRow = op.kind === "update" || op.kind === "increment" ? { ...op.data, ...op.set ?? {} } : { ...op.data };
|
|
192
438
|
baseRow._upsert_keys = undefined;
|
|
193
439
|
baseRow._upsert_fallback_keys = undefined;
|
|
194
440
|
baseRow._upsert_fallback_set = undefined;
|
|
195
441
|
return {
|
|
196
|
-
op: op.kind,
|
|
442
|
+
op: op.kind === "increment" ? "update" : op.kind,
|
|
197
443
|
table: op.table,
|
|
198
444
|
row: jsonSafe(baseRow),
|
|
199
445
|
pk: { blockHeight, txId, rowIndex }
|
|
@@ -218,6 +464,35 @@ class SubgraphContext {
|
|
|
218
464
|
const batchKey = `${op.table}:${[...cols].sort().join(",")}:${upsertKeys ? [...upsertKeys].sort().join(",") : ""}`;
|
|
219
465
|
return { data, cols, vals, upsertKeys, batchKey };
|
|
220
466
|
}
|
|
467
|
+
async ensureJournalTable() {
|
|
468
|
+
if (!this.journal || journalEnsured.has(this.pgSchemaName))
|
|
469
|
+
return;
|
|
470
|
+
const { rows } = await sql.raw(`SELECT to_regclass('"${this.pgSchemaName}"."_journal"') AS r`).execute(this.db);
|
|
471
|
+
if (rows[0]?.r) {
|
|
472
|
+
journalEnsured.add(this.pgSchemaName);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
for (const stmt of emitJournalDDL(this.pgSchemaName)) {
|
|
476
|
+
await sql.raw(stmt).execute(this.db);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
columnSqlType(table, col) {
|
|
480
|
+
const def = this.subgraphSchema[table]?.columns?.[col];
|
|
481
|
+
return def ? TYPE_MAP[def.type] : undefined;
|
|
482
|
+
}
|
|
483
|
+
journalCaptureSQL(table, keyCols, keyLiteralRows) {
|
|
484
|
+
const cast = (col, expr) => {
|
|
485
|
+
const t = this.columnSqlType(table, col);
|
|
486
|
+
return t ? `CAST(${expr} AS ${t})` : expr;
|
|
487
|
+
};
|
|
488
|
+
const keyObj = keyCols.map((k) => `'${k}', ${cast(k, `v."${k}"`)}`).join(", ");
|
|
489
|
+
const joinCond = keyCols.map((k) => `t."${k}" = ${cast(k, `v."${k}"`)}`).join(" AND ");
|
|
490
|
+
const valuesList = keyLiteralRows.map((r) => `(${r.join(", ")})`).join(", ");
|
|
491
|
+
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}`;
|
|
492
|
+
}
|
|
493
|
+
journalCaptureByWhereSQL(table, clause) {
|
|
494
|
+
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}`;
|
|
495
|
+
}
|
|
221
496
|
buildStatements(ops) {
|
|
222
497
|
const statements = [];
|
|
223
498
|
if (this.byo) {
|
|
@@ -231,6 +506,38 @@ class SubgraphContext {
|
|
|
231
506
|
}
|
|
232
507
|
let currentBatch = null;
|
|
233
508
|
let currentBatchKey = "";
|
|
509
|
+
let incBatch = null;
|
|
510
|
+
let incBatchKey = "";
|
|
511
|
+
const flushIncrementBatch = () => {
|
|
512
|
+
if (!incBatch)
|
|
513
|
+
return;
|
|
514
|
+
const batch = incBatch;
|
|
515
|
+
const qualifiedTable = `"${this.pgSchemaName}"."${batch.table}"`;
|
|
516
|
+
const cols = [
|
|
517
|
+
...batch.keyCols,
|
|
518
|
+
...batch.deltaCols,
|
|
519
|
+
"_block_height",
|
|
520
|
+
"_tx_id",
|
|
521
|
+
"_created_at"
|
|
522
|
+
];
|
|
523
|
+
const valuesList = Array.from(batch.rows.values()).map((r) => {
|
|
524
|
+
const vals = [
|
|
525
|
+
...batch.keyCols.map((k) => escapeLiteral(r.keys[k])),
|
|
526
|
+
...batch.deltaCols.map((c) => String(r.deltas[c] ?? 0n)),
|
|
527
|
+
escapeLiteral(r.meta.blockHeight),
|
|
528
|
+
escapeLiteral(r.meta.txId),
|
|
529
|
+
"NOW()"
|
|
530
|
+
];
|
|
531
|
+
return `(${vals.join(", ")})`;
|
|
532
|
+
}).join(", ");
|
|
533
|
+
const setClauses = batch.deltaCols.map((c) => `"${c}" = COALESCE("${batch.table}"."${c}", 0) + EXCLUDED."${c}"`);
|
|
534
|
+
if (this.journal) {
|
|
535
|
+
statements.push(this.journalCaptureSQL(batch.table, batch.keyCols, Array.from(batch.rows.values()).map((r) => batch.keyCols.map((k) => escapeLiteral(r.keys[k])))));
|
|
536
|
+
}
|
|
537
|
+
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(", ")}`);
|
|
538
|
+
incBatch = null;
|
|
539
|
+
incBatchKey = "";
|
|
540
|
+
};
|
|
234
541
|
const flushInsertBatch = () => {
|
|
235
542
|
if (!currentBatch)
|
|
236
543
|
return;
|
|
@@ -252,6 +559,11 @@ class SubgraphContext {
|
|
|
252
559
|
}
|
|
253
560
|
const valuesList = rows.map((r) => `(${r.join(", ")})`).join(", ");
|
|
254
561
|
let stmt = `INSERT INTO ${qualifiedTable} (${colList}) VALUES ${valuesList}`;
|
|
562
|
+
if (this.journal && batch.upsertKeys && batch.upsertKeys.length > 0) {
|
|
563
|
+
const uKeys = batch.upsertKeys;
|
|
564
|
+
const keyIndices = uKeys.map((k) => batch.cols.indexOf(k));
|
|
565
|
+
statements.push(this.journalCaptureSQL(batch.table, uKeys, rows.map((r) => keyIndices.map((ki) => r[ki]))));
|
|
566
|
+
}
|
|
255
567
|
if (batch.upsertKeys && batch.upsertKeys.length > 0) {
|
|
256
568
|
const batchKeys = batch.upsertKeys;
|
|
257
569
|
const updateCols = batch.cols.filter((c) => !batchKeys.includes(c) && !c.startsWith("_"));
|
|
@@ -269,6 +581,7 @@ class SubgraphContext {
|
|
|
269
581
|
for (const op of ops) {
|
|
270
582
|
const qualifiedTable = `"${this.pgSchemaName}"."${op.table}"`;
|
|
271
583
|
if (op.kind === "insert") {
|
|
584
|
+
flushIncrementBatch();
|
|
272
585
|
const { cols, vals, upsertKeys, batchKey } = this.prepareInsert(op);
|
|
273
586
|
if (batchKey === currentBatchKey && currentBatch) {
|
|
274
587
|
currentBatch.rows.push(vals);
|
|
@@ -277,22 +590,60 @@ class SubgraphContext {
|
|
|
277
590
|
currentBatch = { table: op.table, cols, rows: [vals], upsertKeys };
|
|
278
591
|
currentBatchKey = batchKey;
|
|
279
592
|
}
|
|
593
|
+
} else if (op.kind === "increment") {
|
|
594
|
+
flushInsertBatch();
|
|
595
|
+
const keyCols = [...op.data._upsert_keys].sort();
|
|
596
|
+
const deltaCols = Object.keys(op.set ?? {}).sort();
|
|
597
|
+
const batchKey = `inc:${op.table}:${keyCols.join(",")}:${deltaCols.join(",")}`;
|
|
598
|
+
if (batchKey !== incBatchKey || !incBatch) {
|
|
599
|
+
flushIncrementBatch();
|
|
600
|
+
incBatch = { table: op.table, keyCols, deltaCols, rows: new Map };
|
|
601
|
+
incBatchKey = batchKey;
|
|
602
|
+
}
|
|
603
|
+
const clean = stripControlKeys(op.data);
|
|
604
|
+
const keySig = keyCols.map((k) => escapeLiteral(clean[k])).join("\x00");
|
|
605
|
+
const existing = incBatch.rows.get(keySig);
|
|
606
|
+
if (existing) {
|
|
607
|
+
for (const c of deltaCols) {
|
|
608
|
+
existing.deltas[c] = (existing.deltas[c] ?? 0n) + toBigIntOr0(op.set?.[c]);
|
|
609
|
+
}
|
|
610
|
+
} else {
|
|
611
|
+
const deltas = {};
|
|
612
|
+
for (const c of deltaCols)
|
|
613
|
+
deltas[c] = toBigIntOr0(op.set?.[c]);
|
|
614
|
+
incBatch.rows.set(keySig, {
|
|
615
|
+
keys: clean,
|
|
616
|
+
deltas,
|
|
617
|
+
meta: {
|
|
618
|
+
blockHeight: op.data._block_height ?? this.block.height,
|
|
619
|
+
txId: op.data._tx_id ?? this._tx.txId
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
}
|
|
280
623
|
} else {
|
|
281
624
|
flushInsertBatch();
|
|
625
|
+
flushIncrementBatch();
|
|
282
626
|
if (op.kind === "update") {
|
|
283
627
|
const setEntries = Object.entries(op.set ?? {});
|
|
284
628
|
for (const [k] of setEntries)
|
|
285
629
|
validateColumnName(k);
|
|
286
630
|
const setClauses = setEntries.map(([k, v]) => `"${k}" = ${escapeLiteral(v)}`);
|
|
287
631
|
const { clause } = buildWhereClause(op.data);
|
|
632
|
+
if (this.journal) {
|
|
633
|
+
statements.push(this.journalCaptureByWhereSQL(op.table, clause));
|
|
634
|
+
}
|
|
288
635
|
statements.push(`UPDATE ${qualifiedTable} SET ${setClauses.join(", ")} WHERE ${clause}`);
|
|
289
636
|
} else if (op.kind === "delete") {
|
|
290
637
|
const { clause } = buildWhereClause(op.data);
|
|
638
|
+
if (this.journal) {
|
|
639
|
+
statements.push(this.journalCaptureByWhereSQL(op.table, clause));
|
|
640
|
+
}
|
|
291
641
|
statements.push(`DELETE FROM ${qualifiedTable} WHERE ${clause}`);
|
|
292
642
|
}
|
|
293
643
|
}
|
|
294
644
|
}
|
|
295
645
|
flushInsertBatch();
|
|
646
|
+
flushIncrementBatch();
|
|
296
647
|
return statements;
|
|
297
648
|
}
|
|
298
649
|
validateTable(table) {
|
|
@@ -301,6 +652,36 @@ class SubgraphContext {
|
|
|
301
652
|
}
|
|
302
653
|
}
|
|
303
654
|
}
|
|
655
|
+
function stripControlKeys(data) {
|
|
656
|
+
const {
|
|
657
|
+
_upsert_keys: _a,
|
|
658
|
+
_upsert_fallback_keys: _b,
|
|
659
|
+
_upsert_fallback_set: _c,
|
|
660
|
+
...clean
|
|
661
|
+
} = data;
|
|
662
|
+
return clean;
|
|
663
|
+
}
|
|
664
|
+
function valEq(a, b) {
|
|
665
|
+
if (a === b)
|
|
666
|
+
return true;
|
|
667
|
+
if (a == null || b == null)
|
|
668
|
+
return false;
|
|
669
|
+
return String(a) === String(b);
|
|
670
|
+
}
|
|
671
|
+
function rowMatches(row, where) {
|
|
672
|
+
return Object.entries(where).every(([k, v]) => valEq(row[k], v));
|
|
673
|
+
}
|
|
674
|
+
function toBigIntOr0(v) {
|
|
675
|
+
if (typeof v === "bigint")
|
|
676
|
+
return v;
|
|
677
|
+
if (v == null)
|
|
678
|
+
return 0n;
|
|
679
|
+
try {
|
|
680
|
+
return BigInt(String(v));
|
|
681
|
+
} catch {
|
|
682
|
+
return 0n;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
304
685
|
function jsonSafe(row) {
|
|
305
686
|
const out = {};
|
|
306
687
|
for (const [k, v] of Object.entries(row)) {
|
|
@@ -619,7 +1000,25 @@ async function runHandlers(subgraph, matched, ctx, opts) {
|
|
|
619
1000
|
filterLookup.set(name, filter);
|
|
620
1001
|
}
|
|
621
1002
|
}
|
|
1003
|
+
const units = [];
|
|
622
1004
|
for (const { tx, events, sourceName } of matched) {
|
|
1005
|
+
if (events.length === 0) {
|
|
1006
|
+
units.push({ tx, sourceName, event: null });
|
|
1007
|
+
} else {
|
|
1008
|
+
for (const event of events)
|
|
1009
|
+
units.push({ tx, sourceName, event });
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
units.sort((a, b) => (a.tx.tx_index ?? 0) - (b.tx.tx_index ?? 0) || (a.event?.event_index ?? -1) - (b.event?.event_index ?? -1));
|
|
1013
|
+
for (const { tx, event, sourceName } of units) {
|
|
1014
|
+
if (errors >= threshold) {
|
|
1015
|
+
logger2.error("Subgraph error threshold reached, skipping remaining events", {
|
|
1016
|
+
subgraph: subgraph.name,
|
|
1017
|
+
errors,
|
|
1018
|
+
threshold
|
|
1019
|
+
});
|
|
1020
|
+
return { processed, errors };
|
|
1021
|
+
}
|
|
623
1022
|
const handler = subgraph.handlers[sourceName] ?? subgraph.handlers["*"] ?? null;
|
|
624
1023
|
if (!handler) {
|
|
625
1024
|
logger2.warn("No handler found for source", {
|
|
@@ -638,9 +1037,29 @@ async function runHandlers(subgraph, matched, ctx, opts) {
|
|
|
638
1037
|
functionName: tx.function_name ?? null
|
|
639
1038
|
});
|
|
640
1039
|
const filter = filterLookup.get(sourceName);
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
1040
|
+
const checkpoint = ctx.opsCheckpoint();
|
|
1041
|
+
try {
|
|
1042
|
+
let payload;
|
|
1043
|
+
if (event === null) {
|
|
1044
|
+
payload = filter ? buildEventPayload(filter, tx, null) : {
|
|
1045
|
+
tx: {
|
|
1046
|
+
txId: tx.tx_id,
|
|
1047
|
+
sender: tx.sender,
|
|
1048
|
+
type: tx.type,
|
|
1049
|
+
status: tx.status,
|
|
1050
|
+
contractId: tx.contract_id,
|
|
1051
|
+
functionName: tx.function_name
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1054
|
+
} else if (filter) {
|
|
1055
|
+
payload = buildEventPayload(filter, tx, event);
|
|
1056
|
+
} else {
|
|
1057
|
+
const decoded = decodeEventData(event.data);
|
|
1058
|
+
payload = {
|
|
1059
|
+
...decoded,
|
|
1060
|
+
_eventId: event.id,
|
|
1061
|
+
_eventType: event.type,
|
|
1062
|
+
_eventIndex: event.event_index,
|
|
644
1063
|
tx: {
|
|
645
1064
|
txId: tx.tx_id,
|
|
646
1065
|
sender: tx.sender,
|
|
@@ -650,62 +1069,22 @@ async function runHandlers(subgraph, matched, ctx, opts) {
|
|
|
650
1069
|
functionName: tx.function_name
|
|
651
1070
|
}
|
|
652
1071
|
};
|
|
653
|
-
await handler(payload, ctx);
|
|
654
|
-
processed++;
|
|
655
|
-
} catch (err) {
|
|
656
|
-
errors++;
|
|
657
|
-
logger2.error("Subgraph handler error", {
|
|
658
|
-
subgraph: subgraph.name,
|
|
659
|
-
sourceName,
|
|
660
|
-
txId: tx.tx_id,
|
|
661
|
-
error: getErrorMessage(err)
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
continue;
|
|
665
|
-
}
|
|
666
|
-
for (const event of events) {
|
|
667
|
-
if (errors >= threshold) {
|
|
668
|
-
logger2.error("Subgraph error threshold reached, skipping remaining events", {
|
|
669
|
-
subgraph: subgraph.name,
|
|
670
|
-
errors,
|
|
671
|
-
threshold
|
|
672
|
-
});
|
|
673
|
-
return { processed, errors };
|
|
674
1072
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
const decoded = decodeEventData(event.data);
|
|
678
|
-
return {
|
|
679
|
-
...decoded,
|
|
680
|
-
_eventId: event.id,
|
|
681
|
-
_eventType: event.type,
|
|
682
|
-
_eventIndex: event.event_index,
|
|
683
|
-
tx: {
|
|
684
|
-
txId: tx.tx_id,
|
|
685
|
-
sender: tx.sender,
|
|
686
|
-
type: tx.type,
|
|
687
|
-
status: tx.status,
|
|
688
|
-
contractId: tx.contract_id,
|
|
689
|
-
functionName: tx.function_name
|
|
690
|
-
}
|
|
691
|
-
};
|
|
692
|
-
})();
|
|
693
|
-
if (filter?.type === "print_event" && filter.topic && payload.topic !== filter.topic) {
|
|
694
|
-
continue;
|
|
695
|
-
}
|
|
696
|
-
await handler(payload, ctx);
|
|
697
|
-
processed++;
|
|
698
|
-
} catch (err) {
|
|
699
|
-
errors++;
|
|
700
|
-
logger2.error("Subgraph handler error", {
|
|
701
|
-
subgraph: subgraph.name,
|
|
702
|
-
sourceName,
|
|
703
|
-
txId: tx.tx_id,
|
|
704
|
-
eventId: event.id,
|
|
705
|
-
eventType: event.type,
|
|
706
|
-
error: getErrorMessage(err)
|
|
707
|
-
});
|
|
1073
|
+
if (event !== null && filter?.type === "print_event" && filter.topic && payload.topic !== filter.topic) {
|
|
1074
|
+
continue;
|
|
708
1075
|
}
|
|
1076
|
+
await handler(payload, ctx);
|
|
1077
|
+
processed++;
|
|
1078
|
+
} catch (err) {
|
|
1079
|
+
ctx.rollbackTo(checkpoint);
|
|
1080
|
+
errors++;
|
|
1081
|
+
logger2.error("Subgraph handler error", {
|
|
1082
|
+
subgraph: subgraph.name,
|
|
1083
|
+
sourceName,
|
|
1084
|
+
txId: tx.tx_id,
|
|
1085
|
+
...event !== null ? { eventId: event.id, eventType: event.type } : {},
|
|
1086
|
+
error: getErrorMessage(err)
|
|
1087
|
+
});
|
|
709
1088
|
}
|
|
710
1089
|
}
|
|
711
1090
|
return { processed, errors };
|
|
@@ -962,9 +1341,6 @@ import {
|
|
|
962
1341
|
import { logger as logger5 } from "@secondlayer/shared/logger";
|
|
963
1342
|
import { sql as sql3 } from "kysely";
|
|
964
1343
|
|
|
965
|
-
// src/schema/utils.ts
|
|
966
|
-
import { pgSchemaName } from "@secondlayer/shared/db/queries/subgraphs";
|
|
967
|
-
|
|
968
1344
|
// src/runtime/block-source.ts
|
|
969
1345
|
import { getSourceDb } from "@secondlayer/shared/db";
|
|
970
1346
|
import { IndexHttpClient } from "@secondlayer/shared/index-http";
|
|
@@ -1310,7 +1686,7 @@ function resolveBlockSource(subgraph) {
|
|
|
1310
1686
|
}
|
|
1311
1687
|
|
|
1312
1688
|
// src/runtime/outbox-emit.ts
|
|
1313
|
-
import { createHash } from "node:crypto";
|
|
1689
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1314
1690
|
import { logger as logger4 } from "@secondlayer/shared/logger";
|
|
1315
1691
|
var loggedKillSwitch = false;
|
|
1316
1692
|
var OP_VERB = {
|
|
@@ -1323,7 +1699,7 @@ function isEmitOutboxEnabled() {
|
|
|
1323
1699
|
}
|
|
1324
1700
|
function dedupKey(subgraphName, tableName, blockHeight, txId, rowIndex, row) {
|
|
1325
1701
|
const canonical = `${subgraphName}:${tableName}:${blockHeight}:${txId}:${rowIndex}:${stableStringify(row)}`;
|
|
1326
|
-
return
|
|
1702
|
+
return createHash2("sha256").update(canonical).digest("hex").slice(0, 32);
|
|
1327
1703
|
}
|
|
1328
1704
|
function stableStringify(obj) {
|
|
1329
1705
|
const keys = Object.keys(obj).sort();
|
|
@@ -1575,6 +1951,32 @@ async function resolveTraitContracts(subgraph, blockHeight, db) {
|
|
|
1575
1951
|
}
|
|
1576
1952
|
return resolved;
|
|
1577
1953
|
}
|
|
1954
|
+
var BLOCK_RETRY_DELAYS_MS = [500, 2000, 5000];
|
|
1955
|
+
function journalEnabled(opts) {
|
|
1956
|
+
return !opts?.skipProgressUpdate;
|
|
1957
|
+
}
|
|
1958
|
+
async function processBlockWithRetry(subgraph, subgraphName, blockHeight, opts, retryDelaysMs = BLOCK_RETRY_DELAYS_MS) {
|
|
1959
|
+
let lastError;
|
|
1960
|
+
for (let attempt = 0;attempt <= retryDelaysMs.length; attempt++) {
|
|
1961
|
+
try {
|
|
1962
|
+
return await processBlock(subgraph, subgraphName, blockHeight, opts);
|
|
1963
|
+
} catch (err) {
|
|
1964
|
+
lastError = err;
|
|
1965
|
+
const delay = retryDelaysMs[attempt];
|
|
1966
|
+
if (delay === undefined)
|
|
1967
|
+
break;
|
|
1968
|
+
logger5.warn("Block processing failed, retrying", {
|
|
1969
|
+
subgraph: subgraphName,
|
|
1970
|
+
blockHeight,
|
|
1971
|
+
attempt: attempt + 1,
|
|
1972
|
+
retryInMs: delay,
|
|
1973
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1974
|
+
});
|
|
1975
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
throw lastError;
|
|
1979
|
+
}
|
|
1578
1980
|
async function processBlock(subgraph, subgraphName, blockHeight, opts) {
|
|
1579
1981
|
const targetDb = getTargetDb();
|
|
1580
1982
|
const blockStart = performance.now();
|
|
@@ -1642,10 +2044,17 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
|
|
|
1642
2044
|
}
|
|
1643
2045
|
};
|
|
1644
2046
|
if (route.byo) {
|
|
2047
|
+
if (opts?.atomicProgress) {
|
|
2048
|
+
const row = await targetDb.selectFrom("subgraphs").select("last_processed_block").where("name", "=", subgraphName).executeTakeFirst();
|
|
2049
|
+
if (row && Number(row.last_processed_block) >= blockHeight) {
|
|
2050
|
+
result.skipped = true;
|
|
2051
|
+
return result;
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
1645
2054
|
let runResult = { processed: 0, errors: 0 };
|
|
1646
2055
|
let manifest;
|
|
1647
2056
|
await route.dataDb.transaction().execute(async (tx) => {
|
|
1648
|
-
const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx, true);
|
|
2057
|
+
const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx, true, journalEnabled(opts));
|
|
1649
2058
|
const handlerStart = performance.now();
|
|
1650
2059
|
runResult = await runHandlers(subgraph, matched, ctx);
|
|
1651
2060
|
handlerMs = performance.now() - handlerStart;
|
|
@@ -1661,24 +2070,39 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
|
|
|
1661
2070
|
if (manifest && manifest.count > 0) {
|
|
1662
2071
|
await emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, block.height);
|
|
1663
2072
|
}
|
|
2073
|
+
if (opts?.atomicProgress && manifest && manifest.count > 0) {
|
|
2074
|
+
await updateSubgraphStatus(tx, subgraphName, opts.atomicProgress.status, blockHeight);
|
|
2075
|
+
}
|
|
1664
2076
|
await applyProgress(tx, runResult);
|
|
1665
2077
|
});
|
|
1666
2078
|
} else {
|
|
1667
2079
|
await targetDb.transaction().execute(async (tx) => {
|
|
1668
|
-
|
|
2080
|
+
if (opts?.atomicProgress) {
|
|
2081
|
+
const row = await tx.selectFrom("subgraphs").select("last_processed_block").where("name", "=", subgraphName).executeTakeFirst();
|
|
2082
|
+
if (row && Number(row.last_processed_block) >= blockHeight) {
|
|
2083
|
+
result.skipped = true;
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx, false, journalEnabled(opts));
|
|
1669
2088
|
const handlerStart = performance.now();
|
|
1670
2089
|
const runResult = await runHandlers(subgraph, matched, ctx);
|
|
1671
2090
|
handlerMs = performance.now() - handlerStart;
|
|
1672
2091
|
result.processed = runResult.processed;
|
|
1673
2092
|
result.errors = runResult.errors;
|
|
2093
|
+
let flushedWrites = false;
|
|
1674
2094
|
if (ctx.pendingOps > 0) {
|
|
1675
2095
|
const flushStart = performance.now();
|
|
1676
2096
|
const manifest = await ctx.flush();
|
|
2097
|
+
flushedWrites = manifest.count > 0;
|
|
1677
2098
|
if (manifest.count > 0) {
|
|
1678
2099
|
await emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, block.height);
|
|
1679
2100
|
}
|
|
1680
2101
|
flushMs = performance.now() - flushStart;
|
|
1681
2102
|
}
|
|
2103
|
+
if (opts?.atomicProgress && flushedWrites) {
|
|
2104
|
+
await updateSubgraphStatus(tx, subgraphName, opts.atomicProgress.status, blockHeight);
|
|
2105
|
+
}
|
|
1682
2106
|
await applyProgress(tx, runResult);
|
|
1683
2107
|
});
|
|
1684
2108
|
}
|
|
@@ -1708,6 +2132,9 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
|
|
|
1708
2132
|
error: err instanceof Error ? err.message : String(err)
|
|
1709
2133
|
});
|
|
1710
2134
|
}
|
|
2135
|
+
if (journalEnabled(opts)) {
|
|
2136
|
+
await sql3.raw(`DELETE FROM "${schemaName}"."_journal" WHERE "block_height" < ${blockHeight - JOURNAL_RETENTION_BLOCKS}`).execute(route.dataDb).catch(() => {});
|
|
2137
|
+
}
|
|
1711
2138
|
}
|
|
1712
2139
|
return result;
|
|
1713
2140
|
}
|
|
@@ -1782,118 +2209,16 @@ import {
|
|
|
1782
2209
|
recordGapBatch,
|
|
1783
2210
|
resolveGaps
|
|
1784
2211
|
} from "@secondlayer/shared/db/queries/subgraph-gaps";
|
|
2212
|
+
import { updateOperationProcessedEvents } from "@secondlayer/shared/db/queries/subgraph-operations";
|
|
1785
2213
|
import {
|
|
1786
2214
|
recordSubgraphProcessed as recordSubgraphProcessed2,
|
|
1787
2215
|
updateSubgraphStatus as updateSubgraphStatus2
|
|
1788
2216
|
} from "@secondlayer/shared/db/queries/subgraphs";
|
|
1789
2217
|
import { logger as logger6 } from "@secondlayer/shared/logger";
|
|
1790
|
-
|
|
1791
|
-
// src/schema/generator.ts
|
|
1792
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
1793
|
-
var TYPE_MAP = {
|
|
1794
|
-
text: "TEXT",
|
|
1795
|
-
uint: "NUMERIC",
|
|
1796
|
-
int: "NUMERIC",
|
|
1797
|
-
principal: "TEXT",
|
|
1798
|
-
boolean: "BOOLEAN",
|
|
1799
|
-
timestamp: "TIMESTAMPTZ",
|
|
1800
|
-
jsonb: "JSONB"
|
|
1801
|
-
};
|
|
1802
|
-
function escapeLiteralDefault(value) {
|
|
1803
|
-
if (value === null || value === undefined)
|
|
1804
|
-
return "NULL";
|
|
1805
|
-
if (typeof value === "number" || typeof value === "bigint")
|
|
1806
|
-
return String(value);
|
|
1807
|
-
if (typeof value === "boolean")
|
|
1808
|
-
return value ? "TRUE" : "FALSE";
|
|
1809
|
-
return `'${String(value).replace(/'/g, "''")}'`;
|
|
1810
|
-
}
|
|
1811
|
-
function tableNeedsTrgm(tableDef) {
|
|
1812
|
-
return Object.values(tableDef.columns).some((col) => col.search);
|
|
1813
|
-
}
|
|
1814
|
-
function emitTableDDL(schemaName, tableName, tableDef) {
|
|
1815
|
-
const qualifiedName = `${schemaName}.${tableName}`;
|
|
1816
|
-
const statements = [];
|
|
1817
|
-
const columnDefs = [
|
|
1818
|
-
"_id BIGSERIAL PRIMARY KEY",
|
|
1819
|
-
"_block_height BIGINT NOT NULL",
|
|
1820
|
-
"_tx_id TEXT NOT NULL",
|
|
1821
|
-
"_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()"
|
|
1822
|
-
];
|
|
1823
|
-
for (const [colName, col] of Object.entries(tableDef.columns)) {
|
|
1824
|
-
const sqlType = TYPE_MAP[col.type];
|
|
1825
|
-
const nullable = col.nullable ? "" : " NOT NULL";
|
|
1826
|
-
let colDef = `${colName} ${sqlType}${nullable}`;
|
|
1827
|
-
if (col.default !== undefined) {
|
|
1828
|
-
colDef += ` DEFAULT ${escapeLiteralDefault(col.default)}`;
|
|
1829
|
-
}
|
|
1830
|
-
columnDefs.push(colDef);
|
|
1831
|
-
}
|
|
1832
|
-
statements.push(`CREATE TABLE IF NOT EXISTS ${qualifiedName} (
|
|
1833
|
-
${columnDefs.join(`,
|
|
1834
|
-
`)}
|
|
1835
|
-
)`);
|
|
1836
|
-
statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_block_height ON ${qualifiedName} (_block_height)`);
|
|
1837
|
-
statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_tx_id ON ${qualifiedName} (_tx_id)`);
|
|
1838
|
-
for (const [colName, col] of Object.entries(tableDef.columns)) {
|
|
1839
|
-
if (col.indexed) {
|
|
1840
|
-
statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName} ON ${qualifiedName} (${colName})`);
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
for (const [colName, col] of Object.entries(tableDef.columns)) {
|
|
1844
|
-
if (col.search) {
|
|
1845
|
-
statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName}_trgm ON ${qualifiedName} USING gin (${colName} gin_trgm_ops)`);
|
|
1846
|
-
}
|
|
1847
|
-
}
|
|
1848
|
-
if (tableDef.indexes) {
|
|
1849
|
-
for (let i = 0;i < tableDef.indexes.length; i++) {
|
|
1850
|
-
const cols = tableDef.indexes[i];
|
|
1851
|
-
const idxName = `idx_${schemaName}_${tableName}_composite_${i}`;
|
|
1852
|
-
statements.push(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${qualifiedName} (${cols.join(", ")})`);
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
|
-
if (tableDef.uniqueKeys) {
|
|
1856
|
-
for (let i = 0;i < tableDef.uniqueKeys.length; i++) {
|
|
1857
|
-
const cols = tableDef.uniqueKeys[i];
|
|
1858
|
-
const constraintName = `uq_${schemaName}_${tableName}_${cols.join("_")}`;
|
|
1859
|
-
statements.push(`ALTER TABLE ${qualifiedName} ADD CONSTRAINT ${constraintName} UNIQUE (${cols.join(", ")})`);
|
|
1860
|
-
}
|
|
1861
|
-
}
|
|
1862
|
-
return statements;
|
|
1863
|
-
}
|
|
1864
|
-
function emitForeignKeyDDL(schemaName, tableName, tableDef) {
|
|
1865
|
-
return (tableDef.relations ?? []).map((rel) => {
|
|
1866
|
-
const constraintName = `fk_${schemaName}_${tableName}_${rel.name}`;
|
|
1867
|
-
return `ALTER TABLE ${schemaName}.${tableName} ADD CONSTRAINT ${constraintName} ` + `FOREIGN KEY (${rel.fields.join(", ")}) ` + `REFERENCES ${schemaName}.${rel.references} (${rel.referencedColumns.join(", ")})`;
|
|
1868
|
-
});
|
|
1869
|
-
}
|
|
1870
|
-
function generateSubgraphSQL(def, schemaNameOverride) {
|
|
1871
|
-
const schemaName = schemaNameOverride ?? pgSchemaName(def.name);
|
|
1872
|
-
const statements = [];
|
|
1873
|
-
const needsTrgm = Object.values(def.schema).some((table) => Object.values(table.columns).some((col) => col.search));
|
|
1874
|
-
if (needsTrgm) {
|
|
1875
|
-
statements.push("CREATE EXTENSION IF NOT EXISTS pg_trgm");
|
|
1876
|
-
}
|
|
1877
|
-
statements.push(`CREATE SCHEMA IF NOT EXISTS ${schemaName}`);
|
|
1878
|
-
for (const [tableName, tableDef] of Object.entries(def.schema)) {
|
|
1879
|
-
statements.push(...emitTableDDL(schemaName, tableName, tableDef));
|
|
1880
|
-
}
|
|
1881
|
-
for (const [tableName, tableDef] of Object.entries(def.schema)) {
|
|
1882
|
-
statements.push(...emitForeignKeyDDL(schemaName, tableName, tableDef));
|
|
1883
|
-
}
|
|
1884
|
-
const hashInput = JSON.stringify({
|
|
1885
|
-
name: def.name,
|
|
1886
|
-
schema: def.schema,
|
|
1887
|
-
sources: def.sources
|
|
1888
|
-
}, (_key, value) => typeof value === "bigint" ? value.toString() : value);
|
|
1889
|
-
const hash = createHash2("sha256").update(hashInput).digest("hex");
|
|
1890
|
-
return { statements, hash };
|
|
1891
|
-
}
|
|
1892
|
-
|
|
1893
|
-
// src/runtime/reindex.ts
|
|
1894
2218
|
var LOG_INTERVAL = 1000;
|
|
1895
2219
|
var HEALTH_FLUSH_INTERVAL = 1000;
|
|
1896
2220
|
var PROGRESS_FLUSH_INTERVAL_MS = 5000;
|
|
2221
|
+
var EMPTY_BATCH_HALT_THRESHOLD = 3;
|
|
1897
2222
|
var STANDARD_REINDEX_BATCH_CONFIG = {
|
|
1898
2223
|
defaultBatchSize: 500,
|
|
1899
2224
|
minBatchSize: 100,
|
|
@@ -1969,6 +2294,7 @@ async function processBlockRange(def, opts) {
|
|
|
1969
2294
|
let batchSize = batchConfig.defaultBatchSize;
|
|
1970
2295
|
let currentHeight = fromBlock;
|
|
1971
2296
|
let aborted = false;
|
|
2297
|
+
let consecutiveEmptyBatches = 0;
|
|
1972
2298
|
const sparse = Boolean(source.nextDataHeight && canSparseScan(def));
|
|
1973
2299
|
const flushHealth = async () => {
|
|
1974
2300
|
if (pendingEventsProcessed === 0 && pendingErrors === 0)
|
|
@@ -1980,6 +2306,13 @@ async function processBlockRange(def, opts) {
|
|
|
1980
2306
|
lastHealthFlushBlock = blocksProcessed;
|
|
1981
2307
|
lastHealthFlushAt = Date.now();
|
|
1982
2308
|
};
|
|
2309
|
+
const haltRange = async (errorMsg, height) => {
|
|
2310
|
+
pendingErrors++;
|
|
2311
|
+
pendingLastError = errorMsg;
|
|
2312
|
+
await flushHealth().catch(() => {});
|
|
2313
|
+
await updateSubgraphStatus2(targetDb, subgraphName, "error").catch(() => {});
|
|
2314
|
+
throw new Error(`${subgraphName}: halted at block ${height}: ${errorMsg}`);
|
|
2315
|
+
};
|
|
1983
2316
|
let nextBatchEnd = Math.min(currentHeight + batchSize - 1, toBlock);
|
|
1984
2317
|
let nextBatchPromise = source.loadBlockRange(currentHeight, nextBatchEnd);
|
|
1985
2318
|
while (currentHeight <= toBlock) {
|
|
@@ -1994,6 +2327,14 @@ async function processBlockRange(def, opts) {
|
|
|
1994
2327
|
}
|
|
1995
2328
|
const batch = await nextBatchPromise;
|
|
1996
2329
|
const batchEnd = nextBatchEnd;
|
|
2330
|
+
if (batch.size === 0 && batchEnd >= currentHeight) {
|
|
2331
|
+
consecutiveEmptyBatches++;
|
|
2332
|
+
if (consecutiveEmptyBatches >= EMPTY_BATCH_HALT_THRESHOLD) {
|
|
2333
|
+
await haltRange(`block source returned ${consecutiveEmptyBatches} consecutive empty batches (ending ${currentHeight}..${batchEnd}) — source degraded`, currentHeight);
|
|
2334
|
+
}
|
|
2335
|
+
} else {
|
|
2336
|
+
consecutiveEmptyBatches = 0;
|
|
2337
|
+
}
|
|
1997
2338
|
const nextStart = batchEnd + 1;
|
|
1998
2339
|
if (nextStart <= toBlock) {
|
|
1999
2340
|
nextBatchEnd = Math.min(nextStart + batchSize - 1, toBlock);
|
|
@@ -2001,28 +2342,39 @@ async function processBlockRange(def, opts) {
|
|
|
2001
2342
|
}
|
|
2002
2343
|
const batchFailedBlocks = [];
|
|
2003
2344
|
let batchMatched = 0;
|
|
2345
|
+
const atomicProgress = status === "reindexing" ? { status } : undefined;
|
|
2004
2346
|
for (let height = currentHeight;height <= batchEnd; height++) {
|
|
2005
|
-
|
|
2347
|
+
let blockData = batch.get(height);
|
|
2006
2348
|
if (!blockData) {
|
|
2349
|
+
blockData = (await source.loadBlockRange(height, height)).get(height);
|
|
2350
|
+
}
|
|
2351
|
+
if (!blockData) {
|
|
2352
|
+
if (status === "reindexing") {
|
|
2353
|
+
const errorMsg = `block ${height} missing from source — halting reindex (cursor stays at ${height - 1})`;
|
|
2354
|
+
await haltRange(errorMsg, height);
|
|
2355
|
+
}
|
|
2007
2356
|
batchFailedBlocks.push({ height, reason: "block_missing" });
|
|
2008
2357
|
blocksProcessed++;
|
|
2009
2358
|
continue;
|
|
2010
2359
|
}
|
|
2011
2360
|
let result;
|
|
2012
2361
|
try {
|
|
2013
|
-
result = await
|
|
2362
|
+
result = await processBlockWithRetry(def, subgraphName, height, {
|
|
2014
2363
|
skipProgressUpdate: true,
|
|
2364
|
+
atomicProgress,
|
|
2015
2365
|
preloaded: blockData
|
|
2016
2366
|
});
|
|
2017
2367
|
} catch (err) {
|
|
2018
|
-
const errorMsg =
|
|
2019
|
-
logger6.error("Block processing
|
|
2368
|
+
const errorMsg = getErrorMessage2(err);
|
|
2369
|
+
logger6.error("Block processing failed persistently", {
|
|
2020
2370
|
subgraph: subgraphName,
|
|
2021
2371
|
blockHeight: height,
|
|
2022
2372
|
error: errorMsg
|
|
2023
2373
|
});
|
|
2374
|
+
if (status === "reindexing") {
|
|
2375
|
+
await haltRange(`block ${height} failed persistently: ${errorMsg}`, height);
|
|
2376
|
+
}
|
|
2024
2377
|
batchFailedBlocks.push({ height, reason: "processing_error" });
|
|
2025
|
-
await updateSubgraphStatus2(targetDb, subgraphName, status, height).catch(() => {});
|
|
2026
2378
|
blocksProcessed++;
|
|
2027
2379
|
totalErrors++;
|
|
2028
2380
|
pendingErrors++;
|
|
@@ -2048,6 +2400,9 @@ async function processBlockRange(def, opts) {
|
|
|
2048
2400
|
const shouldFlushProgress = blocksProcessed % 100 === 0 || now - lastProgressFlushAt >= PROGRESS_FLUSH_INTERVAL_MS;
|
|
2049
2401
|
if (shouldFlushProgress) {
|
|
2050
2402
|
await updateSubgraphStatus2(targetDb, subgraphName, status, height);
|
|
2403
|
+
if (opts.operationId) {
|
|
2404
|
+
await updateOperationProcessedEvents(targetDb, opts.operationId, totalEventsProcessed).catch(() => {});
|
|
2405
|
+
}
|
|
2051
2406
|
lastProgressFlushAt = now;
|
|
2052
2407
|
}
|
|
2053
2408
|
if (blocksProcessed % LOG_INTERVAL === 0) {
|
|
@@ -2165,6 +2520,7 @@ async function reindexSubgraph(def, opts) {
|
|
|
2165
2520
|
isCatchup: false,
|
|
2166
2521
|
apiKeyId: null,
|
|
2167
2522
|
subgraphId: subgraphRow?.id,
|
|
2523
|
+
operationId: opts?.operationId,
|
|
2168
2524
|
signal: opts?.signal
|
|
2169
2525
|
});
|
|
2170
2526
|
if (result.aborted) {
|
|
@@ -2240,6 +2596,7 @@ async function resumeReindex(def, opts) {
|
|
|
2240
2596
|
isCatchup: false,
|
|
2241
2597
|
apiKeyId: null,
|
|
2242
2598
|
subgraphId: row.id,
|
|
2599
|
+
operationId: opts.operationId,
|
|
2243
2600
|
signal: opts.signal
|
|
2244
2601
|
});
|
|
2245
2602
|
if (result.aborted) {
|
|
@@ -2288,6 +2645,7 @@ async function backfillSubgraph(def, opts) {
|
|
|
2288
2645
|
isCatchup: false,
|
|
2289
2646
|
apiKeyId: null,
|
|
2290
2647
|
subgraphId: subgraphRow?.id,
|
|
2648
|
+
operationId: opts.operationId,
|
|
2291
2649
|
signal: opts.signal
|
|
2292
2650
|
});
|
|
2293
2651
|
if (result.aborted) {
|
|
@@ -2325,5 +2683,5 @@ export {
|
|
|
2325
2683
|
backfillSubgraph
|
|
2326
2684
|
};
|
|
2327
2685
|
|
|
2328
|
-
//# debugId=
|
|
2686
|
+
//# debugId=6F74BAD96C2E3BCE64756E2164756E21
|
|
2329
2687
|
//# sourceMappingURL=reindex.js.map
|