@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.
- package/dist/src/index.d.ts +54 -1
- package/dist/src/index.js +952 -199
- package/dist/src/index.js.map +12 -11
- package/dist/src/runtime/block-processor.d.ts +36 -1
- package/dist/src/runtime/block-processor.js +568 -86
- package/dist/src/runtime/block-processor.js.map +9 -8
- package/dist/src/runtime/catchup.d.ts +7 -0
- package/dist/src/runtime/catchup.js +587 -132
- 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 +665 -248
- package/dist/src/runtime/processor.js.map +13 -13
- package/dist/src/runtime/reindex.d.ts +7 -0
- package/dist/src/runtime/reindex.js +618 -198
- package/dist/src/runtime/reindex.js.map +10 -10
- package/dist/src/runtime/reorg.d.ts +7 -0
- package/dist/src/runtime/reorg.js +588 -87
- 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 +70 -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 +7 -0
- package/dist/src/schema/index.js +20 -2
- package/dist/src/schema/index.js.map +4 -4
- package/dist/src/service.js +665 -248
- package/dist/src/service.js.map +13 -13
- package/dist/src/types.d.ts +7 -0
- package/dist/src/validate.d.ts +7 -0
- package/dist/src/validate.js +3 -2
- package/dist/src/validate.js.map +3 -3
- package/package.json +2 -2
|
@@ -43,6 +43,9 @@ type ComputedValue = RowValue | ((existing: Record<string, unknown> | null) => u
|
|
|
43
43
|
import { Database } from "@secondlayer/shared/db";
|
|
44
44
|
import { Kysely, Transaction } from "kysely";
|
|
45
45
|
type AnyDb = Kysely<Database> | Transaction<Database>;
|
|
46
|
+
/** Reorg journal entries older than this many blocks are prunable — far past
|
|
47
|
+
* Stacks finality (observed reorg depth is single digits). */
|
|
48
|
+
declare const JOURNAL_RETENTION_BLOCKS = 300;
|
|
46
49
|
interface FlushWrite {
|
|
47
50
|
op: "insert" | "update" | "delete";
|
|
48
51
|
table: string;
|
|
@@ -76,7 +79,12 @@ interface TxMeta2 {
|
|
|
76
79
|
/**
|
|
77
80
|
* Runtime context passed to subgraph handlers.
|
|
78
81
|
* Batches writes and flushes them atomically at the end of a block.
|
|
79
|
-
*
|
|
82
|
+
*
|
|
83
|
+
* Row reads (findOne/findMany) are read-your-writes: they overlay the pending
|
|
84
|
+
* ops queue on the DB state, so a handler observes every write queued earlier
|
|
85
|
+
* in the same block. Without this, accumulator patterns (balance = f(existing))
|
|
86
|
+
* silently lose all but the last same-block delta per row (fix-f040 B1).
|
|
87
|
+
* Aggregate reads (count/sum/min/max) remain pre-flush DB state.
|
|
80
88
|
*/
|
|
81
89
|
declare class SubgraphContext2 {
|
|
82
90
|
readonly block: BlockMeta;
|
|
@@ -93,7 +101,14 @@ declare class SubgraphContext2 {
|
|
|
93
101
|
* Non-idempotent `update` handlers are rejected at deploy, not here.
|
|
94
102
|
*/
|
|
95
103
|
private readonly byo;
|
|
96
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Record pre-images of keyed mutations into the schema's `_journal` so a
|
|
106
|
+
* reorg can restore prior row states (fix-f040 B2). Enabled on the live
|
|
107
|
+
* path only — deep reindex/backfill heights are past finality, and the
|
|
108
|
+
* journal would just be churn the pruner deletes.
|
|
109
|
+
*/
|
|
110
|
+
private readonly journal;
|
|
111
|
+
constructor(db: AnyDb, pgSchemaName: string, subgraphSchema: SubgraphSchema, block: BlockMeta, tx: TxMeta2, byo?: boolean, journal?: boolean);
|
|
97
112
|
get tx(): TxMeta2;
|
|
98
113
|
/** Update the current transaction context (used by runner between events) */
|
|
99
114
|
setTx(tx: TxMeta2): void;
|
|
@@ -101,6 +116,24 @@ declare class SubgraphContext2 {
|
|
|
101
116
|
update(table: string, where: Record<string, unknown>, set: Record<string, unknown>): void;
|
|
102
117
|
upsert(table: string, key: Record<string, unknown>, row: Record<string, unknown>): void;
|
|
103
118
|
delete(table: string, where: Record<string, unknown>): void;
|
|
119
|
+
/**
|
|
120
|
+
* Atomic counter update — the blessed accumulator primitive. Compiles to
|
|
121
|
+
* `INSERT ... ON CONFLICT (key) DO UPDATE SET col = COALESCE(t.col,0) + delta`,
|
|
122
|
+
* so deltas commute: same-block, replayed-in-order, and concurrent updates
|
|
123
|
+
* all land correctly without read-modify-write. Missing row inserts the
|
|
124
|
+
* delta as the initial value. Requires a uniqueKeys constraint matching
|
|
125
|
+
* `key`; deltas may be negative.
|
|
126
|
+
*/
|
|
127
|
+
increment(table: string, key: Record<string, unknown>, deltas: Record<string, bigint | number>): void;
|
|
128
|
+
/** Current length of the pending-ops queue. Pair with {@link rollbackTo}. */
|
|
129
|
+
opsCheckpoint(): number;
|
|
130
|
+
/**
|
|
131
|
+
* Discard ops queued after a checkpoint. The runner wraps each handler
|
|
132
|
+
* invocation so a thrown handler contributes nothing — without this, a
|
|
133
|
+
* transfer handler that debited then threw flushes a one-sided debit
|
|
134
|
+
* (fix-f040 B6).
|
|
135
|
+
*/
|
|
136
|
+
rollbackTo(checkpoint: number): void;
|
|
104
137
|
/** Partial update — sets only specified fields, preserves everything else */
|
|
105
138
|
patch(table: string, where: Record<string, unknown>, set: Record<string, unknown>): void;
|
|
106
139
|
/**
|
|
@@ -112,6 +145,17 @@ declare class SubgraphContext2 {
|
|
|
112
145
|
formatUnits(value: bigint, decimals: number): string;
|
|
113
146
|
findOne(table: string, where: Record<string, unknown>): Promise<Record<string, unknown> | null>;
|
|
114
147
|
findMany(table: string, where: Record<string, unknown>): Promise<Record<string, unknown>[]>;
|
|
148
|
+
/**
|
|
149
|
+
* Replay pending ops for `table` over a single DB-read result so reads
|
|
150
|
+
* observe earlier same-block writes. Mirrors flush semantics: upserts
|
|
151
|
+
* merge non-key/non-meta columns, increments add deltas, updates/deletes
|
|
152
|
+
* apply by where-match. Overlaid rows synthesized from pending inserts
|
|
153
|
+
* lack DB-generated columns (_id, _created_at).
|
|
154
|
+
*/
|
|
155
|
+
private overlayOne;
|
|
156
|
+
private overlayMany;
|
|
157
|
+
/** Apply one pending op to a candidate row state (null = row absent). */
|
|
158
|
+
private applyOpToRow;
|
|
115
159
|
count(table: string, where?: Record<string, unknown>): Promise<number>;
|
|
116
160
|
sum(table: string, column: string, where?: Record<string, unknown>): Promise<bigint>;
|
|
117
161
|
min(table: string, column: string, where?: Record<string, unknown>): Promise<bigint | null>;
|
|
@@ -132,8 +176,26 @@ declare class SubgraphContext2 {
|
|
|
132
176
|
flush(): Promise<FlushManifest>;
|
|
133
177
|
/** Prepare a single insert row, returning its data, columns, upsert key for batching. */
|
|
134
178
|
private prepareInsert;
|
|
179
|
+
/**
|
|
180
|
+
* Lazily create `_journal` for schemas deployed before it existed. Cached
|
|
181
|
+
* per process only once CONFIRMED present (to_regclass) — a CREATE issued
|
|
182
|
+
* inside a block tx could roll back with it, so self-created tables are
|
|
183
|
+
* re-verified on the next flush instead of trusted.
|
|
184
|
+
*/
|
|
185
|
+
private ensureJournalTable;
|
|
186
|
+
/** SQL type of a user column (for casting journal key VALUES), if known. */
|
|
187
|
+
private columnSqlType;
|
|
188
|
+
/**
|
|
189
|
+
* Journal pre-images for a keyed batch: one `_journal` row per key with the
|
|
190
|
+
* row's current state (`prev_row`), or NULL when the key doesn't exist yet
|
|
191
|
+
* (the mutation will create it — a revert deletes it). Emitted BEFORE the
|
|
192
|
+
* mutation statement, same transaction.
|
|
193
|
+
*/
|
|
194
|
+
private journalCaptureSQL;
|
|
195
|
+
/** Journal pre-images of rows a where-clause mutation will touch, keyed by `_id`. */
|
|
196
|
+
private journalCaptureByWhereSQL;
|
|
135
197
|
/** Build SQL statements from write ops, batching compatible INSERTs. */
|
|
136
198
|
private buildStatements;
|
|
137
199
|
private validateTable;
|
|
138
200
|
}
|
|
139
|
-
export { TxMeta2 as TxMeta, SubgraphContext2 as SubgraphContext, FlushWrite, FlushManifest, BlockMeta };
|
|
201
|
+
export { TxMeta2 as TxMeta, SubgraphContext2 as SubgraphContext, JOURNAL_RETENTION_BLOCKS, FlushWrite, FlushManifest, BlockMeta };
|
|
@@ -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)) {
|
|
@@ -329,8 +710,9 @@ function buildWhereClause(where) {
|
|
|
329
710
|
return { clause: parts.join(" AND "), values: [] };
|
|
330
711
|
}
|
|
331
712
|
export {
|
|
332
|
-
SubgraphContext
|
|
713
|
+
SubgraphContext,
|
|
714
|
+
JOURNAL_RETENTION_BLOCKS
|
|
333
715
|
};
|
|
334
716
|
|
|
335
|
-
//# debugId=
|
|
717
|
+
//# debugId=5D4174CEFDCF4E9364756E2164756E21
|
|
336
718
|
//# sourceMappingURL=context.js.map
|