envio 3.15.0-subgraph → 3.16.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 (186) hide show
  1. package/README.md +40 -34
  2. package/evm.schema.json +3 -3
  3. package/fuel.schema.json +2 -2
  4. package/index.d.ts +3 -3
  5. package/licenses/README.md +1 -1
  6. package/package.json +9 -10
  7. package/src/Batch.res.mjs +5 -5
  8. package/src/BatchProcessing.res +1 -0
  9. package/src/BatchProcessing.res.mjs +1 -0
  10. package/src/Bin.res +0 -7
  11. package/src/Bin.res.mjs +0 -8
  12. package/src/ChainFetching.res +17 -456
  13. package/src/ChainFetching.res.mjs +33 -404
  14. package/src/ChainState.res +48 -83
  15. package/src/ChainState.res.mjs +83 -115
  16. package/src/ChainState.resi +0 -26
  17. package/src/Clock.res +57 -0
  18. package/src/Clock.res.mjs +72 -0
  19. package/src/Config.res +7 -16
  20. package/src/Config.res.mjs +4 -11
  21. package/src/ContractRegisterContext.res +6 -3
  22. package/src/ContractRegisterContext.res.mjs +2 -4
  23. package/src/Core.res +5 -136
  24. package/src/Core.res.mjs +1 -90
  25. package/src/CrossChainState.res +2 -8
  26. package/src/CrossChainState.res.mjs +3 -3
  27. package/src/Ecosystem.res +4 -7
  28. package/src/Ecosystem.res.mjs +8 -8
  29. package/src/EffectState.res +10 -2
  30. package/src/EffectState.res.mjs +8 -4
  31. package/src/EffectState.resi +3 -1
  32. package/src/EntityId.res +2 -7
  33. package/src/EntityId.res.mjs +1 -8
  34. package/src/EntityTables.res +4 -1
  35. package/src/EntityTables.res.mjs +1 -1
  36. package/src/Env.res +0 -16
  37. package/src/Env.res.mjs +0 -11
  38. package/src/EventProcessing.res +1 -38
  39. package/src/EventProcessing.res.mjs +1 -28
  40. package/src/FetchState.res +160 -319
  41. package/src/FetchState.res.mjs +182 -292
  42. package/src/FinalizeBackfill.res +2 -4
  43. package/src/FinalizeBackfill.res.mjs +3 -4
  44. package/src/HandlerLoader.res +15 -39
  45. package/src/HandlerLoader.res.mjs +8 -27
  46. package/src/HandlerRegister.res +0 -4
  47. package/src/HandlerRegister.res.mjs +2 -5
  48. package/src/HandlerRegister.resi +0 -2
  49. package/src/InMemoryTable.res +17 -16
  50. package/src/InMemoryTable.res.mjs +14 -15
  51. package/src/IndexerState.res +28 -4
  52. package/src/IndexerState.res.mjs +19 -6
  53. package/src/IndexerState.resi +4 -0
  54. package/src/Internal.res +0 -110
  55. package/src/Internal.res.mjs +1 -115
  56. package/src/LoadLayer.res +21 -47
  57. package/src/LoadLayer.res.mjs +47 -72
  58. package/src/LoadLayer.resi +0 -26
  59. package/src/Main.res +1 -14
  60. package/src/Main.res.mjs +3 -4
  61. package/src/Persistence.res +3 -23
  62. package/src/Persistence.res.mjs +3 -3
  63. package/src/Rollback.res +0 -1
  64. package/src/Rollback.res.mjs +1 -1
  65. package/src/RunScope.res +0 -3
  66. package/src/SafeCheckpointTracking.res +4 -4
  67. package/src/SafeCheckpointTracking.res.mjs +1 -1
  68. package/src/SimulateItems.res +26 -42
  69. package/src/SimulateItems.res.mjs +16 -40
  70. package/src/StagedRows.res +106 -0
  71. package/src/StagedRows.res.mjs +102 -0
  72. package/src/Staging.res +32 -14
  73. package/src/Staging.res.mjs +17 -20
  74. package/src/Storage.res +277 -998
  75. package/src/Storage.res.mjs +216 -731
  76. package/src/Supervisor.res +75 -56
  77. package/src/Supervisor.res.mjs +55 -26
  78. package/src/TestIndexer.res +22 -27
  79. package/src/TestIndexer.res.mjs +20 -17
  80. package/src/Throttler.res +15 -8
  81. package/src/Throttler.res.mjs +9 -6
  82. package/src/Throttler.resi +1 -1
  83. package/src/UserContext.res +46 -410
  84. package/src/UserContext.res.mjs +30 -318
  85. package/src/Utils.res +0 -23
  86. package/src/Utils.res.mjs +1 -17
  87. package/src/Writing.res +0 -1
  88. package/src/Writing.res.mjs +1 -1
  89. package/src/bindings/ClickHouse.res +0 -322
  90. package/src/bindings/ClickHouse.res.mjs +0 -338
  91. package/src/bindings/StorageClient.res +107 -175
  92. package/src/bindings/StorageClient.res.mjs +19 -8
  93. package/src/bindings/Vitest.res +0 -26
  94. package/src/bindings/Vitest.res.mjs +0 -3
  95. package/src/db/Table.res +24 -150
  96. package/src/db/Table.res.mjs +38 -272
  97. package/src/sources/AddressSet.res +0 -2
  98. package/src/sources/AddressStore.res +0 -3
  99. package/src/sources/ChainSources.res +2 -0
  100. package/src/sources/ChainSources.res.mjs +2 -2
  101. package/src/sources/EvmChain.res +4 -7
  102. package/src/sources/EvmChain.res.mjs +5 -4
  103. package/src/sources/EvmHyperSyncSource.res +2 -2
  104. package/src/sources/EvmHyperSyncSource.res.mjs +1 -1
  105. package/src/sources/EvmRpcClient.res +13 -3
  106. package/src/sources/EvmRpcClient.res.mjs +4 -3
  107. package/src/sources/EvmRpcWs.res +4 -3
  108. package/src/sources/EvmRpcWs.res.mjs +2 -2
  109. package/src/sources/HeightFeed.res +31 -10
  110. package/src/sources/HeightFeed.res.mjs +10 -6
  111. package/src/sources/HeightStream.res +32 -17
  112. package/src/sources/HeightStream.res.mjs +10 -10
  113. package/src/sources/HyperSync.res +1 -2
  114. package/src/sources/HyperSync.res.mjs +1 -2
  115. package/src/sources/HyperSyncSSE.res +4 -6
  116. package/src/sources/HyperSyncSSE.res.mjs +2 -2
  117. package/src/sources/RpcSource.res +10 -1
  118. package/src/sources/RpcSource.res.mjs +4 -3
  119. package/src/sources/Source.res +3 -0
  120. package/src/sources/SourceManager.res +85 -104
  121. package/src/sources/SourceManager.res.mjs +18 -37
  122. package/src/sources/SourceManager.resi +3 -5
  123. package/src/sources/StartBlockResolver.res +1 -0
  124. package/src/sources/StartBlockResolver.res.mjs +4 -3
  125. package/src/sources/SvmHyperSyncSource.res +2 -2
  126. package/src/sources/SvmHyperSyncSource.res.mjs +1 -1
  127. package/svm.schema.json +3 -3
  128. package/src/EffectCacheLookup.res +0 -98
  129. package/src/EffectCacheLookup.res.mjs +0 -116
  130. package/src/PgWriting.res +0 -36
  131. package/src/PgWriting.res.mjs +0 -34
  132. package/src/PreloadLookahead.res +0 -184
  133. package/src/PreloadLookahead.res.mjs +0 -197
  134. package/src/SubgraphLog.res +0 -19
  135. package/src/SubgraphLog.res.mjs +0 -26
  136. package/src/SyncWrites.res +0 -101
  137. package/src/SyncWrites.res.mjs +0 -98
  138. package/src/db/IndexDefinition.res +0 -47
  139. package/src/db/IndexDefinition.res.mjs +0 -60
  140. package/src/sources/CallSource.res +0 -274
  141. package/src/sources/CallSource.res.mjs +0 -196
  142. package/src/subgraph/abi-types.ts +0 -84
  143. package/src/subgraph/aggregations.ts +0 -745
  144. package/src/subgraph/as-package.ts +0 -143
  145. package/src/subgraph/assemblyscript-regex.ts +0 -67
  146. package/src/subgraph/assemblyscript.ts +0 -731
  147. package/src/subgraph/blocks.ts +0 -398
  148. package/src/subgraph/calls-source.ts +0 -702
  149. package/src/subgraph/calls.ts +0 -258
  150. package/src/subgraph/conformance.ts +0 -106
  151. package/src/subgraph/errors.ts +0 -85
  152. package/src/subgraph/graft.ts +0 -73
  153. package/src/subgraph/graph-cli-worker.mjs +0 -35
  154. package/src/subgraph/graph-cli.ts +0 -900
  155. package/src/subgraph/graph-node-decimal.ts +0 -199
  156. package/src/subgraph/graph-ts-enums.ts +0 -28
  157. package/src/subgraph/graph-ts-types/VERSION +0 -2
  158. package/src/subgraph/graph-ts-types/chain/arweave.d.ts +0 -70
  159. package/src/subgraph/graph-ts-types/chain/cosmos.d.ts +0 -327
  160. package/src/subgraph/graph-ts-types/chain/ethereum.d.ts +0 -233
  161. package/src/subgraph/graph-ts-types/chain/near.d.ts +0 -253
  162. package/src/subgraph/graph-ts-types/chain/starknet.d.ts +0 -32
  163. package/src/subgraph/graph-ts-types/common/collections.d.ts +0 -136
  164. package/src/subgraph/graph-ts-types/common/conversion.d.ts +0 -11
  165. package/src/subgraph/graph-ts-types/common/datasource.d.ts +0 -30
  166. package/src/subgraph/graph-ts-types/common/eager-offset.d.ts +0 -0
  167. package/src/subgraph/graph-ts-types/common/json.d.ts +0 -17
  168. package/src/subgraph/graph-ts-types/common/numbers.d.ts +0 -120
  169. package/src/subgraph/graph-ts-types/common/value.d.ts +0 -120
  170. package/src/subgraph/graph-ts-types/common/yaml.d.ts +0 -90
  171. package/src/subgraph/graph-ts-types/global/global.d.ts +0 -194
  172. package/src/subgraph/graph-ts-types/helper-functions.d.ts +0 -22
  173. package/src/subgraph/graph-ts-types/index.d.ts +0 -102
  174. package/src/subgraph/graph-ts.ts +0 -4280
  175. package/src/subgraph/hosts.ts +0 -554
  176. package/src/subgraph/log.ts +0 -28
  177. package/src/subgraph/receipts.ts +0 -184
  178. package/src/subgraph/register-slots.ts +0 -93
  179. package/src/subgraph/rollup.ts +0 -109
  180. package/src/subgraph/rpc.ts +0 -337
  181. package/src/subgraph/runtime.ts +0 -2060
  182. package/src/subgraph/scope.ts +0 -177
  183. package/src/subgraph/stats.ts +0 -42
  184. package/src/subgraph/template-instances.ts +0 -66
  185. package/src/subgraph/toolchain.ts +0 -327
  186. package/src/subgraph/transient.ts +0 -176
package/src/Storage.res CHANGED
@@ -1,10 +1,3 @@
1
- let formatSeconds = seconds => (Math.round(seconds *. 100.) /. 100.)->Float.toString
2
-
3
- // Every index build blocks writes to its table for as long as it runs, and on a
4
- // large database that is not quick. Both build paths say so up front, so a
5
- // stalled-looking indexer is explainable from the logs alone.
6
- let slowOnLargeDatabaseNotice = "This can take a long time on a large database."
7
-
8
1
  let storeName = (store: StorageClient.store) =>
9
2
  switch store {
10
3
  | Postgres => "postgres"
@@ -13,88 +6,11 @@ let storeName = (store: StorageClient.store) =>
13
6
 
14
7
  let postgresName = Postgres->storeName
15
8
 
16
- let logIndexEvent = (~logger, ~pgSchema, event: StorageClient.indexEvent) =>
17
- switch event {
18
- | Invalid({names}) =>
19
- logger->Logger.warn(
20
- `Ignoring invalid PostgreSQL indexes in schema "${pgSchema}". They can't serve queries, so the indexer builds its own alongside them.`,
21
- ~params={"storage": postgresName, "indexes": names},
22
- )
23
- | Planned({declared, missing, rebuilt}) =>
24
- if rebuilt->Utils.Array.notEmpty {
25
- logger->Logger.warn(
26
- `PostgreSQL reports ${rebuilt
27
- ->Array.length
28
- ->Int.toString} of the indexer's own indexes as invalid, so they can't serve queries. Rebuilding them.`,
29
- ~params={"storage": postgresName, "indexes": rebuilt},
30
- )
31
- }
32
- switch missing {
33
- // The line that matters next is the indexer reporting itself ready, which
34
- // finalization logs.
35
- | [] =>
36
- logger->Logger.info(
37
- `All ${declared->Int.toString} schema indexes are already in place. Marking the indexer ready.`,
38
- ~params={"storage": postgresName},
39
- )
40
- | _ =>
41
- logger->Logger.info(
42
- `Creating the ${missing
43
- ->Array.length
44
- ->Int.toString} remaining schema indexes before the indexer reports ready. Writes are paused until they are committed. ${slowOnLargeDatabaseNotice}`,
45
- ~params={"storage": postgresName, "indexes": missing},
46
- )
47
- }
48
- | Building({purpose: Query, name, tableName, isRebuild}) =>
49
- logger->Logger.info(
50
- `${isRebuild
51
- ? "Rebuilding unusable index"
52
- : "Creating index"} "${name}" to serve a getWhere query on "${tableName}". Writes to the table are paused until it completes. ${slowOnLargeDatabaseNotice}`,
53
- ~params={"storage": postgresName},
54
- )
55
- | Building({purpose: Schema, name, isRebuild}) =>
56
- logger->Logger.info(
57
- `${isRebuild
58
- ? "Rebuilding unusable index"
59
- : "Creating missing index"} "${name}" the schema promises but the database no longer has. Writes to the table are paused until it completes. ${slowOnLargeDatabaseNotice}`,
60
- ~params={"storage": postgresName},
61
- )
62
- | Built({purpose, name, seconds}) =>
63
- logger->Logger.info(
64
- `Index "${name}" is ready after ${seconds->formatSeconds}s.${switch purpose {
65
- | Query => " Resuming indexing."
66
- | Schema => ""
67
- }}`,
68
- ~params={"storage": postgresName},
69
- )
70
- | Failed({purpose: Query, tableName, columns, error, ?code}) =>
71
- logger->Logger.warn(
72
- `Failed to create an index on "${tableName}"(${columns
73
- ->Array.map(column => `"${column}"`)
74
- ->Array.joinUnsafe(", ")}) for a getWhere query. The query runs without it.`,
75
- ~params={"storage": postgresName, "err": error, "code": code},
76
- )
77
- | Failed({purpose: Schema, name, error, ?code}) =>
78
- logger->Logger.warn(
79
- `Failed to restore the schema index "${name}". Queries relying on it run unindexed until the next restart.`,
80
- ~params={"storage": postgresName, "err": error, "code": code},
81
- )
82
- | ResyncFailed({name, error, ?code}) =>
83
- logger->Logger.trace(
84
- `Could not re-read the index "${name}" after a failed build. The next attempt reads it again.`,
85
- ~params={"storage": postgresName, "err": error, "code": code},
86
- )
87
- | Committed({count, seconds}) =>
88
- logger->Logger.info(
89
- `Committed ${count->Int.toString} schema indexes and the ready timestamp in ${seconds->formatSeconds}s.`,
90
- ~params={"storage": postgresName},
91
- )
92
- }
93
-
94
9
  let postgresOptions = (
95
10
  ~pgSchema=Env.Db.publicSchema,
96
11
  ~isHasuraEnabled=false,
97
12
  ~maxConnections=Env.Db.maxConnections,
13
+ ~isolated=false,
98
14
  ): StorageClient.postgresOptions => {
99
15
  {
100
16
  host: Env.Db.host,
@@ -111,456 +27,27 @@ let postgresOptions = (
111
27
  // Hasura cannot read a `numeric[]`, so a schema it tracks stores those as
112
28
  // `text[]` (issue #788).
113
29
  isHasuraEnabled,
30
+ isolated,
114
31
  }
115
32
  }
116
33
 
117
- // One client per storage: the stores and the chain-id mode it was created with
118
- // decide every statement the addon builds.
34
+ // One client per storage: the config it was created with decides every table
35
+ // and every statement the addon builds. Without one it keeps no entities.
119
36
  let makeClient = (
120
37
  ~logger: Logger.t,
121
- ~chainIdMode: ChainId.mode=Int32,
38
+ ~config: option<Config.t>=?,
122
39
  ~postgres: StorageClient.postgresOptions,
123
40
  ~clickhouse: option<StorageClient.clickHouseOptions>=?,
124
41
  ): StorageClient.t =>
125
42
  StorageClient.make(
126
- {chainIdMode: (chainIdMode :> string), postgres, ?clickhouse},
127
- ~onIndexEvent=event => logIndexEvent(~logger, ~pgSchema=postgres.pgSchema, event),
128
- ~onWarning=message => logger->Logger.warn(message, ~params={"storage": ClickHouse->storeName}),
129
- )
130
-
131
- // A per-chain entity's rows are partitioned by the chain that owns them, so a
132
- // chain-filtered read scans one chain's partition rather than the whole table.
133
- // `$` can't occur in a GraphQL entity name, so a partition name can never
134
- // collide with the table another entity claims; past the identifier limit the
135
- // entity index keeps what survives truncation unique.
136
- let partitionTableName = (~entityConfig: Internal.entityConfig, ~chainId: ChainId.t) => {
137
- let chainIdStr = chainId->ChainId.toString
138
- Table.fitPgTableName(
139
- `${entityConfig.table.tableName}$${chainIdStr}`,
140
- ~uniqueSuffix=`$${entityConfig.index->Int.toString}$${chainIdStr}`,
141
- )
142
- }
143
-
144
- // The physical tables an index on the entity is built on. A per-chain entity's
145
- // rows are partitioned by chain, and an index declared on the parent cascades
146
- // to every partition, which is what a run driving every chain wants: one
147
- // declaration, one build. An isolated run instead builds on its own chains'
148
- // partitions only: the planner uses a partition's own index either way, and
149
- // building one chain's index then neither waits for nor locks the rows of a
150
- // chain a sibling process drives.
151
- let indexTableNames = (entityConfig: Internal.entityConfig, ~partitionChainIds) =>
152
- switch (entityConfig.table->Table.getChainIdField, partitionChainIds) {
153
- | (Some(_), Some(chainIds)) =>
154
- chainIds->Array.map(chainId => partitionTableName(~entityConfig, ~chainId))
155
- | _ => [entityConfig.table.tableName]
156
- }
157
-
158
- // Every index the entity schema promises: an `@index` field, a composite index,
159
- // or the index backing a derived relationship. Deferred past the initial DDL
160
- // and created once backfill completes, so a chain that reports itself ready
161
- // always has all of them. With `partitionChainIds`, a per-chain entity's index
162
- // is one per partition of those chains rather than one on the parent.
163
- //
164
- // `entities` is the Postgres-backed set, and every `@derivedFrom` target within
165
- // it resolves: config parsing rejects a Postgres entity deriving from one that
166
- // isn't in Postgres (`validate_relationship_storage`).
167
- let getSchemaIndexes = (
168
- ~entities: array<Internal.entityConfig>,
169
- ~partitionChainIds: option<array<ChainId.t>>=?,
170
- ): array<IndexDefinition.t> => {
171
- let derivedSchema = Schema.make(entities->Array.map(e => e.table))
172
- let all = []
173
-
174
- entities->Array.forEach(({table}) => {
175
- table
176
- ->Table.getSingleIndexes
177
- ->Array.forEach(column =>
178
- all->Array.push(IndexDefinition.single(~tableName=table.tableName, ~column))->ignore
179
- )
180
- table
181
- ->Table.getCompositeIndexes
182
- ->Array.forEach(indexFields =>
183
- all
184
- ->Array.push(IndexDefinition.fromIndexFields(~tableName=table.tableName, ~indexFields))
185
- ->ignore
186
- )
187
- })
188
-
189
- entities->Array.forEach(({table}) =>
190
- table
191
- ->Table.getDerivedFromFields
192
- ->Array.forEach(derivedFromField => {
193
- let column =
194
- derivedSchema->Schema.getDerivedFromPgFieldName(derivedFromField)->Utils.unwrapResultExn
195
- // A list column holds every id in one value; a btree entry over it
196
- // outgrows a page (Hats' `wearers`), so it gets GIN, as graph-node gives
197
- // list attributes.
198
- let isList =
199
- derivedSchema
200
- ->Utils.Dict.dangerouslyGetNonOption(derivedFromField.derivedFromEntity)
201
- ->Option.flatMap(table => table->Table.getFieldByName(derivedFromField.derivedFromField))
202
- ->Option.mapOr(
203
- false,
204
- field =>
205
- switch field {
206
- | Table.Field({isArray}) => isArray
207
- | DerivedFrom(_) => false
208
- },
209
- )
210
- all
211
- ->Array.push(
212
- IndexDefinition.make(
213
- ~tableName=derivedFromField.derivedFromEntity,
214
- ~columns=[{name: column, direction: Table.Asc}],
215
- ~method=?isList ? Some("gin") : None,
216
- ),
217
- )
218
- ->ignore
219
- })
220
- )
221
-
222
- // An `@index` field and a derived relationship pointing at it describe the
223
- // same index, so the list is deduped on identity rather than on name.
224
- let seen = Utils.Set.make()
225
- let entityByTableName = Dict.make()
226
- entities->Array.forEach(entityConfig =>
227
- entityByTableName->Dict.set(entityConfig.table.tableName, entityConfig)
228
- )
229
-
230
- all
231
- ->Array.filter(definition => {
232
- let identity = definition->IndexDefinition.describe
233
- if seen->Utils.Set.has(identity) {
234
- false
235
- } else {
236
- seen->Utils.Set.add(identity)->ignore
237
- true
238
- }
239
- })
240
- ->Array.flatMap(definition =>
241
- entityByTableName
242
- ->Dict.get(definition.tableName)
243
- ->Option.getOrThrow
244
- ->indexTableNames(~partitionChainIds)
245
- ->Array.map(tableName => {...definition, IndexDefinition.tableName})
246
- )
247
- }
248
-
249
- let pgColumnInput = (field: Table.field): Core.pgColumnInput => {
250
- let (fieldType, precision, scale, enumName) = field.fieldType->Table.pgFieldTypeParts
251
- {
252
- name: field->Table.getPgDbFieldName,
253
- fieldType,
254
- isArray: field.isArray,
255
- isNullable: field.isNullable,
256
- isPrimaryKey: field.isPrimaryKey,
257
- defaultValue: ?field.defaultValue,
258
- ?precision,
259
- ?scale,
260
- ?enumName,
261
- }
262
- }
263
-
264
- // The entity as it's stored: the handler-visible schema plus the chain-id
265
- // column a per-chain entity's table carries. The value for that column is
266
- // stamped from the flush group's scope right before serialization, so it never
267
- // has to be re-derived from a checkpoint.
268
- let rowSchemaCache = Utils.WeakMap.make()
269
- let getRowSchema = (entityConfig: Internal.entityConfig): S.t<Internal.entity> =>
270
- switch rowSchemaCache->Utils.WeakMap.get(entityConfig) {
271
- | Some(cached) => cached
272
- | None =>
273
- let schema = switch entityConfig.table->Table.getChainIdField {
274
- | None => entityConfig.schema
275
- | Some(chainIdField) =>
276
- S.schema(s => {
277
- let dict = Dict.make()
278
- switch entityConfig.schema->S.classify {
279
- | Object({items}) =>
280
- items->Array.forEach(({location, schema}) => dict->Dict.set(location, s.matches(schema)))
281
- | _ =>
282
- JsError.throwWithMessage(
283
- `Unexpected non-object schema for entity "${entityConfig.name}".`,
284
- )
285
- }
286
- dict->Dict.set(chainIdField.fieldName, s.matches(ChainId.schema->S.toUnknown))
287
- dict
288
- })->(Utils.magic: S.t<dict<unknown>> => S.t<Internal.entity>)
289
- }
290
- rowSchemaCache->Utils.WeakMap.set(entityConfig, schema)->ignore
291
- schema
292
- }
293
-
294
- // How a batch of a Postgres table's rows crosses over: staged into an arena
295
- // column by column, or rendered a parameter per cell when it has an array
296
- // column.
297
- type postgresTable = {staged: option<array<Staging.column>>}
298
-
299
- // A table as the addon writes it: its handle, the columns a batch's rows carry
300
- // in the order the addon expects them, and what each store that keeps it made
301
- // of it.
302
- type registered = {
303
- handle: int,
304
- home: StorageClient.store,
305
- table: Table.table,
306
- fields: array<Table.field>,
307
- postgres: option<postgresTable>,
308
- clickhouse: option<ClickHouse.table>,
309
- }
310
-
311
- %%private(
312
- let register = (
313
- client: StorageClient.t,
314
- ~table: Table.table,
315
- ~itemSchema: S.t<unknown>,
316
- ~postgres=true,
317
- ~clickhouse: option<StorageClient.clickHouseTable>=?,
318
- ~appendOnly=false,
319
- ~history=?,
320
- ): registered => {
321
- let fields = table->Table.schemaOrderedFields(~schema=itemSchema)
322
- let chainIdField = table->Table.getChainIdField
323
- let registered = client->StorageClient.registerTable({
324
- name: table.tableName,
325
- postgres: ?(
326
- postgres
327
- ? Some({
328
- Core.columns: table->Table.getFields->Array.map(pgColumnInput),
329
- writeColumns: fields->Array.map(Table.getPgDbFieldName),
330
- partitionByColumn: ?(chainIdField->Option.map(Table.getPgDbFieldName)),
331
- appendOnly,
332
- historyTable: ?history,
333
- chainIdColumn: ?switch history {
334
- | Some(_) => chainIdField->Option.map(Table.getPgDbFieldName)
335
- | None => None
336
- },
337
- })
338
- : None
339
- ),
340
- ?clickhouse,
341
- })
342
- {
343
- handle: registered.handle,
344
- home: registered.home,
345
- table,
346
- fields,
347
- postgres: registered.postgres->Option.map(({?kinds}): postgresTable => {
348
- staged: kinds->Option.map(kinds =>
349
- PgWriting.columns(fields, ~kinds=kinds->Array.map(Staging.kindOfOrdinal))
350
- ),
351
- }),
352
- // Named for where its rows go, which for an entity is its history table.
353
- clickhouse: registered.clickhouse->Option.map(
354
- ClickHouse.makeTable(
355
- ~handle=registered.handle,
356
- ~name=history->Option.getOr(table.tableName),
357
- _,
358
- ),
359
- ),
360
- }
361
- }
362
- )
363
-
364
- let registerEntity = (client, entityConfig: Internal.entityConfig) =>
365
- client->register(
366
- ~table=entityConfig.table,
367
- ~itemSchema=entityConfig->getRowSchema->S.toUnknown,
368
- ~postgres=entityConfig.storage.postgres,
369
- ~clickhouse=?entityConfig.storage.clickhouse
370
- ? Some(ClickHouse.entityTable(entityConfig))
371
- : None,
372
- ~history=EntityHistory.historyTableName(
373
- ~entityName=entityConfig.name,
374
- ~entityIndex=entityConfig.index,
375
- ),
376
- )
377
-
378
- // A json column holds a document, which is as readily a string or a boolean as
379
- // an object. By the time a parameter is rendered there is only the value to go
380
- // on, and there a string is text and a boolean is `t` — neither of which the
381
- // server will read back as the document it was. So the document becomes its own
382
- // text here, where the column it belongs to is still known.
383
- //
384
- // JSON `null` is a document of its own, which a required column stores as one.
385
- // A column that takes NULL keeps the two apart the other way: there `null` is
386
- // the absent value, since only one of the two can survive the field's schema.
387
- %%private(
388
- let renderDocument = (value, ~isNullable) =>
389
- switch value->(Utils.magic: unknown => Nullable.t<unknown>) {
390
- | Value(_) => value->Sql.stringifyDocument->(Utils.magic: string => unknown)
391
- | Null if !isNullable => "null"->(Utils.magic: string => unknown)
392
- | Null | Undefined => value
393
- }
394
- )
395
-
396
- // A list of documents is bound as an array whose elements are each their own
397
- // document's text.
398
- %%private(
399
- let renderDocuments = (columns: array<array<unknown>>, ~at: array<(int, Table.field)>) => {
400
- at->Array.forEach(((index, field)) => {
401
- let values = columns->Array.getUnsafe(index)
402
- for row in 0 to values->Array.length - 1 {
403
- let value = values->Array.getUnsafe(row)
404
- values->Array.setUnsafe(
405
- row,
406
- switch value->(Utils.magic: unknown => Nullable.t<array<unknown>>)->Nullable.toOption {
407
- | Some(documents) if field.isArray =>
408
- documents
409
- ->Array.map(document => document->renderDocument(~isNullable=true))
410
- ->(Utils.magic: array<unknown> => unknown)
411
- | _ => value->renderDocument(~isNullable=field.isNullable)
412
- },
413
- )
414
- }
415
- })
416
- columns
417
- }
418
- )
419
-
420
- // An item schema compiled for one table's columns: rows in, one array per
421
- // column out. The values are validated by the type system on the way in, and a
422
- // value its column cannot take is refused by the server, which says why.
423
- type converters = Utils.WeakMap.t<S.t<unknown>, array<unknown> => array<array<unknown>>>
424
- let arenaConverters: converters = Utils.WeakMap.make()
425
- let cellConverters: converters = Utils.WeakMap.make()
426
- let converterOf = (registered, ~forArena, ~itemSchema) => {
427
- let converters = forArena ? arenaConverters : cellConverters
428
- switch converters->Utils.WeakMap.get(itemSchema) {
429
- | Some(convert) => convert
430
- | None =>
431
- let documents =
432
- registered.fields->Array.filterMapWithIndex((field, index) =>
433
- field.fieldType === Table.Json ? Some((index, field)) : None
434
- )
435
- // Booleans travel as 1/0 and bigints as their digits, which is what an
436
- // arena's slots hold.
437
- let schema = forArena ? registered.table->Table.toDbSchema(~schema=itemSchema) : itemSchema
438
- let convert = S.compile(
439
- S.unnest(schema)
440
- ->S.preprocess(_ => {
441
- serializer: columns =>
442
- columns
443
- ->(Utils.magic: unknown => array<array<unknown>>)
444
- ->renderDocuments(~at=documents)
445
- ->(Utils.magic: array<array<unknown>> => unknown),
446
- })
447
- ->S.toUnknown,
448
- ~input=Value,
449
- ~output=Unknown,
450
- ~mode=Sync,
451
- ~typeValidation=false,
452
- )->(Utils.magic: (unknown => unknown) => array<unknown> => array<array<unknown>>)
453
- converters->Utils.WeakMap.set(itemSchema, convert)->ignore
454
- convert
455
- }
456
- }
457
-
458
- // Staging refuses a non-finite float for a staged table. A table bound a cell
459
- // at a time has to refuse it too: the server would store it, and the field's
460
- // schema would then refuse it on every load.
461
- %%private(
462
- let refuseNonFinite = (values: array<array<unknown>>, ~fields: array<Table.field>) =>
463
- fields->Array.forEachWithIndex((field, index) =>
464
- if field.fieldType === Table.Number {
465
- let column = field->Table.getPgDbFieldName
466
- let check = (value: Nullable.t<float>) =>
467
- switch value {
468
- | Value(number) => number->Staging.finiteOrThrow(~column)->ignore
469
- | Null | Undefined => ()
470
- }
471
- values
472
- ->Array.getUnsafe(index)
473
- ->Array.forEach(value =>
474
- if field.isArray {
475
- switch value->(Utils.magic: unknown => Nullable.t<array<Nullable.t<float>>>) {
476
- | Value(elements) => elements->Array.forEach(check)
477
- | Null | Undefined => ()
478
- }
479
- } else {
480
- check(value->(Utils.magic: unknown => Nullable.t<float>))
481
- }
482
- )
483
- }
484
- )
485
- )
486
-
487
- // What a batch has laid out on its way to the addon: the staged handles, freed
488
- // if the write never reaches it, and each store's time spent laying its rows
489
- // out, which is part of writing to it.
490
- type staged = {handles: array<int>, seconds: dict<float>}
491
-
492
- let stagedFor = (staged, ~store, stage) => {
493
- let started = Performance.now()
494
- let result = stage()
495
- let name = store->storeName
496
- staged.seconds->Dict.set(
497
- name,
498
- staged.seconds->Dict.get(name)->Option.getOr(0.) +. started->Performance.secondsSince,
43
+ {postgres, ?clickhouse},
44
+ ~configJson=?config->Option.map(config => config.envioInfo->JSON.stringify),
45
+ ~logger,
499
46
  )
500
- result
501
- }
502
-
503
- // A Postgres table's rows for a write. A staged batch is handed back by handle.
504
- %%private(
505
- let rowsOrThrow = (
506
- client: StorageClient.t,
507
- registered,
508
- ~itemSchema,
509
- items: array<unknown>,
510
- ~staged: staged,
511
- ): StorageClient.rows =>
512
- staged->stagedFor(~store=Postgres, () => {
513
- let rows = items->Array.length
514
- try {
515
- switch registered.postgres {
516
- | Some({staged: Some(columns)}) =>
517
- let values = (registered->converterOf(~forArena=true, ~itemSchema))(items)
518
- let handle =
519
- client
520
- ->StorageClient.arena(~store=Postgres)
521
- ->PgWriting.stage(~table=registered.handle, ~columns, ~values, ~rows)
522
- staged.handles->Array.push(handle)
523
- ({staged: handle, rows}: StorageClient.rows)
524
- | Some({staged: None}) =>
525
- let values = (registered->converterOf(~forArena=false, ~itemSchema))(items)
526
- values->refuseNonFinite(~fields=registered.fields)
527
- {
528
- cells: values->Utils.Array.flatten->Sql.params,
529
- rows,
530
- }
531
- | None => JsError.throwWithMessage(`Postgres doesn't keep "${registered.table.tableName}"`)
532
- }
533
- } catch {
534
- | S.Raised(_) as exn =>
535
- throw(
536
- Persistence.StorageError({
537
- message: `Failed to convert items for table "${registered.table.tableName}"`,
538
- reason: exn,
539
- }),
540
- )
541
- | exn =>
542
- throw(
543
- Persistence.StorageError({
544
- message: `Failed to insert items into table "${registered.table.tableName}"`,
545
- reason: exn->Utils.prettifyExn,
546
- }),
547
- )
548
- }
549
- })
550
- )
551
-
552
- // Ids as the text their column reads them from.
553
- let renderIds = (table: Table.table, ids: array<EntityId.t>) =>
554
- table
555
- ->Table.encodeIdsToJson(ids)
556
- ->(Utils.magic: JSON.t => array<unknown>)
557
- ->Array.map(Sql.render)
558
47
 
559
- let bounds = (
560
- {sequence, byChain}: CheckpointSequence.checkpointBoundsByChain,
561
- ): StorageClient.bounds => {
562
- let (chainIds, checkpointIds) = byChain->Frontier.unnestParams
563
- {sequence, chainIds, checkpointIds}
48
+ let bounds = ({byChain}: CheckpointSequence.checkpointBoundsByChain): StorageClient.bounds => {
49
+ chainIds: byChain->Frontier.chainIds,
50
+ checkpointIds: byChain->Frontier.chainIds->Array.map(chainId => byChain->Frontier.get(chainId)),
564
51
  }
565
52
 
566
53
  let progress = (chain: InternalTable.Chains.progressedChain): StorageClient.progress => {
@@ -604,16 +91,6 @@ let storageErrorOf = exn =>
604
91
  | _ => exn
605
92
  }
606
93
 
607
- let rollbackRowStateSchema: Table.table => S.t<(
608
- EntityId.t,
609
- EntityHistory.RowAction.t,
610
- )> = Utils.WeakMap.memoize(table =>
611
- S.object(s => (
612
- s.field(Table.idFieldName, table->Table.getIdSchema),
613
- s.field(EntityHistory.changeFieldName, EntityHistory.RowAction.schema),
614
- ))
615
- )
616
-
617
94
  // The chain a rollback row belongs to, read from the chain-id column both
618
95
  // rollback queries select. None for a cross-chain entity, which has no column.
619
96
  let rollbackChainIdSchema: Table.table => option<S.t<ChainId.t>> = Utils.WeakMap.memoize(table =>
@@ -629,41 +106,96 @@ let rollbackRemovedIdSchema: Table.table => S.t<EntityId.t> = Utils.WeakMap.memo
629
106
  S.object(s => s.field(Table.idFieldName, table->Table.getIdSchema))
630
107
  )
631
108
 
109
+ // A change set is the entity's fields, then each change's checkpoint and 1
110
+ // for a change that deletes the entity.
111
+ let changeColumns: array<Staging.column> = [
112
+ {name: EntityHistory.checkpointIdFieldName, kind: I64, isNullable: false},
113
+ {name: EntityHistory.changeFieldName, kind: F64, isNullable: false},
114
+ ]
115
+
116
+ // Fills a stage the addon lent for `rows`, handing it back once every value
117
+ // is in, or abandoning it if one is refused.
118
+ let stageOrThrow = (arena, ~columns, ~rows, fill) => {
119
+ let stage = arena->Staging.begin(~rows, ~columns)
120
+ try {
121
+ fill(stage)
122
+ stage->Staging.commit
123
+ } catch {
124
+ | exn =>
125
+ stage->Staging.abort
126
+ throw(exn)
127
+ }
128
+ }
129
+
130
+ // Rows of a fixed layout, each value read off the item by its column's name.
131
+ let stageRowsOrThrow = (arena, ~writers, items: array<dict<unknown>>) =>
132
+ arena->stageOrThrow(~columns=writers->StagedRows.columnsOf, ~rows=items->Array.length, stage =>
133
+ items->Array.forEachWithIndex((item, row) =>
134
+ writers->Array.forEachWithIndex(
135
+ ({field}: StagedRows.writer, column) =>
136
+ stage->StagedRows.writeField(~writers, ~column, ~row, item->Dict.getUnsafe(field.name)),
137
+ )
138
+ )
139
+ )
140
+
141
+ // Every change a group made, in order: what a set wrote, or the id a delete
142
+ // removed. Each store reads what it keeps off the same rows.
143
+ let stageChangesOrThrow = (arena, ~writers, ~changes: array<Change.t<Internal.entity>>) => {
144
+ let fields = writers->Array.length
145
+ arena->stageOrThrow(
146
+ ~columns=writers->StagedRows.columnsOf->Array.concat(changeColumns),
147
+ ~rows=changes->Array.length,
148
+ stage =>
149
+ changes->Array.forEachWithIndex((change, row) => {
150
+ let entityId = change->Change.getEntityId->(Utils.magic: EntityId.t => unknown)
151
+ switch change {
152
+ | Set({entity}) =>
153
+ let values = entity->(Utils.magic: Internal.entity => dict<unknown>)
154
+ writers->Array.forEachWithIndex(
155
+ ({field}: StagedRows.writer, column) =>
156
+ stage->StagedRows.writeField(
157
+ ~writers,
158
+ ~column,
159
+ ~row,
160
+ field.name === Table.idFieldName ? entityId : values->Dict.getUnsafe(field.name),
161
+ ),
162
+ )
163
+ | Delete(_) =>
164
+ writers->Array.forEachWithIndex(
165
+ ({field}: StagedRows.writer, column) =>
166
+ if field.name === Table.idFieldName {
167
+ stage->StagedRows.writeField(~writers, ~column, ~row, entityId)
168
+ } else {
169
+ stage->Staging.writeDeletedValue(~column, ~row, %raw(`undefined`))
170
+ },
171
+ )
172
+ }
173
+ stage->Staging.writeValue(
174
+ ~column=fields,
175
+ ~row,
176
+ change->Change.getCheckpointId->(Utils.magic: bigint => unknown),
177
+ )
178
+ stage->Staging.writeValue(
179
+ ~column=fields + 1,
180
+ ~row,
181
+ switch change {
182
+ | Set(_) => 0.
183
+ | Delete(_) => 1.
184
+ }->(Utils.magic: float => unknown),
185
+ )
186
+ }),
187
+ )
188
+ }
189
+
632
190
  let make = (
633
191
  ~client: StorageClient.t,
634
- ~logger: Logger.t,
635
- // The entities a load can name before initialize or resume has.
636
- ~entities: array<Internal.entityConfig>=[],
637
192
  // Where the effect cache is dumped to and uploaded from.
638
193
  ~cacheDir: option<NodeJs.Path.t>=?,
639
194
  // Decides how wide an address key is, both when the config's addresses are
640
195
  // encoded at initialize and when stored rows are grouped on resume.
641
196
  ~ecosystem: Ecosystem.name,
642
- // An `envio start --chain` process: builds and looks for a per-chain entity's
643
- // indexes on its own chains' partitions, never on the parent table, so no
644
- // build reaches into the rows a sibling process drives.
645
- ~isolated=false,
646
197
  ~onInitialize=?,
647
198
  ): Persistence.storage => {
648
- // A subgraph's Graph endpoint serves while it syncs, as graph-node's does,
649
- // and graph-node builds every index with the table: the schema's indexes
650
- // are built at initialize rather than once the backfill is done.
651
- let eagerSchemaIndexes = Env.subgraphMode.contents
652
- // The subgraph runtime's caches whose rows can't go stale: a contract call,
653
- // a balance or code at a block number, and a file by its content id.
654
- // graph-node keeps its call cache when a subgraph is synced again from
655
- // scratch; so does a reset here, and a resync asks an RPC only for what it
656
- // never asked before.
657
- let keptEffectCaches = Env.subgraphMode.contents
658
- ? Some([
659
- "envio_subgraph_eth_call",
660
- "envio_subgraph_eth_balance",
661
- "envio_subgraph_has_code",
662
- "envio_subgraph_file",
663
- "envio_subgraph_ipfs_cat",
664
- "envio_subgraph_arweave",
665
- ])
666
- : None
667
199
  let cacheDirPath = switch cacheDir {
668
200
  | Some(cacheDir) => cacheDir
669
201
  | None =>
@@ -674,148 +206,43 @@ let make = (
674
206
  ])
675
207
  }
676
208
 
677
- // Every table is registered with the addon once, by name: the handle is what
678
- // an operation names it by.
679
- let registry: dict<registered> = Dict.make()
680
- let registered = (~table: Table.table, register) =>
681
- switch registry->Utils.Dict.dangerouslyGetNonOption(table.tableName) {
682
- | Some(registered) => registered
683
- | None =>
684
- let registered = register()
685
- registry->Dict.set(table.tableName, registered)
686
- registered
687
- }
688
- let entityTable = (entityConfig: Internal.entityConfig) =>
689
- registered(~table=entityConfig.table, () => client->registerEntity(entityConfig))
690
- let ownTable = (~table: Table.table, ~itemSchema: S.t<unknown>, ~appendOnly=?) =>
691
- registered(~table, () => client->register(~table, ~itemSchema, ~appendOnly?))
692
- let rawEventsTable = () =>
693
- ownTable(
694
- ~table=InternalTable.RawEvents.table,
695
- ~itemSchema=InternalTable.RawEvents.schema->S.toUnknown,
696
- ~appendOnly=true,
697
- )
698
- // Every cache table has the same two columns, whatever its effect's output.
699
- let cacheTable = (table: Table.table) =>
700
- ownTable(~table, ~itemSchema=Internal.cacheItemSchema->S.toUnknown)
701
- entities->Array.forEach(entityConfig => entityConfig->entityTable->ignore)
702
- let tableOf = (table: Table.table) =>
703
- switch registry->Utils.Dict.dangerouslyGetNonOption(table.tableName) {
704
- | Some(registered) => registered
705
- // An effect's cache is loaded before anything wrote or created it.
706
- | None if Internal.EffectCache.fromTableName(table.tableName)->Option.isSome =>
707
- cacheTable(table)
708
- | None => JsError.throwWithMessage(`The storage has no table "${table.tableName}".`)
709
- }
710
-
711
- // A per-chain entity's Postgres rows live in one partition per chain.
712
- let partitions = (entities: array<Internal.entityConfig>, ~chainIds) =>
713
- entities
714
- ->Array.filter(entityConfig =>
715
- entityConfig.storage.postgres && entityConfig.table->Table.getChainIdField->Option.isSome
716
- )
717
- ->Array.flatMap(entityConfig =>
718
- chainIds->Array.map((chainId): StorageClient.partition => {
719
- table: (entityConfig->entityTable).handle,
720
- chainId,
721
- name: partitionTableName(~entityConfig, ~chainId),
722
- })
723
- )
209
+ let layouts = client->StorageClient.layouts
210
+ let entityIndexByName = Dict.make()
211
+ layouts.entities->Array.forEachWithIndex((layout, index) =>
212
+ entityIndexByName->Dict.set(layout.name, index)
213
+ )
214
+ let entityWriters = layouts.entities->Array.map(layout => layout.fields->StagedRows.writersOf)
215
+ let rawEventWriters = layouts.rawEvents->StagedRows.writersOf
216
+ let effectCacheWriters = layouts.effectCache->StagedRows.writersOf
724
217
 
725
218
  let holding = () => client->StorageClient.holding
726
219
 
727
- // Scans .envio/cache into a list of (cache table, absolute TSV path). Flat
728
- // `<name>.tsv` files map to cross-chain caches; a numeric subdirectory
729
- // `<chainId>/<name>.tsv` maps to a chain-scoped cache. Exactly one directory
730
- // level is supported. A non-numeric directory that contains TSVs is rejected.
731
- // Returns [] when .envio/cache doesn't exist.
732
- let scanCacheDir = async () => {
733
- let topEntries = try {
734
- await NodeJs.Fs.Promises.readdir(cacheDirPath)
735
- } catch {
736
- | _ => []
737
- }
738
- let result = []
739
- let _ = await topEntries
740
- ->Array.map(async entry => {
741
- let entryPath = NodeJs.Path.join(cacheDirPath, entry)
742
- let isDir = (await NodeJs.Fs.Promises.stat(entryPath))->NodeJs.Fs.Promises.statsIsDirectory
743
- if isDir {
744
- let subEntries = await NodeJs.Fs.Promises.readdir(entryPath)
745
- let tsvs = subEntries->Array.filter(sub => sub->String.endsWith(".tsv"))
746
- switch Internal.EffectCache.parseChainId(entry) {
747
- | Some(chainId) =>
748
- tsvs->Array.forEach(sub => {
749
- let effectName = sub->String.slice(~start=0, ~end=-4)
750
- let table = Internal.makeCacheTable(~effectName, ~scope=Chain(chainId))
751
- result
752
- ->Array.push((table, NodeJs.Path.join(entryPath, sub)->NodeJs.Path.toString))
753
- ->ignore
754
- })
755
- | None =>
756
- if tsvs->Utils.Array.notEmpty {
757
- JsError.throwWithMessage(
758
- `Invalid effect cache directory ".envio/cache/${entry}". Chain cache directories must be named by a numeric chain id (e.g. "1"). Found cache files: ${tsvs->Array.joinUnsafe(
759
- ", ",
760
- )}.`,
761
- )
762
- }
763
- }
764
- } else if entry->String.endsWith(".tsv") {
765
- let effectName = entry->String.slice(~start=0, ~end=-4)
766
- let table = Internal.makeCacheTable(~effectName, ~scope=CrossChain)
767
- result->Array.push((table, entryPath->NodeJs.Path.toString))->ignore
768
- }
769
- })
770
- ->Promise.all
771
- result
772
- }
773
-
774
220
  let restoreEffectCache = async (~withUpload) => {
775
- if withUpload {
776
- switch await scanCacheDir() {
777
- | [] => logger->Logger.info("No saved effect cache to load from .envio/cache.")
778
- | entries =>
779
- try {
780
- await client->StorageClient.uploadEffectCache(
781
- entries->Array.map(((table, path)): StorageClient.cacheUpload => {
782
- table: (table->cacheTable).handle,
783
- path,
784
- }),
785
- )
786
- logger->Logger.info("Successfully uploaded cache.")
787
- } catch {
788
- | exn => logger->Logger.errorWithExn("Failed to upload cache, continuing without it.", exn)
789
- }
790
- }
791
- }
792
-
793
221
  let cache = Dict.make()
794
- (await client->StorageClient.effectCacheTables)->Array.forEach(({tableName, rows}) => {
795
- switch Internal.EffectCache.fromTableName(tableName) {
796
- | Some((effectName, scope)) =>
797
- cache->Dict.set(
798
- tableName,
799
- ({effectName, scope, tableName, count: rows}: Persistence.effectCacheRecord),
800
- )
801
- | None => ()
802
- }
803
- })
222
+
223
+ (
224
+ await client->StorageClient.restoreEffectCache(cacheDirPath->NodeJs.Path.toString, withUpload)
225
+ )->Array.forEach(({effectName, ?chainId, tableName, rows}) =>
226
+ cache->Dict.set(
227
+ tableName,
228
+ (
229
+ {
230
+ effectName,
231
+ scope: switch chainId {
232
+ | Some(chainId) => Chain(chainId)
233
+ | None => CrossChain
234
+ },
235
+ tableName,
236
+ count: rows,
237
+ }: Persistence.effectCacheRecord
238
+ ),
239
+ )
240
+ )
804
241
  cache
805
242
  }
806
243
 
807
- let partitionChainIds = chainIds => isolated ? Some(chainIds) : None
808
-
809
- let schemaIndexes = (~entities: array<Internal.entityConfig>, ~chainIds) =>
810
- getSchemaIndexes(
811
- ~entities=entities->Array.filter((e: Internal.entityConfig) => e.storage.postgres),
812
- ~partitionChainIds=?partitionChainIds(chainIds),
813
- )->Array.map(IndexDefinition.toInput)
814
-
815
244
  let initialize = async (
816
245
  ~chainConfigs=[],
817
- ~entities=[],
818
- ~enums=[],
819
246
  ~contractMapping,
820
247
  ~envioInfo,
821
248
  ): Persistence.initialState => {
@@ -825,33 +252,12 @@ let make = (
825
252
  )
826
253
 
827
254
  await client->StorageClient.initialize({
828
- sequence: CheckpointSequence.fromEntities(entities),
829
- tables: [rawEventsTable().handle]->Array.concat(
830
- entities->Array.map(entityConfig => (entityConfig->entityTable).handle),
831
- ),
832
- partitions: entities->partitions(
833
- ~chainIds=chainConfigs->Array.map((chainConfig: Config.chain) => chainConfig.id),
834
- ),
835
- enums: enums->Array.map((enumConfig: Table.enumConfig<Table.enum>): StorageClient.enum => {
836
- name: enumConfig.name,
837
- variants: enumConfig.variants->(Utils.magic: array<Table.enum> => array<string>),
838
- }),
839
255
  chains: chainConfigs->Array.map(toChainConfig),
840
256
  envioInfo: envioInfo->JSON.stringify,
841
257
  contractNames: contractMapping->ContractMapping.names,
842
258
  addresses: rowsByChain->Array.flat->addressColumns,
843
- ?keptEffectCaches,
844
259
  })
845
260
 
846
- if eagerSchemaIndexes {
847
- await client->StorageClient.ensureSchemaIndexes(
848
- schemaIndexes(
849
- ~entities,
850
- ~chainIds=chainConfigs->Array.map((chainConfig: Config.chain) => chainConfig.id),
851
- ),
852
- )
853
- }
854
-
855
261
  let cache = await restoreEffectCache(~withUpload=true)
856
262
 
857
263
  // Integration with other tools like Hasura
@@ -887,7 +293,11 @@ let make = (
887
293
 
888
294
  let loadOrThrow = async (~filter: EntityFilter.t, ~table: Table.table) => {
889
295
  let conditions = LoadFilter.make(~filter, ~table, ~column=field => field.pgDbFieldName)
890
- switch await client->StorageClient.loadWhere(~table=(table->tableOf).handle, ~conditions) {
296
+ let loading = switch entityIndexByName->Utils.Dict.dangerouslyGetNonOption(table.tableName) {
297
+ | Some(entity) => client->StorageClient.loadWhere(~entity, ~conditions)
298
+ | None => client->StorageClient.loadEffectCache(~table=table.tableName, ~conditions)
299
+ }
300
+ switch await loading {
891
301
  | exception exn =>
892
302
  throw(
893
303
  Persistence.StorageError({
@@ -931,28 +341,15 @@ let make = (
931
341
  columns
932
342
  }
933
343
 
934
- // The physical table a query index for `scope` is built on. A per-chain
935
- // entity's query carries the scope's chain id, so it is planned against that
936
- // chain's partition, which an index on the parent covers by cascading.
937
- let queryTableName = (~entityConfig: Internal.entityConfig, ~scope: Internal.chainScope) =>
938
- switch scope {
939
- | Chain(chainId) =>
940
- indexTableNames(
941
- entityConfig,
942
- ~partitionChainIds=partitionChainIds([chainId]),
943
- )->Array.getUnsafe(0)
944
- | CrossChain => entityConfig.table.tableName
945
- }
946
-
947
344
  let ensureQueryIndexes = async (
948
345
  ~entityConfig: Internal.entityConfig,
949
346
  ~scope: Internal.chainScope,
950
347
  ~filters: array<EntityFilter.t>,
951
348
  ) =>
952
349
  switch client->StorageClient.ensureQueryIndexes(
953
- (entityConfig->entityTable).handle,
954
- queryTableName(~entityConfig, ~scope),
955
- filterColumns(~table=entityConfig.table, ~filters),
350
+ ~entity=entityConfig.index,
351
+ ~chainId=scope->Internal.chainScopeChainId->Null.fromOption,
352
+ ~columns=filterColumns(~table=entityConfig.table, ~filters),
956
353
  ) {
957
354
  | Value(building) => await building
958
355
  | Null => ()
@@ -961,47 +358,13 @@ let make = (
961
358
  // Runs on a resumed indexer that is already ready, so handlers may be issuing
962
359
  // getWhere queries alongside it. Nothing here writes `ready_at` — the chains
963
360
  // already carry theirs.
964
- let ensureSchemaIndexes = (~entities, ~chainIds) =>
965
- client->StorageClient.ensureSchemaIndexes(schemaIndexes(~entities, ~chainIds))
361
+ let ensureSchemaIndexes = (~chainIds) => client->StorageClient.ensureSchemaIndexes(chainIds)
966
362
 
967
- let finalizeBackfill = (~entities, ~chainIds, ~readyAt: Date.t) =>
968
- client->StorageClient.finalizeBackfill(
969
- schemaIndexes(~entities, ~chainIds),
970
- chainIds,
971
- readyAt->Date.getTime,
972
- )
363
+ let finalizeBackfill = (~chainIds, ~readyAt: Date.t) =>
364
+ client->StorageClient.finalizeBackfill(chainIds, readyAt->Date.getTime)
973
365
 
974
- let dumpEffectCache = async () => {
975
- try {
976
- let tables =
977
- (await client->StorageClient.effectCacheTables)->Array.filter(({rows}) => rows > 0)
978
- if tables->Utils.Array.notEmpty {
979
- logger->Logger.info(
980
- `Dumping cache: ${tables
981
- ->Array.map(({tableName, rows}) => tableName ++ " (" ++ rows->Int.toString ++ " rows)")
982
- ->Array.joinUnsafe(", ")}`,
983
- )
984
- await client->StorageClient.dumpEffectCache(
985
- tables->Array.filterMap(({tableName}) =>
986
- Internal.EffectCache.fromTableName(tableName)->Option.map(((
987
- effectName,
988
- scope,
989
- )): StorageClient.cacheDump => {
990
- tableName,
991
- // Chain-scoped caches dump into a directory of their chain's.
992
- path: NodeJs.Path.join(
993
- cacheDirPath,
994
- Internal.EffectCache.toCachePath(~effectName, ~scope),
995
- )->NodeJs.Path.toString,
996
- })
997
- ),
998
- )
999
- logger->Logger.info(`Successfully dumped cache to ${cacheDirPath->NodeJs.Path.toString}`)
1000
- }
1001
- } catch {
1002
- | exn => logger->Logger.errorWithExn(`Failed to dump cache.`, exn)
1003
- }
1004
- }
366
+ let dumpEffectCache = () =>
367
+ client->StorageClient.dumpEffectCache(cacheDirPath->NodeJs.Path.toString)
1005
368
 
1006
369
  let readAddressRows = (result): array<AddressRows.row> =>
1007
370
  client
@@ -1044,27 +407,18 @@ let make = (
1044
407
  }
1045
408
  }
1046
409
 
1047
- let addChain = (~chainConfig: Config.chain, ~entities, ~contractMapping) =>
410
+ let addChain = (~chainConfig: Config.chain, ~contractMapping) =>
1048
411
  client->StorageClient.addChain({
1049
412
  chain: chainConfig->toChainConfig,
1050
- partitions: entities->partitions(~chainIds=[chainConfig.id]),
1051
413
  addresses: chainConfig
1052
414
  ->ChainState.configStorageRows(~ecosystem, ~contractMapping)
1053
415
  ->addressColumns,
1054
416
  })
1055
417
 
1056
- let resumeInitialState = async (
1057
- ~entities: array<Internal.entityConfig>,
1058
- ~chainIds,
1059
- ~contractMapping,
1060
- ): Persistence.initialState => {
418
+ let resumeInitialState = async (~chainIds, ~contractMapping): Persistence.initialState => {
1061
419
  let (cache, resumed) = await Promise.all2((
1062
420
  restoreEffectCache(~withUpload=false),
1063
- client->StorageClient.resume({
1064
- sequence: CheckpointSequence.fromEntities(entities),
1065
- tables: entities->Array.map(entityConfig => (entityConfig->entityTable).handle),
1066
- chainIds,
1067
- }),
421
+ client->StorageClient.resume(chainIds),
1068
422
  ))
1069
423
  let addressRowsByChainId = resumed.addresses->readAddressRows->AddressRows.group
1070
424
  let stored =
@@ -1087,12 +441,12 @@ let make = (
1087
441
  sourceBlockNumber: chain.sourceBlock,
1088
442
  })
1089
443
  let checkpointFrontier = Frontier.fromEntries(
1090
- stored->Array.map(((id, chain)) => (id, chain.checkpointId->BigInt.fromStringOrThrow)),
444
+ stored->Array.map(((id, chain)) => (id, chain.checkpointId)),
1091
445
  )
1092
446
  let reorgCheckpoints = resumed.reorgCheckpoints->Array.map((
1093
447
  checkpoint
1094
448
  ): Internal.reorgCheckpoint => {
1095
- checkpointId: checkpoint.id->BigInt.fromStringOrThrow,
449
+ checkpointId: checkpoint.id,
1096
450
  chainId: checkpoint.chainId->ChainId.normalizeOrThrow,
1097
451
  blockNumber: checkpoint.blockNumber,
1098
452
  blockHash: checkpoint.blockHash,
@@ -1129,19 +483,13 @@ let make = (
1129
483
  let pruneStaleCheckpoints = (~safeCheckpoints) =>
1130
484
  client->StorageClient.pruneCheckpoints(safeCheckpoints->bounds)
1131
485
 
1132
- let pruneStaleEntityHistory = (~entityConfig, ~safeCheckpoints) =>
1133
- client->StorageClient.pruneHistory((entityConfig->entityTable).handle, safeCheckpoints->bounds)
486
+ let pruneStaleEntityHistory = (~entityConfig: Internal.entityConfig, ~safeCheckpoints) =>
487
+ client->StorageClient.pruneHistory(entityConfig.index, safeCheckpoints->bounds)
1134
488
 
1135
- let getRollbackTargetCheckpoint = async (~reorgChainId, ~lastKnownValidBlockNumber, ~sequence) =>
489
+ let getRollbackTargetCheckpoint = async (~reorgChainId, ~lastKnownValidBlockNumber) =>
1136
490
  (
1137
- await client->StorageClient.rollbackTargetCheckpoint(
1138
- reorgChainId,
1139
- lastKnownValidBlockNumber,
1140
- sequence,
1141
- )
1142
- )
1143
- ->Nullable.toOption
1144
- ->Option.map(BigInt.fromStringOrThrow)
491
+ await client->StorageClient.rollbackTargetCheckpoint(reorgChainId, lastKnownValidBlockNumber)
492
+ )->Nullable.toOption
1145
493
 
1146
494
  let getRollbackProgressDiff = async (~floors: RollbackFloors.t) =>
1147
495
  (
@@ -1156,7 +504,7 @@ let make = (
1156
504
 
1157
505
  let getRollbackData = async (~entityConfig: Internal.entityConfig, ~floors: RollbackFloors.t) => {
1158
506
  let {removed, restored} = await client->StorageClient.rollbackData(
1159
- (entityConfig->entityTable).handle,
507
+ entityConfig.index,
1160
508
  floors.checkpointBounds->bounds,
1161
509
  )
1162
510
  let removedIdRows = try client->StorageClient.read(removed) catch {
@@ -1172,183 +520,57 @@ let make = (
1172
520
  | None => Internal.CrossChain
1173
521
  | Some(schema) => Internal.Chain(row->S.parseOrThrow(schema))
1174
522
  }
1175
- let removals = removedIdRows->Array.map((row): Persistence.rollbackRemoval => {
1176
- entityId: row->S.parseOrThrow(rollbackRemovedIdSchema(entityConfig.table)),
1177
- scope: scopeOf(row),
1178
- })
1179
- let restoredEntitiesResult = []
1180
- rollbackRows->Array.forEach(row => {
1181
- let (entityId, action) = row->S.parseOrThrow(rollbackRowStateSchema(entityConfig.table))
1182
- switch action {
1183
- | SET => restoredEntitiesResult->Array.push(row)->ignore
1184
- | DELETE => removals->Array.push({entityId, scope: scopeOf(row)})->ignore
1185
- }
1186
- })
1187
-
1188
523
  (
1189
- removals,
1190
- restoredEntitiesResult
524
+ removedIdRows->Array.map((row): Persistence.rollbackRemoval => {
525
+ entityId: row->S.parseOrThrow(rollbackRemovedIdSchema(entityConfig.table)),
526
+ scope: scopeOf(row),
527
+ }),
528
+ rollbackRows
1191
529
  ->(Utils.magic: array<dict<unknown>> => array<unknown>)
1192
530
  ->S.parseOrThrow(entityConfig.table->Table.pgRowsSchema)
1193
531
  ->(Utils.magic: array<unknown> => array<Internal.entity>),
1194
532
  )
1195
533
  }
1196
534
 
1197
- // One write group's changes, laid out for every store that keeps the entity:
1198
- // for Postgres the latest change of each id for the entity table, and every
1199
- // change for its history; for ClickHouse every change as a history row.
1200
- let entityWrite = (
1201
- {entityConfig, scope, changes, shouldSaveHistory}: Persistence.updatedEntity,
1202
- ~rollback: option<Persistence.rollback>,
1203
- ~config: Config.t,
1204
- ~staged,
1205
- ): StorageClient.entityWrite => {
1206
- let table = entityConfig->entityTable
1207
-
1208
- let chainIdStamp = switch (entityConfig.table->Table.getChainIdField, scope) {
1209
- | (Some(field), Internal.Chain(chainId)) => Some((field, chainId))
1210
- | _ => None
1211
- }
1212
-
1213
- let postgresWrite = (): StorageClient.postgresEntityWrite => {
1214
- // Every row in this group belongs to the group's scope, so the chain id
1215
- // is stamped once here instead of being looked up per row downstream.
1216
- let stamped = switch chainIdStamp {
1217
- | Some((field, chainId)) =>
1218
- changes->Array.map(change =>
1219
- switch change {
1220
- | Change.Set(set) =>
1221
- Change.Set({
1222
- ...set,
1223
- entity: set.entity->Internal.stampChainId(~fieldName=field.fieldName, ~chainId),
1224
- })
1225
- | Delete(_) => change
1226
- }
1227
- )
1228
- | None => changes
1229
- }
1230
-
1231
- // The rollback-diff change is written to the entity table only, never the
1232
- // history table; when present it is an id's oldest change.
1233
- let diffCheckpointId =
1234
- rollback->Option.flatMap(r =>
1235
- config.checkpointSequence->CheckpointSequence.findForScope(r.diffFrontier, ~scope)
1236
- )
1237
-
1238
- let historySets = []
1239
- let historySetCheckpointIds = []
1240
- let historyDeleteIds = []
1241
- let historyDeleteCheckpointIds = []
1242
- let idsWithDiff = Utils.Set.make()
1243
-
1244
- // Each id's latest change (the last one seen) and, when saving history,
1245
- // every change but the diff for the history table. Keyed by the id's
1246
- // string key, while what goes to SQL keeps the real id so it serializes
1247
- // with the id column's type.
1248
- let latestChangeById = Dict.make()
1249
- let orderedIds = []
1250
- stamped->Array.forEach(change => {
1251
- let entityId = change->Change.getEntityId
1252
- let entityKey = entityId->EntityId.toKey
1253
- if latestChangeById->Utils.Dict.dangerouslyGetNonOption(entityKey)->Option.isNone {
1254
- orderedIds->Array.push(entityId)
1255
- }
1256
- latestChangeById->Dict.set(entityKey, change)
1257
- if shouldSaveHistory {
1258
- if Some(change->Change.getCheckpointId) === diffCheckpointId {
1259
- idsWithDiff->Utils.Set.add(entityKey)->ignore
1260
- } else {
1261
- switch change {
1262
- | Delete({entityId, checkpointId}) =>
1263
- historyDeleteIds->Array.push(entityId)
1264
- historyDeleteCheckpointIds->Array.push(checkpointId->BigInt.toString)
1265
- | Set({entity, checkpointId}) =>
1266
- historySets->Array.push(entity)
1267
- historySetCheckpointIds->Array.push(checkpointId->BigInt.toString)
1268
- }
1269
- }
1270
- }
1271
- })
1272
-
1273
- let sets = []
1274
- let deletes = []
1275
- let backfill = []
1276
- orderedIds->Array.forEach(entityId => {
1277
- let entityKey = entityId->EntityId.toKey
1278
- switch latestChangeById->Dict.getUnsafe(entityKey) {
1279
- | Set({entity}) => sets->Array.push(entity)
1280
- | Delete({entityId}) => deletes->Array.push(entityId)
1281
- }
1282
-
1283
- // An id needs a history backfill iff none of its changes is the diff.
1284
- if shouldSaveHistory && !(idsWithDiff->Utils.Set.has(entityKey)) {
1285
- backfill->Array.push(entityId)
1286
- }
1287
- })
1288
-
1289
- let rowsOf = entities =>
1290
- entities->Utils.Array.notEmpty
1291
- ? Some(
1292
- client->rowsOrThrow(
1293
- table,
1294
- ~itemSchema=entityConfig->getRowSchema->S.toUnknown,
1295
- entities->(Utils.magic: array<Internal.entity> => array<unknown>),
1296
- ~staged,
1297
- ),
1298
- )
1299
- : None
1300
- {
1301
- sets: ?rowsOf(sets),
1302
- deletes: entityConfig.table->renderIds(deletes),
1303
- history: ?(
1304
- shouldSaveHistory
1305
- ? Some(
1306
- (
1307
- {
1308
- backfill: entityConfig.table->renderIds(backfill),
1309
- sets: ?rowsOf(historySets),
1310
- setCheckpointIds: historySetCheckpointIds,
1311
- deleteIds: entityConfig.table->renderIds(historyDeleteIds),
1312
- deleteCheckpointIds: historyDeleteCheckpointIds,
1313
- }: StorageClient.historyWrite
1314
- ),
1315
- )
1316
- : None
1317
- ),
1318
- }
1319
- }
535
+ let stageRows = (~table, ~writers, items) =>
536
+ client
537
+ ->StorageClient.arena((~rows) => client->StorageClient.beginRows(~table, ~rows))
538
+ ->stageRowsOrThrow(~writers, items)
1320
539
 
1321
- {
1322
- table: table.handle,
1323
- chainId: ?(chainIdStamp->Option.map(((_, chainId)) => chainId)),
1324
- postgres: ?(table.postgres->Option.map(_ => postgresWrite())),
1325
- clickhouse: ?switch (table.clickhouse, changes) {
1326
- | (_, []) | (None, _) => None
1327
- | (Some(chTable), _) =>
1328
- let handle = staged->stagedFor(~store=ClickHouse, () =>
1329
- client
1330
- ->StorageClient.arena(~store=ClickHouse)
1331
- ->ClickHouse.stageChangesOrThrow(chTable, ~entityConfig, ~scope, ~changes)
1332
- )
1333
- staged.handles->Array.push(handle)
1334
- Some(handle)
1335
- },
1336
- }
1337
- }
540
+ let stageChanges = ({entityConfig, changes}: Persistence.updatedEntity) =>
541
+ client
542
+ ->StorageClient.arena((~rows) =>
543
+ client->StorageClient.beginChanges(~entity=entityConfig.index, ~rows)
544
+ )
545
+ ->stageChangesOrThrow(~writers=entityWriters->Array.getUnsafe(entityConfig.index), ~changes)
546
+
547
+ // A value a store can't take is reported as the store's own refusal: the
548
+ // store of record's, for an entity it keeps.
549
+ let changesErrorOf = (entityConfig: Internal.entityConfig, exn) => Persistence.StorageError({
550
+ message: entityConfig.storage.postgres
551
+ ? `Failed to insert items into table "${entityConfig.table.tableName}"`
552
+ : `Failed to convert items for ClickHouse table "${EntityHistory.historyTableName(
553
+ ~entityName=entityConfig.name,
554
+ ~entityIndex=entityConfig.index,
555
+ )}"`,
556
+ reason: exn->Utils.prettifyExn,
557
+ })
1338
558
 
1339
559
  let writeBatch = async (
1340
560
  ~batch: Batch.t,
1341
561
  ~rollback: option<Persistence.rollback>,
1342
562
  ~config: Config.t,
1343
- ~allEntities: array<Internal.entityConfig>,
1344
563
  ~updatedEffectsCache: array<Persistence.updatedEffectCache>,
1345
564
  ~updatedEntities: array<Persistence.updatedEntity>,
1346
565
  ~registeredAddresses: array<AddressRows.staged>,
1347
566
  ~chainMetaData,
1348
567
  ~onWrite,
1349
568
  ) => {
1350
- let (frontierChainIds, frontierCheckpointIds) =
1351
- Persistence.writtenFrontier(~batch, ~rollback)->Frontier.unnestParams
569
+ let (frontierChainIds, frontierCheckpointIds) = {
570
+ let frontier = Persistence.writtenFrontier(~batch, ~rollback)
571
+ let chainIds = frontier->Frontier.chainIds
572
+ (chainIds, chainIds->Array.map(chainId => frontier->Frontier.get(chainId)))
573
+ }
1352
574
 
1353
575
  // A single on-chain log fans out to one item per matching registration;
1354
576
  // `raw_events` records the log itself, so it is deduped by its coordinate
@@ -1373,7 +595,14 @@ let make = (
1373
595
  []
1374
596
  }
1375
597
 
1376
- let staged = {handles: [], seconds: Dict.make()}
598
+ let handles = []
599
+ let staged = handle => {
600
+ handles->Array.push(handle)
601
+ handle
602
+ }
603
+ let stagingStarted = Performance.now()
604
+ let clickHouseStagingSeconds = ref(0.)
605
+ let clickHouseOnlyStagingSeconds = ref(0.)
1377
606
  let input: StorageClient.batch = try {
1378
607
  rollback: ?(
1379
608
  rollback->Option.map(({
@@ -1383,7 +612,6 @@ let make = (
1383
612
  diffCheckpoints,
1384
613
  }): StorageClient.rollbackWrite => {
1385
614
  bounds: floors.checkpointBounds->bounds,
1386
- tables: allEntities->Array.map(entityConfig => (entityConfig->entityTable).handle),
1387
615
  progress: progressedChains->Array.map(progress),
1388
616
  removedAddresses: {
1389
617
  chainIds: rolledBackAddresses->Array.map(key => key.chainId),
@@ -1392,7 +620,7 @@ let make = (
1392
620
  },
1393
621
  diffCheckpoints: diffCheckpoints->Array.map((diff): StorageClient.diffCheckpoint => {
1394
622
  chainId: diff.chainId,
1395
- checkpointId: diff.checkpointId->BigInt.toString,
623
+ checkpointId: diff.checkpointId,
1396
624
  blockNumber: diff.blockNumber,
1397
625
  }),
1398
626
  })
@@ -1408,26 +636,49 @@ let make = (
1408
636
  ),
1409
637
  rawEvents: ?(
1410
638
  rawEvents->Utils.Array.notEmpty
1411
- ? {
1412
- let table = rawEventsTable()
1413
- Some(
1414
- (
1415
- {
1416
- table: table.handle,
1417
- rows: client->rowsOrThrow(
1418
- table,
1419
- ~itemSchema=InternalTable.RawEvents.schema->S.toUnknown,
1420
- rawEvents->(Utils.magic: array<Internal.rawEvent> => array<unknown>),
1421
- ~staged,
1422
- ),
1423
- }: StorageClient.tableWrite
1424
- ),
1425
- )
1426
- }
639
+ ? Some(
640
+ try stageRows(
641
+ ~table=RawEvents,
642
+ ~writers=rawEventWriters,
643
+ rawEvents->(Utils.magic: array<Internal.rawEvent> => array<dict<unknown>>),
644
+ )->staged catch {
645
+ | exn =>
646
+ throw(
647
+ Persistence.StorageError({
648
+ message: `Failed to insert items into table "${InternalTable.RawEvents.table.tableName}"`,
649
+ reason: exn->Utils.prettifyExn,
650
+ }),
651
+ )
652
+ },
653
+ )
1427
654
  : None
1428
655
  ),
1429
- entities: updatedEntities->Array.map(update =>
1430
- update->entityWrite(~rollback, ~config, ~staged)
656
+ changes: updatedEntities->Array.filterMap(update =>
657
+ switch update.changes {
658
+ | [] => None
659
+ | _ =>
660
+ let started = Performance.now()
661
+ let handle = try update->stageChanges->staged catch {
662
+ | exn => throw(changesErrorOf(update.entityConfig, exn))
663
+ }
664
+ let seconds = started->Performance.secondsSince
665
+ if update.entityConfig.storage.clickhouse {
666
+ clickHouseStagingSeconds := clickHouseStagingSeconds.contents +. seconds
667
+ }
668
+ if !update.entityConfig.storage.postgres {
669
+ clickHouseOnlyStagingSeconds := clickHouseOnlyStagingSeconds.contents +. seconds
670
+ }
671
+ Some(
672
+ (
673
+ {
674
+ entity: update.entityConfig.index,
675
+ chainId: ?(update.scope->Internal.chainScopeChainId),
676
+ keepsHistory: update.shouldSaveHistory,
677
+ changes: handle,
678
+ }: StorageClient.changes
679
+ ),
680
+ )
681
+ }
1431
682
  ),
1432
683
  chainMeta: chainMetaData->Option.mapOr([], chainMeta),
1433
684
  addresses: registeredAddresses->Array.map(staged => staged.row)->addressColumns,
@@ -1435,7 +686,7 @@ let make = (
1435
686
  // A checkpoint anchors the history its chain keeps, which the batch
1436
687
  // decides chain by chain.
1437
688
  checkpoints: {
1438
- ids: batch.checkpointIds->Array.map(id => id->BigInt.toString),
689
+ ids: batch.checkpointIds,
1439
690
  chainIds: batch.checkpointChainIds,
1440
691
  blockNumbers: batch.checkpointBlockNumbers,
1441
692
  blockHashes: batch.checkpointBlockHashes,
@@ -1451,44 +702,70 @@ let make = (
1451
702
  items,
1452
703
  shouldInitialize,
1453
704
  }): StorageClient.effectCacheWrite => {
1454
- let registered = table->cacheTable
1455
- {
1456
- table: registered.handle,
1457
- create: shouldInitialize,
1458
- rows: client->rowsOrThrow(
1459
- registered,
1460
- ~itemSchema=itemSchema->S.toUnknown,
1461
- items->(Utils.magic: array<Internal.effectCacheItem> => array<unknown>),
1462
- ~staged,
705
+ table: table.tableName,
706
+ create: shouldInitialize,
707
+ rows: try stageRows(
708
+ ~table=EffectCache,
709
+ ~writers=effectCacheWriters,
710
+ items->Array.map(item =>
711
+ item
712
+ ->S.reverseConvertOrThrow(itemSchema)
713
+ ->(Utils.magic: unknown => dict<unknown>)
1463
714
  ),
1464
- }
715
+ )->staged catch {
716
+ | S.Raised(_) as exn =>
717
+ throw(
718
+ Persistence.StorageError({
719
+ message: `Failed to convert items for table "${table.tableName}"`,
720
+ reason: exn,
721
+ }),
722
+ )
723
+ | exn =>
724
+ throw(
725
+ Persistence.StorageError({
726
+ message: `Failed to insert items into table "${table.tableName}"`,
727
+ reason: exn->Utils.prettifyExn,
728
+ }),
729
+ )
730
+ },
1465
731
  }),
1466
732
  } catch {
1467
733
  | exn =>
1468
- client->StorageClient.discardStaged(staged.handles)
734
+ client->StorageClient.discardStaged(handles)
1469
735
  throw(exn)
1470
736
  }
737
+ // Each store is charged for staging what it writes: an entity both keep
738
+ // is staged once for the two.
739
+ let postgresStagingSeconds =
740
+ stagingStarted->Performance.secondsSince -. clickHouseOnlyStagingSeconds.contents
1471
741
 
1472
742
  let timings = try await client->StorageClient.writeBatch(input) catch {
1473
743
  | exn =>
1474
744
  // Batches the addon never took, because it refused the call itself.
1475
- client->StorageClient.discardStaged(staged.handles)
745
+ client->StorageClient.discardStaged(handles)
1476
746
  throw(exn->storageErrorOf)
1477
747
  }
1478
- timings->Array.forEach(({store, seconds}) => {
1479
- let name = store->storeName
748
+ timings->Array.forEach(({store, seconds}) =>
1480
749
  onWrite(
1481
- ~storage=name,
1482
- ~timeSeconds=seconds +. staged.seconds->Dict.get(name)->Option.getOr(0.),
750
+ ~storage=store->storeName,
751
+ ~timeSeconds=seconds +.
752
+ switch store {
753
+ | Postgres => postgresStagingSeconds
754
+ | ClickHouse => clickHouseStagingSeconds.contents
755
+ },
1483
756
  )
1484
- })
757
+ )
1485
758
  }
1486
759
 
1487
760
  let close = () => client->StorageClient.close
1488
761
 
1489
762
  {
1490
763
  name: postgresName,
1491
- entityStoreName: entityConfig => (entityConfig->entityTable).home->storeName,
764
+ entityStoreName: entityConfig =>
765
+ (layouts.entities->Array.getUnsafe(entityConfig.index)).home->Option.mapOr(
766
+ postgresName,
767
+ storeName,
768
+ ),
1492
769
  holding,
1493
770
  initialize,
1494
771
  readStoredConfig,
@@ -1520,8 +797,13 @@ let clientFromEnv = (
1520
797
  ) =>
1521
798
  makeClient(
1522
799
  ~logger,
1523
- ~chainIdMode=config.chainIdMode,
1524
- ~postgres=postgresOptions(~pgSchema, ~isHasuraEnabled, ~maxConnections?),
800
+ ~config,
801
+ ~postgres=postgresOptions(
802
+ ~pgSchema,
803
+ ~isHasuraEnabled,
804
+ ~maxConnections?,
805
+ ~isolated=config.isolated,
806
+ ),
1525
807
  ~clickhouse=?config.storage.clickhouse ? Some(ClickHouse.optionsFromEnvOrThrow()) : None,
1526
808
  )
1527
809
 
@@ -1535,11 +817,8 @@ let makeStorageFromEnv = (
1535
817
  ) => {
1536
818
  make(
1537
819
  ~client=clientFromEnv(~config, ~logger, ~pgSchema, ~isHasuraEnabled, ~maxConnections?),
1538
- ~logger,
1539
- ~entities=config.userEntities,
1540
820
  ~cacheDir?,
1541
821
  ~ecosystem=config.ecosystem.name,
1542
- ~isolated=config.isolated,
1543
822
  ~onInitialize=?isHasuraEnabled
1544
823
  ? Some(
1545
824
  () =>