envio 3.5.0-alpha.3 → 3.5.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/package.json +6 -6
  2. package/src/Batch.res +8 -8
  3. package/src/Batch.res.mjs +4 -3
  4. package/src/BatchProcessing.res +17 -7
  5. package/src/BatchProcessing.res.mjs +8 -4
  6. package/src/ChainFetching.res +20 -19
  7. package/src/ChainFetching.res.mjs +14 -14
  8. package/src/ChainId.res +58 -0
  9. package/src/ChainId.res.mjs +49 -0
  10. package/src/ChainId.resi +27 -0
  11. package/src/ChainMap.res +12 -26
  12. package/src/ChainMap.res.mjs +5 -41
  13. package/src/ChainMap.resi +5 -20
  14. package/src/ChainMetadata.res +1 -1
  15. package/src/ChainMetadata.res.mjs +3 -2
  16. package/src/ChainState.res +35 -20
  17. package/src/ChainState.res.mjs +34 -11
  18. package/src/ChainState.resi +2 -0
  19. package/src/Config.res +41 -24
  20. package/src/Config.res.mjs +17 -14
  21. package/src/ContractRegisterContext.res +1 -1
  22. package/src/ContractRegisterContext.res.mjs +1 -1
  23. package/src/Core.res +1 -0
  24. package/src/CrossChainState.res +61 -16
  25. package/src/CrossChainState.res.mjs +37 -13
  26. package/src/CrossChainState.resi +3 -2
  27. package/src/Env.res +0 -3
  28. package/src/Env.res.mjs +0 -3
  29. package/src/EventConfigBuilder.res +1 -1
  30. package/src/EventProcessing.res +4 -4
  31. package/src/EventProcessing.res.mjs +5 -5
  32. package/src/FetchState.res +261 -89
  33. package/src/FetchState.res.mjs +187 -63
  34. package/src/FinalizeBackfill.res +33 -6
  35. package/src/FinalizeBackfill.res.mjs +19 -1
  36. package/src/HandlerRegister.res +8 -8
  37. package/src/HandlerRegister.res.mjs +5 -4
  38. package/src/HandlerRegister.resi +1 -1
  39. package/src/InMemoryStore.res +4 -0
  40. package/src/InMemoryStore.res.mjs +1 -1
  41. package/src/IndexerLoop.res +9 -2
  42. package/src/IndexerLoop.res.mjs +8 -1
  43. package/src/IndexerState.res +41 -24
  44. package/src/IndexerState.res.mjs +36 -17
  45. package/src/IndexerState.resi +9 -7
  46. package/src/Internal.res +19 -17
  47. package/src/Internal.res.mjs +18 -9
  48. package/src/LoadLayer.res +1 -1
  49. package/src/LoadLayer.res.mjs +2 -1
  50. package/src/LogSelection.res +2 -2
  51. package/src/Main.res +7 -11
  52. package/src/Main.res.mjs +4 -6
  53. package/src/Metrics.res +6 -6
  54. package/src/Metrics.res.mjs +4 -3
  55. package/src/Persistence.res +15 -7
  56. package/src/PgStorage.res +284 -258
  57. package/src/PgStorage.res.mjs +181 -208
  58. package/src/RawEvent.res +1 -2
  59. package/src/RawEvent.res.mjs +1 -1
  60. package/src/Rollback.res +18 -20
  61. package/src/Rollback.res.mjs +9 -9
  62. package/src/RollbackCommit.res +2 -2
  63. package/src/RollbackCommit.res.mjs +3 -3
  64. package/src/SafeCheckpointTracking.res +2 -2
  65. package/src/SimulateDeadInputTracker.res +17 -21
  66. package/src/SimulateDeadInputTracker.res.mjs +9 -8
  67. package/src/SimulateItems.res +8 -10
  68. package/src/SimulateItems.res.mjs +9 -10
  69. package/src/Sink.res +2 -2
  70. package/src/Sink.res.mjs +3 -2
  71. package/src/TestIndexer.res +11 -14
  72. package/src/TestIndexer.res.mjs +14 -10
  73. package/src/UserContext.res +2 -2
  74. package/src/UserContext.res.mjs +2 -1
  75. package/src/Utils.res +0 -4
  76. package/src/Utils.res.mjs +1 -6
  77. package/src/bindings/ClickHouse.res +20 -2
  78. package/src/bindings/ClickHouse.res.mjs +25 -14
  79. package/src/db/EntityHistory.res +1 -1
  80. package/src/db/IndexCatalog.res +248 -0
  81. package/src/db/IndexCatalog.res.mjs +222 -0
  82. package/src/db/IndexDefinition.res +131 -0
  83. package/src/db/IndexDefinition.res.mjs +138 -0
  84. package/src/db/IndexManager.res +145 -0
  85. package/src/db/IndexManager.res.mjs +130 -0
  86. package/src/db/InternalTable.res +50 -26
  87. package/src/db/InternalTable.res.mjs +32 -24
  88. package/src/db/Table.res +11 -1
  89. package/src/db/Table.res.mjs +9 -4
  90. package/src/sources/AddressSet.res +5 -4
  91. package/src/sources/AddressStore.res +25 -14
  92. package/src/sources/AddressStore.res.mjs +10 -12
  93. package/src/sources/Evm.res +2 -2
  94. package/src/sources/Evm.res.mjs +1 -1
  95. package/src/sources/EvmChain.res +3 -3
  96. package/src/sources/EvmChain.res.mjs +3 -3
  97. package/src/sources/EvmHyperSyncSource.res +7 -7
  98. package/src/sources/EvmHyperSyncSource.res.mjs +6 -6
  99. package/src/sources/Fuel.res +2 -2
  100. package/src/sources/Fuel.res.mjs +1 -1
  101. package/src/sources/FuelHyperSyncClient.res +3 -0
  102. package/src/sources/FuelHyperSyncClient.res.mjs +1 -0
  103. package/src/sources/FuelHyperSyncSource.res +5 -7
  104. package/src/sources/FuelHyperSyncSource.res.mjs +5 -5
  105. package/src/sources/HyperSync.resi +2 -2
  106. package/src/sources/HyperSyncClient.res +5 -0
  107. package/src/sources/HyperSyncClient.res.mjs +1 -0
  108. package/src/sources/RpcSource.res +13 -14
  109. package/src/sources/RpcSource.res.mjs +13 -12
  110. package/src/sources/SimulateSource.res +42 -15
  111. package/src/sources/SimulateSource.res.mjs +27 -9
  112. package/src/sources/Source.res +1 -5
  113. package/src/sources/SourceManager.res +12 -12
  114. package/src/sources/SourceManager.res.mjs +11 -10
  115. package/src/sources/SourceManager.resi +2 -2
  116. package/src/sources/Svm.res +4 -5
  117. package/src/sources/Svm.res.mjs +5 -4
  118. package/src/sources/SvmHyperSyncClient.res +3 -0
  119. package/src/sources/SvmHyperSyncClient.res.mjs +1 -0
  120. package/src/sources/SvmHyperSyncSource.res +4 -4
  121. package/src/sources/SvmHyperSyncSource.res.mjs +3 -3
  122. package/src/tui/Tui.res +1 -1
  123. package/src/tui/Tui.res.mjs +2 -1
  124. package/src/tui/components/CustomHooks.res +5 -4
  125. package/src/tui/components/CustomHooks.res.mjs +4 -2
  126. package/src/db/IndexRegistry.res +0 -219
  127. package/src/db/IndexRegistry.res.mjs +0 -195
package/src/PgStorage.res CHANGED
@@ -20,105 +20,6 @@ let makeClient = () => {
20
20
  )
21
21
  }
22
22
 
23
- let makeCreateIndexQuery = (~tableName, ~indexFields, ~pgSchema) => {
24
- let indexName = tableName ++ "_" ++ indexFields->Array.joinUnsafe("_")
25
-
26
- // Case for indexer before envio@2.28
27
- let index = indexFields->Array.map(idx => `"${idx}"`)->Array.joinUnsafe(", ")
28
- `CREATE INDEX IF NOT EXISTS "${indexName}" ON "${pgSchema}"."${tableName}"(${index});`
29
- }
30
-
31
- let directionToSql = (direction: Table.indexFieldDirection) =>
32
- switch direction {
33
- | Asc => ""
34
- | Desc => " DESC"
35
- }
36
-
37
- let directionToIndexName = (direction: Table.indexFieldDirection) =>
38
- switch direction {
39
- | Asc => ""
40
- | Desc => "_desc"
41
- }
42
-
43
- let makeCreateCompositeIndexQuery = (
44
- ~tableName,
45
- ~indexFields: array<Table.compositeIndexField>,
46
- ~pgSchema,
47
- ) => {
48
- let indexName =
49
- tableName ++
50
- "_" ++
51
- indexFields
52
- ->Array.map(f => f.fieldName ++ directionToIndexName(f.direction))
53
- ->Array.joinUnsafe("_")
54
- let index =
55
- indexFields
56
- ->Array.map(f => `"${f.fieldName}"${directionToSql(f.direction)}`)
57
- ->Array.joinUnsafe(", ")
58
- `CREATE INDEX IF NOT EXISTS "${indexName}" ON "${pgSchema}"."${tableName}"(${index});`
59
- }
60
-
61
- let makeCreateTableIndexesQuery = (table: Table.table, ~pgSchema) => {
62
- let tableName = table.tableName
63
- let createIndex = indexField =>
64
- makeCreateIndexQuery(~tableName, ~indexFields=[indexField], ~pgSchema)
65
- let createCompositeIndex = indexFields => {
66
- makeCreateCompositeIndexQuery(~tableName, ~indexFields, ~pgSchema)
67
- }
68
-
69
- let singleIndexes = table->Table.getSingleIndexes
70
- let compositeIndexes = table->Table.getCompositeIndexes
71
-
72
- singleIndexes->Array.map(createIndex)->Array.joinUnsafe("\n") ++
73
- compositeIndexes->Array.map(createCompositeIndex)->Array.joinUnsafe("\n")
74
- }
75
-
76
- // One index the schema promises: an `@index` field, a composite index, or the
77
- // index backing a derived relationship. Carries enough to derive its name, its
78
- // DDL and its registry key, so the deferred creation at the end of backfill and
79
- // the registry stay described by the same value.
80
- type schemaIndex = {
81
- tableName: string,
82
- indexFields: array<Table.compositeIndexField>,
83
- }
84
-
85
- // `<Entity>_<column>` for a single column, with each further column appended in
86
- // order (and `_desc` where it applies). The full description, before Postgres'
87
- // identifier limit is applied — `validateIndexNamesOrThrow` compares these, so
88
- // it can tell two descriptions apart after they truncate to the same name.
89
- let schemaIndexDescription = ({tableName, indexFields}: schemaIndex) =>
90
- tableName ++
91
- "_" ++
92
- indexFields
93
- ->Array.map(f => f.fieldName ++ directionToIndexName(f.direction))
94
- ->Array.joinUnsafe("_")
95
-
96
- // The identifier Postgres actually stores. Automatic and schema-defined indexes
97
- // share it, so both are named the same way and both stay reachable through
98
- // `CREATE INDEX IF NOT EXISTS`.
99
- let schemaIndexName = schemaIndex =>
100
- schemaIndex->schemaIndexDescription->IndexRegistry.truncateIndexName
101
-
102
- let schemaIndexKey = ({tableName, indexFields}: schemaIndex) =>
103
- IndexRegistry.makeKey(
104
- ~tableName,
105
- ~columns=indexFields->IndexRegistry.fromIndexFields,
106
- ~method=IndexRegistry.btree,
107
- )
108
-
109
- // Plain DDL, not CONCURRENTLY: it builds from a single table scan instead of
110
- // two, and the SHARE lock it takes blocks writes but not reads, so queries keep
111
- // being served while it runs. The indexer is the only writer, and both callers
112
- // are happy to wait on it.
113
- let makeCreateSchemaIndexQuery = (schemaIndex, ~pgSchema) => {
114
- let {tableName, indexFields} = schemaIndex
115
- let columns =
116
- indexFields
117
- ->Array.map(f => `"${f.fieldName}"${directionToSql(f.direction)}`)
118
- ->Array.joinUnsafe(", ")
119
- `CREATE INDEX IF NOT EXISTS "${schemaIndex->schemaIndexName}" ON "${pgSchema}"."${tableName}"(${columns});`
120
- }
121
-
122
23
  let formatSeconds = (timeRef: Performance.timeRef) =>
123
24
  (Math.round(timeRef->Performance.secondsSince *. 100.) /. 100.)->Float.toString
124
25
 
@@ -127,35 +28,15 @@ let formatSeconds = (timeRef: Performance.timeRef) =>
127
28
  // stalled-looking indexer is explainable from the logs alone.
128
29
  let slowOnLargeDatabaseNotice = "This can take a long time on a large database."
129
30
 
130
- // An index Postgres reports as INVALID still owns its name, so
131
- // `CREATE INDEX IF NOT EXISTS` would quietly skip it and leave the query
132
- // unserved. REINDEX rebuilds it from its own stored definition — a repair, not
133
- // a replacement — which is right only when that definition is the one we
134
- // wanted. Runs inside a transaction, unlike REINDEX CONCURRENTLY.
135
- let makeReindexQuery = (~pgSchema, ~indexName) => `REINDEX INDEX "${pgSchema}"."${indexName}";`
136
-
137
- // A name taken by an invalid index the indexer didn't create isn't ours to
138
- // touch: rebuilding it reuses its own definition, so it would come back valid
139
- // but still not serve the query, and dropping it could take out a unique
140
- // constraint someone else relies on.
141
- let nameCollisionError = (~indexName, ~pgSchema) =>
142
- Utils.Error.make(
143
- `Cannot create the index "${indexName}" in schema "${pgSchema}". An index of that name already exists and PostgreSQL reports it as invalid, but it isn't one the indexer created — it covers different columns, or it is a unique or partial index — so it is left untouched. Drop or repair it by hand, then restart the indexer.`,
144
- )
145
-
146
- let makeSingleColumnSchemaIndex = (~tableName, ~column): schemaIndex => {
147
- tableName,
148
- indexFields: [{fieldName: column, direction: Table.Asc}],
149
- }
150
-
151
- // Every index the entity schema promises. Deferred past the initial DDL and
31
+ // Every index the entity schema promises: an `@index` field, a composite index,
32
+ // or the index backing a derived relationship. Deferred past the initial DDL and
152
33
  // created in one transaction once backfill completes, so a resumed indexer that
153
34
  // reports itself ready always has all of them.
154
35
  //
155
36
  // `entities` is the Postgres-backed set, and every `@derivedFrom` target within
156
37
  // it resolves: config parsing rejects a Postgres entity deriving from one that
157
38
  // isn't in Postgres (`validate_relationship_storage`).
158
- let getSchemaIndexes = (~entities: array<Internal.entityConfig>): array<schemaIndex> => {
39
+ let getSchemaIndexes = (~entities: array<Internal.entityConfig>): array<IndexDefinition.t> => {
159
40
  let derivedSchema = Schema.make(entities->Array.map(e => e.table))
160
41
  let all = []
161
42
 
@@ -163,12 +44,14 @@ let getSchemaIndexes = (~entities: array<Internal.entityConfig>): array<schemaIn
163
44
  table
164
45
  ->Table.getSingleIndexes
165
46
  ->Array.forEach(column =>
166
- all->Array.push(makeSingleColumnSchemaIndex(~tableName=table.tableName, ~column))->ignore
47
+ all->Array.push(IndexDefinition.single(~tableName=table.tableName, ~column))->ignore
167
48
  )
168
49
  table
169
50
  ->Table.getCompositeIndexes
170
51
  ->Array.forEach(indexFields =>
171
- all->Array.push({tableName: table.tableName, indexFields})->ignore
52
+ all
53
+ ->Array.push(IndexDefinition.fromIndexFields(~tableName=table.tableName, ~indexFields))
54
+ ->ignore
172
55
  )
173
56
  })
174
57
 
@@ -179,9 +62,7 @@ let getSchemaIndexes = (~entities: array<Internal.entityConfig>): array<schemaIn
179
62
  let column =
180
63
  derivedSchema->Schema.getDerivedFromPgFieldName(derivedFromField)->Utils.unwrapResultExn
181
64
  all
182
- ->Array.push(
183
- makeSingleColumnSchemaIndex(~tableName=derivedFromField.derivedFromEntity, ~column),
184
- )
65
+ ->Array.push(IndexDefinition.single(~tableName=derivedFromField.derivedFromEntity, ~column))
185
66
  ->ignore
186
67
  })
187
68
  )
@@ -189,8 +70,8 @@ let getSchemaIndexes = (~entities: array<Internal.entityConfig>): array<schemaIn
189
70
  // An `@index` field and a derived relationship pointing at it describe the
190
71
  // same index, so the list is deduped on identity rather than on name.
191
72
  let seen = Utils.Set.make()
192
- all->Array.filter(schemaIndex => {
193
- let key = schemaIndex->schemaIndexKey
73
+ all->Array.filter(definition => {
74
+ let key = definition->IndexDefinition.key
194
75
  if seen->Utils.Set.has(key) {
195
76
  false
196
77
  } else {
@@ -200,7 +81,12 @@ let getSchemaIndexes = (~entities: array<Internal.entityConfig>): array<schemaIn
200
81
  })
201
82
  }
202
83
 
203
- let makeCreateTableQuery = (table: Table.table, ~pgSchema, ~isNumericArrayAsText) => {
84
+ let makeCreateTableQuery = (
85
+ table: Table.table,
86
+ ~pgSchema,
87
+ ~isNumericArrayAsText,
88
+ ~chainIdMode: ChainId.mode=Int32,
89
+ ) => {
204
90
  let fieldsMapped =
205
91
  table
206
92
  ->Table.getFields
@@ -210,6 +96,7 @@ let makeCreateTableQuery = (table: Table.table, ~pgSchema, ~isNumericArrayAsText
210
96
 
211
97
  {
212
98
  `"${fieldName}" ${Table.getPgFieldType(
99
+ ~chainIdMode,
213
100
  ~fieldType,
214
101
  ~pgSchema,
215
102
  ~isArray,
@@ -314,6 +201,7 @@ let makeInitializeTransaction = (
314
201
  // initial DDL creates only tables, primary keys, views and chain rows; the
315
202
  // rest is created by `finalizeBackfill` before the indexer reports ready.
316
203
  ~deferSchemaIndexes=false,
204
+ ~chainIdMode: ChainId.mode=Int32,
317
205
  ) => {
318
206
  let generalTables = [
319
207
  InternalTable.Chains.table,
@@ -329,7 +217,6 @@ let makeInitializeTransaction = (
329
217
  })
330
218
 
331
219
  let schemaIndexes = getSchemaIndexes(~entities)
332
- IndexRegistry.validateIndexNamesOrThrow(schemaIndexes->Array.map(schemaIndexDescription))
333
220
 
334
221
  let query = ref(
335
222
  (
@@ -360,13 +247,13 @@ GRANT ALL ON SCHEMA "${pgSchema}" TO public;`,
360
247
  query :=
361
248
  query.contents ++
362
249
  "\n" ++
363
- makeCreateTableQuery(table, ~pgSchema, ~isNumericArrayAsText=isHasuraEnabled)
250
+ makeCreateTableQuery(table, ~pgSchema, ~isNumericArrayAsText=isHasuraEnabled, ~chainIdMode)
364
251
  })
365
252
 
366
253
  // Then batch all indexes (better performance when tables exist)
367
254
  if !deferSchemaIndexes {
368
- schemaIndexes->Array.forEach(schemaIndex => {
369
- query := query.contents ++ "\n" ++ makeCreateSchemaIndexQuery(schemaIndex, ~pgSchema)
255
+ schemaIndexes->Array.forEach(definition => {
256
+ query := query.contents ++ "\n" ++ definition->IndexDefinition.makeCreateQuery(~pgSchema)
370
257
  })
371
258
  }
372
259
 
@@ -478,9 +365,15 @@ let makeLoadAllQuery = (~pgSchema, ~tableName) => {
478
365
  `SELECT * FROM "${pgSchema}"."${tableName}";`
479
366
  }
480
367
 
481
- let makeInsertUnnestSetQuery = (~pgSchema, ~table: Table.table, ~itemSchema, ~isRawEvents) => {
368
+ let makeInsertUnnestSetQuery = (
369
+ ~pgSchema,
370
+ ~table: Table.table,
371
+ ~itemSchema,
372
+ ~isRawEvents,
373
+ ~chainIdMode: ChainId.mode=Int32,
374
+ ) => {
482
375
  let {quotedFieldNames, quotedNonPrimaryFieldNames, arrayFieldTypes} =
483
- table->Table.toSqlParams(~schema=itemSchema, ~pgSchema)
376
+ table->Table.toSqlParams(~schema=itemSchema, ~pgSchema, ~chainIdMode)
484
377
 
485
378
  let primaryKeyFieldNames = Table.getPgPrimaryKeyFieldNames(table)
486
379
 
@@ -508,9 +401,15 @@ SELECT * FROM unnest(${arrayFieldTypes
508
401
  } ++ ";"
509
402
  }
510
403
 
511
- let makeInsertValuesSetQuery = (~pgSchema, ~table: Table.table, ~itemSchema, ~itemsCount) => {
404
+ let makeInsertValuesSetQuery = (
405
+ ~pgSchema,
406
+ ~table: Table.table,
407
+ ~itemSchema,
408
+ ~itemsCount,
409
+ ~chainIdMode: ChainId.mode=Int32,
410
+ ) => {
512
411
  let {quotedFieldNames, quotedNonPrimaryFieldNames} =
513
- table->Table.toSqlParams(~schema=itemSchema, ~pgSchema)
412
+ table->Table.toSqlParams(~schema=itemSchema, ~pgSchema, ~chainIdMode)
514
413
 
515
414
  let primaryKeyFieldNames = Table.getPgPrimaryKeyFieldNames(table)
516
415
  let fieldsCount = quotedFieldNames->Array.length
@@ -553,8 +452,14 @@ VALUES${placeholders.contents}` ++
553
452
  // Constants for chunking
554
453
  let maxItemsPerQuery = 500
555
454
 
556
- let makeTableBatchSetQuery = (~pgSchema, ~table: Table.table, ~itemSchema: S.t<'item>) => {
557
- let {dbSchema, hasArrayField} = table->Table.toSqlParams(~schema=itemSchema, ~pgSchema)
455
+ let makeTableBatchSetQuery = (
456
+ ~pgSchema,
457
+ ~table: Table.table,
458
+ ~itemSchema: S.t<'item>,
459
+ ~chainIdMode: ChainId.mode=Int32,
460
+ ) => {
461
+ let {dbSchema, hasArrayField} =
462
+ table->Table.toSqlParams(~schema=itemSchema, ~pgSchema, ~chainIdMode)
558
463
 
559
464
  // Should move this to a better place
560
465
  // We need it for the isRawEvents check in makeTableBatchSet
@@ -578,7 +483,7 @@ let makeTableBatchSetQuery = (~pgSchema, ~table: Table.table, ~itemSchema: S.t<'
578
483
 
579
484
  if (isRawEvents || !hasArrayField) && !isHistoryUpdate {
580
485
  {
581
- "query": makeInsertUnnestSetQuery(~pgSchema, ~table, ~itemSchema, ~isRawEvents),
486
+ "query": makeInsertUnnestSetQuery(~pgSchema, ~table, ~itemSchema, ~isRawEvents, ~chainIdMode),
582
487
  "convertOrThrow": S.compile(
583
488
  S.unnest(dbSchema),
584
489
  ~input=Value,
@@ -595,6 +500,7 @@ let makeTableBatchSetQuery = (~pgSchema, ~table: Table.table, ~itemSchema: S.t<'
595
500
  ~table,
596
501
  ~itemSchema,
597
502
  ~itemsCount=maxItemsPerQuery,
503
+ ~chainIdMode,
598
504
  ),
599
505
  "convertOrThrow": S.compile(
600
506
  S.unnest(itemSchema)->S.preprocess(_ => {
@@ -683,7 +589,14 @@ let classifyWriteError = (~specificError: ref<option<exn>>, ~table: Table.table,
683
589
 
684
590
  // WeakMap for caching table batch set queries
685
591
  let setQueryCache = Utils.WeakMap.make()
686
- let setOrThrow = async (sql, ~items, ~table: Table.table, ~itemSchema, ~pgSchema) => {
592
+ let setOrThrow = async (
593
+ sql,
594
+ ~items,
595
+ ~table: Table.table,
596
+ ~itemSchema,
597
+ ~pgSchema,
598
+ ~chainIdMode: ChainId.mode=Int32,
599
+ ) => {
687
600
  if items->Array.length === 0 {
688
601
  ()
689
602
  } else {
@@ -695,6 +608,7 @@ let setOrThrow = async (sql, ~items, ~table: Table.table, ~itemSchema, ~pgSchema
695
608
  ~pgSchema,
696
609
  ~table,
697
610
  ~itemSchema=itemSchema->S.toUnknown,
611
+ ~chainIdMode,
698
612
  )
699
613
  setQueryCache->Utils.WeakMap.set(table, newQuery)->ignore
700
614
  newQuery
@@ -715,7 +629,13 @@ let setOrThrow = async (sql, ~items, ~table: Table.table, ~itemSchema, ~pgSchema
715
629
  let response = isFullChunk
716
630
  ? sql->Postgres.preparedUnsafe(data["query"], params)
717
631
  : sql->Postgres.unpreparedUnsafe(
718
- makeInsertValuesSetQuery(~pgSchema, ~table, ~itemSchema, ~itemsCount=chunkSize),
632
+ makeInsertValuesSetQuery(
633
+ ~pgSchema,
634
+ ~table,
635
+ ~itemSchema,
636
+ ~itemsCount=chunkSize,
637
+ ~chainIdMode,
638
+ ),
719
639
  params,
720
640
  )
721
641
  responses->Array.push(response)->ignore
@@ -969,6 +889,7 @@ let rec writeBatch = async (
969
889
  ~escapeTables=?,
970
890
  ) => {
971
891
  try {
892
+ let chainIdMode = config.chainIdMode
972
893
  let shouldSaveHistory = config->Config.shouldSaveHistory(~isInReorgThreshold)
973
894
 
974
895
  let specificError = ref(None)
@@ -983,7 +904,7 @@ let rec writeBatch = async (
983
904
  | Internal.Event(_) =>
984
905
  let coordinate = `${item
985
906
  ->Internal.getItemChainId
986
- ->Int.toString}-${item
907
+ ->ChainId.toString}-${item
987
908
  ->Internal.getItemBlockNumber
988
909
  ->Int.toString}-${item->Internal.getItemLogIndex->Int.toString}`
989
910
  if seenLogCoordinates->Utils.Set.has(coordinate) {
@@ -1013,6 +934,7 @@ let rec writeBatch = async (
1013
934
  ~table=InternalTable.RawEvents.table,
1014
935
  ~itemSchema=InternalTable.RawEvents.schema,
1015
936
  ~pgSchema,
937
+ ~chainIdMode,
1016
938
  )
1017
939
  }, ~items=rawEvents)
1018
940
  } catch {
@@ -1132,6 +1054,7 @@ let rec writeBatch = async (
1132
1054
  ~itemSchema=entityHistory.setChangeSchema,
1133
1055
  ~table=entityHistory.table,
1134
1056
  ~pgSchema,
1057
+ ~chainIdMode,
1135
1058
  ),
1136
1059
  )
1137
1060
  ->ignore
@@ -1148,6 +1071,7 @@ let rec writeBatch = async (
1148
1071
  ~table=entityConfig.table,
1149
1072
  ~itemSchema=entityConfig.schema,
1150
1073
  ~pgSchema,
1074
+ ~chainIdMode,
1151
1075
  ),
1152
1076
  )
1153
1077
  }
@@ -1179,14 +1103,19 @@ let rec writeBatch = async (
1179
1103
  | Some({targetCheckpointId: rollbackTargetCheckpointId}) =>
1180
1104
  Some(
1181
1105
  sql => {
1182
- let promises = allEntities->Array.map(entityConfig => {
1183
- sql->EntityHistory.rollback(
1184
- ~pgSchema,
1185
- ~entityName=entityConfig.name,
1186
- ~entityIndex=entityConfig.index,
1187
- ~rollbackTargetCheckpointId,
1188
- )
1189
- })
1106
+ // Postgres owns history tables only for Postgres-backed entities;
1107
+ // ClickHouse-only entities have none to roll back.
1108
+ let promises =
1109
+ allEntities
1110
+ ->Array.filter(entityConfig => entityConfig.storage.postgres)
1111
+ ->Array.map(entityConfig => {
1112
+ sql->EntityHistory.rollback(
1113
+ ~pgSchema,
1114
+ ~entityName=entityConfig.name,
1115
+ ~entityIndex=entityConfig.index,
1116
+ ~rollbackTargetCheckpointId,
1117
+ )
1118
+ })
1190
1119
  promises
1191
1120
  ->Array.push(
1192
1121
  sql->InternalTable.Checkpoints.rollback(~pgSchema, ~rollbackTargetCheckpointId),
@@ -1243,6 +1172,7 @@ let rec writeBatch = async (
1243
1172
  ~checkpointBlockNumbers=batch.checkpointBlockNumbers,
1244
1173
  ~checkpointBlockHashes=batch.checkpointBlockHashes,
1245
1174
  ~checkpointEventsProcessed=batch.checkpointEventsProcessed,
1175
+ ~chainIdMode,
1246
1176
  )
1247
1177
  )
1248
1178
  }
@@ -1389,6 +1319,7 @@ let make = (
1389
1319
  ~pgDatabase,
1390
1320
  ~pgPassword,
1391
1321
  ~isHasuraEnabled,
1322
+ ~chainIdMode: ChainId.mode=Int32,
1392
1323
  ~sink: option<Sink.t>=?,
1393
1324
  ~onInitialize=?,
1394
1325
  ~onNewTables=?,
@@ -1409,29 +1340,36 @@ let make = (
1409
1340
  // storage record's own name are all the same string.
1410
1341
  let storageName = "postgres"
1411
1342
 
1412
- let indexRegistry = IndexRegistry.make()
1343
+ let indexManager = IndexManager.make()
1413
1344
 
1414
- // The only point where the catalog is consulted. Afterwards this process is
1415
- // the sole writer of the schema, so the registry is kept in sync by adding
1416
- // each index right after its DDL succeeds.
1417
- let reloadIndexRegistry = async () => {
1418
- let rows =
1419
- (await sql->Postgres.unsafe(IndexRegistry.makeCatalogQuery(~pgSchema)))->S.parseOrThrow(
1420
- IndexRegistry.catalogRowsSchema,
1421
- )
1422
- switch indexRegistry->IndexRegistry.reload(~rows) {
1345
+ let loadCatalogRows = (sql, ~indexName=?) =>
1346
+ sql
1347
+ ->Postgres.unsafe(IndexCatalog.makeQuery(~pgSchema, ~indexName?))
1348
+ ->Promise.thenResolve(rows => rows->S.parseOrThrow(IndexCatalog.rowsSchema))
1349
+
1350
+ // The whole-schema snapshot, taken on a clean initialize and on every resume.
1351
+ // Individual indexes are re-read from the catalog as they are built, so this
1352
+ // is the only point where the full listing is needed.
1353
+ let refreshIndexCatalog = async () => {
1354
+ let rows = await sql->loadCatalogRows
1355
+ indexManager->IndexManager.reload(~rows)
1356
+ }
1357
+
1358
+ let reloadIndexCatalog = async () => {
1359
+ let catalog = await refreshIndexCatalog()
1360
+ switch catalog->IndexCatalog.invalidNames {
1423
1361
  | [] => ()
1424
1362
  | invalidIndexNames =>
1425
1363
  Logging.warn({
1426
1364
  "storage": storageName,
1427
- "msg": `Ignoring invalid PostgreSQL indexes in schema "${pgSchema}". They are left untouched drop and recreate them manually if you need them.`,
1365
+ "msg": `Ignoring invalid PostgreSQL indexes in schema "${pgSchema}". They can't serve queries, so the indexer builds its own alongside them.`,
1428
1366
  "indexes": invalidIndexNames,
1429
1367
  })
1430
1368
  }
1431
1369
  Logging.info({
1432
1370
  "storage": storageName,
1433
- "msg": `Found ${indexRegistry
1434
- ->IndexRegistry.size
1371
+ "msg": `Found ${catalog
1372
+ ->IndexCatalog.size
1435
1373
  ->Int.toString} existing indexes in the indexer schema.`,
1436
1374
  })
1437
1375
  }
@@ -1628,6 +1566,7 @@ let make = (
1628
1566
  ~isEmptyPgSchema=schemaTableNames->Utils.Array.isEmpty,
1629
1567
  ~isHasuraEnabled,
1630
1568
  ~deferSchemaIndexes=true,
1569
+ ~chainIdMode,
1631
1570
  )
1632
1571
  // Execute all queries within a single transaction for integrity.
1633
1572
  // The envio_info row is written in the same transaction so a successful
@@ -1655,16 +1594,24 @@ let make = (
1655
1594
  })
1656
1595
  })
1657
1596
  if ids->Array.length > 0 {
1597
+ let addrChainIdArrayType = Table.getPgFieldType(
1598
+ ~fieldType=ChainId,
1599
+ ~pgSchema,
1600
+ ~isArray=true,
1601
+ ~isNumericArrayAsText=false,
1602
+ ~isNullable=false,
1603
+ ~chainIdMode,
1604
+ )
1658
1605
  await sql->Postgres.unpreparedUnsafe(
1659
1606
  `INSERT INTO "${pgSchema}"."${Config.EnvioAddresses.table.tableName}" ("id", "chain_id", "registration_block", "registration_log_index", "contract_name")
1660
- SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::text[]) AS t(id, chain_id, contract_name);`,
1607
+ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::${addrChainIdArrayType},$3::text[]) AS t(id, chain_id, contract_name);`,
1661
1608
  (ids, addrChainIds, addrContractNames)->(Utils.magic: _ => unknown),
1662
1609
  )
1663
1610
  }
1664
1611
 
1665
1612
  let cache = await restoreEffectCache(~withUpload=true)
1666
1613
 
1667
- await reloadIndexRegistry()
1614
+ await reloadIndexCatalog()
1668
1615
 
1669
1616
  // Integration with other tools like Hasura
1670
1617
  switch onInitialize {
@@ -1745,95 +1692,164 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1745
1692
  columns
1746
1693
  }
1747
1694
 
1748
- // The DDL that leaves `schemaIndex` in place. Normally a create; a repair
1749
- // when an invalid index already holds the name with the definition we want.
1750
- let makeEnsureIndexQuery = schemaIndex => {
1751
- let indexName = schemaIndex->schemaIndexName
1752
- switch indexRegistry->IndexRegistry.getInvalid(indexName) {
1753
- | None => makeCreateSchemaIndexQuery(schemaIndex, ~pgSchema)
1754
- | Some({key, isRepairable: true}) if key === schemaIndex->schemaIndexKey =>
1755
- makeReindexQuery(~pgSchema, ~indexName)
1756
- | Some(_) => throw(nameCollisionError(~indexName, ~pgSchema))
1695
+ // Runs a prepared build's DDL and reads the index back from pg_catalog before
1696
+ // it counts as existing. `sql` is the transaction's handle when there is one,
1697
+ // so a failed verification rolls the DDL back with it.
1698
+ let runAndVerify = async (sql, prepared: IndexManager.prepared) => {
1699
+ // Sequential rather than Promise.all: inside a transaction they share one
1700
+ // connection, and a rebuild's DROP has to land before its CREATE.
1701
+ for idx in 0 to prepared.queries->Array.length - 1 {
1702
+ let _ = await sql->Postgres.unsafe(prepared.queries->Array.getUnsafe(idx))
1757
1703
  }
1704
+ let rows = await sql->loadCatalogRows(~indexName=prepared.name)
1705
+ prepared->IndexManager.verifyOrThrow(~rows, ~pgSchema)
1758
1706
  }
1759
1707
 
1760
- let isRepair = schemaIndex =>
1761
- indexRegistry->IndexRegistry.getInvalid(schemaIndex->schemaIndexName)->Option.isSome
1708
+ // A build outside a transaction can commit its DDL and still fail — the
1709
+ // read-back is a second round trip. Re-reading the index puts the catalog
1710
+ // back in step, so the next attempt plans against what the database holds
1711
+ // instead of retrying a create that can only raise "already exists".
1712
+ //
1713
+ // If this read fails too, the next attempt does waste a create before landing
1714
+ // here again — bounded, and it recovers as soon as the database answers.
1715
+ let resyncIndex = async name =>
1716
+ switch await sql->loadCatalogRows(~indexName=name) {
1717
+ | rows => indexManager->IndexManager.resync(~name, ~rows)
1718
+ | exception exn =>
1719
+ Logging.debug({
1720
+ "storage": storageName,
1721
+ "msg": `Could not re-read the index "${name}" after a failed build. The next attempt reads it again.`,
1722
+ "err": exn->Utils.prettifyExn,
1723
+ })
1724
+ }
1762
1725
 
1763
1726
  let ensureQueryIndexes = async (~table: Table.table, ~filters: array<EntityFilter.t>) => {
1764
- let missing = filterColumns(~table, ~filters)->Array.filterMap(column => {
1765
- let schemaIndex = makeSingleColumnSchemaIndex(~tableName=table.tableName, ~column)
1766
- indexRegistry->IndexRegistry.has(schemaIndex->schemaIndexKey) ? None : Some(schemaIndex)
1727
+ let columns = filterColumns(~table, ~filters)
1728
+ let _ = await columns
1729
+ ->Array.map(column => {
1730
+ let definition = IndexDefinition.single(~tableName=table.tableName, ~column)
1731
+ indexManager
1732
+ ->IndexManager.ensure(~definition, ~coverage=LeadingColumns, ~build=async () => {
1733
+ // Resolved before logging so a rebuild is reported as one, and an
1734
+ // unrelated index holding the name fails before any DDL runs.
1735
+ switch indexManager->IndexManager.prepare(
1736
+ ~definition,
1737
+ ~coverage=LeadingColumns,
1738
+ ~pgSchema,
1739
+ ) {
1740
+ | None => ()
1741
+ | Some(prepared) =>
1742
+ let verb = prepared.isRebuild ? "Rebuilding unusable index" : "Creating index"
1743
+ // Logged from inside the build so it reports the one request that
1744
+ // actually creates the index, not the ones waiting on it.
1745
+ Logging.info({
1746
+ "storage": storageName,
1747
+ "msg": `${verb} "${prepared.name}" to serve a getWhere query on "${table.tableName}". Writes to the table are paused until it completes. ${slowOnLargeDatabaseNotice}`,
1748
+ })
1749
+ let timeRef = Performance.now()
1750
+ let entry = await sql->runAndVerify(prepared)
1751
+ indexManager->IndexManager.record(entry)
1752
+ Logging.info({
1753
+ "storage": storageName,
1754
+ "msg": `Index "${prepared.name}" is ready after ${timeRef->formatSeconds}s. Resuming indexing.`,
1755
+ })
1756
+ }
1757
+ })
1758
+ // A failed build records nothing, so the next getWhere retries. Meanwhile
1759
+ // the query still runs — just without the index.
1760
+ ->Promise.catch(async exn => {
1761
+ Logging.warn({
1762
+ "storage": storageName,
1763
+ "msg": `Failed to create an index on "${table.tableName}"("${column}") for a getWhere query. The query runs without it.`,
1764
+ "err": exn->Utils.prettifyExn,
1765
+ })
1766
+ await resyncIndex(definition->IndexDefinition.name)
1767
+ })
1767
1768
  })
1769
+ ->Promise.all
1770
+ }
1768
1771
 
1769
- if missing->Utils.Array.notEmpty {
1770
- let _ = await missing
1771
- ->Array.map(schemaIndex => {
1772
- let indexName = schemaIndex->schemaIndexName
1773
- indexRegistry
1774
- ->IndexRegistry.ensure(
1775
- ~key=schemaIndex->schemaIndexKey,
1776
- ~tableName=table.tableName,
1777
- ~build=async () => {
1778
- // Resolved before logging so a repair is reported as one, and an
1779
- // unrelated index holding the name fails before any DDL runs.
1780
- let query = makeEnsureIndexQuery(schemaIndex)
1781
- let verb = schemaIndex->isRepair ? "Repairing invalid index" : "Creating index"
1782
- // Logged from inside the build so it reports the one request that
1783
- // actually creates the index, not the ones waiting on it.
1784
- Logging.info({
1785
- "storage": storageName,
1786
- "msg": `${verb} "${indexName}" to serve a getWhere query on "${table.tableName}". Writes to the table are paused until it completes. ${slowOnLargeDatabaseNotice}`,
1787
- })
1788
- let timeRef = Performance.now()
1789
- let _ = await sql->Postgres.unsafe(query)
1790
- indexRegistry->IndexRegistry.clearInvalidName(indexName)
1791
- Logging.info({
1792
- "storage": storageName,
1793
- "msg": `Index "${indexName}" is ready after ${timeRef->formatSeconds}s. Resuming indexing.`,
1794
- })
1795
- },
1796
- )
1797
- // A failed build leaves the registry untouched, so the next getWhere
1798
- // retries. Meanwhile the query still runs — just without the index.
1799
- ->Utils.Promise.catchResolve(exn => {
1800
- Logging.warn({
1772
+ // Goes through `IndexManager.ensure`, unlike `finalizeBackfill` below: that
1773
+ // one runs with processing paused, while this runs on a resumed indexer that
1774
+ // is already ready, so handlers may be issuing getWhere queries alongside it
1775
+ // and the per-table queues are what keep the two from colliding. Nothing here
1776
+ // writes `ready_at` — the chains already carry theirs.
1777
+ let ensureSchemaIndexes = async (~entities: array<Internal.entityConfig>) => {
1778
+ let schemaIndexes = getSchemaIndexes(
1779
+ ~entities=entities->Array.filter((e: Internal.entityConfig) => e.storage.postgres),
1780
+ )
1781
+
1782
+ let _ = await schemaIndexes
1783
+ ->Array.map(definition =>
1784
+ indexManager
1785
+ ->IndexManager.ensure(~definition, ~coverage=Exact, ~build=async () => {
1786
+ switch indexManager->IndexManager.prepare(~definition, ~coverage=Exact, ~pgSchema) {
1787
+ | None => ()
1788
+ | Some(prepared) =>
1789
+ let verb = prepared.isRebuild ? "Rebuilding unusable index" : "Creating missing index"
1790
+ Logging.info({
1791
+ "storage": storageName,
1792
+ "msg": `${verb} "${prepared.name}" the schema promises but the database no longer has. Writes to the table are paused until it completes. ${slowOnLargeDatabaseNotice}`,
1793
+ })
1794
+ let timeRef = Performance.now()
1795
+ let entry = await sql->runAndVerify(prepared)
1796
+ indexManager->IndexManager.record(entry)
1797
+ Logging.info({
1801
1798
  "storage": storageName,
1802
- "msg": `Failed to create the index "${indexName}" for a getWhere query. The query runs without it.`,
1803
- "err": exn->Utils.prettifyExn,
1799
+ "msg": `Index "${prepared.name}" is ready after ${timeRef->formatSeconds}s.`,
1804
1800
  })
1801
+ }
1802
+ })
1803
+ ->Promise.catch(async exn => {
1804
+ Logging.warn({
1805
+ "storage": storageName,
1806
+ "msg": `Failed to restore the schema index "${definition->IndexDefinition.name}". Queries relying on it run unindexed until the next restart.`,
1807
+ "err": exn->Utils.prettifyExn,
1805
1808
  })
1809
+ await resyncIndex(definition->IndexDefinition.name)
1806
1810
  })
1807
- ->Promise.all
1808
- }
1811
+ )
1812
+ ->Promise.all
1809
1813
  }
1810
1814
 
1815
+ // Unlike `ensureQueryIndexes`, this doesn't go through `IndexManager.ensure`.
1816
+ // It's safe because the caller guarantees exclusivity — `FinalizeBackfill.run`
1817
+ // is reached from the processing loop with processing already paused, so no
1818
+ // handler can be running a getWhere.
1819
+ //
1820
+ // Each index is built on its own rather than in one transaction with
1821
+ // `ready_at`: a build that dies half way through a large schema would
1822
+ // otherwise roll back every index before it and make the retry start over.
1811
1823
  let finalizeBackfill = async (
1812
1824
  ~entities: array<Internal.entityConfig>,
1813
- ~chainIds: array<int>,
1825
+ ~chainIds: array<ChainId.t>,
1814
1826
  ~readyAt: Date.t,
1815
1827
  ) => {
1816
1828
  let schemaIndexes = getSchemaIndexes(
1817
1829
  ~entities=entities->Array.filter((e: Internal.entityConfig) => e.storage.postgres),
1818
1830
  )
1819
- IndexRegistry.validateIndexNamesOrThrow(schemaIndexes->Array.map(schemaIndexDescription))
1820
1831
 
1832
+ // Resolved up front so a name held by an unrelated index fails before any
1833
+ // DDL runs, rather than part way through the set.
1834
+ //
1835
+ // `Exact`, so the set built here is decided by the schema alone: matching a
1836
+ // declared index against a composite that happens to lead with the same
1837
+ // column would make a fresh database and an upgraded one end up holding
1838
+ // different tables.
1821
1839
  let missing =
1822
- schemaIndexes->Array.filter(schemaIndex =>
1823
- !(indexRegistry->IndexRegistry.has(schemaIndex->schemaIndexKey))
1840
+ schemaIndexes->Array.filterMap(definition =>
1841
+ indexManager->IndexManager.prepare(~definition, ~coverage=Exact, ~pgSchema)
1824
1842
  )
1825
1843
 
1826
- // `IF NOT EXISTS` would quietly skip an index whose name is taken by an
1827
- // invalid one, and the indexer would then report ready with an index the
1828
- // planner can't use. Those are repaired in the same transaction instead.
1829
- let repaired = missing->Array.filter(isRepair)
1830
- if repaired->Utils.Array.notEmpty {
1844
+ switch missing->Array.filter((prepared: IndexManager.prepared) => prepared.isRebuild) {
1845
+ | [] => ()
1846
+ | rebuilt =>
1831
1847
  Logging.warn({
1832
1848
  "storage": storageName,
1833
- "msg": `PostgreSQL reports ${repaired
1849
+ "msg": `PostgreSQL reports ${rebuilt
1834
1850
  ->Array.length
1835
- ->Int.toString} existing indexes as invalid, so they can't serve queries. Rebuilding them.`,
1836
- "indexes": repaired->Array.map(schemaIndexName),
1851
+ ->Int.toString} of the indexer's own indexes as invalid, so they can't serve queries. Rebuilding them.`,
1852
+ "indexes": rebuilt->Array.map((prepared: IndexManager.prepared) => prepared.name),
1837
1853
  })
1838
1854
  }
1839
1855
 
@@ -1851,39 +1867,46 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1851
1867
  "msg": `Creating the ${missing
1852
1868
  ->Array.length
1853
1869
  ->Int.toString} remaining schema indexes before the indexer reports ready. Writes are paused until they are committed. ${slowOnLargeDatabaseNotice}`,
1854
- "indexes": missing->Array.map(schemaIndexName),
1870
+ "indexes": missing->Array.map((prepared: IndexManager.prepared) => prepared.name),
1855
1871
  })
1856
1872
  }
1857
- // Resolved up front so a name held by an unrelated invalid index fails
1858
- // before the transaction opens, rather than rolling one back.
1859
- let indexQueries = missing->Array.map(makeEnsureIndexQuery)
1860
1873
  let timeRef = Performance.now()
1861
1874
 
1862
- await sql->Postgres.beginSql(async sql => {
1863
- // Sequential rather than Promise.all: they share one connection inside
1864
- // the transaction, and several of them target the same table.
1865
- for idx in 0 to indexQueries->Array.length - 1 {
1866
- let _ = await sql->Postgres.unsafe(indexQueries->Array.getUnsafe(idx))
1875
+ // Sequential, one committed index at a time: whatever is built before a
1876
+ // failure stays built and recorded, so the retry owes only the rest.
1877
+ for idx in 0 to missing->Array.length - 1 {
1878
+ let prepared = missing->Array.getUnsafe(idx)
1879
+ switch await sql->runAndVerify(prepared) {
1880
+ | entry => indexManager->IndexManager.record(entry)
1881
+ | exception exn =>
1882
+ // The DDL is outside a transaction, so a create that commits and then
1883
+ // fails its read-back leaves the index in place and unrecorded.
1884
+ // Re-reading it means the retry plans against the database rather than
1885
+ // replaying a create that can only raise "already exists".
1886
+ await resyncIndex(prepared.name)
1887
+ throw(exn)
1867
1888
  }
1868
- await sql
1869
- ->Postgres.preparedUnsafe(
1870
- InternalTable.Chains.makeSetReadyAtQuery(~pgSchema),
1871
- [
1872
- readyAt->(Utils.magic: Date.t => unknown),
1873
- chainIds->(Utils.magic: array<int> => unknown),
1874
- ]->(Utils.magic: array<unknown> => unknown),
1875
- )
1876
- ->Utils.Promise.ignoreValue
1877
- })
1889
+ }
1878
1890
 
1879
- // Only after the commit: a rollback takes both the indexes and ready_at
1880
- // with it, and the registry must not claim what the schema doesn't have.
1881
- missing->Array.forEach(schemaIndex =>
1882
- indexRegistry->IndexRegistry.add(schemaIndex->schemaIndexKey)
1883
- )
1884
- repaired->Array.forEach(schemaIndex =>
1885
- indexRegistry->IndexRegistry.clearInvalidName(schemaIndex->schemaIndexName)
1886
- )
1891
+ // Reached only once every definition is verified against pg_catalog, so a
1892
+ // crash either leaves `ready_at` null and the retry finds the indexes
1893
+ // already built, or commits readiness the schema backs.
1894
+ //
1895
+ // One transaction for the whole set: readiness is an indexer-wide fact, and
1896
+ // a crash part way through would otherwise leave some chains stamped and
1897
+ // some not, reporting the indexer as half ready.
1898
+ let setReadyAtQuery = InternalTable.Chains.makeSetReadyAtQuery(~pgSchema)
1899
+ let _ = await sql->Postgres.beginSql(async sql => {
1900
+ for idx in 0 to chainIds->Array.length - 1 {
1901
+ let _ = await sql->Postgres.preparedUnsafe(
1902
+ setReadyAtQuery,
1903
+ [
1904
+ readyAt->(Utils.magic: Date.t => unknown),
1905
+ chainIds->Array.getUnsafe(idx)->(Utils.magic: ChainId.t => unknown),
1906
+ ]->(Utils.magic: array<unknown> => unknown),
1907
+ )
1908
+ }
1909
+ })
1887
1910
 
1888
1911
  Logging.info({
1889
1912
  "storage": storageName,
@@ -2037,14 +2060,14 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
2037
2060
  InternalTable.EnvioInfo.read(sql, ~pgSchema),
2038
2061
  ))
2039
2062
 
2040
- await reloadIndexRegistry()
2063
+ await reloadIndexCatalog()
2041
2064
 
2042
2065
  let checkpointId = (checkpointIdResult->Array.getUnsafe(0))["id"]->BigInt.fromStringOrThrow
2043
2066
 
2044
2067
  // Convert string checkpoint IDs from DB to bigint
2045
2068
  let reorgCheckpoints = Array.map(reorgCheckpoints, (raw): Internal.reorgCheckpoint => {
2046
2069
  checkpointId: raw["id"]->BigInt.fromStringOrThrow,
2047
- chainId: raw["chain_id"],
2070
+ chainId: raw["chain_id"]->ChainId.normalizeOrThrow,
2048
2071
  blockNumber: raw["block_number"],
2049
2072
  blockHash: raw["block_hash"],
2050
2073
  })
@@ -2200,6 +2223,7 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
2200
2223
  resumeInitialState,
2201
2224
  loadOrThrow,
2202
2225
  ensureQueryIndexes,
2226
+ ensureSchemaIndexes,
2203
2227
  finalizeBackfill,
2204
2228
  dumpEffectCache,
2205
2229
  reset,
@@ -2228,6 +2252,7 @@ let makeStorageFromEnv = (
2228
2252
  ~pgPort=Env.Db.port,
2229
2253
  ~pgDatabase=Env.Db.database,
2230
2254
  ~pgPassword=Env.Db.password,
2255
+ ~chainIdMode=config.chainIdMode,
2231
2256
  ~sink=?{
2232
2257
  // Internally ClickHouse storage is implemented as a sync of the
2233
2258
  // Postgres storage. Required env vars are validated here only when
@@ -2260,6 +2285,7 @@ let makeStorageFromEnv = (
2260
2285
  ~database=database->Option.getUnsafe,
2261
2286
  ~username=username->Option.getUnsafe,
2262
2287
  ~password=password->Option.getUnsafe,
2288
+ ~chainIdMode=config.chainIdMode,
2263
2289
  ),
2264
2290
  )
2265
2291
  } else {