envio 3.5.0-alpha.1 → 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.
Files changed (99) hide show
  1. package/index.d.ts +10 -6
  2. package/package.json +6 -6
  3. package/src/BatchProcessing.res +27 -0
  4. package/src/BatchProcessing.res.mjs +12 -0
  5. package/src/ChainFetching.res +5 -5
  6. package/src/ChainFetching.res.mjs +3 -4
  7. package/src/ChainState.res +48 -25
  8. package/src/ChainState.res.mjs +23 -29
  9. package/src/ChainState.resi +2 -6
  10. package/src/Change.res +3 -3
  11. package/src/Config.res +2 -6
  12. package/src/Config.res.mjs +2 -12
  13. package/src/Core.res +3 -0
  14. package/src/CrossChainState.res +33 -10
  15. package/src/CrossChainState.res.mjs +21 -6
  16. package/src/CrossChainState.resi +3 -0
  17. package/src/EntityId.res +15 -0
  18. package/src/EntityId.res.mjs +9 -0
  19. package/src/EventConfigBuilder.res +1 -58
  20. package/src/EventConfigBuilder.res.mjs +1 -33
  21. package/src/FetchState.res +510 -460
  22. package/src/FetchState.res.mjs +413 -428
  23. package/src/FinalizeBackfill.res +34 -0
  24. package/src/FinalizeBackfill.res.mjs +26 -0
  25. package/src/HandlerRegister.res +45 -0
  26. package/src/HandlerRegister.res.mjs +43 -0
  27. package/src/InMemoryStore.res +11 -4
  28. package/src/InMemoryTable.res +26 -24
  29. package/src/InMemoryTable.res.mjs +19 -18
  30. package/src/IndexerState.res +71 -2
  31. package/src/IndexerState.res.mjs +67 -3
  32. package/src/IndexerState.resi +5 -0
  33. package/src/Internal.res +17 -8
  34. package/src/LoadLayer.res +9 -1
  35. package/src/LoadLayer.res.mjs +1 -0
  36. package/src/LogSelection.res +2 -1
  37. package/src/Metrics.res +37 -6
  38. package/src/Metrics.res.mjs +5 -1
  39. package/src/Persistence.res +12 -1
  40. package/src/PgStorage.res +411 -57
  41. package/src/PgStorage.res.mjs +262 -41
  42. package/src/SimulateItems.res +2 -2
  43. package/src/TestIndexer.res +9 -5
  44. package/src/TestIndexer.res.mjs +5 -2
  45. package/src/UserContext.res +3 -3
  46. package/src/Writing.res +12 -2
  47. package/src/Writing.res.mjs +13 -2
  48. package/src/bindings/ClickHouse.res +5 -3
  49. package/src/bindings/ClickHouse.res.mjs +2 -5
  50. package/src/bindings/Vitest.res +22 -1
  51. package/src/bindings/Vitest.res.mjs +15 -0
  52. package/src/db/EntityFilter.res +0 -4
  53. package/src/db/EntityFilter.res.mjs +1 -8
  54. package/src/db/EntityHistory.res +16 -7
  55. package/src/db/EntityHistory.res.mjs +7 -6
  56. package/src/db/IndexRegistry.res +219 -0
  57. package/src/db/IndexRegistry.res.mjs +195 -0
  58. package/src/db/InternalTable.res +7 -0
  59. package/src/db/InternalTable.res.mjs +7 -0
  60. package/src/db/Table.res +53 -22
  61. package/src/db/Table.res.mjs +56 -18
  62. package/src/sources/AddressSet.res +48 -0
  63. package/src/sources/AddressSet.res.mjs +11 -0
  64. package/src/sources/AddressStore.res +152 -0
  65. package/src/sources/AddressStore.res.mjs +97 -0
  66. package/src/sources/EvmChain.res +3 -0
  67. package/src/sources/EvmChain.res.mjs +4 -2
  68. package/src/sources/EvmHyperSyncSource.res +6 -3
  69. package/src/sources/EvmHyperSyncSource.res.mjs +3 -3
  70. package/src/sources/EvmRpcClient.res +6 -3
  71. package/src/sources/EvmRpcClient.res.mjs +3 -3
  72. package/src/sources/FuelHyperSync.res +4 -3
  73. package/src/sources/FuelHyperSync.res.mjs +3 -3
  74. package/src/sources/FuelHyperSync.resi +2 -1
  75. package/src/sources/FuelHyperSyncClient.res +12 -6
  76. package/src/sources/FuelHyperSyncClient.res.mjs +2 -2
  77. package/src/sources/FuelHyperSyncSource.res +7 -4
  78. package/src/sources/FuelHyperSyncSource.res.mjs +3 -3
  79. package/src/sources/HyperSync.res +6 -4
  80. package/src/sources/HyperSync.res.mjs +2 -3
  81. package/src/sources/HyperSync.resi +1 -1
  82. package/src/sources/HyperSyncClient.res +18 -9
  83. package/src/sources/HyperSyncClient.res.mjs +4 -4
  84. package/src/sources/RpcSource.res +15 -10
  85. package/src/sources/RpcSource.res.mjs +3 -4
  86. package/src/sources/SimulateSource.res +26 -11
  87. package/src/sources/SimulateSource.res.mjs +13 -5
  88. package/src/sources/Source.res +4 -2
  89. package/src/sources/SourceManager.res +2 -3
  90. package/src/sources/SourceManager.res.mjs +2 -3
  91. package/src/sources/Svm.res +1 -2
  92. package/src/sources/Svm.res.mjs +1 -1
  93. package/src/sources/SvmHyperSyncClient.res +12 -3
  94. package/src/sources/SvmHyperSyncClient.res.mjs +2 -2
  95. package/src/sources/SvmHyperSyncSource.res +10 -5
  96. package/src/sources/SvmHyperSyncSource.res.mjs +5 -4
  97. package/src/IndexingAddresses.res +0 -151
  98. package/src/IndexingAddresses.res.mjs +0 -130
  99. package/src/IndexingAddresses.resi +0 -39
@@ -10,6 +10,7 @@ import * as Config from "./Config.res.mjs";
10
10
  import * as Hasura from "./Hasura.res.mjs";
11
11
  import * as Schema from "./db/Schema.res.mjs";
12
12
  import * as Logging from "./Logging.res.mjs";
13
+ import * as EntityId from "./EntityId.res.mjs";
13
14
  import * as Internal from "./Internal.res.mjs";
14
15
  import Postgres from "postgres";
15
16
  import * as ChainState from "./ChainState.res.mjs";
@@ -17,6 +18,7 @@ import * as Performance from "./bindings/Performance.res.mjs";
17
18
  import * as Persistence from "./Persistence.res.mjs";
18
19
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
19
20
  import * as EntityHistory from "./db/EntityHistory.res.mjs";
21
+ import * as IndexRegistry from "./db/IndexRegistry.res.mjs";
20
22
  import * as InternalTable from "./db/InternalTable.res.mjs";
21
23
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
22
24
  import * as Child_process from "child_process";
@@ -71,13 +73,87 @@ function makeCreateCompositeIndexQuery(tableName, indexFields, pgSchema) {
71
73
  return `CREATE INDEX IF NOT EXISTS "` + indexName + `" ON "` + pgSchema + `"."` + tableName + `"(` + index + `);`;
72
74
  }
73
75
 
74
- function makeCreateTableIndicesQuery(table, pgSchema) {
76
+ function makeCreateTableIndexesQuery(table, pgSchema) {
75
77
  let tableName = table.tableName;
76
78
  let createIndex = indexField => makeCreateIndexQuery(tableName, [indexField], pgSchema);
77
79
  let createCompositeIndex = indexFields => makeCreateCompositeIndexQuery(tableName, indexFields, pgSchema);
78
- let singleIndices = Table.getSingleIndices(table);
79
- let compositeIndices = Table.getCompositeIndices(table);
80
- return singleIndices.map(createIndex).join("\n") + compositeIndices.map(createCompositeIndex).join("\n");
80
+ let singleIndexes = Table.getSingleIndexes(table);
81
+ let compositeIndexes = Table.getCompositeIndexes(table);
82
+ return singleIndexes.map(createIndex).join("\n") + compositeIndexes.map(createCompositeIndex).join("\n");
83
+ }
84
+
85
+ function schemaIndexDescription(param) {
86
+ return param.tableName + "_" + param.indexFields.map(f => f.fieldName + directionToIndexName(f.direction)).join("_");
87
+ }
88
+
89
+ function schemaIndexName(schemaIndex) {
90
+ return IndexRegistry.truncateIndexName(schemaIndexDescription(schemaIndex));
91
+ }
92
+
93
+ function schemaIndexKey(param) {
94
+ return IndexRegistry.makeKey(param.tableName, IndexRegistry.fromIndexFields(param.indexFields), IndexRegistry.btree);
95
+ }
96
+
97
+ function makeCreateSchemaIndexQuery(schemaIndex, pgSchema) {
98
+ let columns = schemaIndex.indexFields.map(f => `"` + f.fieldName + `"` + directionToSql(f.direction)).join(", ");
99
+ return `CREATE INDEX IF NOT EXISTS "` + IndexRegistry.truncateIndexName(schemaIndexDescription(schemaIndex)) + `" ON "` + pgSchema + `"."` + schemaIndex.tableName + `"(` + columns + `);`;
100
+ }
101
+
102
+ function formatSeconds(timeRef) {
103
+ return (Math.round(Performance.secondsSince(timeRef) * 100) / 100).toString();
104
+ }
105
+
106
+ let slowOnLargeDatabaseNotice = "This can take a long time on a large database.";
107
+
108
+ function makeReindexQuery(pgSchema, indexName) {
109
+ return `REINDEX INDEX "` + pgSchema + `"."` + indexName + `";`;
110
+ }
111
+
112
+ function nameCollisionError(indexName, pgSchema) {
113
+ return new Error(`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.`);
114
+ }
115
+
116
+ function makeSingleColumnSchemaIndex(tableName, column) {
117
+ return {
118
+ tableName: tableName,
119
+ indexFields: [{
120
+ fieldName: column,
121
+ direction: "Asc"
122
+ }]
123
+ };
124
+ }
125
+
126
+ function getSchemaIndexes(entities) {
127
+ let derivedSchema = Schema.make(entities.map(e => e.table));
128
+ let all = [];
129
+ entities.forEach(param => {
130
+ let table = param.table;
131
+ Table.getSingleIndexes(table).forEach(column => {
132
+ all.push(makeSingleColumnSchemaIndex(table.tableName, column));
133
+ });
134
+ Table.getCompositeIndexes(table).forEach(indexFields => {
135
+ all.push({
136
+ tableName: table.tableName,
137
+ indexFields: indexFields
138
+ });
139
+ });
140
+ });
141
+ entities.forEach(param => {
142
+ Table.getDerivedFromFields(param.table).forEach(derivedFromField => {
143
+ let column = Utils.unwrapResultExn(Schema.getDerivedFromPgFieldName(derivedSchema, derivedFromField));
144
+ all.push(makeSingleColumnSchemaIndex(derivedFromField.derivedFromEntity, column));
145
+ });
146
+ });
147
+ let seen = new Set();
148
+ return all.filter(schemaIndex => {
149
+ let key = schemaIndexKey(schemaIndex);
150
+ if (seen.has(key)) {
151
+ return false;
152
+ } else {
153
+ seen.add(key);
154
+ return true;
155
+ }
156
+ });
81
157
  }
82
158
 
83
159
  function makeCreateTableQuery(table, pgSchema, isNumericArrayAsText) {
@@ -157,7 +233,7 @@ function getEntityHistory(entityConfig) {
157
233
  checkpointIdField,
158
234
  actionField
159
235
  ]), undefined);
160
- let setChangeSchema = EntityHistory.makeSetUpdateSchema(entityConfig.schema);
236
+ let setChangeSchema = EntityHistory.makeSetUpdateSchema(Table.getIdSchema(entityConfig.table), entityConfig.schema);
161
237
  let cache_setChangeSchemaRows = S$RescriptSchema.array(setChangeSchema);
162
238
  let cache$1 = {
163
239
  table: table,
@@ -168,11 +244,12 @@ function getEntityHistory(entityConfig) {
168
244
  return cache$1;
169
245
  }
170
246
 
171
- function makeInitializeTransaction(pgSchema, pgUser, isHasuraEnabled, chainConfigsOpt, entitiesOpt, enumsOpt, isEmptyPgSchemaOpt) {
247
+ function makeInitializeTransaction(pgSchema, pgUser, isHasuraEnabled, chainConfigsOpt, entitiesOpt, enumsOpt, isEmptyPgSchemaOpt, deferSchemaIndexesOpt) {
172
248
  let chainConfigs = chainConfigsOpt !== undefined ? chainConfigsOpt : [];
173
249
  let entities = entitiesOpt !== undefined ? entitiesOpt : [];
174
250
  let enums = enumsOpt !== undefined ? enumsOpt : [];
175
251
  let isEmptyPgSchema = isEmptyPgSchemaOpt !== undefined ? isEmptyPgSchemaOpt : false;
252
+ let deferSchemaIndexes = deferSchemaIndexesOpt !== undefined ? deferSchemaIndexesOpt : false;
176
253
  let generalTables = [
177
254
  InternalTable.Chains.table,
178
255
  InternalTable.EnvioInfo.table,
@@ -180,13 +257,12 @@ function makeInitializeTransaction(pgSchema, pgUser, isHasuraEnabled, chainConfi
180
257
  InternalTable.RawEvents.table
181
258
  ];
182
259
  let allTables = generalTables.slice();
183
- let allEntityTables = [];
184
260
  entities.forEach(entityConfig => {
185
- allEntityTables.push(entityConfig.table);
186
261
  allTables.push(entityConfig.table);
187
262
  allTables.push(getEntityHistory(entityConfig).table);
188
263
  });
189
- let derivedSchema = Schema.make(allEntityTables);
264
+ let schemaIndexes = getSchemaIndexes(entities);
265
+ IndexRegistry.validateIndexNamesOrThrow(schemaIndexes.map(schemaIndexDescription));
190
266
  let query = {
191
267
  contents: (
192
268
  isEmptyPgSchema && pgSchema === "public" ? `CREATE SCHEMA IF NOT EXISTS "` + pgSchema + `";\n` : `DROP SCHEMA IF EXISTS "` + pgSchema + `" CASCADE;
@@ -201,19 +277,11 @@ GRANT ALL ON SCHEMA "` + pgSchema + `" TO public;`)
201
277
  allTables.forEach(table => {
202
278
  query.contents = query.contents + "\n" + makeCreateTableQuery(table, pgSchema, isHasuraEnabled);
203
279
  });
204
- allTables.forEach(table => {
205
- let indices = makeCreateTableIndicesQuery(table, pgSchema);
206
- if (indices !== "") {
207
- query.contents = query.contents + "\n" + indices;
208
- return;
209
- }
210
- });
211
- entities.forEach(entity => {
212
- Table.getDerivedFromFields(entity.table).forEach(derivedFromField => {
213
- let indexField = Utils.unwrapResultExn(Schema.getDerivedFromPgFieldName(derivedSchema, derivedFromField));
214
- query.contents = query.contents + "\n" + makeCreateIndexQuery(derivedFromField.derivedFromEntity, [indexField], pgSchema);
280
+ if (!deferSchemaIndexes) {
281
+ schemaIndexes.forEach(schemaIndex => {
282
+ query.contents = query.contents + "\n" + makeCreateSchemaIndexQuery(schemaIndex, pgSchema);
215
283
  });
216
- });
284
+ }
217
285
  query.contents = query.contents + "\n" + InternalTable.Views.makeMetaViewQuery(pgSchema);
218
286
  query.contents = query.contents + "\n" + InternalTable.Views.makeChainMetadataViewQuery(pgSchema);
219
287
  let initialChainsValuesQuery = InternalTable.Chains.makeInitialValuesQuery(pgSchema, chainConfigs);
@@ -292,8 +360,8 @@ function makeDeleteByIdQuery(pgSchema, tableName) {
292
360
  return `DELETE FROM "` + pgSchema + `"."` + tableName + `" WHERE id = $1;`;
293
361
  }
294
362
 
295
- function makeDeleteByIdsQuery(pgSchema, tableName) {
296
- return `DELETE FROM "` + pgSchema + `"."` + tableName + `" WHERE id = ANY($1::text[]);`;
363
+ function makeDeleteByIdsQuery(pgSchema, tableName, idPgType) {
364
+ return `DELETE FROM "` + pgSchema + `"."` + tableName + `" WHERE id = ANY($1::` + idPgType + `[]);`;
297
365
  }
298
366
 
299
367
  function makeLoadAllQuery(pgSchema, tableName) {
@@ -547,9 +615,10 @@ async function getConnectedPsqlExec(pgUser, pgHost, pgDatabase, pgPort, containe
547
615
  }
548
616
 
549
617
  async function deleteByIdsOrThrow(sql, pgSchema, ids, table) {
618
+ let idsJson = Table.encodeIdsToJson(table, ids);
550
619
  try {
551
620
  await (
552
- ids.length !== 1 ? sql.unsafe(makeDeleteByIdsQuery(pgSchema, table.tableName), [ids], {prepare: true}) : sql.unsafe(makeDeleteByIdQuery(pgSchema, table.tableName), ids, {prepare: true})
621
+ ids.length !== 1 ? sql.unsafe(makeDeleteByIdsQuery(pgSchema, table.tableName, Table.getIdPgFieldType(table, pgSchema)), [idsJson], {prepare: true}) : sql.unsafe(makeDeleteByIdQuery(pgSchema, table.tableName), idsJson, {prepare: true})
553
622
  );
554
623
  return;
555
624
  } catch (raw_exn) {
@@ -586,9 +655,10 @@ function makeInsertDeleteUpdatesQuery(entityConfig, pgSchema) {
586
655
  });
587
656
  let selectPartsStr = selectParts.join(", ");
588
657
  let checkpointIdPgType = Table.getPgFieldType(EntityHistory.checkpointIdFieldType, pgSchema, false, false, false);
658
+ let idPgType = Table.getIdPgFieldType(entityConfig.table, pgSchema);
589
659
  return `INSERT INTO "` + pgSchema + `"."` + historyTableName + `" (` + allHistoryFieldNamesStr + `)
590
660
  SELECT ` + selectPartsStr + `
591
- FROM UNNEST($1::text[], $2::` + checkpointIdPgType + `[]) AS u(` + Table.idFieldName + `, ` + EntityHistory.checkpointIdFieldName + `)`;
661
+ FROM UNNEST($1::` + idPgType + `[], $2::` + checkpointIdPgType + `[]) AS u(` + Table.idFieldName + `, ` + EntityHistory.checkpointIdFieldName + `)`;
592
662
  }
593
663
 
594
664
  function executeSet(sql, items, dbFunction) {
@@ -648,15 +718,16 @@ async function writeBatch(sql, batch, pgSchema, rollback, isInReorgThreshold, co
648
718
  let orderedIds = [];
649
719
  param.changes.forEach(change => {
650
720
  let entityId = change.entityId;
651
- if (Stdlib_Option.isNone(latestChangeById[entityId])) {
721
+ let entityKey = EntityId.toKey(entityId);
722
+ if (Stdlib_Option.isNone(latestChangeById[entityKey])) {
652
723
  orderedIds.push(entityId);
653
724
  }
654
- latestChangeById[entityId] = change;
725
+ latestChangeById[entityKey] = change;
655
726
  if (!shouldSaveHistory) {
656
727
  return;
657
728
  }
658
729
  if (change.checkpointId === diffCheckpointId) {
659
- idsWithDiff.add(entityId);
730
+ idsWithDiff.add(entityKey);
660
731
  return;
661
732
  }
662
733
  if (change.type === "SET") {
@@ -668,13 +739,14 @@ async function writeBatch(sql, batch, pgSchema, rollback, isInReorgThreshold, co
668
739
  });
669
740
  let backfillHistoryIds = new Set();
670
741
  orderedIds.forEach(entityId => {
671
- let match = latestChangeById[entityId];
742
+ let entityKey = EntityId.toKey(entityId);
743
+ let match = latestChangeById[entityKey];
672
744
  if (match.type === "SET") {
673
745
  entitiesToSet.push(match.entity);
674
746
  } else {
675
747
  idsToDelete.push(match.entityId);
676
748
  }
677
- if (shouldSaveHistory && !idsWithDiff.has(entityId)) {
749
+ if (shouldSaveHistory && !idsWithDiff.has(entityKey)) {
678
750
  backfillHistoryIds.add(entityId);
679
751
  return;
680
752
  }
@@ -685,11 +757,11 @@ async function writeBatch(sql, batch, pgSchema, rollback, isInReorgThreshold, co
685
757
  let promises = [];
686
758
  if (shouldSaveHistory) {
687
759
  if (backfillHistoryIds.size !== 0) {
688
- await EntityHistory.backfillHistory(sql, pgSchema, entityConfig.name, entityConfig.index, Array.from(backfillHistoryIds));
760
+ await EntityHistory.backfillHistory(sql, pgSchema, entityConfig.table, entityConfig.index, Array.from(backfillHistoryIds));
689
761
  }
690
762
  if (Utils.$$Array.notEmpty(batchDeleteCheckpointIds)) {
691
763
  promises.push(sql.unsafe(makeInsertDeleteUpdatesQuery(entityConfig, pgSchema), [
692
- batchDeleteEntityIds,
764
+ Table.encodeIdsToJson(entityConfig.table, batchDeleteEntityIds),
693
765
  Utils.$$BigInt.arrayToStringArray(batchDeleteCheckpointIds)
694
766
  ], {prepare: true}));
695
767
  }
@@ -822,10 +894,12 @@ function makeGetRollbackRemovedIdsQuery(entityConfig, pgSchema) {
822
894
  )`;
823
895
  }
824
896
 
825
- let rollbackRowStateSchema = S$RescriptSchema.object(s => [
826
- s.f(Table.idFieldName, S$RescriptSchema.string),
897
+ let rollbackRowStateSchema = Utils.$$WeakMap.memoize(table => S$RescriptSchema.object(s => [
898
+ s.f(Table.idFieldName, Table.getIdSchema(table)),
827
899
  s.f(EntityHistory.changeFieldName, EntityHistory.RowAction.schema)
828
- ]);
900
+ ]));
901
+
902
+ let rollbackRemovedIdsSchema = Utils.$$WeakMap.memoize(table => S$RescriptSchema.array(S$RescriptSchema.object(s => s.f(Table.idFieldName, Table.getIdSchema(table)))));
829
903
 
830
904
  function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isHasuraEnabled, sink, onInitialize, onNewTables) {
831
905
  let containerName = "envio-postgres";
@@ -843,6 +917,23 @@ function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isH
843
917
  env: psqlExecOptions_env
844
918
  };
845
919
  let cacheDirPath = Path.resolve(".envio", "cache");
920
+ let storageName = "postgres";
921
+ let indexRegistry = IndexRegistry.make();
922
+ let reloadIndexRegistry = async () => {
923
+ let rows = S$RescriptSchema.parseOrThrow(await sql.unsafe(IndexRegistry.makeCatalogQuery(pgSchema)), IndexRegistry.catalogRowsSchema);
924
+ let invalidIndexNames = IndexRegistry.reload(indexRegistry, rows);
925
+ if (invalidIndexNames.length !== 0) {
926
+ Logging.warn({
927
+ storage: storageName,
928
+ msg: `Ignoring invalid PostgreSQL indexes in schema "` + pgSchema + `". They are left untouched — drop and recreate them manually if you need them.`,
929
+ indexes: invalidIndexNames
930
+ });
931
+ }
932
+ return Logging.info({
933
+ storage: storageName,
934
+ msg: `Found ` + IndexRegistry.size(indexRegistry).toString() + ` existing indexes in the indexer schema.`
935
+ });
936
+ };
846
937
  let isInitialized = async () => Utils.$$Array.notEmpty(await sql.unsafe(`SELECT table_schema FROM information_schema.tables WHERE table_schema = '` + pgSchema + `' AND (table_name = '` + "event_sync_state" + `' OR table_name = '` + InternalTable.Chains.table.tableName + `');`));
847
938
  let scanCacheDir = async () => {
848
939
  let topEntries;
@@ -962,7 +1053,7 @@ function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isH
962
1053
  if (sink !== undefined) {
963
1054
  await sink.initialize(chainConfigs, chEntities, enums);
964
1055
  }
965
- let queries = makeInitializeTransaction(pgSchema, pgUser, isHasuraEnabled, chainConfigs, pgEntities, enums, Utils.$$Array.isEmpty(schemaTableNames));
1056
+ let queries = makeInitializeTransaction(pgSchema, pgUser, isHasuraEnabled, chainConfigs, pgEntities, enums, Utils.$$Array.isEmpty(schemaTableNames), true);
966
1057
  await sql.begin(async sql => {
967
1058
  await Promise.all(queries.map(query => sql.unsafe(query)));
968
1059
  return await InternalTable.EnvioInfo.write(sql, pgSchema, envioInfo);
@@ -988,6 +1079,7 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
988
1079
  ]);
989
1080
  }
990
1081
  let cache = await restoreEffectCache(true);
1082
+ await reloadIndexRegistry();
991
1083
  if (onInitialize !== undefined) {
992
1084
  await onInitialize();
993
1085
  }
@@ -1038,6 +1130,121 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1038
1130
  };
1039
1131
  }
1040
1132
  };
1133
+ let filterColumns = (table, filters) => {
1134
+ let queryFields = Table.queryFields(table);
1135
+ let columns = [];
1136
+ let seen = new Set();
1137
+ let collect = filter => {
1138
+ if (filter.operator === "and") {
1139
+ filter.filters.forEach(collect);
1140
+ return;
1141
+ }
1142
+ let match = queryFields[filter.fieldName];
1143
+ if (match === undefined) {
1144
+ return;
1145
+ }
1146
+ let pgDbFieldName = match.pgDbFieldName;
1147
+ if (!seen.has(pgDbFieldName)) {
1148
+ seen.add(pgDbFieldName);
1149
+ columns.push(pgDbFieldName);
1150
+ return;
1151
+ }
1152
+ };
1153
+ filters.forEach(collect);
1154
+ return columns;
1155
+ };
1156
+ let makeEnsureIndexQuery = schemaIndex => {
1157
+ let indexName = IndexRegistry.truncateIndexName(schemaIndexDescription(schemaIndex));
1158
+ let match = IndexRegistry.getInvalid(indexRegistry, indexName);
1159
+ if (match === undefined) {
1160
+ return makeCreateSchemaIndexQuery(schemaIndex, pgSchema);
1161
+ }
1162
+ if (match.isRepairable) {
1163
+ if (match.key === schemaIndexKey(schemaIndex)) {
1164
+ return makeReindexQuery(pgSchema, indexName);
1165
+ }
1166
+ throw nameCollisionError(indexName, pgSchema);
1167
+ }
1168
+ throw nameCollisionError(indexName, pgSchema);
1169
+ };
1170
+ let isRepair = schemaIndex => Stdlib_Option.isSome(IndexRegistry.getInvalid(indexRegistry, IndexRegistry.truncateIndexName(schemaIndexDescription(schemaIndex))));
1171
+ let ensureQueryIndexes = async (table, filters) => {
1172
+ let missing = Stdlib_Array.filterMap(filterColumns(table, filters), column => {
1173
+ let schemaIndex = makeSingleColumnSchemaIndex(table.tableName, column);
1174
+ if (IndexRegistry.has(indexRegistry, schemaIndexKey(schemaIndex))) {
1175
+ return;
1176
+ } else {
1177
+ return schemaIndex;
1178
+ }
1179
+ });
1180
+ if (Utils.$$Array.notEmpty(missing)) {
1181
+ await Promise.all(missing.map(schemaIndex => {
1182
+ let indexName = IndexRegistry.truncateIndexName(schemaIndexDescription(schemaIndex));
1183
+ return IndexRegistry.ensure(indexRegistry, schemaIndexKey(schemaIndex), table.tableName, async () => {
1184
+ let query = makeEnsureIndexQuery(schemaIndex);
1185
+ let verb = isRepair(schemaIndex) ? "Repairing invalid index" : "Creating index";
1186
+ Logging.info({
1187
+ storage: storageName,
1188
+ msg: verb + ` "` + indexName + `" to serve a getWhere query on "` + table.tableName + `". Writes to the table are paused until it completes. ` + slowOnLargeDatabaseNotice
1189
+ });
1190
+ let timeRef = Performance.now();
1191
+ await sql.unsafe(query);
1192
+ IndexRegistry.clearInvalidName(indexRegistry, indexName);
1193
+ return Logging.info({
1194
+ storage: storageName,
1195
+ msg: `Index "` + indexName + `" is ready after ` + formatSeconds(timeRef) + `s. Resuming indexing.`
1196
+ });
1197
+ }).catch(exn => Logging.warn({
1198
+ storage: storageName,
1199
+ msg: `Failed to create the index "` + indexName + `" for a getWhere query. The query runs without it.`,
1200
+ err: Utils.prettifyExn(exn)
1201
+ }));
1202
+ }));
1203
+ return;
1204
+ }
1205
+ };
1206
+ let finalizeBackfill = async (entities, chainIds, readyAt) => {
1207
+ let schemaIndexes = getSchemaIndexes(entities.filter(e => e.storage.postgres));
1208
+ IndexRegistry.validateIndexNamesOrThrow(schemaIndexes.map(schemaIndexDescription));
1209
+ let missing = schemaIndexes.filter(schemaIndex => !IndexRegistry.has(indexRegistry, schemaIndexKey(schemaIndex)));
1210
+ let repaired = missing.filter(isRepair);
1211
+ if (Utils.$$Array.notEmpty(repaired)) {
1212
+ Logging.warn({
1213
+ storage: storageName,
1214
+ msg: `PostgreSQL reports ` + repaired.length.toString() + ` existing indexes as invalid, so they can't serve queries. Rebuilding them.`,
1215
+ indexes: repaired.map(schemaIndexName)
1216
+ });
1217
+ }
1218
+ if (missing.length !== 0) {
1219
+ Logging.info({
1220
+ storage: storageName,
1221
+ msg: `Creating the ` + missing.length.toString() + ` remaining schema indexes before the indexer reports ready. Writes are paused until they are committed. ` + slowOnLargeDatabaseNotice,
1222
+ indexes: missing.map(schemaIndexName)
1223
+ });
1224
+ } else {
1225
+ Logging.info({
1226
+ storage: storageName,
1227
+ msg: `All ` + schemaIndexes.length.toString() + ` schema indexes are already in place. Marking the indexer ready.`
1228
+ });
1229
+ }
1230
+ let indexQueries = missing.map(makeEnsureIndexQuery);
1231
+ let timeRef = Performance.now();
1232
+ await sql.begin(async sql => {
1233
+ for (let idx = 0, idx_finish = indexQueries.length; idx < idx_finish; ++idx) {
1234
+ await sql.unsafe(indexQueries[idx]);
1235
+ }
1236
+ return await sql.unsafe(InternalTable.Chains.makeSetReadyAtQuery(pgSchema), [
1237
+ readyAt,
1238
+ chainIds
1239
+ ], {prepare: true});
1240
+ });
1241
+ missing.forEach(schemaIndex => IndexRegistry.add(indexRegistry, schemaIndexKey(schemaIndex)));
1242
+ repaired.forEach(schemaIndex => IndexRegistry.clearInvalidName(indexRegistry, IndexRegistry.truncateIndexName(schemaIndexDescription(schemaIndex))));
1243
+ return Logging.info({
1244
+ storage: storageName,
1245
+ msg: `Committed ` + missing.length.toString() + ` schema indexes and the ready timestamp in ` + formatSeconds(timeRef) + `s.`
1246
+ });
1247
+ };
1041
1248
  let setOrThrow$1 = (items, table, itemSchema) => setOrThrow(sql, items, table, itemSchema, pgSchema);
1042
1249
  let setEffectCacheOrThrow = async (table, itemSchema, items, initialize) => {
1043
1250
  if (initialize) {
@@ -1114,6 +1321,7 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1114
1321
  sql.unsafe(InternalTable.Checkpoints.makeGetReorgCheckpointsQuery(pgSchema)),
1115
1322
  InternalTable.EnvioInfo.read(sql, pgSchema)
1116
1323
  ]);
1324
+ await reloadIndexRegistry();
1117
1325
  let checkpointId = BigInt(match[2][0].id);
1118
1326
  let reorgCheckpoints = match[3].map(raw => ({
1119
1327
  id: BigInt(raw.id),
@@ -1147,10 +1355,10 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1147
1355
  sql.unsafe(makeGetRollbackRemovedIdsQuery(entityConfig, pgSchema), [rollbackTargetCheckpointId.toString()], {prepare: true}),
1148
1356
  sql.unsafe(makeGetRollbackPreTargetRowsQuery(entityConfig, pgSchema), [rollbackTargetCheckpointId.toString()], {prepare: true})
1149
1357
  ]);
1150
- let removedIds = match[0].map(row => row.id);
1358
+ let removedIds = S$RescriptSchema.parseOrThrow(match[0], rollbackRemovedIdsSchema(entityConfig.table));
1151
1359
  let restoredEntitiesResult = [];
1152
1360
  match[1].forEach(row => {
1153
- let match = S$RescriptSchema.parseOrThrow(row, rollbackRowStateSchema);
1361
+ let match = S$RescriptSchema.parseOrThrow(row, rollbackRowStateSchema(entityConfig.table));
1154
1362
  if (match[1] === "SET") {
1155
1363
  restoredEntitiesResult.push(row);
1156
1364
  return;
@@ -1186,15 +1394,17 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1186
1394
  }
1187
1395
  let primaryTimerRef = Performance.now();
1188
1396
  await writeBatch(sql, batch, pgSchema, rollback, isInReorgThreshold, config, allEntities, setEffectCacheOrThrow, updatedEffectsCache, pgUpdates, sinkPromise, chainMetaData, undefined);
1189
- return onWrite("postgres", Performance.secondsSince(primaryTimerRef));
1397
+ return onWrite(storageName, Performance.secondsSince(primaryTimerRef));
1190
1398
  };
1191
1399
  let close = () => sql.end();
1192
1400
  return {
1193
- name: "postgres",
1401
+ name: storageName,
1194
1402
  isInitialized: isInitialized,
1195
1403
  initialize: initialize,
1196
1404
  resumeInitialState: resumeInitialState,
1197
1405
  loadOrThrow: loadOrThrow,
1406
+ ensureQueryIndexes: ensureQueryIndexes,
1407
+ finalizeBackfill: finalizeBackfill,
1198
1408
  dumpEffectCache: dumpEffectCache,
1199
1409
  reset: reset,
1200
1410
  setChainMeta: setChainMeta,
@@ -1264,7 +1474,17 @@ export {
1264
1474
  directionToSql,
1265
1475
  directionToIndexName,
1266
1476
  makeCreateCompositeIndexQuery,
1267
- makeCreateTableIndicesQuery,
1477
+ makeCreateTableIndexesQuery,
1478
+ schemaIndexDescription,
1479
+ schemaIndexName,
1480
+ schemaIndexKey,
1481
+ makeCreateSchemaIndexQuery,
1482
+ formatSeconds,
1483
+ slowOnLargeDatabaseNotice,
1484
+ makeReindexQuery,
1485
+ nameCollisionError,
1486
+ makeSingleColumnSchemaIndex,
1487
+ getSchemaIndexes,
1268
1488
  makeCreateTableQuery,
1269
1489
  entityHistoryCache,
1270
1490
  getEntityHistory,
@@ -1297,6 +1517,7 @@ export {
1297
1517
  makeGetRollbackPreTargetRowsQuery,
1298
1518
  makeGetRollbackRemovedIdsQuery,
1299
1519
  rollbackRowStateSchema,
1520
+ rollbackRemovedIdsSchema,
1300
1521
  make,
1301
1522
  makeStorageFromEnv,
1302
1523
  makePersistenceFromConfig,
@@ -343,8 +343,8 @@ let parse = (
343
343
  // run here the way real routing does (one item per registration).
344
344
  // Registrations are built the same way `HandlerRegister.finishRegistration`
345
345
  // does at startup (not stubs), so the address filter and `where` behave
346
- // identically to real indexing — the dead-input tracker relies on
347
- // `clientAddressFilter` actually gating unrouted items. Drop registrations
346
+ // identically to real indexing — the dead-input tracker relies on the
347
+ // source's address gate actually dropping unrouted items. Drop registrations
348
348
  // whose `where` excludes this chain (they wouldn't be fetched live), and
349
349
  // ignore the bare handler-less fallback so a missing handler surfaces below
350
350
  // instead of silently running nothing.
@@ -22,7 +22,7 @@ type t<'processConfig> = {process: 'processConfig => promise<processResult>}
22
22
 
23
23
  type entityChange = {
24
24
  sets: array<unknown>,
25
- deleted: array<string>,
25
+ deleted: array<EntityId.t>,
26
26
  }
27
27
 
28
28
  type testIndexerState = {
@@ -136,11 +136,12 @@ let handleWriteBatch = (
136
136
  switch change {
137
137
  | Set({entityId, entity, checkpointId}) =>
138
138
  // The store keeps decoded entities so load comparisons (bigint /
139
- // BigDecimal) work on real values.
140
- entityDict->Dict.set(entityId, entity)
139
+ // BigDecimal) work on real values. Ids are keyed by their string form
140
+ // since they may be string/int/bigint.
141
+ entityDict->Dict.set(entityId->EntityId.toKey, entity)
141
142
  entityChangeFor(checkpointId).sets->Array.push(entity->Utils.magic)->ignore
142
143
  | Delete({entityId, checkpointId}) =>
143
- Dict.delete(entityDict->Obj.magic, entityId)
144
+ Dict.delete(entityDict->Obj.magic, entityId->EntityId.toKey)
144
145
  entityChangeFor(checkpointId).deleted->Array.push(entityId)->ignore
145
146
  }
146
147
  }
@@ -197,7 +198,7 @@ let handleWriteBatch = (
197
198
  entityObj->Dict.set("sets", sets->(Utils.magic: array<unknown> => unknown))
198
199
  }
199
200
  if deleted->Array.length > 0 {
200
- entityObj->Dict.set("deleted", deleted->(Utils.magic: array<string> => unknown))
201
+ entityObj->Dict.set("deleted", deleted->(Utils.magic: array<EntityId.t> => unknown))
201
202
  }
202
203
  // Match the capitalized entity accessor the generated change types expose.
203
204
  change->Dict.set(
@@ -484,6 +485,9 @@ let makeInMemoryStorage = (~state: testIndexerState): Persistence.storage => {
484
485
  state
485
486
  ->handleLoad(~tableName=table.tableName, ~filter)
486
487
  ->(Utils.magic: array<Internal.entity> => array<unknown>),
488
+ // The in-memory storage has no indexes to build, and it's always ready.
489
+ ensureQueryIndexes: async (~table as _, ~filters as _) => (),
490
+ finalizeBackfill: async (~entities as _, ~chainIds as _, ~readyAt as _) => (),
487
491
  writeBatch: async (
488
492
  ~batch,
489
493
  ~rollback as _,
@@ -5,6 +5,7 @@ import * as Utils from "./Utils.res.mjs";
5
5
  import * as Config from "./Config.res.mjs";
6
6
  import * as Logging from "./Logging.res.mjs";
7
7
  import * as ChainMap from "./ChainMap.res.mjs";
8
+ import * as EntityId from "./EntityId.res.mjs";
8
9
  import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
9
10
  import * as IndexerLoop from "./IndexerLoop.res.mjs";
10
11
  import * as Persistence from "./Persistence.res.mjs";
@@ -102,12 +103,12 @@ function handleWriteBatch(state, updatedEntities, checkpointIds, checkpointChain
102
103
  let processChange = change => {
103
104
  if (change.type === "SET") {
104
105
  let entity = change.entity;
105
- entityDict[change.entityId] = entity;
106
+ entityDict[EntityId.toKey(change.entityId)] = entity;
106
107
  entityChangeFor(change.checkpointId).sets.push(entity);
107
108
  return;
108
109
  }
109
110
  let entityId = change.entityId;
110
- Stdlib_Dict.$$delete(entityDict, entityId);
111
+ Stdlib_Dict.$$delete(entityDict, EntityId.toKey(entityId));
111
112
  entityChangeFor(change.checkpointId).deleted.push(entityId);
112
113
  };
113
114
  param.changes.forEach(processChange);
@@ -314,6 +315,8 @@ function makeInMemoryStorage(state) {
314
315
  initialize: async (param, param$1, param$2, param$3) => Stdlib_JsError.throwWithMessage("TestIndexer: initialize should not be called; the initial state is derived from config."),
315
316
  resumeInitialState: async () => Stdlib_JsError.throwWithMessage("TestIndexer: resumeInitialState should not be called; the initial state is derived from config."),
316
317
  loadOrThrow: async (filter, table) => handleLoad(state, table.tableName, filter),
318
+ ensureQueryIndexes: async (param, param$1) => {},
319
+ finalizeBackfill: async (param, param$1, param$2) => {},
317
320
  dumpEffectCache: async () => {},
318
321
  reset: async () => {},
319
322
  setChainMeta: async param => {},
@@ -144,7 +144,7 @@ let getWhereHandler = (params: entityContextParams, filter: dict<dict<unknown>>)
144
144
  }
145
145
 
146
146
  let noopSet = (_entity: Internal.entity) => ()
147
- let noopDeleteUnsafe = (_entityId: string) => ()
147
+ let noopDeleteUnsafe = (_entityId: EntityId.t) => ()
148
148
 
149
149
  // Reads against ClickHouse-only entities have no Postgres table to hit;
150
150
  // surface a friendly error instead of letting the SQL layer fail with
@@ -168,7 +168,7 @@ let entityTraps: Utils.Proxy.traps<entityContextParams> = {
168
168
  ->InMemoryTable.Entity.set(
169
169
  ~committedCheckpointId=params.indexerState->IndexerState.committedCheckpointId,
170
170
  Set({
171
- entityId: entity.id,
171
+ entityId: entity.id->EntityId.unsafeOfString,
172
172
  checkpointId: params.checkpointId,
173
173
  entity,
174
174
  }),
@@ -282,7 +282,7 @@ let entityTraps: Utils.Proxy.traps<entityContextParams> = {
282
282
  }),
283
283
  )
284
284
  }
285
- }->(Utils.magic: (string => unit) => unknown)
285
+ }->(Utils.magic: (EntityId.t => unit) => unknown)
286
286
  | _ =>
287
287
  JsError.throwWithMessage(`Invalid context.${params.entityConfig.name}.${prop} operation.`)
288
288
  }
package/src/Writing.res CHANGED
@@ -237,7 +237,7 @@ let dropCommitted = (state: IndexerState.t, ~keepLoadedFromDb) => {
237
237
 
238
238
  // Blocks until the store holds fewer than keepLatestChangesLimit changes,
239
239
  // freeing committed changes first and awaiting commits as a last resort.
240
- let rec awaitCapacity = async (state: IndexerState.t) => {
240
+ let rec awaitCapacityLoop = async (state: IndexerState.t) => {
241
241
  // After a failed write nothing will free capacity, so bail instead of waiting
242
242
  // on a commit that won't come (the error already went to onError).
243
243
  if !(state->IndexerState.hasFailedWrite) && state->getChangesCount >= keepLatestChangesLimit {
@@ -259,11 +259,21 @@ let rec awaitCapacity = async (state: IndexerState.t) => {
259
259
  ) {
260
260
  state->schedule
261
261
  await state->waitForCommit
262
- await state->awaitCapacity
262
+ await state->awaitCapacityLoop
263
263
  }
264
264
  }
265
265
  }
266
266
 
267
+ // Only the over-limit path is a real stall. Timing every call would charge each
268
+ // batch the microtask hop of awaiting an already-resolved promise, which reads
269
+ // as storage backpressure that isn't there.
270
+ let awaitCapacity = async (state: IndexerState.t) =>
271
+ if state->getChangesCount >= keepLatestChangesLimit {
272
+ let timeRef = Performance.now()
273
+ await state->awaitCapacityLoop
274
+ state->IndexerState.recordStalledOnStorageWrite(~seconds=timeRef->Performance.secondsSince)
275
+ }
276
+
267
277
  // Awaits until everything processed is persisted. On a failed write we stop
268
278
  // draining (onError already surfaced it) rather than throw.
269
279
  let rec flush = async (state: IndexerState.t) => {