envio 3.5.1 → 3.6.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/evm.schema.json +7 -0
- package/fuel.schema.json +7 -0
- package/index.d.ts +33 -10
- package/package.json +6 -6
- package/src/ChainState.res +12 -0
- package/src/ChainState.res.mjs +15 -2
- package/src/ChainState.resi +3 -0
- package/src/Config.res +60 -2
- package/src/Config.res.mjs +49 -13
- package/src/EffectState.res +2 -2
- package/src/EffectState.res.mjs +2 -2
- package/src/EntityTables.res +32 -0
- package/src/EntityTables.res.mjs +44 -0
- package/src/Envio.res +7 -7
- package/src/Envio.res.mjs +5 -6
- package/src/Hasura.res +37 -3
- package/src/Hasura.res.mjs +23 -5
- package/src/InMemoryStore.res +48 -8
- package/src/InMemoryStore.res.mjs +37 -7
- package/src/IndexerState.res +30 -24
- package/src/IndexerState.res.mjs +20 -35
- package/src/IndexerState.resi +8 -6
- package/src/Internal.res +29 -11
- package/src/Internal.res.mjs +27 -10
- package/src/LoadLayer.res +39 -8
- package/src/LoadLayer.res.mjs +36 -9
- package/src/LoadLayer.resi +2 -0
- package/src/Persistence.res +12 -1
- package/src/PgStorage.res +183 -37
- package/src/PgStorage.res.mjs +149 -34
- package/src/PruneStaleHistory.res +1 -0
- package/src/PruneStaleHistory.res.mjs +2 -1
- package/src/Sink.res +3 -3
- package/src/Sink.res.mjs +2 -2
- package/src/TestIndexer.res +128 -17
- package/src/TestIndexer.res.mjs +75 -9
- package/src/UserContext.res +19 -4
- package/src/UserContext.res.mjs +15 -9
- package/src/Writing.res +8 -16
- package/src/Writing.res.mjs +10 -10
- package/src/bindings/ClickHouse.res +40 -4
- package/src/bindings/ClickHouse.res.mjs +37 -5
- package/src/db/EntityHistory.res +64 -21
- package/src/db/EntityHistory.res.mjs +43 -22
- package/src/db/InternalTable.res.mjs +31 -31
- package/src/db/Table.res +24 -1
- package/src/db/Table.res.mjs +26 -4
- package/svm.schema.json +7 -0
|
@@ -248,15 +248,21 @@ type setUpdatesCache = {
|
|
|
248
248
|
|
|
249
249
|
let setUpdatesOrThrow = async (
|
|
250
250
|
client,
|
|
251
|
-
~cache:
|
|
251
|
+
~cache: dict<setUpdatesCache>,
|
|
252
252
|
~changes: array<Change.t<Internal.entity>>,
|
|
253
253
|
~entityConfig: Internal.entityConfig,
|
|
254
|
+
~scope: Internal.chainScope,
|
|
254
255
|
~database: string,
|
|
255
256
|
) => {
|
|
256
257
|
if changes->Array.length === 0 {
|
|
257
258
|
()
|
|
258
259
|
} else {
|
|
259
|
-
let
|
|
260
|
+
let chainIdField = entityConfig.table->Table.getChainIdField
|
|
261
|
+
let scopeChainId = scope->Internal.chainScopeChainId
|
|
262
|
+
// A delete row carries no entity to stamp, so the chain id is baked into
|
|
263
|
+
// the schema instead — which is why the cache is keyed per scope.
|
|
264
|
+
let cacheKey = `${entityConfig.name}|${scope->Internal.chainScopeToString}`
|
|
265
|
+
let {convertOrThrow, tableName} = switch cache->Utils.Dict.dangerouslyGetNonOption(cacheKey) {
|
|
260
266
|
| Some(cached) => cached
|
|
261
267
|
| None =>
|
|
262
268
|
let cached: setUpdatesCache = {
|
|
@@ -273,6 +279,11 @@ let setUpdatesOrThrow = async (
|
|
|
273
279
|
),
|
|
274
280
|
S.object(s => {
|
|
275
281
|
s.tag(EntityHistory.changeFieldName, EntityHistory.RowAction.DELETE)
|
|
282
|
+
switch (chainIdField, scopeChainId) {
|
|
283
|
+
| (Some(field), Some(chainId)) =>
|
|
284
|
+
s.tag(field->Table.getClickHouseDbFieldName, chainId)
|
|
285
|
+
| _ => ()
|
|
286
|
+
}
|
|
276
287
|
Change.Delete({
|
|
277
288
|
entityId: s.field(Table.idFieldName, entityConfig.table->Table.getIdSchema),
|
|
278
289
|
checkpointId: s.field(
|
|
@@ -294,10 +305,25 @@ let setUpdatesOrThrow = async (
|
|
|
294
305
|
),
|
|
295
306
|
}
|
|
296
307
|
|
|
297
|
-
cache->
|
|
308
|
+
cache->Dict.set(cacheKey, cached)
|
|
298
309
|
cached
|
|
299
310
|
}
|
|
300
311
|
|
|
312
|
+
let changes = switch (chainIdField, scopeChainId) {
|
|
313
|
+
| (Some(field), Some(chainId)) =>
|
|
314
|
+
changes->Array.map(change =>
|
|
315
|
+
switch change {
|
|
316
|
+
| Change.Set(set) =>
|
|
317
|
+
Change.Set({
|
|
318
|
+
...set,
|
|
319
|
+
entity: set.entity->Internal.stampChainId(~fieldName=field.fieldName, ~chainId),
|
|
320
|
+
})
|
|
321
|
+
| Delete(_) => change
|
|
322
|
+
}
|
|
323
|
+
)
|
|
324
|
+
| _ => changes
|
|
325
|
+
}
|
|
326
|
+
|
|
301
327
|
try {
|
|
302
328
|
// The entity history table is the source of truth for ClickHouse, so every
|
|
303
329
|
// intermediate change must be persisted, not only the current value.
|
|
@@ -505,6 +531,16 @@ let makeCreateViewQuery = (
|
|
|
505
531
|
~entityIndex=entityConfig.index,
|
|
506
532
|
)
|
|
507
533
|
|
|
534
|
+
// A per-chain entity's rows are only comparable within a chain, so the
|
|
535
|
+
// current-state dedup keys on (id, chain id).
|
|
536
|
+
let dedupKey =
|
|
537
|
+
switch entityConfig.table->Table.getChainIdField {
|
|
538
|
+
| Some(field) => [Table.idFieldName, field->Table.getClickHouseDbFieldName]
|
|
539
|
+
| None => [Table.idFieldName]
|
|
540
|
+
}
|
|
541
|
+
->Array.map(name => `\`${name}\``)
|
|
542
|
+
->Array.joinUnsafe(", ")
|
|
543
|
+
|
|
508
544
|
let checkpointsTableName = InternalTable.Checkpoints.table.tableName
|
|
509
545
|
let checkpointIdField = (#id: InternalTable.Checkpoints.field :> string)
|
|
510
546
|
|
|
@@ -528,7 +564,7 @@ FROM (
|
|
|
528
564
|
FROM ${database}.\`${historyTableName}\`
|
|
529
565
|
WHERE \`${EntityHistory.checkpointIdFieldName}\` <= (SELECT max(${checkpointIdField}) FROM ${database}.\`${checkpointsTableName}\`)
|
|
530
566
|
ORDER BY \`${EntityHistory.checkpointIdFieldName}\` DESC
|
|
531
|
-
LIMIT 1 BY
|
|
567
|
+
LIMIT 1 BY ${dedupKey}
|
|
532
568
|
)
|
|
533
569
|
WHERE \`${EntityHistory.changeFieldName}\` = '${(EntityHistory.RowAction.SET :> string)}'`
|
|
534
570
|
}
|
|
@@ -5,6 +5,7 @@ import * as Table from "../db/Table.res.mjs";
|
|
|
5
5
|
import * as Utils from "../Utils.res.mjs";
|
|
6
6
|
import * as ChainId from "../ChainId.res.mjs";
|
|
7
7
|
import * as Logging from "../Logging.res.mjs";
|
|
8
|
+
import * as Internal from "../Internal.res.mjs";
|
|
8
9
|
import * as Persistence from "../Persistence.res.mjs";
|
|
9
10
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
10
11
|
import * as EntityHistory from "../db/EntityHistory.res.mjs";
|
|
@@ -197,11 +198,14 @@ async function setCheckpointsOrThrow(client, batch, database) {
|
|
|
197
198
|
}
|
|
198
199
|
}
|
|
199
200
|
|
|
200
|
-
async function setUpdatesOrThrow(client, cache, changes, entityConfig, database) {
|
|
201
|
+
async function setUpdatesOrThrow(client, cache, changes, entityConfig, scope, database) {
|
|
201
202
|
if (changes.length === 0) {
|
|
202
203
|
return;
|
|
203
204
|
}
|
|
204
|
-
let
|
|
205
|
+
let chainIdField = Table.getChainIdField(entityConfig.table);
|
|
206
|
+
let scopeChainId = Internal.chainScopeChainId(scope);
|
|
207
|
+
let cacheKey = entityConfig.name + `|` + Internal.chainScopeToString(scope);
|
|
208
|
+
let cached = cache[cacheKey];
|
|
205
209
|
let match;
|
|
206
210
|
if (cached !== undefined) {
|
|
207
211
|
match = cached;
|
|
@@ -211,6 +215,9 @@ async function setUpdatesOrThrow(client, cache, changes, entityConfig, database)
|
|
|
211
215
|
EntityHistory.makeSetUpdateSchema(Table.getIdSchema(entityConfig.table), makeClickHouseEntitySchema(entityConfig.table)),
|
|
212
216
|
S$RescriptSchema.object(s => {
|
|
213
217
|
s.tag(EntityHistory.changeFieldName, "DELETE");
|
|
218
|
+
if (chainIdField !== undefined && scopeChainId !== undefined) {
|
|
219
|
+
s.tag(Table.getClickHouseDbFieldName(chainIdField), Primitive_option.valFromOption(scopeChainId));
|
|
220
|
+
}
|
|
214
221
|
return {
|
|
215
222
|
type: "DELETE",
|
|
216
223
|
entityId: s.f(Table.idFieldName, Table.getIdSchema(entityConfig.table)),
|
|
@@ -222,12 +229,30 @@ async function setUpdatesOrThrow(client, cache, changes, entityConfig, database)
|
|
|
222
229
|
tableName: cached_tableName,
|
|
223
230
|
convertOrThrow: cached_convertOrThrow
|
|
224
231
|
};
|
|
225
|
-
cache
|
|
232
|
+
cache[cacheKey] = cached$1;
|
|
226
233
|
match = cached$1;
|
|
227
234
|
}
|
|
228
235
|
let tableName = match.tableName;
|
|
236
|
+
let changes$1;
|
|
237
|
+
if (chainIdField !== undefined && scopeChainId !== undefined) {
|
|
238
|
+
let chainId = Primitive_option.valFromOption(scopeChainId);
|
|
239
|
+
changes$1 = changes.map(change => {
|
|
240
|
+
if (change.type === "SET") {
|
|
241
|
+
return {
|
|
242
|
+
type: "SET",
|
|
243
|
+
entityId: change.entityId,
|
|
244
|
+
entity: Internal.stampChainId(change.entity, chainIdField.fieldName, chainId),
|
|
245
|
+
checkpointId: change.checkpointId
|
|
246
|
+
};
|
|
247
|
+
} else {
|
|
248
|
+
return change;
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
} else {
|
|
252
|
+
changes$1 = changes;
|
|
253
|
+
}
|
|
229
254
|
try {
|
|
230
|
-
let values = match.convertOrThrow(changes);
|
|
255
|
+
let values = match.convertOrThrow(changes$1);
|
|
231
256
|
return await insertWithRetry(client, tableName, values, "JSONEachRow", undefined);
|
|
232
257
|
} catch (raw_exn) {
|
|
233
258
|
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
@@ -346,6 +371,13 @@ ORDER BY (` + "id" + `)`;
|
|
|
346
371
|
function makeCreateViewQuery(entityConfig, database, onClusterOpt) {
|
|
347
372
|
let onCluster = onClusterOpt !== undefined ? onClusterOpt : false;
|
|
348
373
|
let historyTableName = EntityHistory.historyTableName(entityConfig.name, entityConfig.index);
|
|
374
|
+
let field = Table.getChainIdField(entityConfig.table);
|
|
375
|
+
let dedupKey = (
|
|
376
|
+
field !== undefined ? [
|
|
377
|
+
Table.idFieldName,
|
|
378
|
+
Table.getClickHouseDbFieldName(field)
|
|
379
|
+
] : [Table.idFieldName]
|
|
380
|
+
).map(name => `\`` + name + `\``).join(", ");
|
|
349
381
|
let checkpointsTableName = InternalTable.Checkpoints.table.tableName;
|
|
350
382
|
let entityFields = Stdlib_Array.filterMap(entityConfig.table.fields, field => {
|
|
351
383
|
if (field.TAG !== "Field") {
|
|
@@ -363,7 +395,7 @@ FROM (
|
|
|
363
395
|
FROM ` + database + `.\`` + historyTableName + `\`
|
|
364
396
|
WHERE \`` + EntityHistory.checkpointIdFieldName + `\` <= (SELECT max(` + "id" + `) FROM ` + database + `.\`` + checkpointsTableName + `\`)
|
|
365
397
|
ORDER BY \`` + EntityHistory.checkpointIdFieldName + `\` DESC
|
|
366
|
-
LIMIT 1 BY
|
|
398
|
+
LIMIT 1 BY ` + dedupKey + `
|
|
367
399
|
)
|
|
368
400
|
WHERE \`` + EntityHistory.changeFieldName + `\` = '` + "SET" + `'`;
|
|
369
401
|
}
|
package/src/db/EntityHistory.res
CHANGED
|
@@ -82,27 +82,41 @@ type safeReorgBlocks = {
|
|
|
82
82
|
// - Rollbacks will not cross the safe checkpoint id, so rows older than the anchor can never be referenced again.
|
|
83
83
|
// - If nothing changed in reorg threshold (after the safe checkpoint), the current state for that id can be reconstructed from the
|
|
84
84
|
// origin table; we do not need a pre-safe anchor for it.
|
|
85
|
-
|
|
85
|
+
// A per-chain entity's rows are only comparable within a chain, so every id
|
|
86
|
+
// correlation in the history SQL widens to (id, chain id).
|
|
87
|
+
let makeKeyColumns = (~chainIdColumn: option<string>) =>
|
|
88
|
+
switch chainIdColumn {
|
|
89
|
+
| Some(column) => ["id", `"${column}"`]
|
|
90
|
+
| None => ["id"]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let makeKeyMatch = (~chainIdColumn, ~left, ~right) =>
|
|
94
|
+
makeKeyColumns(~chainIdColumn)
|
|
95
|
+
->Array.map(column => `${left}.${column} = ${right}.${column}`)
|
|
96
|
+
->Array.joinUnsafe(" AND ")
|
|
97
|
+
|
|
98
|
+
let makePruneStaleEntityHistoryQuery = (~entityName, ~entityIndex, ~pgSchema, ~chainIdColumn) => {
|
|
86
99
|
let historyTableRef = `"${pgSchema}"."${historyTableName(~entityName, ~entityIndex)}"`
|
|
100
|
+
let keyColumns = makeKeyColumns(~chainIdColumn)
|
|
101
|
+
let anchorKeys = keyColumns->Array.map(column => `t.${column}`)->Array.joinUnsafe(", ")
|
|
87
102
|
|
|
103
|
+
// Whether a key still has a row above the safe checkpoint is an aggregate
|
|
104
|
+
// over the same groups as the anchor, so it's computed in the one pass
|
|
105
|
+
// rather than as a per-row correlated lookup.
|
|
106
|
+
// `d.envio_checkpoint_id <= $1` is implied by the rest of the predicate — it
|
|
107
|
+
// is there to keep the rows above the safe checkpoint out of the join.
|
|
88
108
|
`WITH anchors AS (
|
|
89
|
-
SELECT
|
|
90
|
-
|
|
91
|
-
|
|
109
|
+
SELECT ${anchorKeys},
|
|
110
|
+
MAX(t.${checkpointIdFieldName}) FILTER (WHERE t.${checkpointIdFieldName} <= $1) AS keep_checkpoint_id,
|
|
111
|
+
bool_or(t.${checkpointIdFieldName} > $1) AS has_above
|
|
112
|
+
FROM ${historyTableRef} t
|
|
113
|
+
GROUP BY ${anchorKeys}
|
|
92
114
|
)
|
|
93
115
|
DELETE FROM ${historyTableRef} d
|
|
94
116
|
USING anchors a
|
|
95
|
-
WHERE d
|
|
96
|
-
AND
|
|
97
|
-
|
|
98
|
-
OR (
|
|
99
|
-
d.${checkpointIdFieldName} = a.keep_checkpoint_id AND
|
|
100
|
-
NOT EXISTS (
|
|
101
|
-
SELECT 1 FROM ${historyTableRef} ps
|
|
102
|
-
WHERE ps.id = d.id AND ps.${checkpointIdFieldName} > $1
|
|
103
|
-
)
|
|
104
|
-
)
|
|
105
|
-
);`
|
|
117
|
+
WHERE ${makeKeyMatch(~chainIdColumn, ~left="d", ~right="a")}
|
|
118
|
+
AND d.${checkpointIdFieldName} <= $1
|
|
119
|
+
AND (d.${checkpointIdFieldName} < a.keep_checkpoint_id OR NOT a.has_above);`
|
|
106
120
|
}
|
|
107
121
|
|
|
108
122
|
let pruneStaleEntityHistory = (
|
|
@@ -110,26 +124,40 @@ let pruneStaleEntityHistory = (
|
|
|
110
124
|
~entityName,
|
|
111
125
|
~entityIndex,
|
|
112
126
|
~pgSchema,
|
|
127
|
+
~chainIdColumn,
|
|
113
128
|
~safeCheckpointId,
|
|
114
129
|
): promise<unit> => {
|
|
115
130
|
sql->Postgres.preparedUnsafe(
|
|
116
|
-
makePruneStaleEntityHistoryQuery(~entityName, ~entityIndex, ~pgSchema),
|
|
131
|
+
makePruneStaleEntityHistoryQuery(~entityName, ~entityIndex, ~pgSchema, ~chainIdColumn),
|
|
117
132
|
[safeCheckpointId->BigInt.toString]->(Utils.magic: array<string> => unknown),
|
|
118
133
|
)
|
|
119
134
|
}
|
|
120
135
|
|
|
121
136
|
// If an entity doesn't have a history before the update
|
|
122
137
|
// we create it automatically with envio_checkpoint_id 0
|
|
123
|
-
|
|
138
|
+
// The ids belong to a single chain (the flush group's scope), so the chain is
|
|
139
|
+
// bound once as $2 rather than unnested alongside them.
|
|
140
|
+
let makeBackfillHistoryQuery = (
|
|
141
|
+
~pgSchema,
|
|
142
|
+
~entityName,
|
|
143
|
+
~entityIndex,
|
|
144
|
+
~idPgType,
|
|
145
|
+
~chainIdColumn,
|
|
146
|
+
~chainId: option<ChainId.t>,
|
|
147
|
+
) => {
|
|
124
148
|
let historyTableRef = `"${pgSchema}"."${historyTableName(~entityName, ~entityIndex)}"`
|
|
149
|
+
let chainFilter = switch (chainIdColumn, chainId) {
|
|
150
|
+
| (Some(column), Some(_)) => ` AND e."${column}" = $2`
|
|
151
|
+
| _ => ""
|
|
152
|
+
}
|
|
125
153
|
`WITH target_ids AS (
|
|
126
154
|
SELECT UNNEST($1::${idPgType}[]) AS id
|
|
127
155
|
),
|
|
128
156
|
missing_history AS (
|
|
129
157
|
SELECT e.*
|
|
130
158
|
FROM "${pgSchema}"."${entityName}" e
|
|
131
|
-
JOIN target_ids t ON e.id = t.id
|
|
132
|
-
LEFT JOIN ${historyTableRef} h ON h
|
|
159
|
+
JOIN target_ids t ON e.id = t.id${chainFilter}
|
|
160
|
+
LEFT JOIN ${historyTableRef} h ON ${makeKeyMatch(~chainIdColumn, ~left="h", ~right="e")}
|
|
133
161
|
WHERE h.id IS NULL
|
|
134
162
|
)
|
|
135
163
|
INSERT INTO ${historyTableRef}
|
|
@@ -142,13 +170,28 @@ let backfillHistory = (
|
|
|
142
170
|
~pgSchema,
|
|
143
171
|
~table: Table.table,
|
|
144
172
|
~entityIndex,
|
|
173
|
+
~chainId: option<ChainId.t>,
|
|
145
174
|
~ids: array<EntityId.t>,
|
|
146
175
|
) => {
|
|
147
176
|
let idPgType = table->Table.getIdPgFieldType(~pgSchema)
|
|
177
|
+
let chainIdColumn = table->Table.getChainIdField->Option.map(Table.getPgDbFieldName)
|
|
178
|
+
let params = [table->Table.encodeIdsToJson(ids)->(Utils.magic: JSON.t => unknown)]
|
|
179
|
+
switch (chainIdColumn, chainId) {
|
|
180
|
+
| (Some(_), Some(chainId)) =>
|
|
181
|
+
params->Array.push(chainId->(Utils.magic: ChainId.t => unknown))->ignore
|
|
182
|
+
| _ => ()
|
|
183
|
+
}
|
|
148
184
|
sql
|
|
149
185
|
->Postgres.preparedUnsafe(
|
|
150
|
-
makeBackfillHistoryQuery(
|
|
151
|
-
|
|
186
|
+
makeBackfillHistoryQuery(
|
|
187
|
+
~entityName=table.tableName,
|
|
188
|
+
~entityIndex,
|
|
189
|
+
~pgSchema,
|
|
190
|
+
~idPgType,
|
|
191
|
+
~chainIdColumn,
|
|
192
|
+
~chainId,
|
|
193
|
+
),
|
|
194
|
+
params->Obj.magic,
|
|
152
195
|
)
|
|
153
196
|
->Utils.Promise.ignoreValue
|
|
154
197
|
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import * as Table from "./Table.res.mjs";
|
|
4
4
|
import * as Js_string from "@rescript/runtime/lib/es6/Js_string.js";
|
|
5
5
|
import * as Stdlib_BigInt from "@rescript/runtime/lib/es6/Stdlib_BigInt.js";
|
|
6
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
6
8
|
import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
|
|
7
9
|
|
|
8
10
|
let variants = [
|
|
@@ -71,42 +73,54 @@ function historyTableName(entityName, entityIndex) {
|
|
|
71
73
|
return Js_string.slice(0, 63 - entityIndexStr.length | 0, fullName) + entityIndexStr;
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
function
|
|
76
|
+
function makeKeyColumns(chainIdColumn) {
|
|
77
|
+
if (chainIdColumn !== undefined) {
|
|
78
|
+
return [
|
|
79
|
+
"id",
|
|
80
|
+
`"` + chainIdColumn + `"`
|
|
81
|
+
];
|
|
82
|
+
} else {
|
|
83
|
+
return ["id"];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function makeKeyMatch(chainIdColumn, left, right) {
|
|
88
|
+
return makeKeyColumns(chainIdColumn).map(column => left + `.` + column + ` = ` + right + `.` + column).join(" AND ");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function makePruneStaleEntityHistoryQuery(entityName, entityIndex, pgSchema, chainIdColumn) {
|
|
75
92
|
let historyTableRef = `"` + pgSchema + `"."` + historyTableName(entityName, entityIndex) + `"`;
|
|
93
|
+
let keyColumns = makeKeyColumns(chainIdColumn);
|
|
94
|
+
let anchorKeys = keyColumns.map(column => `t.` + column).join(", ");
|
|
76
95
|
return `WITH anchors AS (
|
|
77
|
-
SELECT
|
|
78
|
-
|
|
79
|
-
|
|
96
|
+
SELECT ` + anchorKeys + `,
|
|
97
|
+
MAX(t.` + checkpointIdFieldName + `) FILTER (WHERE t.` + checkpointIdFieldName + ` <= $1) AS keep_checkpoint_id,
|
|
98
|
+
bool_or(t.` + checkpointIdFieldName + ` > $1) AS has_above
|
|
99
|
+
FROM ` + historyTableRef + ` t
|
|
100
|
+
GROUP BY ` + anchorKeys + `
|
|
80
101
|
)
|
|
81
102
|
DELETE FROM ` + historyTableRef + ` d
|
|
82
103
|
USING anchors a
|
|
83
|
-
WHERE d
|
|
84
|
-
AND
|
|
85
|
-
|
|
86
|
-
OR (
|
|
87
|
-
d.` + checkpointIdFieldName + ` = a.keep_checkpoint_id AND
|
|
88
|
-
NOT EXISTS (
|
|
89
|
-
SELECT 1 FROM ` + historyTableRef + ` ps
|
|
90
|
-
WHERE ps.id = d.id AND ps.` + checkpointIdFieldName + ` > $1
|
|
91
|
-
)
|
|
92
|
-
)
|
|
93
|
-
);`;
|
|
104
|
+
WHERE ` + makeKeyMatch(chainIdColumn, "d", "a") + `
|
|
105
|
+
AND d.` + checkpointIdFieldName + ` <= $1
|
|
106
|
+
AND (d.` + checkpointIdFieldName + ` < a.keep_checkpoint_id OR NOT a.has_above);`;
|
|
94
107
|
}
|
|
95
108
|
|
|
96
|
-
function pruneStaleEntityHistory(sql, entityName, entityIndex, pgSchema, safeCheckpointId) {
|
|
97
|
-
return sql.unsafe(makePruneStaleEntityHistoryQuery(entityName, entityIndex, pgSchema), [safeCheckpointId.toString()], {prepare: true});
|
|
109
|
+
function pruneStaleEntityHistory(sql, entityName, entityIndex, pgSchema, chainIdColumn, safeCheckpointId) {
|
|
110
|
+
return sql.unsafe(makePruneStaleEntityHistoryQuery(entityName, entityIndex, pgSchema, chainIdColumn), [safeCheckpointId.toString()], {prepare: true});
|
|
98
111
|
}
|
|
99
112
|
|
|
100
|
-
function makeBackfillHistoryQuery(pgSchema, entityName, entityIndex, idPgType) {
|
|
113
|
+
function makeBackfillHistoryQuery(pgSchema, entityName, entityIndex, idPgType, chainIdColumn, chainId) {
|
|
101
114
|
let historyTableRef = `"` + pgSchema + `"."` + historyTableName(entityName, entityIndex) + `"`;
|
|
115
|
+
let chainFilter = chainIdColumn !== undefined && chainId !== undefined ? ` AND e."` + chainIdColumn + `" = $2` : "";
|
|
102
116
|
return `WITH target_ids AS (
|
|
103
117
|
SELECT UNNEST($1::` + idPgType + `[]) AS id
|
|
104
118
|
),
|
|
105
119
|
missing_history AS (
|
|
106
120
|
SELECT e.*
|
|
107
121
|
FROM "` + pgSchema + `"."` + entityName + `" e
|
|
108
|
-
JOIN target_ids t ON e.id = t.id
|
|
109
|
-
LEFT JOIN ` + historyTableRef + ` h ON h
|
|
122
|
+
JOIN target_ids t ON e.id = t.id` + chainFilter + `
|
|
123
|
+
LEFT JOIN ` + historyTableRef + ` h ON ` + makeKeyMatch(chainIdColumn, "h", "e") + `
|
|
110
124
|
WHERE h.id IS NULL
|
|
111
125
|
)
|
|
112
126
|
INSERT INTO ` + historyTableRef + `
|
|
@@ -114,9 +128,14 @@ SELECT *, 0 AS ` + checkpointIdFieldName + `, '` + "SET" + `' as ` + changeField
|
|
|
114
128
|
FROM missing_history;`;
|
|
115
129
|
}
|
|
116
130
|
|
|
117
|
-
function backfillHistory(sql, pgSchema, table, entityIndex, ids) {
|
|
131
|
+
function backfillHistory(sql, pgSchema, table, entityIndex, chainId, ids) {
|
|
118
132
|
let idPgType = Table.getIdPgFieldType(table, pgSchema);
|
|
119
|
-
|
|
133
|
+
let chainIdColumn = Stdlib_Option.map(Table.getChainIdField(table), Table.getPgDbFieldName);
|
|
134
|
+
let params = [Table.encodeIdsToJson(table, ids)];
|
|
135
|
+
if (chainIdColumn !== undefined && chainId !== undefined) {
|
|
136
|
+
params.push(Primitive_option.valFromOption(chainId));
|
|
137
|
+
}
|
|
138
|
+
return sql.unsafe(makeBackfillHistoryQuery(pgSchema, table.tableName, entityIndex, idPgType, chainIdColumn, chainId), params, {prepare: true});
|
|
120
139
|
}
|
|
121
140
|
|
|
122
141
|
function rollback(sql, pgSchema, entityName, entityIndex, rollbackTargetCheckpointId) {
|
|
@@ -138,6 +157,8 @@ export {
|
|
|
138
157
|
maxPgTableNameLength,
|
|
139
158
|
historyTablePrefix,
|
|
140
159
|
historyTableName,
|
|
160
|
+
makeKeyColumns,
|
|
161
|
+
makeKeyMatch,
|
|
141
162
|
makePruneStaleEntityHistoryQuery,
|
|
142
163
|
pruneStaleEntityHistory,
|
|
143
164
|
makeBackfillHistoryQuery,
|
|
@@ -26,17 +26,17 @@ let fields = [
|
|
|
26
26
|
];
|
|
27
27
|
|
|
28
28
|
let table = Table.mkTable("envio_chains", undefined, [
|
|
29
|
-
Table.mkField("id", "ChainId", ChainId.schema, undefined, undefined, undefined, true, undefined, undefined, undefined, undefined, undefined),
|
|
30
|
-
Table.mkField("start_block", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
31
|
-
Table.mkField("end_block", "Int32", S$RescriptSchema.$$null(S$RescriptSchema.int), undefined, undefined, true, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
32
|
-
Table.mkField("max_reorg_depth", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
33
|
-
Table.mkField("buffer_block", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
34
|
-
Table.mkField("source_block", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
35
|
-
Table.mkField("first_event_block", "Int32", S$RescriptSchema.$$null(S$RescriptSchema.int), undefined, undefined, true, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
36
|
-
Table.mkField("ready_at", "Date", S$RescriptSchema.$$null(Utils.Schema.dbDate), undefined, undefined, true, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
37
|
-
Table.mkField("events_processed", "UInt52", S$RescriptSchema.float, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
38
|
-
Table.mkField("_is_hyper_sync", "Boolean", S$RescriptSchema.bool, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
39
|
-
Table.mkField("progress_block", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined)
|
|
29
|
+
Table.mkField("id", "ChainId", ChainId.schema, undefined, undefined, undefined, true, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
30
|
+
Table.mkField("start_block", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
31
|
+
Table.mkField("end_block", "Int32", S$RescriptSchema.$$null(S$RescriptSchema.int), undefined, undefined, true, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
32
|
+
Table.mkField("max_reorg_depth", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
33
|
+
Table.mkField("buffer_block", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
34
|
+
Table.mkField("source_block", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
35
|
+
Table.mkField("first_event_block", "Int32", S$RescriptSchema.$$null(S$RescriptSchema.int), undefined, undefined, true, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
36
|
+
Table.mkField("ready_at", "Date", S$RescriptSchema.$$null(Utils.Schema.dbDate), undefined, undefined, true, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
37
|
+
Table.mkField("events_processed", "UInt52", S$RescriptSchema.float, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
38
|
+
Table.mkField("_is_hyper_sync", "Boolean", S$RescriptSchema.bool, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
39
|
+
Table.mkField("progress_block", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined)
|
|
40
40
|
], undefined);
|
|
41
41
|
|
|
42
42
|
function initialFromConfig(chainConfig) {
|
|
@@ -235,8 +235,8 @@ let Chains = {
|
|
|
235
235
|
};
|
|
236
236
|
|
|
237
237
|
let table$1 = Table.mkTable("envio_info", undefined, [
|
|
238
|
-
Table.mkField("id", "Int32", S$RescriptSchema.int, "1", undefined, undefined, true, undefined, undefined, undefined, undefined, undefined),
|
|
239
|
-
Table.mkField("config", "String", S$RescriptSchema.string, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined)
|
|
238
|
+
Table.mkField("id", "Int32", S$RescriptSchema.int, "1", undefined, undefined, true, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
239
|
+
Table.mkField("config", "String", S$RescriptSchema.string, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined)
|
|
240
240
|
], undefined);
|
|
241
241
|
|
|
242
242
|
let undefinedTableSqlState = "42P01";
|
|
@@ -284,11 +284,11 @@ let dbSchema = S$RescriptSchema.object(s => ({
|
|
|
284
284
|
}));
|
|
285
285
|
|
|
286
286
|
let table$2 = Table.mkTable("envio_checkpoints", undefined, [
|
|
287
|
-
Table.mkField("id", "UInt64", S$RescriptSchema.bigint, undefined, undefined, undefined, true, undefined, undefined, undefined, undefined, undefined),
|
|
288
|
-
Table.mkField("chain_id", "ChainId", ChainId.schema, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
289
|
-
Table.mkField("block_number", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
290
|
-
Table.mkField("block_hash", "String", S$RescriptSchema.$$null(S$RescriptSchema.string), undefined, undefined, true, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
291
|
-
Table.mkField("events_processed", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined)
|
|
287
|
+
Table.mkField("id", "UInt64", S$RescriptSchema.bigint, undefined, undefined, undefined, true, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
288
|
+
Table.mkField("chain_id", "ChainId", ChainId.schema, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
289
|
+
Table.mkField("block_number", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
290
|
+
Table.mkField("block_hash", "String", S$RescriptSchema.$$null(S$RescriptSchema.string), undefined, undefined, true, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
291
|
+
Table.mkField("events_processed", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined)
|
|
292
292
|
], undefined);
|
|
293
293
|
|
|
294
294
|
function makeGetReorgCheckpointsQuery(pgSchema) {
|
|
@@ -412,19 +412,19 @@ let schema = S$RescriptSchema.schema(s => ({
|
|
|
412
412
|
}));
|
|
413
413
|
|
|
414
414
|
let table$3 = Table.mkTable("raw_events", undefined, [
|
|
415
|
-
Table.mkField("chain_id", "ChainId", ChainId.schema, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
416
|
-
Table.mkField("event_id", "UInt64", S$RescriptSchema.bigint, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
417
|
-
Table.mkField("event_name", "String", S$RescriptSchema.string, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
418
|
-
Table.mkField("contract_name", "String", S$RescriptSchema.string, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
419
|
-
Table.mkField("block_number", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
420
|
-
Table.mkField("log_index", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
421
|
-
Table.mkField("src_address", "String", Address.schema, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
422
|
-
Table.mkField("block_hash", "String", S$RescriptSchema.string, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
423
|
-
Table.mkField("block_timestamp", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
424
|
-
Table.mkField("block_fields", "Json", S$RescriptSchema.json(false), undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
425
|
-
Table.mkField("transaction_fields", "Json", S$RescriptSchema.json(false), undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
426
|
-
Table.mkField("params", "Json", S$RescriptSchema.json(false), undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
427
|
-
Table.mkField("serial", "BigSerial", S$RescriptSchema.$$null(S$RescriptSchema.bigint), undefined, undefined, true, true, undefined, undefined, undefined, undefined, undefined)
|
|
415
|
+
Table.mkField("chain_id", "ChainId", ChainId.schema, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
416
|
+
Table.mkField("event_id", "UInt64", S$RescriptSchema.bigint, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
417
|
+
Table.mkField("event_name", "String", S$RescriptSchema.string, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
418
|
+
Table.mkField("contract_name", "String", S$RescriptSchema.string, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
419
|
+
Table.mkField("block_number", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
420
|
+
Table.mkField("log_index", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
421
|
+
Table.mkField("src_address", "String", Address.schema, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
422
|
+
Table.mkField("block_hash", "String", S$RescriptSchema.string, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
423
|
+
Table.mkField("block_timestamp", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
424
|
+
Table.mkField("block_fields", "Json", S$RescriptSchema.json(false), undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
425
|
+
Table.mkField("transaction_fields", "Json", S$RescriptSchema.json(false), undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
426
|
+
Table.mkField("params", "Json", S$RescriptSchema.json(false), undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
427
|
+
Table.mkField("serial", "BigSerial", S$RescriptSchema.$$null(S$RescriptSchema.bigint), undefined, undefined, true, true, undefined, undefined, undefined, undefined, undefined, undefined)
|
|
428
428
|
], undefined);
|
|
429
429
|
|
|
430
430
|
let RawEvents = {
|
package/src/db/Table.res
CHANGED
|
@@ -43,6 +43,10 @@ type field = {
|
|
|
43
43
|
isNullable: bool,
|
|
44
44
|
isPrimaryKey: bool,
|
|
45
45
|
isIndex: bool,
|
|
46
|
+
// The chain-id column appended to a per-chain entity's table. It's part of
|
|
47
|
+
// the primary key but not of the entity schema the handlers see, so the
|
|
48
|
+
// read paths skip it and the write path stamps it from the flush scope.
|
|
49
|
+
isChainId: bool,
|
|
46
50
|
linkedEntity: option<string>,
|
|
47
51
|
defaultValue: option<string>,
|
|
48
52
|
description: option<string>,
|
|
@@ -71,6 +75,7 @@ let mkField = (
|
|
|
71
75
|
~isNullable=false,
|
|
72
76
|
~isPrimaryKey=false,
|
|
73
77
|
~isIndex=false,
|
|
78
|
+
~isChainId=false,
|
|
74
79
|
~linkedEntity=?,
|
|
75
80
|
~description=?,
|
|
76
81
|
~postgresDbName=?,
|
|
@@ -84,6 +89,7 @@ let mkField = (
|
|
|
84
89
|
isNullable,
|
|
85
90
|
isPrimaryKey,
|
|
86
91
|
isIndex,
|
|
92
|
+
isChainId,
|
|
87
93
|
linkedEntity,
|
|
88
94
|
defaultValue: default,
|
|
89
95
|
description,
|
|
@@ -252,6 +258,16 @@ let getDerivedFromFields = table =>
|
|
|
252
258
|
}
|
|
253
259
|
)
|
|
254
260
|
|
|
261
|
+
// The chain-id column of a per-chain entity table. None for cross-chain
|
|
262
|
+
// entities and every internal table.
|
|
263
|
+
let getChainIdField = (table): option<field> =>
|
|
264
|
+
table.fields->Array.findMap(field =>
|
|
265
|
+
switch field {
|
|
266
|
+
| Field({isChainId: true} as field) => Some(field)
|
|
267
|
+
| _ => None
|
|
268
|
+
}
|
|
269
|
+
)
|
|
270
|
+
|
|
255
271
|
let getFieldByName = (table, fieldName) =>
|
|
256
272
|
table.fields->Array.find(field => field->getUserDefinedFieldName === fieldName)
|
|
257
273
|
|
|
@@ -334,12 +350,13 @@ let queryFields: table => dict<queryField> = Utils.WeakMap.memoize(table => {
|
|
|
334
350
|
// Parses rows into entity objects keyed by API field names (the camelCase
|
|
335
351
|
// record field names are type-level only), reading each value from the
|
|
336
352
|
// row key produced by ~rowFieldName.
|
|
337
|
-
let makeRowsSchema = (table, ~rowFieldName) =>
|
|
353
|
+
let makeRowsSchema = (table, ~rowFieldName, ~skipChainId=false) =>
|
|
338
354
|
S.array(
|
|
339
355
|
S.object(s => {
|
|
340
356
|
let dict = Dict.make()
|
|
341
357
|
table.fields->Array.forEach(field =>
|
|
342
358
|
switch field {
|
|
359
|
+
| Field({isChainId: true}) if skipChainId => ()
|
|
343
360
|
| Field(field) =>
|
|
344
361
|
dict->Dict.set(field->getApiFieldName, s.field(field->rowFieldName, field.fieldSchema))
|
|
345
362
|
| DerivedFrom(_) => ()
|
|
@@ -361,6 +378,12 @@ let pgRowsSchema: table => S.t<array<unknown>> = Utils.WeakMap.memoize(table =>
|
|
|
361
378
|
table->makeRowsSchema(~rowFieldName=getPgDbFieldName)
|
|
362
379
|
)
|
|
363
380
|
|
|
381
|
+
// Rows shaped like the entity the handlers see: the chain-id column is dropped,
|
|
382
|
+
// since the chain is already known from the scope the rows were loaded for.
|
|
383
|
+
let pgEntityRowsSchema: table => S.t<array<unknown>> = Utils.WeakMap.memoize(table =>
|
|
384
|
+
table->makeRowsSchema(~rowFieldName=getPgDbFieldName, ~skipChainId=true)
|
|
385
|
+
)
|
|
386
|
+
|
|
364
387
|
exception NonExistingTableField(string)
|
|
365
388
|
|
|
366
389
|
/*
|