envio 3.11.0 → 3.12.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 (89) hide show
  1. package/index.d.ts +3 -0
  2. package/package.json +6 -6
  3. package/src/Batch.res +111 -78
  4. package/src/Batch.res.mjs +53 -46
  5. package/src/BatchProcessing.res +1 -11
  6. package/src/BatchProcessing.res.mjs +1 -4
  7. package/src/ChainState.res +72 -19
  8. package/src/ChainState.res.mjs +50 -12
  9. package/src/ChainState.resi +5 -0
  10. package/src/Config.res +58 -37
  11. package/src/Config.res.mjs +63 -53
  12. package/src/CrossChainState.res +49 -52
  13. package/src/CrossChainState.res.mjs +25 -23
  14. package/src/CrossChainState.resi +6 -13
  15. package/src/EffectState.res +13 -4
  16. package/src/EffectState.resi +7 -1
  17. package/src/EventProcessing.res +6 -6
  18. package/src/EventProcessing.res.mjs +6 -6
  19. package/src/FetchState.res +14 -0
  20. package/src/FetchState.res.mjs +14 -0
  21. package/src/FinalizeBackfill.res +16 -12
  22. package/src/FinalizeBackfill.res.mjs +3 -3
  23. package/src/HistoryPolicy.res +46 -0
  24. package/src/HistoryPolicy.res.mjs +50 -0
  25. package/src/InMemoryStore.res +30 -32
  26. package/src/InMemoryStore.res.mjs +33 -28
  27. package/src/IndexerState.res +66 -45
  28. package/src/IndexerState.res.mjs +57 -29
  29. package/src/IndexerState.resi +8 -13
  30. package/src/Internal.res +4 -2
  31. package/src/LoadLayer.res +11 -4
  32. package/src/LoadLayer.res.mjs +8 -5
  33. package/src/Main.res +25 -24
  34. package/src/Main.res.mjs +24 -25
  35. package/src/Metrics.res +49 -1
  36. package/src/Metrics.res.mjs +26 -3
  37. package/src/Persistence.res +55 -16
  38. package/src/Persistence.res.mjs +18 -3
  39. package/src/PgStorage.res +194 -118
  40. package/src/PgStorage.res.mjs +132 -72
  41. package/src/PruneStaleHistory.res +9 -15
  42. package/src/PruneStaleHistory.res.mjs +11 -20
  43. package/src/Rollback.res +9 -14
  44. package/src/Rollback.res.mjs +5 -3
  45. package/src/Sink.res +7 -5
  46. package/src/Sink.res.mjs +4 -4
  47. package/src/Staging.res +298 -0
  48. package/src/Staging.res.mjs +257 -0
  49. package/src/TestIndexer.res +5 -5
  50. package/src/TestIndexer.res.mjs +7 -6
  51. package/src/UserContext.res +7 -4
  52. package/src/UserContext.res.mjs +5 -2
  53. package/src/Utils.res +9 -0
  54. package/src/Utils.res.mjs +9 -0
  55. package/src/Writing.res +34 -20
  56. package/src/Writing.res.mjs +15 -20
  57. package/src/bindings/ClickHouse.res +41 -34
  58. package/src/bindings/ClickHouse.res.mjs +36 -30
  59. package/src/bindings/ClickHouseSink.res +45 -186
  60. package/src/bindings/ClickHouseSink.res.mjs +21 -174
  61. package/src/db/CheckpointSequence.res +109 -0
  62. package/src/db/CheckpointSequence.res.mjs +99 -0
  63. package/src/db/EntityHistory.res +6 -5
  64. package/src/db/EntityHistory.res.mjs +5 -5
  65. package/src/db/Frontier.res +86 -0
  66. package/src/db/Frontier.res.mjs +126 -0
  67. package/src/db/InternalTable.res +145 -27
  68. package/src/db/InternalTable.res.mjs +115 -29
  69. package/src/db/RollbackFloors.res +34 -41
  70. package/src/db/RollbackFloors.res.mjs +27 -71
  71. package/src/sources/BlockStore.res +3 -0
  72. package/src/sources/EvmHyperSyncSource.res +3 -1
  73. package/src/sources/EvmHyperSyncSource.res.mjs +2 -1
  74. package/src/sources/HyperSync.res +2 -0
  75. package/src/sources/HyperSync.res.mjs +1 -0
  76. package/src/sources/HyperSync.resi +1 -0
  77. package/src/sources/HyperSyncClient.res +3 -0
  78. package/src/sources/RequestStat.res +8 -1
  79. package/src/sources/Source.res +4 -3
  80. package/src/sources/SourceManager.res +30 -10
  81. package/src/sources/SourceManager.res.mjs +24 -7
  82. package/src/sources/SourceManager.resi +2 -0
  83. package/src/tui/Tui.res +56 -143
  84. package/src/tui/Tui.res.mjs +70 -166
  85. package/src/tui/components/TuiData.res +61 -16
  86. package/src/tui/components/TuiData.res.mjs +53 -15
  87. package/svm.schema.json +0 -7
  88. package/src/db/CheckpointBounds.res +0 -53
  89. package/src/db/CheckpointBounds.res.mjs +0 -44
package/src/PgStorage.res CHANGED
@@ -28,15 +28,46 @@ let formatSeconds = (timeRef: Performance.timeRef) =>
28
28
  // stalled-looking indexer is explainable from the logs alone.
29
29
  let slowOnLargeDatabaseNotice = "This can take a long time on a large database."
30
30
 
31
+ // A per-chain entity's rows are partitioned by the chain that owns them, so a
32
+ // chain-filtered read scans one chain's partition rather than the whole table.
33
+ // `$` can't occur in a GraphQL entity name, so a partition name can never
34
+ // collide with the table another entity claims; past the identifier limit the
35
+ // entity index keeps what survives truncation unique.
36
+ let partitionTableName = (~entityConfig: Internal.entityConfig, ~chainId: ChainId.t) => {
37
+ let chainIdStr = chainId->ChainId.toString
38
+ Table.fitPgTableName(
39
+ `${entityConfig.table.tableName}$${chainIdStr}`,
40
+ ~uniqueSuffix=`$${entityConfig.index->Int.toString}$${chainIdStr}`,
41
+ )
42
+ }
43
+
44
+ // The physical tables an index on the entity is built on. A per-chain entity's
45
+ // rows are partitioned by chain, and an index declared on the parent cascades
46
+ // to every partition, which is what a run driving every chain wants: one
47
+ // declaration, one build. An isolated run instead builds on its own chains'
48
+ // partitions only: the planner uses a partition's own index either way, and
49
+ // building one chain's index then neither waits for nor locks the rows of a
50
+ // chain a sibling process drives.
51
+ let indexTableNames = (entityConfig: Internal.entityConfig, ~partitionChainIds) =>
52
+ switch (entityConfig.table->Table.getChainIdField, partitionChainIds) {
53
+ | (Some(_), Some(chainIds)) =>
54
+ chainIds->Array.map(chainId => partitionTableName(~entityConfig, ~chainId))
55
+ | _ => [entityConfig.table.tableName]
56
+ }
57
+
31
58
  // 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
33
- // created in one transaction once backfill completes, so a resumed indexer that
34
- // reports itself ready always has all of them.
59
+ // or the index backing a derived relationship. Deferred past the initial DDL
60
+ // and created once backfill completes, so a chain that reports itself ready
61
+ // always has all of them. With `partitionChainIds`, a per-chain entity's index
62
+ // is one per partition of those chains rather than one on the parent.
35
63
  //
36
64
  // `entities` is the Postgres-backed set, and every `@derivedFrom` target within
37
65
  // it resolves: config parsing rejects a Postgres entity deriving from one that
38
66
  // isn't in Postgres (`validate_relationship_storage`).
39
- let getSchemaIndexes = (~entities: array<Internal.entityConfig>): array<IndexDefinition.t> => {
67
+ let getSchemaIndexes = (
68
+ ~entities: array<Internal.entityConfig>,
69
+ ~partitionChainIds: option<array<ChainId.t>>=?,
70
+ ): array<IndexDefinition.t> => {
40
71
  let derivedSchema = Schema.make(entities->Array.map(e => e.table))
41
72
  let all = []
42
73
 
@@ -70,7 +101,13 @@ let getSchemaIndexes = (~entities: array<Internal.entityConfig>): array<IndexDef
70
101
  // An `@index` field and a derived relationship pointing at it describe the
71
102
  // same index, so the list is deduped on identity rather than on name.
72
103
  let seen = Utils.Set.make()
73
- all->Array.filter(definition => {
104
+ let entityByTableName = Dict.make()
105
+ entities->Array.forEach(entityConfig =>
106
+ entityByTableName->Dict.set(entityConfig.table.tableName, entityConfig)
107
+ )
108
+
109
+ all
110
+ ->Array.filter(definition => {
74
111
  let key = definition->IndexDefinition.key
75
112
  if seen->Utils.Set.has(key) {
76
113
  false
@@ -79,6 +116,13 @@ let getSchemaIndexes = (~entities: array<Internal.entityConfig>): array<IndexDef
79
116
  true
80
117
  }
81
118
  })
119
+ ->Array.flatMap(definition =>
120
+ entityByTableName
121
+ ->Dict.get(definition.tableName)
122
+ ->Option.getOrThrow
123
+ ->indexTableNames(~partitionChainIds)
124
+ ->Array.map(tableName => {...definition, IndexDefinition.tableName})
125
+ )
82
126
  }
83
127
 
84
128
  let makeCreateTableQuery = (
@@ -122,19 +166,6 @@ let makeCreateTableQuery = (
122
166
  }};`
123
167
  }
124
168
 
125
- // A per-chain entity's rows are partitioned by the chain that owns them, so a
126
- // chain-filtered read scans one chain's partition rather than the whole table.
127
- // `$` can't occur in a GraphQL entity name, so a partition name can never
128
- // collide with the table another entity claims; past the identifier limit the
129
- // entity index keeps what survives truncation unique.
130
- let partitionTableName = (~entityConfig: Internal.entityConfig, ~chainId: ChainId.t) => {
131
- let chainIdStr = chainId->ChainId.toString
132
- Table.fitPgTableName(
133
- `${entityConfig.table.tableName}$${chainIdStr}`,
134
- ~uniqueSuffix=`$${entityConfig.index->Int.toString}$${chainIdStr}`,
135
- )
136
- }
137
-
138
169
  // The entity as it's stored: the handler-visible schema plus the chain-id
139
170
  // column a per-chain entity's table carries. The value for that column is
140
171
  // stamped from the flush group's scope right before serialization, so it never
@@ -277,6 +308,10 @@ let makeInitializeTransaction = (
277
308
  ~pgSchema,
278
309
  ~pgUser,
279
310
  ~isHasuraEnabled,
311
+ // The whole schema's sequence, not one derived from `entities`: those are the
312
+ // entities Postgres stores, and an entity kept only in a sink still decides
313
+ // how the run counts its checkpoints.
314
+ ~checkpointSequence: CheckpointSequence.t,
280
315
  ~chainConfigs=[],
281
316
  ~entities=[],
282
317
  ~enums=[],
@@ -292,7 +327,7 @@ let makeInitializeTransaction = (
292
327
  InternalTable.EnvioInfo.table,
293
328
  InternalTable.EnvioContracts.table,
294
329
  InternalTable.EnvioAddresses.table,
295
- InternalTable.Checkpoints.table,
330
+ InternalTable.Checkpoints.tableFor(checkpointSequence),
296
331
  InternalTable.RawEvents.table,
297
332
  ]
298
333
 
@@ -1042,12 +1077,21 @@ let executeSet = (
1042
1077
  }
1043
1078
  }
1044
1079
 
1080
+ // The checkpoints a write inserts: every one the batch made, or those of the
1081
+ // chains whose history it keeps.
1082
+ type pickedCheckpoints = AllCheckpoints | CheckpointIndexes(array<int>)
1083
+
1084
+ let pickCheckpoints = (column, picked) =>
1085
+ switch picked {
1086
+ | AllCheckpoints => column
1087
+ | CheckpointIndexes(indexes) => indexes->Array.map(index => column->Array.getUnsafe(index))
1088
+ }
1089
+
1045
1090
  let rec writeBatch = async (
1046
1091
  sql,
1047
1092
  ~batch: Batch.t,
1048
1093
  ~pgSchema,
1049
1094
  ~rollback: option<Persistence.rollback>,
1050
- ~isInReorgThreshold,
1051
1095
  ~config: Config.t,
1052
1096
  ~allEntities: array<Internal.entityConfig>,
1053
1097
  ~setEffectCacheOrThrow,
@@ -1061,7 +1105,22 @@ let rec writeBatch = async (
1061
1105
  ) => {
1062
1106
  try {
1063
1107
  let chainIdMode = config.chainIdMode
1064
- let shouldSaveHistory = config->Config.shouldSaveHistory(~isInReorgThreshold)
1108
+ // A checkpoint anchors the history its chain keeps, so the batch's
1109
+ // decision picks the checkpoints chain by chain.
1110
+ let pickedCheckpoints = {
1111
+ let indexes =
1112
+ batch.checkpointChainIds->Array.filterMapWithIndex((chainId, index) =>
1113
+ batch.history->HistoryPolicy.forChain(chainId) ? Some(index) : None
1114
+ )
1115
+ if indexes->Utils.Array.isEmpty {
1116
+ None
1117
+ } else if indexes->Array.length === batch.checkpointIds->Array.length {
1118
+ Some(AllCheckpoints)
1119
+ } else {
1120
+ Some(CheckpointIndexes(indexes))
1121
+ }
1122
+ }
1123
+ let writtenFrontier = Persistence.writtenFrontier(~batch, ~rollback)
1065
1124
 
1066
1125
  let specificError = ref(None)
1067
1126
 
@@ -1111,7 +1170,12 @@ let rec writeBatch = async (
1111
1170
  }
1112
1171
  }
1113
1172
 
1114
- let setEntities = updatedEntities->Array.map(({entityConfig, scope, changes}) => {
1173
+ let setEntities = updatedEntities->Array.map(({
1174
+ entityConfig,
1175
+ scope,
1176
+ changes,
1177
+ shouldSaveHistory,
1178
+ }) => {
1115
1179
  let entitiesToSet = []
1116
1180
  let idsToDelete = []
1117
1181
 
@@ -1121,8 +1185,6 @@ let rec writeBatch = async (
1121
1185
  | Internal.CrossChain => None
1122
1186
  | Chain(chainId) => Some(chainId)
1123
1187
  }
1124
- let shouldSaveHistory =
1125
- config->Config.shouldSaveHistory(~isInReorgThreshold, ~chainId=?scopeChainId)
1126
1188
  let changes = switch (entityConfig.table->Table.getChainIdField, scopeChainId) {
1127
1189
  | (Some(field), Some(chainId)) =>
1128
1190
  changes->Array.map(change =>
@@ -1147,7 +1209,10 @@ let rec writeBatch = async (
1147
1209
 
1148
1210
  // The rollback-diff change is written to the entity table only, never the
1149
1211
  // history table; when present it is an id's oldest change.
1150
- let diffCheckpointId = rollback->Option.map(r => r.diffCheckpointId)
1212
+ let diffCheckpointId =
1213
+ rollback->Option.flatMap(r =>
1214
+ config.checkpointSequence->CheckpointSequence.findForScope(r.diffFrontier, ~scope)
1215
+ )
1151
1216
 
1152
1217
  // History batches, populated only when saving history.
1153
1218
  let batchSetUpdates = []
@@ -1384,6 +1449,7 @@ let rec writeBatch = async (
1384
1449
  ): InternalTable.Chains.progressedChain => {
1385
1450
  chainId: chainAfterBatch.fetchState.chainId,
1386
1451
  progressBlockNumber: chainAfterBatch.progressBlockNumber,
1452
+ progressBlockTime: chainAfterBatch.progressBlockTime,
1387
1453
  sourceBlockNumber: chainAfterBatch.sourceBlockNumber,
1388
1454
  totalEventsProcessed: chainAfterBatch.totalEventsProcessed,
1389
1455
  }),
@@ -1411,18 +1477,30 @@ let rec writeBatch = async (
1411
1477
  )
1412
1478
  }
1413
1479
 
1414
- if shouldSaveHistory {
1480
+ if !(writtenFrontier->Utils.Dict.isEmpty) {
1481
+ setOperations->Array.push(sql =>
1482
+ sql->InternalTable.Chains.setCheckpointFrontier(
1483
+ ~pgSchema,
1484
+ ~frontier=writtenFrontier,
1485
+ ~chainIdMode,
1486
+ )
1487
+ )
1488
+ }
1489
+
1490
+ switch pickedCheckpoints {
1491
+ | Some(picked) =>
1415
1492
  setOperations->Array.push(sql =>
1416
1493
  sql->InternalTable.Checkpoints.insert(
1417
1494
  ~pgSchema,
1418
- ~checkpointIds=batch.checkpointIds,
1419
- ~checkpointChainIds=batch.checkpointChainIds,
1420
- ~checkpointBlockNumbers=batch.checkpointBlockNumbers,
1421
- ~checkpointBlockHashes=batch.checkpointBlockHashes,
1422
- ~checkpointEventsProcessed=batch.checkpointEventsProcessed,
1495
+ ~checkpointIds=batch.checkpointIds->pickCheckpoints(picked),
1496
+ ~checkpointChainIds=batch.checkpointChainIds->pickCheckpoints(picked),
1497
+ ~checkpointBlockNumbers=batch.checkpointBlockNumbers->pickCheckpoints(picked),
1498
+ ~checkpointBlockHashes=batch.checkpointBlockHashes->pickCheckpoints(picked),
1499
+ ~checkpointEventsProcessed=batch.checkpointEventsProcessed->pickCheckpoints(picked),
1423
1500
  ~chainIdMode,
1424
1501
  )
1425
1502
  )
1503
+ | None => ()
1426
1504
  }
1427
1505
 
1428
1506
  await setOperations
@@ -1479,7 +1557,6 @@ let rec writeBatch = async (
1479
1557
  ~pgSchema,
1480
1558
  ~setQueryCache,
1481
1559
  ~rollback,
1482
- ~isInReorgThreshold,
1483
1560
  ~config,
1484
1561
  ~setEffectCacheOrThrow,
1485
1562
  ~updatedEffectsCache,
@@ -1532,7 +1609,7 @@ let makeGetRollbackPreTargetRowsQuery = (
1532
1609
  let keyMatch =
1533
1610
  keyColumns->Array.map(c => `h."${c}" = ${tableRef}."${c}"`)->Array.joinUnsafe(" AND ")
1534
1611
  let bounds =
1535
- floors.floors->CheckpointBounds.sql(
1612
+ floors.checkpointBounds->CheckpointSequence.sql(
1536
1613
  ~chainIdColumn=entityConfig.table->Table.getPgChainIdColumn,
1537
1614
  ~tableRef,
1538
1615
  )
@@ -1565,7 +1642,7 @@ let makeGetRollbackRemovedIdsQuery = (
1565
1642
  let keyMatch =
1566
1643
  keyColumns->Array.map(c => `h."${c}" = ${tableRef}."${c}"`)->Array.joinUnsafe(" AND ")
1567
1644
  let bounds =
1568
- floors.floors->CheckpointBounds.sql(
1645
+ floors.checkpointBounds->CheckpointSequence.sql(
1569
1646
  ~chainIdColumn=entityConfig.table->Table.getPgChainIdColumn,
1570
1647
  ~tableRef,
1571
1648
  )
@@ -1623,8 +1700,11 @@ let make = (
1623
1700
  // encoded at initialize and when stored rows are grouped on resume.
1624
1701
  ~ecosystem: Ecosystem.name,
1625
1702
  ~sink: option<Sink.t>=?,
1703
+ // An `envio start --chain` process: builds and looks for a per-chain entity's
1704
+ // indexes on its own chains' partitions, never on the parent table, so no
1705
+ // build reaches into the rows a sibling process drives.
1706
+ ~isolated=false,
1626
1707
  ~onInitialize=?,
1627
- ~onNewTables=?,
1628
1708
  ): Persistence.storage => {
1629
1709
  // Must match PG_CONTAINER in packages/cli/src/docker_env.rs
1630
1710
  let containerName = "envio-postgres"
@@ -1784,19 +1864,6 @@ let make = (
1784
1864
 
1785
1865
  let cacheTableInfo = await queryCacheTableInfo()
1786
1866
 
1787
- if withUpload && cacheTableInfo->Utils.Array.notEmpty {
1788
- // Integration with other tools like Hasura
1789
- switch onNewTables {
1790
- | Some(onNewTables) =>
1791
- await onNewTables(
1792
- ~tableNames=cacheTableInfo->Array.map(info => {
1793
- info.tableName
1794
- }),
1795
- )
1796
- | None => ()
1797
- }
1798
- }
1799
-
1800
1867
  let cache = Dict.make()
1801
1868
  cacheTableInfo->Array.forEach(({tableName, count}) => {
1802
1869
  switch Internal.EffectCache.fromTableName(tableName) {
@@ -1856,6 +1923,7 @@ let make = (
1856
1923
  let queries = makeInitializeTransaction(
1857
1924
  ~pgSchema,
1858
1925
  ~pgUser,
1926
+ ~checkpointSequence=CheckpointSequence.fromEntities(entities),
1859
1927
  ~entities=pgEntities,
1860
1928
  ~enums,
1861
1929
  ~chainConfigs,
@@ -1925,9 +1993,10 @@ let make = (
1925
1993
  firstEventBlockNumber: None,
1926
1994
  timestampCaughtUpToHeadOrEndblock: None,
1927
1995
  addressRows: rowsByChain->Array.getUnsafe(idx)->AddressRows.seedRowsOf,
1996
+ progressBlockTime: None,
1928
1997
  sourceBlockNumber: 0,
1929
1998
  }),
1930
- checkpointId: InternalTable.Checkpoints.initialCheckpointId,
1999
+ checkpointFrontier: Frontier.empty(),
1931
2000
  }
1932
2001
  }
1933
2002
 
@@ -2012,11 +2081,31 @@ let make = (
2012
2081
  })
2013
2082
  }
2014
2083
 
2015
- let ensureQueryIndexes = async (~table: Table.table, ~filters: array<EntityFilter.t>) => {
2016
- let columns = filterColumns(~table, ~filters)
2084
+ let partitionChainIds = chainIds => isolated ? Some(chainIds) : None
2085
+
2086
+ // The physical table a query index for `scope` is built on. A per-chain
2087
+ // entity's query carries the scope's chain id, so it is planned against that
2088
+ // chain's partition, which an index on the parent covers by cascading.
2089
+ let queryTableName = (~entityConfig: Internal.entityConfig, ~scope: Internal.chainScope) =>
2090
+ switch scope {
2091
+ | Chain(chainId) =>
2092
+ indexTableNames(
2093
+ entityConfig,
2094
+ ~partitionChainIds=partitionChainIds([chainId]),
2095
+ )->Array.getUnsafe(0)
2096
+ | CrossChain => entityConfig.table.tableName
2097
+ }
2098
+
2099
+ let ensureQueryIndexes = async (
2100
+ ~entityConfig: Internal.entityConfig,
2101
+ ~scope: Internal.chainScope,
2102
+ ~filters: array<EntityFilter.t>,
2103
+ ) => {
2104
+ let tableName = queryTableName(~entityConfig, ~scope)
2105
+ let columns = filterColumns(~table=entityConfig.table, ~filters)
2017
2106
  let _ = await columns
2018
2107
  ->Array.map(column => {
2019
- let definition = IndexDefinition.single(~tableName=table.tableName, ~column)
2108
+ let definition = IndexDefinition.single(~tableName, ~column)
2020
2109
  indexManager
2021
2110
  ->IndexManager.ensure(~definition, ~coverage=LeadingColumns, ~build=async () => {
2022
2111
  // Resolved before logging so a rebuild is reported as one, and an
@@ -2033,7 +2122,7 @@ let make = (
2033
2122
  // actually creates the index, not the ones waiting on it.
2034
2123
  Logging.info({
2035
2124
  "storage": storageName,
2036
- "msg": `${verb} "${prepared.name}" to serve a getWhere query on "${table.tableName}". Writes to the table are paused until it completes. ${slowOnLargeDatabaseNotice}`,
2125
+ "msg": `${verb} "${prepared.name}" to serve a getWhere query on "${tableName}". Writes to the table are paused until it completes. ${slowOnLargeDatabaseNotice}`,
2037
2126
  })
2038
2127
  let timeRef = Performance.now()
2039
2128
  let entry = await sql->runAndVerify(prepared)
@@ -2049,7 +2138,7 @@ let make = (
2049
2138
  ->Promise.catch(async exn => {
2050
2139
  Logging.warn({
2051
2140
  "storage": storageName,
2052
- "msg": `Failed to create an index on "${table.tableName}"("${column}") for a getWhere query. The query runs without it.`,
2141
+ "msg": `Failed to create an index on "${tableName}"("${column}") for a getWhere query. The query runs without it.`,
2053
2142
  "err": exn->Utils.prettifyExn,
2054
2143
  })
2055
2144
  await resyncIndex(definition->IndexDefinition.name)
@@ -2063,9 +2152,10 @@ let make = (
2063
2152
  // is already ready, so handlers may be issuing getWhere queries alongside it
2064
2153
  // and the per-table queues are what keep the two from colliding. Nothing here
2065
2154
  // writes `ready_at` — the chains already carry theirs.
2066
- let ensureSchemaIndexes = async (~entities: array<Internal.entityConfig>) => {
2155
+ let ensureSchemaIndexes = async (~entities: array<Internal.entityConfig>, ~chainIds) => {
2067
2156
  let schemaIndexes = getSchemaIndexes(
2068
2157
  ~entities=entities->Array.filter((e: Internal.entityConfig) => e.storage.postgres),
2158
+ ~partitionChainIds=?partitionChainIds(chainIds),
2069
2159
  )
2070
2160
 
2071
2161
  let _ = await schemaIndexes
@@ -2116,6 +2206,7 @@ let make = (
2116
2206
  ) => {
2117
2207
  let schemaIndexes = getSchemaIndexes(
2118
2208
  ~entities=entities->Array.filter((e: Internal.entityConfig) => e.storage.postgres),
2209
+ ~partitionChainIds=?partitionChainIds(chainIds),
2119
2210
  )
2120
2211
 
2121
2212
  // Resolved up front so a name held by an unrelated index fails before any
@@ -2181,9 +2272,9 @@ let make = (
2181
2272
  // crash either leaves `ready_at` null and the retry finds the indexes
2182
2273
  // already built, or commits readiness the schema backs.
2183
2274
  //
2184
- // One transaction for the whole set: readiness is an indexer-wide fact, and
2185
- // a crash part way through would otherwise leave some chains stamped and
2186
- // some not, reporting the indexer as half ready.
2275
+ // One transaction for the whole set: the chains this process drives caught
2276
+ // up together, and a crash part way through would otherwise leave some of
2277
+ // them stamped and some not.
2187
2278
  let setReadyAtQuery = InternalTable.Chains.makeSetReadyAtQuery(~pgSchema)
2188
2279
  let _ = await sql->Postgres.beginSql(async sql => {
2189
2280
  for idx in 0 to chainIds->Array.length - 1 {
@@ -2232,11 +2323,6 @@ let make = (
2232
2323
  let _ = await sql->Postgres.unsafe(
2233
2324
  makeCreateTableQuery(table, ~pgSchema, ~isNumericArrayAsText=false),
2234
2325
  )
2235
- // Integration with other tools like Hasura
2236
- switch onNewTables {
2237
- | Some(onNewTables) => await onNewTables(~tableNames=[table.tableName])
2238
- | None => ()
2239
- }
2240
2326
  }
2241
2327
 
2242
2328
  await setOrThrow(~items, ~table, ~itemSchema)
@@ -2313,36 +2399,49 @@ let make = (
2313
2399
  }
2314
2400
  }
2315
2401
 
2316
- let resumeInitialState = async (~entities, ~throwIfIncompatible): Persistence.initialState => {
2402
+ let resumeInitialState = async (
2403
+ ~entities,
2404
+ ~chainIds,
2405
+ ~throwIfIncompatible,
2406
+ ): Persistence.initialState => {
2317
2407
  let (
2318
2408
  cache,
2319
- chains,
2320
- checkpointIdResult,
2409
+ (chains, checkpointFrontier),
2321
2410
  reorgCheckpoints,
2322
2411
  (storedEnvioInfo, storedContractMapping),
2323
- ) = await Promise.all5((
2412
+ ) = await Promise.all4((
2324
2413
  restoreEffectCache(~withUpload=false),
2325
2414
  InternalTable.Chains.getInitialState(
2326
2415
  sql,
2327
2416
  ~pgSchema,
2328
2417
  )->Promise.thenResolve(rawInitialStates => {
2329
- rawInitialStates->Array.map((rawInitialState): Persistence.initialChainState => {
2330
- id: rawInitialState.id,
2331
- startBlock: rawInitialState.startBlock,
2332
- endBlock: rawInitialState.endBlock->Null.toOption,
2333
- maxReorgDepth: rawInitialState.maxReorgDepth,
2334
- firstEventBlockNumber: rawInitialState.firstEventBlockNumber->Null.toOption,
2335
- timestampCaughtUpToHeadOrEndblock: rawInitialState.timestampCaughtUpToHeadOrEndblock->Null.toOption,
2336
- numEventsProcessed: rawInitialState.numEventsProcessed,
2337
- progressBlockNumber: rawInitialState.progressBlockNumber,
2338
- addressRows: rawInitialState.addressRows,
2339
- sourceBlockNumber: rawInitialState.sourceBlockNumber,
2340
- })
2418
+ let rawInitialStates =
2419
+ rawInitialStates->Array.filter(rawInitialState =>
2420
+ chainIds->Array.includes(rawInitialState.id)
2421
+ )
2422
+ (
2423
+ rawInitialStates->Array.map((rawInitialState): Persistence.initialChainState => {
2424
+ id: rawInitialState.id,
2425
+ startBlock: rawInitialState.startBlock,
2426
+ endBlock: rawInitialState.endBlock->Null.toOption,
2427
+ maxReorgDepth: rawInitialState.maxReorgDepth,
2428
+ firstEventBlockNumber: rawInitialState.firstEventBlockNumber->Null.toOption,
2429
+ timestampCaughtUpToHeadOrEndblock: rawInitialState.timestampCaughtUpToHeadOrEndblock->Null.toOption,
2430
+ numEventsProcessed: rawInitialState.numEventsProcessed,
2431
+ progressBlockNumber: rawInitialState.progressBlockNumber,
2432
+ progressBlockTime: rawInitialState.progressBlockTime->InternalTable.Chains.blockTimeFromDb,
2433
+ addressRows: rawInitialState.addressRows,
2434
+ sourceBlockNumber: rawInitialState.sourceBlockNumber,
2435
+ }),
2436
+ Frontier.fromEntries(
2437
+ rawInitialStates->Array.map(rawInitialState => (
2438
+ rawInitialState.id,
2439
+ rawInitialState.checkpointId->BigInt.fromStringOrThrow,
2440
+ )),
2441
+ ),
2442
+ )
2341
2443
  }),
2342
2444
  sql
2343
- ->Postgres.unsafe(InternalTable.Checkpoints.makeCommitedCheckpointIdQuery(~pgSchema))
2344
- ->(Utils.magic: promise<array<unknown>> => promise<array<{"id": string}>>),
2345
- sql
2346
2445
  ->Postgres.unsafe(InternalTable.Checkpoints.makeGetReorgCheckpointsQuery(~pgSchema))
2347
2446
  ->(
2348
2447
  Utils.magic: promise<array<unknown>> => promise<
@@ -2372,8 +2471,6 @@ let make = (
2372
2471
 
2373
2472
  await reloadIndexCatalog()
2374
2473
 
2375
- let checkpointId = (checkpointIdResult->Array.getUnsafe(0))["id"]->BigInt.fromStringOrThrow
2376
-
2377
2474
  // Convert string checkpoint IDs from DB to bigint
2378
2475
  let reorgCheckpoints = Array.map(reorgCheckpoints, (raw): Internal.reorgCheckpoint => {
2379
2476
  checkpointId: raw["id"]->BigInt.fromStringOrThrow,
@@ -2384,7 +2481,7 @@ let make = (
2384
2481
 
2385
2482
  // Resume sink if present - needed to rollback any reorg changes
2386
2483
  switch sink {
2387
- | Some(sink) => await sink.resume(~checkpointId, ~chains, ~entities)
2484
+ | Some(sink) => await sink.resume(~frontier=checkpointFrontier, ~chains, ~entities)
2388
2485
  | None => ()
2389
2486
  }
2390
2487
 
@@ -2393,7 +2490,7 @@ let make = (
2393
2490
  reorgCheckpoints,
2394
2491
  cache,
2395
2492
  chains,
2396
- checkpointId,
2493
+ checkpointFrontier,
2397
2494
  contractMapping: storedContractMapping,
2398
2495
  envioInfo: storedEnvioInfo,
2399
2496
  }
@@ -2434,7 +2531,7 @@ let make = (
2434
2531
  InternalTable.Checkpoints.getRollbackProgressDiff(sql, ~pgSchema, ~floors)
2435
2532
 
2436
2533
  let getRollbackData = async (~entityConfig: Internal.entityConfig, ~floors: RollbackFloors.t) => {
2437
- let params = floors.floors->CheckpointBounds.params
2534
+ let params = floors.checkpointBounds->CheckpointSequence.params
2438
2535
  let (removedIdRows, rollbackRows) = await Promise.all2((
2439
2536
  // Get IDs of entities that should be deleted (created after rollback target with no prior history)
2440
2537
  sql
@@ -2482,7 +2579,6 @@ let make = (
2482
2579
  let writeBatchMethod = async (
2483
2580
  ~batch,
2484
2581
  ~rollback,
2485
- ~isInReorgThreshold,
2486
2582
  ~config,
2487
2583
  ~allEntities,
2488
2584
  ~updatedEffectsCache,
@@ -2509,7 +2605,14 @@ let make = (
2509
2605
  | Some(sink) => {
2510
2606
  let timerRef = Performance.now()
2511
2607
  Some(
2512
- sink.writeBatch(~batch, ~updatedEntities=chUpdates)
2608
+ sink.writeBatch(
2609
+ ~batch,
2610
+ ~diffCheckpoints=switch (rollback: option<Persistence.rollback>) {
2611
+ | Some({diffCheckpoints}) => diffCheckpoints
2612
+ | None => []
2613
+ },
2614
+ ~updatedEntities=chUpdates,
2615
+ )
2513
2616
  ->Promise.thenResolve(_ => {
2514
2617
  onWrite(~storage=sink.name, ~timeSeconds=timerRef->Performance.secondsSince)
2515
2618
  None
@@ -2528,7 +2631,6 @@ let make = (
2528
2631
  ~pgSchema,
2529
2632
  ~setQueryCache,
2530
2633
  ~rollback,
2531
- ~isInReorgThreshold,
2532
2634
  ~config,
2533
2635
  ~allEntities,
2534
2636
  ~setEffectCacheOrThrow,
@@ -2581,6 +2683,7 @@ let makeStorageFromEnv = (
2581
2683
  ~pgPassword=Env.Db.password,
2582
2684
  ~chainIdMode=config.chainIdMode,
2583
2685
  ~ecosystem=config.ecosystem.name,
2686
+ ~isolated=config.isolated,
2584
2687
  ~sink=?{
2585
2688
  // Internally ClickHouse storage is implemented as a sync of the
2586
2689
  // Postgres storage. Required env vars are validated here only when
@@ -2613,6 +2716,7 @@ let makeStorageFromEnv = (
2613
2716
  ~database=database->Option.getUnsafe,
2614
2717
  ~username=username->Option.getUnsafe,
2615
2718
  ~password=password->Option.getUnsafe,
2719
+ ~sequence=config.checkpointSequence,
2616
2720
  ~chainIdMode=config.chainIdMode,
2617
2721
  ),
2618
2722
  )
@@ -2644,34 +2748,6 @@ let makeStorageFromEnv = (
2644
2748
  None
2645
2749
  }
2646
2750
  },
2647
- ~onNewTables=?{
2648
- if isHasuraEnabled {
2649
- Some(
2650
- (~tableNames) => {
2651
- Hasura.trackTables(
2652
- ~endpoint=Env.Hasura.graphqlEndpoint,
2653
- ~auth={
2654
- role: Env.Hasura.role,
2655
- secret: Env.Hasura.secret,
2656
- },
2657
- ~pgSchema,
2658
- ~tableConfigs=tableNames->Array.map(tableName => {
2659
- Hasura.tableName,
2660
- description: None,
2661
- columnConfigs: dict{},
2662
- }),
2663
- )->Promise.catch(err => {
2664
- Logging.errorWithExn(
2665
- err->Utils.prettifyExn,
2666
- `Error tracking new tables`,
2667
- )->Promise.resolve
2668
- })
2669
- },
2670
- )
2671
- } else {
2672
- None
2673
- }
2674
- },
2675
2751
  ~isHasuraEnabled,
2676
2752
  )
2677
2753
  }