envio 3.5.1 → 3.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/evm.schema.json +7 -0
  2. package/fuel.schema.json +7 -0
  3. package/index.d.ts +33 -10
  4. package/package.json +6 -6
  5. package/src/ChainState.res +22 -4
  6. package/src/ChainState.res.mjs +20 -3
  7. package/src/ChainState.resi +3 -0
  8. package/src/Config.res +60 -2
  9. package/src/Config.res.mjs +49 -13
  10. package/src/EffectState.res +2 -2
  11. package/src/EffectState.res.mjs +2 -2
  12. package/src/EntityTables.res +32 -0
  13. package/src/EntityTables.res.mjs +44 -0
  14. package/src/Envio.res +7 -7
  15. package/src/Envio.res.mjs +5 -6
  16. package/src/Hasura.res +37 -3
  17. package/src/Hasura.res.mjs +23 -5
  18. package/src/InMemoryStore.res +48 -8
  19. package/src/InMemoryStore.res.mjs +37 -7
  20. package/src/IndexerState.res +30 -24
  21. package/src/IndexerState.res.mjs +20 -35
  22. package/src/IndexerState.resi +8 -6
  23. package/src/Internal.res +29 -11
  24. package/src/Internal.res.mjs +27 -10
  25. package/src/LoadLayer.res +39 -8
  26. package/src/LoadLayer.res.mjs +36 -9
  27. package/src/LoadLayer.resi +2 -0
  28. package/src/Metrics.res +5 -5
  29. package/src/Metrics.res.mjs +5 -1
  30. package/src/Persistence.res +12 -1
  31. package/src/PgStorage.res +183 -37
  32. package/src/PgStorage.res.mjs +149 -34
  33. package/src/PruneStaleHistory.res +1 -0
  34. package/src/PruneStaleHistory.res.mjs +2 -1
  35. package/src/Sink.res +3 -3
  36. package/src/Sink.res.mjs +2 -2
  37. package/src/TestIndexer.res +128 -17
  38. package/src/TestIndexer.res.mjs +75 -9
  39. package/src/UserContext.res +19 -4
  40. package/src/UserContext.res.mjs +15 -9
  41. package/src/Writing.res +8 -16
  42. package/src/Writing.res.mjs +10 -10
  43. package/src/bindings/ClickHouse.res +40 -4
  44. package/src/bindings/ClickHouse.res.mjs +37 -5
  45. package/src/db/EntityHistory.res +64 -21
  46. package/src/db/EntityHistory.res.mjs +43 -22
  47. package/src/db/InternalTable.res.mjs +31 -31
  48. package/src/db/Table.res +24 -1
  49. package/src/db/Table.res.mjs +26 -4
  50. package/src/sources/SourceManager.res +6 -12
  51. package/src/sources/SourceManager.res.mjs +8 -5
  52. package/svm.schema.json +7 -0
@@ -248,15 +248,21 @@ type setUpdatesCache = {
248
248
 
249
249
  let setUpdatesOrThrow = async (
250
250
  client,
251
- ~cache: Utils.WeakMap.t<Internal.entityConfig, setUpdatesCache>,
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 {convertOrThrow, tableName} = switch cache->Utils.WeakMap.get(entityConfig) {
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->Utils.WeakMap.set(entityConfig, cached)->ignore
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 \`${Table.idFieldName}\`
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 cached = cache.get(entityConfig);
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.set(entityConfig, cached$1);
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 \`` + Table.idFieldName + `\`
398
+ LIMIT 1 BY ` + dedupKey + `
367
399
  )
368
400
  WHERE \`` + EntityHistory.changeFieldName + `\` = '` + "SET" + `'`;
369
401
  }
@@ -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
- let makePruneStaleEntityHistoryQuery = (~entityName, ~entityIndex, ~pgSchema) => {
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 t.id, MAX(t.${checkpointIdFieldName}) AS keep_checkpoint_id
90
- FROM ${historyTableRef} t WHERE t.${checkpointIdFieldName} <= $1
91
- GROUP BY t.id
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.id = a.id
96
- AND (
97
- d.${checkpointIdFieldName} < a.keep_checkpoint_id
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
- let makeBackfillHistoryQuery = (~pgSchema, ~entityName, ~entityIndex, ~idPgType) => {
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.id = e.id
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(~entityName=table.tableName, ~entityIndex, ~pgSchema, ~idPgType),
151
- [table->Table.encodeIdsToJson(ids)]->(Utils.magic: array<JSON.t> => unknown),
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 makePruneStaleEntityHistoryQuery(entityName, entityIndex, pgSchema) {
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 t.id, MAX(t.` + checkpointIdFieldName + `) AS keep_checkpoint_id
78
- FROM ` + historyTableRef + ` t WHERE t.` + checkpointIdFieldName + ` <= $1
79
- GROUP BY t.id
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.id = a.id
84
- AND (
85
- d.` + checkpointIdFieldName + ` < a.keep_checkpoint_id
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.id = e.id
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
- return sql.unsafe(makeBackfillHistoryQuery(pgSchema, table.tableName, entityIndex, idPgType), [Table.encodeIdsToJson(table, ids)], {prepare: true});
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
  /*