envio 3.5.0-alpha.2 → 3.5.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/PgStorage.res CHANGED
@@ -58,7 +58,7 @@ let makeCreateCompositeIndexQuery = (
58
58
  `CREATE INDEX IF NOT EXISTS "${indexName}" ON "${pgSchema}"."${tableName}"(${index});`
59
59
  }
60
60
 
61
- let makeCreateTableIndicesQuery = (table: Table.table, ~pgSchema) => {
61
+ let makeCreateTableIndexesQuery = (table: Table.table, ~pgSchema) => {
62
62
  let tableName = table.tableName
63
63
  let createIndex = indexField =>
64
64
  makeCreateIndexQuery(~tableName, ~indexFields=[indexField], ~pgSchema)
@@ -66,11 +66,138 @@ let makeCreateTableIndicesQuery = (table: Table.table, ~pgSchema) => {
66
66
  makeCreateCompositeIndexQuery(~tableName, ~indexFields, ~pgSchema)
67
67
  }
68
68
 
69
- let singleIndices = table->Table.getSingleIndices
70
- let compositeIndices = table->Table.getCompositeIndices
69
+ let singleIndexes = table->Table.getSingleIndexes
70
+ let compositeIndexes = table->Table.getCompositeIndexes
71
71
 
72
- singleIndices->Array.map(createIndex)->Array.joinUnsafe("\n") ++
73
- compositeIndices->Array.map(createCompositeIndex)->Array.joinUnsafe("\n")
72
+ singleIndexes->Array.map(createIndex)->Array.joinUnsafe("\n") ++
73
+ compositeIndexes->Array.map(createCompositeIndex)->Array.joinUnsafe("\n")
74
+ }
75
+
76
+ // One index the schema promises: an `@index` field, a composite index, or the
77
+ // index backing a derived relationship. Carries enough to derive its name, its
78
+ // DDL and its registry key, so the deferred creation at the end of backfill and
79
+ // the registry stay described by the same value.
80
+ type schemaIndex = {
81
+ tableName: string,
82
+ indexFields: array<Table.compositeIndexField>,
83
+ }
84
+
85
+ // `<Entity>_<column>` for a single column, with each further column appended in
86
+ // order (and `_desc` where it applies). The full description, before Postgres'
87
+ // identifier limit is applied — `validateIndexNamesOrThrow` compares these, so
88
+ // it can tell two descriptions apart after they truncate to the same name.
89
+ let schemaIndexDescription = ({tableName, indexFields}: schemaIndex) =>
90
+ tableName ++
91
+ "_" ++
92
+ indexFields
93
+ ->Array.map(f => f.fieldName ++ directionToIndexName(f.direction))
94
+ ->Array.joinUnsafe("_")
95
+
96
+ // The identifier Postgres actually stores. Automatic and schema-defined indexes
97
+ // share it, so both are named the same way and both stay reachable through
98
+ // `CREATE INDEX IF NOT EXISTS`.
99
+ let schemaIndexName = schemaIndex =>
100
+ schemaIndex->schemaIndexDescription->IndexRegistry.truncateIndexName
101
+
102
+ let schemaIndexKey = ({tableName, indexFields}: schemaIndex) =>
103
+ IndexRegistry.makeKey(
104
+ ~tableName,
105
+ ~columns=indexFields->IndexRegistry.fromIndexFields,
106
+ ~method=IndexRegistry.btree,
107
+ )
108
+
109
+ // Plain DDL, not CONCURRENTLY: it builds from a single table scan instead of
110
+ // two, and the SHARE lock it takes blocks writes but not reads, so queries keep
111
+ // being served while it runs. The indexer is the only writer, and both callers
112
+ // are happy to wait on it.
113
+ let makeCreateSchemaIndexQuery = (schemaIndex, ~pgSchema) => {
114
+ let {tableName, indexFields} = schemaIndex
115
+ let columns =
116
+ indexFields
117
+ ->Array.map(f => `"${f.fieldName}"${directionToSql(f.direction)}`)
118
+ ->Array.joinUnsafe(", ")
119
+ `CREATE INDEX IF NOT EXISTS "${schemaIndex->schemaIndexName}" ON "${pgSchema}"."${tableName}"(${columns});`
120
+ }
121
+
122
+ let formatSeconds = (timeRef: Performance.timeRef) =>
123
+ (Math.round(timeRef->Performance.secondsSince *. 100.) /. 100.)->Float.toString
124
+
125
+ // Every index build blocks writes to its table for as long as it runs, and on a
126
+ // large database that is not quick. Both build paths say so up front, so a
127
+ // stalled-looking indexer is explainable from the logs alone.
128
+ let slowOnLargeDatabaseNotice = "This can take a long time on a large database."
129
+
130
+ // An index Postgres reports as INVALID still owns its name, so
131
+ // `CREATE INDEX IF NOT EXISTS` would quietly skip it and leave the query
132
+ // unserved. REINDEX rebuilds it from its own stored definition — a repair, not
133
+ // a replacement — which is right only when that definition is the one we
134
+ // wanted. Runs inside a transaction, unlike REINDEX CONCURRENTLY.
135
+ let makeReindexQuery = (~pgSchema, ~indexName) => `REINDEX INDEX "${pgSchema}"."${indexName}";`
136
+
137
+ // A name taken by an invalid index the indexer didn't create isn't ours to
138
+ // touch: rebuilding it reuses its own definition, so it would come back valid
139
+ // but still not serve the query, and dropping it could take out a unique
140
+ // constraint someone else relies on.
141
+ let nameCollisionError = (~indexName, ~pgSchema) =>
142
+ Utils.Error.make(
143
+ `Cannot create the index "${indexName}" in schema "${pgSchema}". An index of that name already exists and PostgreSQL reports it as invalid, but it isn't one the indexer created — it covers different columns, or it is a unique or partial index — so it is left untouched. Drop or repair it by hand, then restart the indexer.`,
144
+ )
145
+
146
+ let makeSingleColumnSchemaIndex = (~tableName, ~column): schemaIndex => {
147
+ tableName,
148
+ indexFields: [{fieldName: column, direction: Table.Asc}],
149
+ }
150
+
151
+ // Every index the entity schema promises. Deferred past the initial DDL and
152
+ // created in one transaction once backfill completes, so a resumed indexer that
153
+ // reports itself ready always has all of them.
154
+ //
155
+ // `entities` is the Postgres-backed set, and every `@derivedFrom` target within
156
+ // it resolves: config parsing rejects a Postgres entity deriving from one that
157
+ // isn't in Postgres (`validate_relationship_storage`).
158
+ let getSchemaIndexes = (~entities: array<Internal.entityConfig>): array<schemaIndex> => {
159
+ let derivedSchema = Schema.make(entities->Array.map(e => e.table))
160
+ let all = []
161
+
162
+ entities->Array.forEach(({table}) => {
163
+ table
164
+ ->Table.getSingleIndexes
165
+ ->Array.forEach(column =>
166
+ all->Array.push(makeSingleColumnSchemaIndex(~tableName=table.tableName, ~column))->ignore
167
+ )
168
+ table
169
+ ->Table.getCompositeIndexes
170
+ ->Array.forEach(indexFields =>
171
+ all->Array.push({tableName: table.tableName, indexFields})->ignore
172
+ )
173
+ })
174
+
175
+ entities->Array.forEach(({table}) =>
176
+ table
177
+ ->Table.getDerivedFromFields
178
+ ->Array.forEach(derivedFromField => {
179
+ let column =
180
+ derivedSchema->Schema.getDerivedFromPgFieldName(derivedFromField)->Utils.unwrapResultExn
181
+ all
182
+ ->Array.push(
183
+ makeSingleColumnSchemaIndex(~tableName=derivedFromField.derivedFromEntity, ~column),
184
+ )
185
+ ->ignore
186
+ })
187
+ )
188
+
189
+ // An `@index` field and a derived relationship pointing at it describe the
190
+ // same index, so the list is deduped on identity rather than on name.
191
+ let seen = Utils.Set.make()
192
+ all->Array.filter(schemaIndex => {
193
+ let key = schemaIndex->schemaIndexKey
194
+ if seen->Utils.Set.has(key) {
195
+ false
196
+ } else {
197
+ seen->Utils.Set.add(key)->ignore
198
+ true
199
+ }
200
+ })
74
201
  }
75
202
 
76
203
  let makeCreateTableQuery = (table: Table.table, ~pgSchema, ~isNumericArrayAsText) => {
@@ -152,7 +279,7 @@ let getEntityHistory = (~entityConfig: Internal.entityConfig): EntityHistory.pgE
152
279
  ~entityName=entityTableName,
153
280
  ~entityIndex=entityConfig.index,
154
281
  )
155
- //ignore composite indices
282
+ //ignore composite indexes
156
283
  let table = Table.mkTable(
157
284
  historyTableName,
158
285
  ~fields=dataFields->Array.concat([checkpointIdField, actionField]),
@@ -183,6 +310,10 @@ let makeInitializeTransaction = (
183
310
  ~entities=[],
184
311
  ~enums=[],
185
312
  ~isEmptyPgSchema=false,
313
+ // Backfill writes are far cheaper without the schema's read indexes, so the
314
+ // initial DDL creates only tables, primary keys, views and chain rows; the
315
+ // rest is created by `finalizeBackfill` before the indexer reports ready.
316
+ ~deferSchemaIndexes=false,
186
317
  ) => {
187
318
  let generalTables = [
188
319
  InternalTable.Chains.table,
@@ -192,13 +323,13 @@ let makeInitializeTransaction = (
192
323
  ]
193
324
 
194
325
  let allTables = generalTables->Array.copy
195
- let allEntityTables = []
196
326
  entities->Array.forEach((entityConfig: Internal.entityConfig) => {
197
- allEntityTables->Array.push(entityConfig.table)->ignore
198
327
  allTables->Array.push(entityConfig.table)->ignore
199
328
  allTables->Array.push(getEntityHistory(~entityConfig).table)->ignore
200
329
  })
201
- let derivedSchema = Schema.make(allEntityTables)
330
+
331
+ let schemaIndexes = getSchemaIndexes(~entities)
332
+ IndexRegistry.validateIndexNamesOrThrow(schemaIndexes->Array.map(schemaIndexDescription))
202
333
 
203
334
  let query = ref(
204
335
  (
@@ -232,31 +363,12 @@ GRANT ALL ON SCHEMA "${pgSchema}" TO public;`,
232
363
  makeCreateTableQuery(table, ~pgSchema, ~isNumericArrayAsText=isHasuraEnabled)
233
364
  })
234
365
 
235
- // Then batch all indices (better performance when tables exist)
236
- allTables->Array.forEach((table: Table.table) => {
237
- let indices = makeCreateTableIndicesQuery(table, ~pgSchema)
238
- if indices !== "" {
239
- query := query.contents ++ "\n" ++ indices
240
- }
241
- })
242
-
243
- // Add derived indices
244
- entities->Array.forEach((entity: Internal.entityConfig) => {
245
- entity.table
246
- ->Table.getDerivedFromFields
247
- ->Array.forEach(derivedFromField => {
248
- let indexField =
249
- derivedSchema->Schema.getDerivedFromPgFieldName(derivedFromField)->Utils.unwrapResultExn
250
- query :=
251
- query.contents ++
252
- "\n" ++
253
- makeCreateIndexQuery(
254
- ~tableName=derivedFromField.derivedFromEntity,
255
- ~indexFields=[indexField],
256
- ~pgSchema,
257
- )
366
+ // Then batch all indexes (better performance when tables exist)
367
+ if !deferSchemaIndexes {
368
+ schemaIndexes->Array.forEach(schemaIndex => {
369
+ query := query.contents ++ "\n" ++ makeCreateSchemaIndexQuery(schemaIndex, ~pgSchema)
258
370
  })
259
- })
371
+ }
260
372
 
261
373
  // Create views for Hasura integration
262
374
  query := query.contents ++ "\n" ++ InternalTable.Views.makeMetaViewQuery(~pgSchema)
@@ -1293,6 +1405,37 @@ let make = (
1293
1405
  "cache",
1294
1406
  ])
1295
1407
 
1408
+ // The metric label, the `storage` field on this backend's logs, and the
1409
+ // storage record's own name are all the same string.
1410
+ let storageName = "postgres"
1411
+
1412
+ let indexRegistry = IndexRegistry.make()
1413
+
1414
+ // The only point where the catalog is consulted. Afterwards this process is
1415
+ // the sole writer of the schema, so the registry is kept in sync by adding
1416
+ // each index right after its DDL succeeds.
1417
+ let reloadIndexRegistry = async () => {
1418
+ let rows =
1419
+ (await sql->Postgres.unsafe(IndexRegistry.makeCatalogQuery(~pgSchema)))->S.parseOrThrow(
1420
+ IndexRegistry.catalogRowsSchema,
1421
+ )
1422
+ switch indexRegistry->IndexRegistry.reload(~rows) {
1423
+ | [] => ()
1424
+ | invalidIndexNames =>
1425
+ Logging.warn({
1426
+ "storage": storageName,
1427
+ "msg": `Ignoring invalid PostgreSQL indexes in schema "${pgSchema}". They are left untouched — drop and recreate them manually if you need them.`,
1428
+ "indexes": invalidIndexNames,
1429
+ })
1430
+ }
1431
+ Logging.info({
1432
+ "storage": storageName,
1433
+ "msg": `Found ${indexRegistry
1434
+ ->IndexRegistry.size
1435
+ ->Int.toString} existing indexes in the indexer schema.`,
1436
+ })
1437
+ }
1438
+
1296
1439
  let isInitialized = async () => {
1297
1440
  let envioTables = await sql->Postgres.unsafe(
1298
1441
  `SELECT table_schema FROM information_schema.tables WHERE table_schema = '${pgSchema}' AND (table_name = '${// This is for indexer before envio@2.28
@@ -1484,6 +1627,7 @@ let make = (
1484
1627
  ~chainConfigs,
1485
1628
  ~isEmptyPgSchema=schemaTableNames->Utils.Array.isEmpty,
1486
1629
  ~isHasuraEnabled,
1630
+ ~deferSchemaIndexes=true,
1487
1631
  )
1488
1632
  // Execute all queries within a single transaction for integrity.
1489
1633
  // The envio_info row is written in the same transaction so a successful
@@ -1520,6 +1664,8 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1520
1664
 
1521
1665
  let cache = await restoreEffectCache(~withUpload=true)
1522
1666
 
1667
+ await reloadIndexRegistry()
1668
+
1523
1669
  // Integration with other tools like Hasura
1524
1670
  switch onInitialize {
1525
1671
  | Some(onInitialize) => await onInitialize()
@@ -1576,6 +1722,177 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1576
1722
  }
1577
1723
  }
1578
1724
 
1725
+ // The physical columns a filter reads, deduped and in first-seen order.
1726
+ // Unknown field names are left to `loadOrThrow`, which reports them properly.
1727
+ let filterColumns = (~table: Table.table, ~filters: array<EntityFilter.t>) => {
1728
+ let queryFields = table->Table.queryFields
1729
+ let columns = []
1730
+ let seen = Utils.Set.make()
1731
+ let rec collect = (filter: EntityFilter.t) =>
1732
+ switch filter {
1733
+ | Eq({fieldName}) | Gt({fieldName}) | Lt({fieldName}) | In({fieldName}) =>
1734
+ switch queryFields->Utils.Dict.dangerouslyGetNonOption(fieldName) {
1735
+ | Some({pgDbFieldName}) =>
1736
+ if !(seen->Utils.Set.has(pgDbFieldName)) {
1737
+ seen->Utils.Set.add(pgDbFieldName)->ignore
1738
+ columns->Array.push(pgDbFieldName)->ignore
1739
+ }
1740
+ | None => ()
1741
+ }
1742
+ | And({filters}) => filters->Array.forEach(collect)
1743
+ }
1744
+ filters->Array.forEach(collect)
1745
+ columns
1746
+ }
1747
+
1748
+ // The DDL that leaves `schemaIndex` in place. Normally a create; a repair
1749
+ // when an invalid index already holds the name with the definition we want.
1750
+ let makeEnsureIndexQuery = schemaIndex => {
1751
+ let indexName = schemaIndex->schemaIndexName
1752
+ switch indexRegistry->IndexRegistry.getInvalid(indexName) {
1753
+ | None => makeCreateSchemaIndexQuery(schemaIndex, ~pgSchema)
1754
+ | Some({key, isRepairable: true}) if key === schemaIndex->schemaIndexKey =>
1755
+ makeReindexQuery(~pgSchema, ~indexName)
1756
+ | Some(_) => throw(nameCollisionError(~indexName, ~pgSchema))
1757
+ }
1758
+ }
1759
+
1760
+ let isRepair = schemaIndex =>
1761
+ indexRegistry->IndexRegistry.getInvalid(schemaIndex->schemaIndexName)->Option.isSome
1762
+
1763
+ let ensureQueryIndexes = async (~table: Table.table, ~filters: array<EntityFilter.t>) => {
1764
+ let missing = filterColumns(~table, ~filters)->Array.filterMap(column => {
1765
+ let schemaIndex = makeSingleColumnSchemaIndex(~tableName=table.tableName, ~column)
1766
+ indexRegistry->IndexRegistry.has(schemaIndex->schemaIndexKey) ? None : Some(schemaIndex)
1767
+ })
1768
+
1769
+ if missing->Utils.Array.notEmpty {
1770
+ let _ = await missing
1771
+ ->Array.map(schemaIndex => {
1772
+ let indexName = schemaIndex->schemaIndexName
1773
+ indexRegistry
1774
+ ->IndexRegistry.ensure(
1775
+ ~key=schemaIndex->schemaIndexKey,
1776
+ ~tableName=table.tableName,
1777
+ ~build=async () => {
1778
+ // Resolved before logging so a repair is reported as one, and an
1779
+ // unrelated index holding the name fails before any DDL runs.
1780
+ let query = makeEnsureIndexQuery(schemaIndex)
1781
+ let verb = schemaIndex->isRepair ? "Repairing invalid index" : "Creating index"
1782
+ // Logged from inside the build so it reports the one request that
1783
+ // actually creates the index, not the ones waiting on it.
1784
+ Logging.info({
1785
+ "storage": storageName,
1786
+ "msg": `${verb} "${indexName}" to serve a getWhere query on "${table.tableName}". Writes to the table are paused until it completes. ${slowOnLargeDatabaseNotice}`,
1787
+ })
1788
+ let timeRef = Performance.now()
1789
+ let _ = await sql->Postgres.unsafe(query)
1790
+ indexRegistry->IndexRegistry.clearInvalidName(indexName)
1791
+ Logging.info({
1792
+ "storage": storageName,
1793
+ "msg": `Index "${indexName}" is ready after ${timeRef->formatSeconds}s. Resuming indexing.`,
1794
+ })
1795
+ },
1796
+ )
1797
+ // A failed build leaves the registry untouched, so the next getWhere
1798
+ // retries. Meanwhile the query still runs — just without the index.
1799
+ ->Utils.Promise.catchResolve(exn => {
1800
+ Logging.warn({
1801
+ "storage": storageName,
1802
+ "msg": `Failed to create the index "${indexName}" for a getWhere query. The query runs without it.`,
1803
+ "err": exn->Utils.prettifyExn,
1804
+ })
1805
+ })
1806
+ })
1807
+ ->Promise.all
1808
+ }
1809
+ }
1810
+
1811
+ let finalizeBackfill = async (
1812
+ ~entities: array<Internal.entityConfig>,
1813
+ ~chainIds: array<int>,
1814
+ ~readyAt: Date.t,
1815
+ ) => {
1816
+ let schemaIndexes = getSchemaIndexes(
1817
+ ~entities=entities->Array.filter((e: Internal.entityConfig) => e.storage.postgres),
1818
+ )
1819
+ IndexRegistry.validateIndexNamesOrThrow(schemaIndexes->Array.map(schemaIndexDescription))
1820
+
1821
+ let missing =
1822
+ schemaIndexes->Array.filter(schemaIndex =>
1823
+ !(indexRegistry->IndexRegistry.has(schemaIndex->schemaIndexKey))
1824
+ )
1825
+
1826
+ // `IF NOT EXISTS` would quietly skip an index whose name is taken by an
1827
+ // invalid one, and the indexer would then report ready with an index the
1828
+ // planner can't use. Those are repaired in the same transaction instead.
1829
+ let repaired = missing->Array.filter(isRepair)
1830
+ if repaired->Utils.Array.notEmpty {
1831
+ Logging.warn({
1832
+ "storage": storageName,
1833
+ "msg": `PostgreSQL reports ${repaired
1834
+ ->Array.length
1835
+ ->Int.toString} existing indexes as invalid, so they can't serve queries. Rebuilding them.`,
1836
+ "indexes": repaired->Array.map(schemaIndexName),
1837
+ })
1838
+ }
1839
+
1840
+ switch missing {
1841
+ | [] =>
1842
+ Logging.info({
1843
+ "storage": storageName,
1844
+ "msg": `All ${schemaIndexes
1845
+ ->Array.length
1846
+ ->Int.toString} schema indexes are already in place. Marking the indexer ready.`,
1847
+ })
1848
+ | _ =>
1849
+ Logging.info({
1850
+ "storage": storageName,
1851
+ "msg": `Creating the ${missing
1852
+ ->Array.length
1853
+ ->Int.toString} remaining schema indexes before the indexer reports ready. Writes are paused until they are committed. ${slowOnLargeDatabaseNotice}`,
1854
+ "indexes": missing->Array.map(schemaIndexName),
1855
+ })
1856
+ }
1857
+ // Resolved up front so a name held by an unrelated invalid index fails
1858
+ // before the transaction opens, rather than rolling one back.
1859
+ let indexQueries = missing->Array.map(makeEnsureIndexQuery)
1860
+ let timeRef = Performance.now()
1861
+
1862
+ await sql->Postgres.beginSql(async sql => {
1863
+ // Sequential rather than Promise.all: they share one connection inside
1864
+ // the transaction, and several of them target the same table.
1865
+ for idx in 0 to indexQueries->Array.length - 1 {
1866
+ let _ = await sql->Postgres.unsafe(indexQueries->Array.getUnsafe(idx))
1867
+ }
1868
+ await sql
1869
+ ->Postgres.preparedUnsafe(
1870
+ InternalTable.Chains.makeSetReadyAtQuery(~pgSchema),
1871
+ [
1872
+ readyAt->(Utils.magic: Date.t => unknown),
1873
+ chainIds->(Utils.magic: array<int> => unknown),
1874
+ ]->(Utils.magic: array<unknown> => unknown),
1875
+ )
1876
+ ->Utils.Promise.ignoreValue
1877
+ })
1878
+
1879
+ // Only after the commit: a rollback takes both the indexes and ready_at
1880
+ // with it, and the registry must not claim what the schema doesn't have.
1881
+ missing->Array.forEach(schemaIndex =>
1882
+ indexRegistry->IndexRegistry.add(schemaIndex->schemaIndexKey)
1883
+ )
1884
+ repaired->Array.forEach(schemaIndex =>
1885
+ indexRegistry->IndexRegistry.clearInvalidName(schemaIndex->schemaIndexName)
1886
+ )
1887
+
1888
+ Logging.info({
1889
+ "storage": storageName,
1890
+ "msg": `Committed ${missing
1891
+ ->Array.length
1892
+ ->Int.toString} schema indexes and the ready timestamp in ${timeRef->formatSeconds}s.`,
1893
+ })
1894
+ }
1895
+
1579
1896
  let setOrThrow = (
1580
1897
  type item,
1581
1898
  ~items: array<item>,
@@ -1720,6 +2037,8 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1720
2037
  InternalTable.EnvioInfo.read(sql, ~pgSchema),
1721
2038
  ))
1722
2039
 
2040
+ await reloadIndexRegistry()
2041
+
1723
2042
  let checkpointId = (checkpointIdResult->Array.getUnsafe(0))["id"]->BigInt.fromStringOrThrow
1724
2043
 
1725
2044
  // Convert string checkpoint IDs from DB to bigint
@@ -1869,17 +2188,19 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1869
2188
  ~sinkPromise,
1870
2189
  ~chainMetaData,
1871
2190
  )
1872
- onWrite(~storage="postgres", ~timeSeconds=primaryTimerRef->Performance.secondsSince)
2191
+ onWrite(~storage=storageName, ~timeSeconds=primaryTimerRef->Performance.secondsSince)
1873
2192
  }
1874
2193
 
1875
2194
  let close = () => sql->Postgres.endSql
1876
2195
 
1877
2196
  {
1878
- name: "postgres",
2197
+ name: storageName,
1879
2198
  isInitialized,
1880
2199
  initialize,
1881
2200
  resumeInitialState,
1882
2201
  loadOrThrow,
2202
+ ensureQueryIndexes,
2203
+ finalizeBackfill,
1883
2204
  dumpEffectCache,
1884
2205
  reset,
1885
2206
  setChainMeta,