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.
- package/package.json +6 -6
- package/src/Batch.res +8 -8
- package/src/Batch.res.mjs +4 -3
- package/src/BatchProcessing.res +17 -7
- package/src/BatchProcessing.res.mjs +8 -4
- package/src/ChainFetching.res +20 -19
- package/src/ChainFetching.res.mjs +14 -14
- package/src/ChainId.res +58 -0
- package/src/ChainId.res.mjs +49 -0
- package/src/ChainId.resi +27 -0
- package/src/ChainMap.res +12 -26
- package/src/ChainMap.res.mjs +5 -41
- package/src/ChainMap.resi +5 -20
- package/src/ChainMetadata.res +1 -1
- package/src/ChainMetadata.res.mjs +3 -2
- package/src/ChainState.res +35 -20
- package/src/ChainState.res.mjs +34 -11
- package/src/ChainState.resi +2 -0
- package/src/Config.res +41 -24
- package/src/Config.res.mjs +17 -14
- package/src/ContractRegisterContext.res +1 -1
- package/src/ContractRegisterContext.res.mjs +1 -1
- package/src/Core.res +1 -0
- package/src/CrossChainState.res +61 -16
- package/src/CrossChainState.res.mjs +37 -13
- package/src/CrossChainState.resi +3 -2
- package/src/Env.res +0 -3
- package/src/Env.res.mjs +0 -3
- package/src/EventConfigBuilder.res +1 -1
- package/src/EventProcessing.res +4 -4
- package/src/EventProcessing.res.mjs +5 -5
- package/src/FetchState.res +261 -89
- package/src/FetchState.res.mjs +187 -63
- package/src/FinalizeBackfill.res +33 -6
- package/src/FinalizeBackfill.res.mjs +19 -1
- package/src/HandlerRegister.res +8 -8
- package/src/HandlerRegister.res.mjs +5 -4
- package/src/HandlerRegister.resi +1 -1
- package/src/InMemoryStore.res +4 -0
- package/src/InMemoryStore.res.mjs +1 -1
- package/src/IndexerLoop.res +9 -2
- package/src/IndexerLoop.res.mjs +8 -1
- package/src/IndexerState.res +41 -24
- package/src/IndexerState.res.mjs +36 -17
- package/src/IndexerState.resi +9 -7
- package/src/Internal.res +19 -17
- package/src/Internal.res.mjs +18 -9
- package/src/LoadLayer.res +1 -1
- package/src/LoadLayer.res.mjs +2 -1
- package/src/LogSelection.res +2 -2
- package/src/Main.res +7 -11
- package/src/Main.res.mjs +4 -6
- package/src/Metrics.res +6 -6
- package/src/Metrics.res.mjs +4 -3
- package/src/Persistence.res +15 -7
- package/src/PgStorage.res +284 -258
- package/src/PgStorage.res.mjs +181 -208
- package/src/RawEvent.res +1 -2
- package/src/RawEvent.res.mjs +1 -1
- package/src/Rollback.res +18 -20
- package/src/Rollback.res.mjs +9 -9
- package/src/RollbackCommit.res +2 -2
- package/src/RollbackCommit.res.mjs +3 -3
- package/src/SafeCheckpointTracking.res +2 -2
- package/src/SimulateDeadInputTracker.res +17 -21
- package/src/SimulateDeadInputTracker.res.mjs +9 -8
- package/src/SimulateItems.res +8 -10
- package/src/SimulateItems.res.mjs +9 -10
- package/src/Sink.res +2 -2
- package/src/Sink.res.mjs +3 -2
- package/src/TestIndexer.res +11 -14
- package/src/TestIndexer.res.mjs +14 -10
- package/src/UserContext.res +2 -2
- package/src/UserContext.res.mjs +2 -1
- package/src/Utils.res +0 -4
- package/src/Utils.res.mjs +1 -6
- package/src/bindings/ClickHouse.res +20 -2
- package/src/bindings/ClickHouse.res.mjs +25 -14
- package/src/db/EntityHistory.res +1 -1
- package/src/db/IndexCatalog.res +248 -0
- package/src/db/IndexCatalog.res.mjs +222 -0
- package/src/db/IndexDefinition.res +131 -0
- package/src/db/IndexDefinition.res.mjs +138 -0
- package/src/db/IndexManager.res +145 -0
- package/src/db/IndexManager.res.mjs +130 -0
- package/src/db/InternalTable.res +50 -26
- package/src/db/InternalTable.res.mjs +32 -24
- package/src/db/Table.res +11 -1
- package/src/db/Table.res.mjs +9 -4
- package/src/sources/AddressSet.res +5 -4
- package/src/sources/AddressStore.res +25 -14
- package/src/sources/AddressStore.res.mjs +10 -12
- package/src/sources/Evm.res +2 -2
- package/src/sources/Evm.res.mjs +1 -1
- package/src/sources/EvmChain.res +3 -3
- package/src/sources/EvmChain.res.mjs +3 -3
- package/src/sources/EvmHyperSyncSource.res +7 -7
- package/src/sources/EvmHyperSyncSource.res.mjs +6 -6
- package/src/sources/Fuel.res +2 -2
- package/src/sources/Fuel.res.mjs +1 -1
- package/src/sources/FuelHyperSyncClient.res +3 -0
- package/src/sources/FuelHyperSyncClient.res.mjs +1 -0
- package/src/sources/FuelHyperSyncSource.res +5 -7
- package/src/sources/FuelHyperSyncSource.res.mjs +5 -5
- package/src/sources/HyperSync.resi +2 -2
- package/src/sources/HyperSyncClient.res +5 -0
- package/src/sources/HyperSyncClient.res.mjs +1 -0
- package/src/sources/RpcSource.res +13 -14
- package/src/sources/RpcSource.res.mjs +13 -12
- package/src/sources/SimulateSource.res +42 -15
- package/src/sources/SimulateSource.res.mjs +27 -9
- package/src/sources/Source.res +1 -5
- package/src/sources/SourceManager.res +12 -12
- package/src/sources/SourceManager.res.mjs +11 -10
- package/src/sources/SourceManager.resi +2 -2
- package/src/sources/Svm.res +4 -5
- package/src/sources/Svm.res.mjs +5 -4
- package/src/sources/SvmHyperSyncClient.res +3 -0
- package/src/sources/SvmHyperSyncClient.res.mjs +1 -0
- package/src/sources/SvmHyperSyncSource.res +4 -4
- package/src/sources/SvmHyperSyncSource.res.mjs +3 -3
- package/src/tui/Tui.res +1 -1
- package/src/tui/Tui.res.mjs +2 -1
- package/src/tui/components/CustomHooks.res +5 -4
- package/src/tui/components/CustomHooks.res.mjs +4 -2
- package/src/db/IndexRegistry.res +0 -219
- 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
|
-
//
|
|
131
|
-
//
|
|
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<
|
|
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(
|
|
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
|
|
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(
|
|
193
|
-
let key =
|
|
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 = (
|
|
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(
|
|
369
|
-
query := query.contents ++ "\n" ++
|
|
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 = (
|
|
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 = (
|
|
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 = (
|
|
557
|
-
|
|
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 (
|
|
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(
|
|
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
|
-
->
|
|
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
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
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
|
|
1343
|
+
let indexManager = IndexManager.make()
|
|
1413
1344
|
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
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
|
|
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 ${
|
|
1434
|
-
->
|
|
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
|
|
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
|
|
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
|
-
//
|
|
1749
|
-
//
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
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
|
-
|
|
1761
|
-
|
|
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
|
|
1765
|
-
|
|
1766
|
-
|
|
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
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
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": `
|
|
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
|
-
|
|
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<
|
|
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.
|
|
1823
|
-
|
|
1840
|
+
schemaIndexes->Array.filterMap(definition =>
|
|
1841
|
+
indexManager->IndexManager.prepare(~definition, ~coverage=Exact, ~pgSchema)
|
|
1824
1842
|
)
|
|
1825
1843
|
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
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 ${
|
|
1849
|
+
"msg": `PostgreSQL reports ${rebuilt
|
|
1834
1850
|
->Array.length
|
|
1835
|
-
->Int.toString}
|
|
1836
|
-
"indexes":
|
|
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(
|
|
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
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1880
|
-
//
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
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
|
|
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 {
|