envio 3.7.0 → 3.9.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 (79) hide show
  1. package/README.md +2 -2
  2. package/index.d.ts +399 -156
  3. package/package.json +6 -6
  4. package/src/AddressRows.res +121 -0
  5. package/src/AddressRows.res.mjs +143 -0
  6. package/src/Batch.res +2 -0
  7. package/src/Batch.res.mjs +2 -1
  8. package/src/ChainState.res +82 -68
  9. package/src/ChainState.res.mjs +81 -65
  10. package/src/ChainState.resi +14 -9
  11. package/src/Config.res +41 -91
  12. package/src/Config.res.mjs +34 -68
  13. package/src/ContractMapping.res +66 -0
  14. package/src/ContractMapping.res.mjs +73 -0
  15. package/src/Core.res +20 -0
  16. package/src/Core.res.mjs +4 -0
  17. package/src/Envio.res +54 -76
  18. package/src/EventConfigBuilder.res +157 -30
  19. package/src/EventConfigBuilder.res.mjs +118 -24
  20. package/src/FetchState.res +59 -67
  21. package/src/FetchState.res.mjs +34 -44
  22. package/src/HandlerRegister.res +20 -11
  23. package/src/HandlerRegister.res.mjs +22 -5
  24. package/src/Hasura.res +30 -22
  25. package/src/Hasura.res.mjs +11 -5
  26. package/src/InMemoryStore.res +18 -36
  27. package/src/InMemoryStore.res.mjs +12 -26
  28. package/src/IndexerState.res +21 -1
  29. package/src/IndexerState.res.mjs +9 -3
  30. package/src/IndexerState.resi +1 -0
  31. package/src/Internal.res +33 -13
  32. package/src/Internal.res.mjs +12 -16
  33. package/src/Main.res +27 -20
  34. package/src/Main.res.mjs +19 -14
  35. package/src/MemoryStorage.res +78 -73
  36. package/src/MemoryStorage.res.mjs +66 -81
  37. package/src/Metrics.res +15 -1
  38. package/src/Metrics.res.mjs +5 -1
  39. package/src/Persistence.res +34 -18
  40. package/src/Persistence.res.mjs +8 -5
  41. package/src/PgStorage.res +189 -66
  42. package/src/PgStorage.res.mjs +93 -59
  43. package/src/Rollback.res +14 -10
  44. package/src/Rollback.res.mjs +4 -2
  45. package/src/SimulateItems.res +254 -9
  46. package/src/SimulateItems.res.mjs +214 -47
  47. package/src/TestIndexer.res +119 -121
  48. package/src/TestIndexer.res.mjs +95 -100
  49. package/src/Utils.res +7 -0
  50. package/src/Utils.res.mjs +9 -2
  51. package/src/Writing.res +2 -0
  52. package/src/Writing.res.mjs +2 -1
  53. package/src/bindings/ClickHouse.res +39 -6
  54. package/src/bindings/ClickHouse.res.mjs +29 -4
  55. package/src/bindings/NodeJs.res +9 -0
  56. package/src/bindings/NodeJs.res.mjs +8 -1
  57. package/src/bindings/Postgres.res +9 -0
  58. package/src/bindings/Postgres.res.mjs +3 -0
  59. package/src/db/EntityHistory.res +12 -18
  60. package/src/db/EntityHistory.res.mjs +3 -14
  61. package/src/db/InternalTable.res +182 -65
  62. package/src/db/InternalTable.res.mjs +183 -73
  63. package/src/db/Table.res +24 -0
  64. package/src/db/Table.res.mjs +20 -1
  65. package/src/sources/AddressSet.res +0 -22
  66. package/src/sources/AddressStore.res +48 -88
  67. package/src/sources/AddressStore.res.mjs +11 -22
  68. package/src/sources/EvmHyperSyncSource.res +3 -2
  69. package/src/sources/SimulateSource.res +8 -4
  70. package/src/sources/SimulateSource.res.mjs +7 -3
  71. package/src/sources/Svm.res +34 -7
  72. package/src/sources/Svm.res.mjs +32 -4
  73. package/src/sources/SvmHyperSyncClient.res +24 -30
  74. package/src/sources/SvmHyperSyncClient.res.mjs +3 -2
  75. package/src/sources/SvmHyperSyncSource.res +88 -36
  76. package/src/sources/SvmHyperSyncSource.res.mjs +71 -39
  77. package/src/sources/TransactionStore.res +38 -0
  78. package/src/sources/TransactionStore.res.mjs +5 -0
  79. package/svm.schema.json +37 -82
@@ -9,13 +9,14 @@ import * as Logging from "./Logging.res.mjs";
9
9
  import * as ChainMap from "./ChainMap.res.mjs";
10
10
  import * as EntityId from "./EntityId.res.mjs";
11
11
  import * as Internal from "./Internal.res.mjs";
12
+ import * as ChainState from "./ChainState.res.mjs";
12
13
  import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
14
+ import * as AddressRows from "./AddressRows.res.mjs";
13
15
  import * as IndexerLoop from "./IndexerLoop.res.mjs";
14
16
  import * as Persistence from "./Persistence.res.mjs";
15
17
  import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
16
18
  import * as EntityFilter from "./db/EntityFilter.res.mjs";
17
19
  import * as IndexerState from "./IndexerState.res.mjs";
18
- import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
19
20
  import * as ErrorHandling from "./ErrorHandling.res.mjs";
20
21
  import * as HandlerLoader from "./HandlerLoader.res.mjs";
21
22
  import * as InternalTable from "./db/InternalTable.res.mjs";
@@ -24,37 +25,17 @@ import * as SimulateItems from "./SimulateItems.res.mjs";
24
25
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
25
26
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
26
27
  import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
28
+ import * as ContractMapping from "./ContractMapping.res.mjs";
27
29
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
28
30
  import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
29
31
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
30
32
 
31
- function toIndexingAddress(dc) {
32
- return {
33
- address: Config.EnvioAddresses.getAddress(dc),
34
- contractName: dc.contract_name,
35
- registrationBlock: dc.registration_block
36
- };
33
+ function addressRowsByChain(state) {
34
+ return AddressRows.Table.groupByChain(state.addresses);
37
35
  }
38
36
 
39
- function getIndexingAddressesByChain(state) {
40
- let byChain = {};
41
- let dcDict = state.entities[Config.EnvioAddresses.name];
42
- if (dcDict !== undefined) {
43
- Object.values(dcDict).forEach(entity => {
44
- let chainIdStr = ChainId.toString(entity.chain_id);
45
- let arr = byChain[chainIdStr];
46
- let contracts;
47
- if (arr !== undefined) {
48
- contracts = arr;
49
- } else {
50
- let arr$1 = [];
51
- byChain[chainIdStr] = arr$1;
52
- contracts = arr$1;
53
- }
54
- contracts.push(toIndexingAddress(entity));
55
- });
56
- }
57
- return byChain;
37
+ function renderRows(rows, config) {
38
+ return AddressRows.render(rows, config.ecosystem.name, !config.lowercaseAddresses);
58
39
  }
59
40
 
60
41
  function rowKey(scope, entityId) {
@@ -88,8 +69,18 @@ function handleLoad(state, tableName, filter) {
88
69
  }
89
70
  }
90
71
 
91
- function handleWriteBatch(state, updatedEntities, checkpointIds, checkpointChainIds, checkpointBlockNumbers, checkpointEventsProcessed) {
72
+ function handleWriteBatch(state, config, updatedEntities, registeredAddresses, checkpointIds, checkpointChainIds, checkpointBlockNumbers, checkpointEventsProcessed) {
92
73
  let changesByCheckpoint = {};
74
+ let addressesByCheckpoint = {};
75
+ let rendered = renderRows(registeredAddresses.map(param => param.row), config);
76
+ registeredAddresses.forEach((param, idx) => {
77
+ let row = param.row;
78
+ AddressRows.Table.insert(state.addresses, row);
79
+ Utils.Dict.push(addressesByCheckpoint, param.checkpointId.toString(), {
80
+ address: rendered[idx],
81
+ contract: ContractMapping.nameOfOrThrow(state.contractMapping, row.contractId)
82
+ });
83
+ });
93
84
  updatedEntities.forEach(param => {
94
85
  let scope = param.scope;
95
86
  let entityConfig = param.entityConfig;
@@ -151,40 +142,33 @@ function handleWriteBatch(state, updatedEntities, checkpointIds, checkpointChain
151
142
  change["chainId"] = checkpointChainIds[i];
152
143
  change["eventsProcessed"] = checkpointEventsProcessed[i];
153
144
  let checkpointKey = checkpointId.toString();
145
+ let rendered$1 = addressesByCheckpoint[checkpointKey];
146
+ if (rendered$1 !== undefined) {
147
+ let addressesObj = {};
148
+ addressesObj["sets"] = rendered$1;
149
+ change["addresses"] = addressesObj;
150
+ }
154
151
  let entityChanges = changesByCheckpoint[checkpointKey];
155
152
  if (entityChanges !== undefined) {
156
153
  Object.entries(entityChanges).forEach(param => {
157
154
  let match = param[1];
158
155
  let deleted = match.deleted;
159
156
  let sets = match.sets;
160
- let entityName = param[0];
161
- if (entityName === Config.EnvioAddresses.name) {
162
- let entityObj = {};
163
- if (sets.length !== 0) {
164
- let simplifiedSets = sets.map(entity => ({
165
- address: Config.EnvioAddresses.getAddress(entity),
166
- contract: entity.contract_name
167
- }));
168
- entityObj["sets"] = simplifiedSets;
169
- }
170
- change["addresses"] = entityObj;
171
- return;
172
- }
173
- let entityObj$1 = {};
157
+ let entityObj = {};
174
158
  if (sets.length !== 0) {
175
- entityObj$1["sets"] = sets;
159
+ entityObj["sets"] = sets;
176
160
  }
177
161
  if (deleted.length !== 0) {
178
- entityObj$1["deleted"] = deleted;
162
+ entityObj["deleted"] = deleted;
179
163
  }
180
- change[Utils.$$String.capitalize(entityName)] = entityObj$1;
164
+ change[Utils.$$String.capitalize(param[0])] = entityObj;
181
165
  });
182
166
  }
183
167
  state.processChanges.push(change);
184
168
  }
185
169
  }
186
170
 
187
- function makeInitialState(config, processConfigChains, indexingAddressesByChain) {
171
+ function makeInitialState(config, processConfigChains, addressRowsByChain, contractMapping) {
188
172
  let chainKeys = Object.keys(processConfigChains);
189
173
  let chains = chainKeys.map(chainIdStr => {
190
174
  let chain = ChainId.normalizeOrThrow(chainIdStr);
@@ -192,7 +176,7 @@ function makeInitialState(config, processConfigChains, indexingAddressesByChain)
192
176
  Stdlib_JsError.throwWithMessage(`Chain ` + chainIdStr + ` is not configured in config.yaml`);
193
177
  }
194
178
  let processChainConfig = processConfigChains[chainIdStr];
195
- let indexingAddresses = Stdlib_Option.getOr(indexingAddressesByChain[chainIdStr], []);
179
+ let addressRows = Stdlib_Option.getOr(addressRowsByChain[chainIdStr], AddressRows.emptySeedRows());
196
180
  return {
197
181
  id: chain,
198
182
  startBlock: processChainConfig.startBlock,
@@ -202,17 +186,18 @@ function makeInitialState(config, processConfigChains, indexingAddressesByChain)
202
186
  numEventsProcessed: 0,
203
187
  firstEventBlockNumber: undefined,
204
188
  timestampCaughtUpToHeadOrEndblock: undefined,
205
- indexingAddresses: indexingAddresses,
189
+ addressRows: addressRows,
206
190
  sourceBlockNumber: Stdlib_Option.getOr(processChainConfig.endBlock, 0)
207
191
  };
208
192
  });
209
193
  return {
210
194
  cleanRun: true,
195
+ contractMapping: contractMapping,
196
+ envioInfo: {},
211
197
  cache: {},
212
198
  chains: chains,
213
199
  checkpointId: InternalTable.Checkpoints.initialCheckpointId,
214
- reorgCheckpoints: [],
215
- envioInfo: Config.stripSensitiveData(Config.getPublicConfigJson())
200
+ reorgCheckpoints: []
216
201
  };
217
202
  }
218
203
 
@@ -230,22 +215,44 @@ function getSimulateEndBlock(simulateItems, config, startBlock) {
230
215
  let maxBlock = {
231
216
  contents: startBlock
232
217
  };
233
- simulateItems.forEach(rawJson => {
234
- let blockJson = rawJson.block;
235
- if (blockJson == null) {
236
- return;
237
- }
238
- let n = Stdlib_Option.flatMap(blockJson[config.ecosystem.blockNumberName], v => {
239
- if (v == null) {
240
- return;
241
- } else {
242
- return Primitive_option.some(v);
243
- }
244
- });
218
+ let match = config.ecosystem.name;
219
+ let blockNumberKey;
220
+ switch (match) {
221
+ case "evm" :
222
+ case "fuel" :
223
+ blockNumberKey = config.ecosystem.blockNumberName;
224
+ break;
225
+ case "svm" :
226
+ blockNumberKey = "slot";
227
+ break;
228
+ }
229
+ let bump = n => {
245
230
  if (n !== undefined && n > maxBlock.contents) {
246
231
  maxBlock.contents = n;
247
232
  return;
248
233
  }
234
+ };
235
+ let getInt = (d, key) => Stdlib_Option.flatMap(d[key], v => {
236
+ if (v == null) {
237
+ return;
238
+ } else {
239
+ return Primitive_option.some(v);
240
+ }
241
+ });
242
+ simulateItems.forEach(rawJson => {
243
+ let match = config.ecosystem.name;
244
+ switch (match) {
245
+ case "evm" :
246
+ case "fuel" :
247
+ break;
248
+ case "svm" :
249
+ bump(getInt(rawJson, "slot"));
250
+ break;
251
+ }
252
+ let blockJson = rawJson.block;
253
+ if (!(blockJson == null)) {
254
+ return bump(getInt(blockJson, blockNumberKey));
255
+ }
249
256
  });
250
257
  return maxBlock.contents;
251
258
  }
@@ -377,7 +384,7 @@ function makeInMemoryStorage(state) {
377
384
  return {
378
385
  name: "test-inmemory",
379
386
  isInitialized: async () => true,
380
- 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."),
387
+ initialize: async (param, param$1, param$2, param$3, param$4) => Stdlib_JsError.throwWithMessage("TestIndexer: initialize should not be called; the initial state is derived from config."),
381
388
  resumeInitialState: async () => Stdlib_JsError.throwWithMessage("TestIndexer: resumeInitialState should not be called; the initial state is derived from config."),
382
389
  loadOrThrow: async (filter, table) => handleLoad(state, table.tableName, filter),
383
390
  ensureQueryIndexes: async (param, param$1) => {},
@@ -391,7 +398,7 @@ function makeInMemoryStorage(state) {
391
398
  getRollbackTargetCheckpoint: async (param, param$1) => Stdlib_JsError.throwWithMessage("TestIndexer: Rollback is not supported. The runner forces rollbackOnReorg off, so this should be unreachable."),
392
399
  getRollbackProgressDiff: async param => Stdlib_JsError.throwWithMessage("TestIndexer: Rollback is not supported. The runner forces rollbackOnReorg off, so this should be unreachable."),
393
400
  getRollbackData: async (param, param$1) => Stdlib_JsError.throwWithMessage("TestIndexer: Rollback is not supported. The runner forces rollbackOnReorg off, so this should be unreachable."),
394
- writeBatch: async (batch, param, param$1, param$2, param$3, param$4, updatedEntities, param$5, param$6) => handleWriteBatch(state, updatedEntities, batch.checkpointIds, batch.checkpointChainIds, batch.checkpointBlockNumbers, batch.checkpointEventsProcessed),
401
+ writeBatch: async (batch, param, param$1, config, param$2, param$3, updatedEntities, registeredAddresses, param$4, param$5) => handleWriteBatch(state, config, updatedEntities, registeredAddresses, batch.checkpointIds, batch.checkpointChainIds, batch.checkpointBlockNumbers, batch.checkpointEventsProcessed),
395
402
  close: async () => {}
396
403
  };
397
404
  }
@@ -424,36 +431,25 @@ function getRegistrations(config) {
424
431
 
425
432
  function createTestIndexer() {
426
433
  let config = Config.load();
427
- let allEntities = config.allEntities;
434
+ let allEntities = config.userEntities;
428
435
  let entities = {};
429
436
  let entityConfigs = {};
430
437
  allEntities.forEach(entityConfig => {
431
438
  entities[entityConfig.name] = {};
432
439
  entityConfigs[entityConfig.name] = entityConfig;
433
440
  });
434
- let envioAddressesDict = entities[Config.EnvioAddresses.name];
435
- ChainMap.values(config.chainMap).forEach(chainConfig => {
436
- chainConfig.contracts.forEach(contract => {
437
- contract.addresses.forEach(address => {
438
- let entity_id = Config.EnvioAddresses.makeId(chainConfig.id, address);
439
- let entity_chain_id = chainConfig.id;
440
- let entity_contract_name = contract.name;
441
- let entity = {
442
- id: entity_id,
443
- chain_id: entity_chain_id,
444
- registration_block: -1,
445
- registration_log_index: -1,
446
- contract_name: entity_contract_name
447
- };
448
- envioAddressesDict[entity_id] = entity;
449
- });
450
- });
451
- });
441
+ let chainConfigs = ChainMap.values(config.chainMap);
442
+ let contractMapping = config.contractMapping;
443
+ let ecosystem = config.ecosystem.name;
444
+ let addresses = AddressRows.Table.make();
445
+ ChainState.seedConfigAddresses(addresses, chainConfigs, ecosystem, contractMapping);
452
446
  let state = {
453
447
  processInProgress: false,
454
448
  progressBlockByChain: {},
455
449
  entities: entities,
456
450
  entityConfigs: entityConfigs,
451
+ addresses: addresses,
452
+ contractMapping: contractMapping,
457
453
  processChanges: []
458
454
  };
459
455
  let storage = makeInMemoryStorage(state);
@@ -465,16 +461,13 @@ function createTestIndexer() {
465
461
  }
466
462
  let entityOpsDict = {};
467
463
  allEntities.forEach(entityConfig => {
468
- if (entityConfig.name !== Config.EnvioAddresses.name) {
469
- entityOpsDict[entityConfig.name] = {
470
- get: makeEntityGet(state, entityConfig),
471
- getAll: makeEntityGetAll(state, entityConfig),
472
- getWhere: makeEntityGetWhere(state, entityConfig),
473
- getOrThrow: makeEntityGetOrThrow(state, entityConfig),
474
- set: makeEntitySet(state, entityConfig)
475
- };
476
- return;
477
- }
464
+ entityOpsDict[entityConfig.name] = {
465
+ get: makeEntityGet(state, entityConfig),
466
+ getAll: makeEntityGetAll(state, entityConfig),
467
+ getWhere: makeEntityGetWhere(state, entityConfig),
468
+ getOrThrow: makeEntityGetOrThrow(state, entityConfig),
469
+ set: makeEntitySet(state, entityConfig)
470
+ };
478
471
  });
479
472
  let chainIds = [];
480
473
  let chains = Object.create(null);
@@ -512,11 +505,14 @@ function createTestIndexer() {
512
505
  if (state.processInProgress) {
513
506
  Stdlib_JsError.throwWithMessage(`Cannot access ` + contract.name + `.addresses while indexer.process() is running. ` + "Wait for process() to complete before reading contract addresses.");
514
507
  }
515
- return Stdlib_Array.filterMap(Stdlib_Option.getOr(getIndexingAddressesByChain(state)[ChainId.toString(chainConfig.id)], []), ia => {
516
- if (ia.contractName === contract.name) {
517
- return Primitive_option.some(ia.address);
508
+ let contractId = ContractMapping.idOfOrThrow(state.contractMapping, contract.name, undefined);
509
+ return renderRows(AddressRows.Table.rows(state.addresses).filter(row => {
510
+ if (row.chainId === chainConfig.id) {
511
+ return row.contractId === contractId;
512
+ } else {
513
+ return false;
518
514
  }
519
- });
515
+ }), config);
520
516
  }
521
517
  });
522
518
  Object.defineProperty(chainObj, contract.name, {
@@ -586,8 +582,7 @@ function createTestIndexer() {
586
582
  let chainIdStr = param[0];
587
583
  let chains = {};
588
584
  chains[chainIdStr] = processChainConfig;
589
- let indexingAddressesByChain = getIndexingAddressesByChain(state);
590
- let initialState = makeInitialState(config, chains, indexingAddressesByChain);
585
+ let initialState = makeInitialState(config, chains, AddressRows.Table.groupByChain(state.addresses), state.contractMapping);
591
586
  let exitAfterFirstEventBlock = Stdlib_Option.isNone(processChainConfig.endBlock);
592
587
  let resolvedChainDict = {};
593
588
  resolvedChainDict["startBlock"] = processChainConfig.startBlock;
@@ -674,8 +669,8 @@ function createTestIndexer() {
674
669
  }
675
670
 
676
671
  export {
677
- toIndexingAddress,
678
- getIndexingAddressesByChain,
672
+ addressRowsByChain,
673
+ renderRows,
679
674
  rowKey,
680
675
  readChainId,
681
676
  handleLoad,
package/src/Utils.res CHANGED
@@ -199,6 +199,13 @@ module Dict = {
199
199
  }
200
200
  `)
201
201
 
202
+ let clearInPlace: dict<'a> => unit = %raw(`(dict) => {
203
+ for (const key in dict) {
204
+ delete dict[key];
205
+ }
206
+ }
207
+ `)
208
+
202
209
  let unsafeDeleteUndefinedFieldsInPlace: 'a => unit = %raw(`(dict) => {
203
210
  for (var key in dict) {
204
211
  if (dict[key] === undefined) {
package/src/Utils.res.mjs CHANGED
@@ -149,6 +149,12 @@ let deleteInPlace = ((dict, key) => {
149
149
  delete dict[key];
150
150
  });
151
151
 
152
+ let clearInPlace = ((dict) => {
153
+ for (const key in dict) {
154
+ delete dict[key];
155
+ }
156
+ });
157
+
152
158
  let unsafeDeleteUndefinedFieldsInPlace = ((dict) => {
153
159
  for (var key in dict) {
154
160
  if (dict[key] === undefined) {
@@ -175,6 +181,7 @@ let Dict = {
175
181
  size: size,
176
182
  isEmpty: isEmpty,
177
183
  deleteInPlace: deleteInPlace,
184
+ clearInPlace: clearInPlace,
178
185
  unsafeDeleteUndefinedFieldsInPlace: unsafeDeleteUndefinedFieldsInPlace,
179
186
  updateImmutable: updateImmutable,
180
187
  shallowCopy: shallowCopy
@@ -249,7 +256,7 @@ function mergeSorted(f, xs, ys) {
249
256
  return result;
250
257
  }
251
258
 
252
- let clearInPlace = ((arr) => {
259
+ let clearInPlace$1 = ((arr) => {
253
260
  arr.length = 0
254
261
  });
255
262
 
@@ -387,7 +394,7 @@ let $$Array$1 = {
387
394
  unzip: Stdlib_Array.unzip,
388
395
  immutableEmpty: immutableEmpty,
389
396
  mergeSorted: mergeSorted,
390
- clearInPlace: clearInPlace,
397
+ clearInPlace: clearInPlace$1,
391
398
  setIndexImmutable: setIndexImmutable,
392
399
  transposeResults: transposeResults,
393
400
  includes: includes,
package/src/Writing.res CHANGED
@@ -120,6 +120,7 @@ let runOneWrite = async (state: IndexerState.t) => {
120
120
  }
121
121
  })
122
122
  let updatedEffectsCache = snapshotEffects(state, ~cache)
123
+ let registeredAddresses = batch.registeredAddresses
123
124
 
124
125
  let writtenEntityNames = Utils.Set.make()
125
126
  updatedEntities->Array.forEach(({entityConfig}) =>
@@ -145,6 +146,7 @@ let runOneWrite = async (state: IndexerState.t) => {
145
146
  ~config,
146
147
  ~allEntities=persistence.allEntities,
147
148
  ~updatedEntities,
149
+ ~registeredAddresses,
148
150
  ~updatedEffectsCache,
149
151
  ~chainMetaData,
150
152
  ~onWrite=(~storage, ~timeSeconds) =>
@@ -118,13 +118,14 @@ async function runOneWrite(state) {
118
118
  }
119
119
  });
120
120
  let updatedEffectsCache = snapshotEffects(state, cache);
121
+ let registeredAddresses = batch.registeredAddresses;
121
122
  let writtenEntityNames = new Set();
122
123
  updatedEntities.forEach(param => {
123
124
  writtenEntityNames.add(param.entityConfig.name);
124
125
  });
125
126
  let pruneTargets = PruneStaleHistory.select(state, writtenEntityNames, Stdlib_Option.isSome(rollback));
126
127
  await Promise.all([
127
- persistence.storage.writeBatch(batch, rollback, batch.isInReorgThreshold, config, persistence.allEntities, updatedEffectsCache, updatedEntities, chainMetaData, (storage, timeSeconds) => IndexerState.recordStorageWrite(state, storage, timeSeconds)),
128
+ persistence.storage.writeBatch(batch, rollback, batch.isInReorgThreshold, config, persistence.allEntities, updatedEffectsCache, updatedEntities, registeredAddresses, chainMetaData, (storage, timeSeconds) => IndexerState.recordStorageWrite(state, storage, timeSeconds)),
128
129
  PruneStaleHistory.runConcurrent(state, pruneTargets)
129
130
  ]);
130
131
  IndexerState.markCommitted(state, upToCheckpointId);
@@ -60,6 +60,12 @@ let getClickHouseFieldType = (
60
60
  }
61
61
  | Uint32 => "UInt32"
62
62
  | UInt52 => "UInt64"
63
+ // Internal-only column types, never on an entity the sink mirrors.
64
+ | SmallInt
65
+ | Bytea =>
66
+ JsError.throwWithMessage(
67
+ "ClickHouse doesn't support the internal SmallInt and Bytea column types",
68
+ )
63
69
  | UInt64 => "UInt64"
64
70
  | Serial => "Int32"
65
71
  | BigSerial => "Int64"
@@ -357,6 +363,17 @@ let setUpdatesOrThrow = async (
357
363
  // The '{cluster}' macro resolves to each node's configured cluster name.
358
364
  let onClusterClause = (~onCluster: bool) => onCluster ? ` ON CLUSTER '{cluster}'` : ""
359
365
 
366
+ // ReplicatedMergeTree drops an insert whose block hash is already in Keeper, and
367
+ // mutations don't clear those hashes. Crash recovery trims the history tail past
368
+ // the committed Postgres checkpoint with ALTER ... DELETE and then replays it, so
369
+ // an identical replayed block would be discarded while still reporting success —
370
+ // a permanent gap that nothing surfaces. Trim-then-replay is what makes recovery
371
+ // correct here, and the duplicates dedup would have caught are already collapsed
372
+ // by the entity view's `LIMIT 1 BY`. Plain MergeTree goes by
373
+ // non_replicated_deduplication_window (0 by default), so it needs no clause.
374
+ let replicatedTableSettingsClause = (~replicated: bool) =>
375
+ replicated ? "\nSETTINGS replicated_deduplication_window = 0" : ""
376
+
360
377
  // Strip both engine arguments `(...)` and a trailing `SETTINGS ...` clause to
361
378
  // get the bare engine name, e.g. `Replicated('/p','{shard}','{replica}') SETTINGS x=1`
362
379
  // and `Replicated SETTINGS x=1` both yield `Replicated`.
@@ -395,9 +412,9 @@ let makeCreateHistoryTableQuery = (
395
412
  }
396
413
  })
397
414
 
398
- let (partitionBy, orderBy, ttl) = switch entityConfig.storage.clickhouseOptions {
399
- | Some(options) => (options.partitionBy, options.orderBy, options.ttl)
400
- | None => (None, None, None)
415
+ let (partitionBy, orderBy, ttl, skippingIndexes) = switch entityConfig.storage.clickhouseOptions {
416
+ | Some(options) => (options.partitionBy, options.orderBy, options.ttl, options.skippingIndexes)
417
+ | None => (None, None, None, None)
401
418
  }
402
419
 
403
420
  // Schema field name -> ClickHouse column name, so @storage(clickhouse: {...})
@@ -461,6 +478,22 @@ let makeCreateHistoryTableQuery = (
461
478
  | None => ""
462
479
  }
463
480
 
481
+ // Data skipping indexes live inside the column list, after the last column.
482
+ // GRANULARITY is omitted when unset, leaving ClickHouse's default of 1.
483
+ let skippingIndexDefinitions = switch skippingIndexes {
484
+ | Some(skippingIndexes) =>
485
+ skippingIndexes
486
+ ->Array.map(index => {
487
+ let granularityClause = switch index.granularity {
488
+ | Some(granularity) => ` GRANULARITY ${granularity->Int.toString}`
489
+ | None => ""
490
+ }
491
+ `,\n INDEX \`${index.name}\` ${index.expr->resolveExpressionColumns} TYPE ${index.type_}${granularityClause}`
492
+ })
493
+ ->Array.joinUnsafe("")
494
+ | None => ""
495
+ }
496
+
464
497
  `CREATE TABLE IF NOT EXISTS ${database}.\`${EntityHistory.historyTableName(
465
498
  ~entityName=entityConfig.name,
466
499
  ~entityIndex=entityConfig.index,
@@ -475,10 +508,10 @@ let makeCreateHistoryTableQuery = (
475
508
  ~fieldType=Enum({config: EntityHistory.RowAction.config->Table.fromGenericEnumConfig}),
476
509
  ~isNullable=false,
477
510
  ~isArray=false,
478
- )}
511
+ )}${skippingIndexDefinitions}
479
512
  )
480
513
  ENGINE = ${tableEngine}${partitionByClause}
481
- ORDER BY (${orderByColumns})${ttlClause}`
514
+ ORDER BY (${orderByColumns})${ttlClause}${replicatedTableSettingsClause(~replicated)}`
482
515
  }
483
516
 
484
517
  // Generate CREATE TABLE query for checkpoints
@@ -522,7 +555,7 @@ let makeCreateCheckpointsTableQuery = (
522
555
  )}
523
556
  )
524
557
  ENGINE = ${tableEngine}
525
- ORDER BY (${idField})`
558
+ ORDER BY (${idField})${replicatedTableSettingsClause(~replicated)}`
526
559
  }
527
560
 
528
561
  // Generate CREATE VIEW query for entity current state
@@ -26,6 +26,10 @@ function getClickHouseFieldType(fieldType, isNullable, isArray, chainIdModeOpt)
26
26
  case "Uint32" :
27
27
  baseType = "UInt32";
28
28
  break;
29
+ case "SmallInt" :
30
+ case "Bytea" :
31
+ baseType = Stdlib_JsError.throwWithMessage("ClickHouse doesn't support the internal SmallInt and Bytea column types");
32
+ break;
29
33
  case "UInt52" :
30
34
  case "UInt64" :
31
35
  baseType = "UInt64";
@@ -273,6 +277,14 @@ function onClusterClause(onCluster) {
273
277
  }
274
278
  }
275
279
 
280
+ function replicatedTableSettingsClause(replicated) {
281
+ if (replicated) {
282
+ return "\nSETTINGS replicated_deduplication_window = 0";
283
+ } else {
284
+ return "";
285
+ }
286
+ }
287
+
276
288
  function databaseEngineName(engineSpec) {
277
289
  return engineSpec.trim().split("(")[0].split(" ")[0].trim();
278
290
  }
@@ -295,12 +307,15 @@ function makeCreateHistoryTableQuery(entityConfig, database, replicatedOpt, onCl
295
307
  let match = options !== undefined ? [
296
308
  options.partitionBy,
297
309
  options.orderBy,
298
- options.ttl
310
+ options.ttl,
311
+ options.skippingIndexes
299
312
  ] : [
313
+ undefined,
300
314
  undefined,
301
315
  undefined,
302
316
  undefined
303
317
  ];
318
+ let skippingIndexes = match[3];
304
319
  let ttl = match[2];
305
320
  let orderBy = match[1];
306
321
  let partitionBy = match[0];
@@ -336,6 +351,11 @@ function makeCreateHistoryTableQuery(entityConfig, database, replicatedOpt, onCl
336
351
  });
337
352
  let partitionByClause = partitionBy !== undefined ? `\nPARTITION BY ` + resolveExpressionColumns(partitionBy) : "";
338
353
  let ttlClause = ttl !== undefined ? `\nTTL ` + resolveExpressionColumns(ttl) : "";
354
+ let skippingIndexDefinitions = skippingIndexes !== undefined ? skippingIndexes.map(index => {
355
+ let granularity = index.granularity;
356
+ let granularityClause = granularity !== undefined ? ` GRANULARITY ` + granularity.toString() : "";
357
+ return `,\n INDEX \`` + index.name + `\` ` + resolveExpressionColumns(index.expr) + ` TYPE ` + index.type + granularityClause;
358
+ }).join("") : "";
339
359
  return `CREATE TABLE IF NOT EXISTS ` + database + `.\`` + EntityHistory.historyTableName(entityConfig.name, entityConfig.index) + `\`` + (
340
360
  onCluster ? ` ON CLUSTER '{cluster}'` : ""
341
361
  ) + ` (
@@ -344,10 +364,12 @@ function makeCreateHistoryTableQuery(entityConfig, database, replicatedOpt, onCl
344
364
  \`` + EntityHistory.changeFieldName + `\` ` + getClickHouseFieldType({
345
365
  type: "Enum",
346
366
  config: EntityHistory.RowAction.config
347
- }, false, false, undefined) + `
367
+ }, false, false, undefined) + skippingIndexDefinitions + `
348
368
  )
349
369
  ENGINE = ` + tableEngine + partitionByClause + `
350
- ORDER BY (` + orderByColumns + `)` + ttlClause;
370
+ ORDER BY (` + orderByColumns + `)` + ttlClause + (
371
+ replicated ? "\nSETTINGS replicated_deduplication_window = 0" : ""
372
+ );
351
373
  }
352
374
 
353
375
  function makeCreateCheckpointsTableQuery(database, replicatedOpt, onClusterOpt, chainIdModeOpt) {
@@ -365,7 +387,9 @@ function makeCreateCheckpointsTableQuery(database, replicatedOpt, onClusterOpt,
365
387
  \`` + "events_processed" + `\` ` + getClickHouseFieldType("UInt64", false, false, undefined) + `
366
388
  )
367
389
  ENGINE = ` + tableEngine + `
368
- ORDER BY (` + "id" + `)`;
390
+ ORDER BY (` + "id" + `)` + (
391
+ replicated ? "\nSETTINGS replicated_deduplication_window = 0" : ""
392
+ );
369
393
  }
370
394
 
371
395
  function makeCreateViewQuery(entityConfig, database, onClusterOpt) {
@@ -505,6 +529,7 @@ export {
505
529
  setCheckpointsOrThrow,
506
530
  setUpdatesOrThrow,
507
531
  onClusterClause,
532
+ replicatedTableSettingsClause,
508
533
  databaseEngineName,
509
534
  makeCreateHistoryTableQuery,
510
535
  makeCreateCheckpointsTableQuery,
@@ -66,6 +66,15 @@ module Process = {
66
66
  external getActiveResourcesInfo: unit => array<string> = "getActiveResourcesInfo"
67
67
  }
68
68
 
69
+ module Buffer = {
70
+ type t
71
+ @val @scope("Buffer") external concat: array<t> => t = "concat"
72
+ @val @scope("Buffer") external alloc: int => t = "alloc"
73
+ @get external length: t => int = "length"
74
+ @send external toBase64: (t, @as("base64") _) => string = "toString"
75
+ let empty = alloc(0)
76
+ }
77
+
69
78
  module V8 = {
70
79
  type heapSpaceStatistics = {
71
80
  @as("space_name") spaceName: string,
@@ -18,6 +18,12 @@ let Util$1 = {
18
18
 
19
19
  let Process = {};
20
20
 
21
+ let empty = globalThis.Buffer.alloc(0);
22
+
23
+ let Buffer = {
24
+ empty: empty
25
+ };
26
+
21
27
  let V8 = {};
22
28
 
23
29
  let PerfHooks = {};
@@ -49,6 +55,7 @@ let Fs = {
49
55
  export {
50
56
  Util$1 as Util,
51
57
  Process,
58
+ Buffer,
52
59
  V8,
53
60
  PerfHooks,
54
61
  ChildProcess,
@@ -59,4 +66,4 @@ export {
59
66
  WorkerThreads,
60
67
  Fs,
61
68
  }
62
- /* url Not a pure module */
69
+ /* empty Not a pure module */
@@ -99,6 +99,13 @@ external makeSql: (~config: poolConfig) => sql = "default"
99
99
  // @send @variadic
100
100
  // external sql: array<string> => (sql, array<string>) => int = "sql"
101
101
 
102
+ // postgres.js infers a Buffer array's type from its first element, so a bytea
103
+ // column's array parameter comes out typed `bytea` and the server refuses the
104
+ // cast. Naming the array type explicitly is what makes it bind. 1001 is
105
+ // Postgres' `bytea[]` OID.
106
+ let byteaArrayOid = 1001
107
+ @send external typed: (sql, 'a, int) => unknown = "typed"
108
+
102
109
  @send external unsafe: (sql, string) => promise<'a> = "unsafe"
103
110
  @send external unpreparedUnsafe: (sql, string, unknown) => promise<'a> = "unsafe"
104
111
  @send
@@ -107,8 +114,10 @@ external preparedUnsafe: (sql, string, unknown, @as(json`{prepare: true}`) _) =>
107
114
 
108
115
  @unboxed
109
116
  type columnType =
117
+ | @as("SMALLINT") SmallInt
110
118
  | @as("INTEGER") Integer
111
119
  | @as("BIGINT") BigInt
120
+ | @as("BYTEA") Bytea
112
121
  | @as("BOOLEAN") Boolean
113
122
  | @as("NUMERIC") Numeric
114
123
  | @as("DOUBLE PRECISION") DoublePrecision