envio 3.7.0-svm-alpha.1 → 3.8.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 (45) hide show
  1. package/index.d.ts +331 -142
  2. package/package.json +6 -6
  3. package/src/ChainState.res +9 -1
  4. package/src/ChainState.res.mjs +6 -1
  5. package/src/Config.res +19 -10
  6. package/src/Config.res.mjs +17 -7
  7. package/src/Core.res +4 -0
  8. package/src/Envio.res +54 -76
  9. package/src/EventConfigBuilder.res +159 -30
  10. package/src/EventConfigBuilder.res.mjs +120 -24
  11. package/src/HandlerRegister.res +20 -11
  12. package/src/HandlerRegister.res.mjs +22 -5
  13. package/src/Hasura.res +30 -22
  14. package/src/Hasura.res.mjs +11 -5
  15. package/src/InMemoryStore.res +1 -6
  16. package/src/InMemoryStore.res.mjs +1 -3
  17. package/src/Internal.res +33 -13
  18. package/src/Internal.res.mjs +12 -16
  19. package/src/Main.res +2 -5
  20. package/src/MemoryStorage.res +40 -8
  21. package/src/MemoryStorage.res.mjs +38 -16
  22. package/src/Persistence.res +4 -2
  23. package/src/PgStorage.res +6 -1
  24. package/src/PgStorage.res.mjs +1 -1
  25. package/src/SimulateItems.res +261 -9
  26. package/src/SimulateItems.res.mjs +218 -47
  27. package/src/TestIndexer.res +26 -10
  28. package/src/TestIndexer.res.mjs +34 -12
  29. package/src/bindings/ClickHouse.res +49 -17
  30. package/src/bindings/ClickHouse.res.mjs +35 -14
  31. package/src/db/InternalTable.res +11 -0
  32. package/src/db/InternalTable.res.mjs +7 -0
  33. package/src/sources/EvmHyperSyncSource.res +8 -3
  34. package/src/sources/EvmHyperSyncSource.res.mjs +1 -1
  35. package/src/sources/SimulateSource.res +8 -4
  36. package/src/sources/SimulateSource.res.mjs +7 -3
  37. package/src/sources/Svm.res +34 -7
  38. package/src/sources/Svm.res.mjs +32 -4
  39. package/src/sources/SvmHyperSyncClient.res +10 -12
  40. package/src/sources/SvmHyperSyncClient.res.mjs +3 -2
  41. package/src/sources/SvmHyperSyncSource.res +96 -35
  42. package/src/sources/SvmHyperSyncSource.res.mjs +78 -38
  43. package/src/sources/TransactionStore.res +38 -0
  44. package/src/sources/TransactionStore.res.mjs +5 -0
  45. package/svm.schema.json +27 -78
@@ -5,11 +5,14 @@ import * as Config from "./Config.res.mjs";
5
5
  import * as Address from "./Address.res.mjs";
6
6
  import * as ChainId from "./ChainId.res.mjs";
7
7
  import * as ChainMap from "./ChainMap.res.mjs";
8
+ import * as BlockStore from "./sources/BlockStore.res.mjs";
8
9
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
9
10
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
10
11
  import * as HandlerRegister from "./HandlerRegister.res.mjs";
11
12
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
12
13
  import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
14
+ import * as TransactionStore from "./sources/TransactionStore.res.mjs";
15
+ import * as SvmHyperSyncSource from "./sources/SvmHyperSyncSource.res.mjs";
13
16
 
14
17
  let evmSimulateBlockSchema = S$RescriptSchema.schema(s => ({
15
18
  number: s.m(S$RescriptSchema.Option.getOr(S$RescriptSchema.$$null(S$RescriptSchema.int), 0)),
@@ -168,6 +171,16 @@ function deriveSrcAddress(providedSrcAddress, eventConfig, chainConfig, config)
168
171
  }
169
172
  }
170
173
 
174
+ function liveRegistrationsFor(config, chainId, eventConfig) {
175
+ return HandlerRegister.getSimulateOnEventRegistrations(config, chainId, eventConfig).filter(reg => {
176
+ if (Stdlib_Option.isSome(reg.handler) || Stdlib_Option.isSome(reg.contractRegister)) {
177
+ return !HandlerRegister.isDroppedByWhere(config, reg);
178
+ } else {
179
+ return false;
180
+ }
181
+ });
182
+ }
183
+
171
184
  function parse(simulateItems, config, chainConfig, onEventRegistrations) {
172
185
  let chainId = chainConfig.id;
173
186
  let startBlock = chainConfig.startBlock;
@@ -178,21 +191,161 @@ function parse(simulateItems, config, chainConfig, onEventRegistrations) {
178
191
  contents: 0
179
192
  };
180
193
  let items = [];
194
+ let svmTxs = [];
195
+ let svmActivities = [];
196
+ let svmBlocks = [];
181
197
  let seenCoordinates = {};
182
198
  simulateItems.forEach((rawJson, itemIndex) => {
183
- let match = rawJson.contract;
184
- let match$1 = rawJson.event;
185
- if (match === undefined) {
186
- return Stdlib_JsError.throwWithMessage(`simulate: Invalid item. Each item must have "contract" and "event" fields.`);
187
- }
188
- if (match$1 === undefined) {
189
- return Stdlib_JsError.throwWithMessage(`simulate: Invalid item. Each item must have "contract" and "event" fields.`);
199
+ let match = config.ecosystem.name;
200
+ let match$1 = rawJson.program;
201
+ let match$2 = rawJson.instruction;
202
+ switch (match) {
203
+ case "evm" :
204
+ case "fuel" :
205
+ break;
206
+ case "svm" :
207
+ if (match$1 === undefined) {
208
+ return Stdlib_JsError.throwWithMessage(`simulate: Invalid item. Each item must have "program" and "instruction" fields.`);
209
+ }
210
+ if (match$2 === undefined) {
211
+ return Stdlib_JsError.throwWithMessage(`simulate: Invalid item. Each item must have "program" and "instruction" fields.`);
212
+ }
213
+ let ec = findEventConfig(config, match$1, match$2);
214
+ let eventConfig = ec !== undefined ? ec : Stdlib_JsError.throwWithMessage(`simulate: Instruction "` + match$2 + `" not found on program "` + match$1 + `". Check that the program and instruction names match your config.yaml.`);
215
+ let blockJson = rawJson.block;
216
+ let s = rawJson.slot;
217
+ let slot;
218
+ if (s !== undefined) {
219
+ slot = s;
220
+ } else if (blockJson == null) {
221
+ slot = currentBlock.contents;
222
+ } else {
223
+ let v = blockJson["slot"];
224
+ slot = v !== undefined ? Stdlib_Option.getOr((v == null) ? undefined : Primitive_option.some(v), currentBlock.contents) : currentBlock.contents;
225
+ }
226
+ currentBlock.contents = slot;
227
+ let tx = rawJson.transaction;
228
+ let transaction = tx !== undefined ? Primitive_option.valFromOption(tx) : ({});
229
+ let transactionIndex = Stdlib_Option.getOr(transaction.transactionIndex, 0);
230
+ let path = Stdlib_Option.getOr(rawJson.path, [0]);
231
+ let programId = Stdlib_Option.getOr(rawJson.programId, eventConfig.programId);
232
+ let args = rawJson.accountArguments;
233
+ let accountArguments;
234
+ if (args !== undefined) {
235
+ accountArguments = args;
236
+ } else {
237
+ let named = rawJson.accounts;
238
+ accountArguments = named !== undefined ? eventConfig.accounts.map(name => {
239
+ let match = named[name];
240
+ if (match !== undefined) {
241
+ return match.address;
242
+ } else {
243
+ return "";
244
+ }
245
+ }) : [];
246
+ }
247
+ let data = Stdlib_Option.getOr(rawJson.data, Stdlib_Option.getOr(eventConfig.discriminator, "0x"));
248
+ let decoded = Stdlib_Option.map(rawJson.args, args => ({
249
+ name: match$2,
250
+ argsJson: JSON.stringify(args),
251
+ accountsJson: "{}",
252
+ extraAccounts: []
253
+ }));
254
+ let logs = Stdlib_Option.map(rawJson.logs, logs => logs.map(log => ({
255
+ kind: log.kind,
256
+ message: log.message
257
+ })));
258
+ let liveRegistrations = liveRegistrationsFor(config, chainId, eventConfig);
259
+ if (Utils.$$Array.isEmpty(liveRegistrations)) {
260
+ Stdlib_JsError.throwWithMessage(`simulate: no handler runs for instruction "` + match$2 + `" on program "` + match$1 + `". Register a handler with indexer.onInstruction before simulating it.`);
261
+ }
262
+ svmTxs.push({
263
+ slot: slot,
264
+ transactionIndex: transactionIndex,
265
+ signature: transaction.signature,
266
+ allSignatures: transaction.allSignatures,
267
+ feePayer: transaction.feePayer,
268
+ success: transaction.success,
269
+ err: transaction.err,
270
+ fee: transaction.fee,
271
+ computeUnitsConsumed: transaction.computeUnitsConsumed,
272
+ accountKeys: transaction.accountKeys,
273
+ recentBlockhash: transaction.recentBlockhash,
274
+ version: transaction.version
275
+ });
276
+ let rows = transaction.accountActivities;
277
+ if (rows !== undefined) {
278
+ rows.forEach(activity => {
279
+ svmActivities.push({
280
+ slot: slot,
281
+ transactionIndex: transactionIndex,
282
+ account: activity.address,
283
+ accountIndex: activity.transactionAccountIndex,
284
+ isSigner: activity.isSigner,
285
+ isWritable: activity.isWritable,
286
+ preBalance: Stdlib_Option.flatMap(activity.lamports, l => l.pre),
287
+ postBalance: Stdlib_Option.flatMap(activity.lamports, l => l.post),
288
+ mint: Stdlib_Option.flatMap(activity.token, t => t.mint),
289
+ owner: Stdlib_Option.flatMap(activity.token, t => t.owner),
290
+ decimals: Stdlib_Option.flatMap(activity.token, t => t.decimals),
291
+ preAmount: Stdlib_Option.flatMap(activity.token, t => t.preAmount),
292
+ postAmount: Stdlib_Option.flatMap(activity.token, t => t.postAmount)
293
+ });
294
+ });
295
+ }
296
+ let block = rawJson.block;
297
+ let blockTime = block !== undefined ? block.time : undefined;
298
+ let block$1 = rawJson.block;
299
+ let blockHash = block$1 !== undefined ? block$1.hash : undefined;
300
+ svmBlocks.push({
301
+ blockNumber: slot,
302
+ blockHash: blockHash,
303
+ blockTimestamp: blockTime
304
+ });
305
+ liveRegistrations.forEach(reg => {
306
+ let onEventRegistrationIndex = onEventRegistrations.length;
307
+ let newrecord = {...reg};
308
+ newrecord.index = onEventRegistrationIndex;
309
+ onEventRegistrations.push(newrecord);
310
+ let payload = SvmHyperSyncSource.toSvmInstruction({
311
+ onEventRegistrationIndex: onEventRegistrationIndex,
312
+ slot: slot,
313
+ transactionIndex: transactionIndex,
314
+ path: path,
315
+ programId: programId,
316
+ accounts: accountArguments,
317
+ data: data,
318
+ isInner: Stdlib_Option.getOr(rawJson.isInner, false),
319
+ decoded: decoded,
320
+ logs: logs
321
+ }, match$1, match$2, eventConfig, newrecord.fieldSelection);
322
+ payload["srcAddress"] = programId;
323
+ items.push({
324
+ kind: 0,
325
+ onEventRegistration: newrecord,
326
+ chainId: chainId,
327
+ blockNumber: slot,
328
+ logIndex: transactionIndex,
329
+ orderPath: path,
330
+ transactionIndex: transactionIndex,
331
+ payload: payload
332
+ });
333
+ });
334
+ return;
190
335
  }
191
- let ec = findEventConfig(config, match, match$1);
192
- let eventConfig = ec !== undefined ? ec : Stdlib_JsError.throwWithMessage(`simulate: Event "` + match$1 + `" not found on contract "` + match + `". Check that the contract and event names match your config.yaml.`);
336
+ let match$3 = rawJson.contract;
337
+ let match$4 = rawJson.event;
338
+ let match$5 = match$3 !== undefined && match$4 !== undefined ? [
339
+ match$3,
340
+ match$4
341
+ ] : Stdlib_JsError.throwWithMessage(`simulate: Invalid item. Each item must have "contract" and "event" fields.`);
342
+ let eventName = match$5[1];
343
+ let contractName = match$5[0];
344
+ let ec$1 = findEventConfig(config, contractName, eventName);
345
+ let eventConfig$1 = ec$1 !== undefined ? ec$1 : Stdlib_JsError.throwWithMessage(`simulate: Event "` + eventName + `" not found on contract "` + contractName + `". Check that the contract and event names match your config.yaml.`);
193
346
  let json = rawJson.params;
194
347
  let paramsJson = json !== undefined ? json : ({});
195
- let params = S$RescriptSchema.convertOrThrow(paramsJson, eventConfig.simulateParamsSchema);
348
+ let params = S$RescriptSchema.convertOrThrow(paramsJson, eventConfig$1.simulateParamsSchema);
196
349
  let li = rawJson.logIndex;
197
350
  let logIndex;
198
351
  if (li !== undefined) {
@@ -205,45 +358,45 @@ function parse(simulateItems, config, chainConfig, onEventRegistrations) {
205
358
  currentLogIndex.contents = li$1 + 1 | 0;
206
359
  logIndex = li$1;
207
360
  }
208
- let srcAddress = deriveSrcAddress(rawJson.srcAddress, eventConfig, chainConfig, config);
209
- let blockJson = rawJson.block;
210
- let blockJson$1 = (blockJson == null) ? undefined : Primitive_option.some(blockJson);
361
+ let srcAddress = deriveSrcAddress(rawJson.srcAddress, eventConfig$1, chainConfig, config);
362
+ let blockJson$1 = rawJson.block;
363
+ let blockJson$2 = (blockJson$1 == null) ? undefined : Primitive_option.some(blockJson$1);
211
364
  let transactionJson = rawJson.transaction;
212
365
  let transactionJson$1 = (transactionJson == null) ? undefined : Primitive_option.some(transactionJson);
213
- let match$2 = config.ecosystem.name;
214
- let match$3;
215
- switch (match$2) {
366
+ let match$6 = config.ecosystem.name;
367
+ let match$7;
368
+ switch (match$6) {
216
369
  case "evm" :
217
- let block = parseEvmSimulateBlock(currentBlock.contents, blockJson$1);
218
- match$3 = [
219
- block,
220
- block.number
370
+ let block$2 = parseEvmSimulateBlock(currentBlock.contents, blockJson$2);
371
+ match$7 = [
372
+ block$2,
373
+ block$2.number
221
374
  ];
222
375
  break;
223
376
  case "fuel" :
224
- let block$1 = parseFuelSimulateBlock(currentBlock.contents, blockJson$1);
225
- match$3 = [
226
- block$1,
227
- block$1.height
377
+ let block$3 = parseFuelSimulateBlock(currentBlock.contents, blockJson$2);
378
+ match$7 = [
379
+ block$3,
380
+ block$3.height
228
381
  ];
229
382
  break;
230
383
  case "svm" :
231
- match$3 = Stdlib_JsError.throwWithMessage("simulate is not supported for SVM ecosystem");
384
+ match$7 = Stdlib_JsError.throwWithMessage("simulate is not supported for SVM ecosystem");
232
385
  break;
233
386
  }
234
- let blockNumber = match$3[1];
235
- let block$2 = match$3[0];
236
- let match$4 = config.ecosystem.name;
237
- let transaction;
238
- switch (match$4) {
387
+ let blockNumber = match$7[1];
388
+ let block$4 = match$7[0];
389
+ let match$8 = config.ecosystem.name;
390
+ let transaction$1;
391
+ switch (match$8) {
239
392
  case "evm" :
240
- transaction = parseEvmSimulateTransaction(transactionJson$1);
393
+ transaction$1 = parseEvmSimulateTransaction(transactionJson$1);
241
394
  break;
242
395
  case "fuel" :
243
- transaction = parseFuelSimulateTransaction(transactionJson$1);
396
+ transaction$1 = parseFuelSimulateTransaction(transactionJson$1);
244
397
  break;
245
398
  case "svm" :
246
- transaction = Stdlib_JsError.throwWithMessage("simulate is not supported for SVM ecosystem");
399
+ transaction$1 = Stdlib_JsError.throwWithMessage("simulate is not supported for SVM ecosystem");
247
400
  break;
248
401
  }
249
402
  currentBlock.contents = blockNumber;
@@ -254,20 +407,20 @@ function parse(simulateItems, config, chainConfig, onEventRegistrations) {
254
407
  } else {
255
408
  seenCoordinates[coordinate] = itemIndex;
256
409
  }
257
- let liveRegistrations = HandlerRegister.getSimulateOnEventRegistrations(config, chainId, eventConfig).filter(reg => {
410
+ let liveRegistrations$1 = HandlerRegister.getSimulateOnEventRegistrations(config, chainId, eventConfig$1).filter(reg => {
258
411
  if (Stdlib_Option.isSome(reg.handler) || Stdlib_Option.isSome(reg.contractRegister)) {
259
412
  return !HandlerRegister.isDroppedByWhere(config, reg);
260
413
  } else {
261
414
  return false;
262
415
  }
263
416
  });
264
- if (Utils.$$Array.isEmpty(liveRegistrations)) {
265
- let match$5 = ChainMap.values(config.chainMap).length;
266
- Stdlib_JsError.throwWithMessage(`simulate: no handler runs for event "` + match$1 + `" on contract "` + match + `"` + (
267
- match$5 !== 1 ? ` on chain ` + ChainId.toString(chainId) : ""
417
+ if (Utils.$$Array.isEmpty(liveRegistrations$1)) {
418
+ let match$9 = ChainMap.values(config.chainMap).length;
419
+ Stdlib_JsError.throwWithMessage(`simulate: no handler runs for event "` + eventName + `" on contract "` + contractName + `"` + (
420
+ match$9 !== 1 ? ` on chain ` + ChainId.toString(chainId) : ""
268
421
  ) + `. Register a handler with indexer.onEvent (and check any \`where\` filter isn't excluding this chain) before simulating it.`);
269
422
  }
270
- liveRegistrations.forEach(reg => {
423
+ liveRegistrations$1.forEach(reg => {
271
424
  let onEventRegistrationIndex = onEventRegistrations.length;
272
425
  let newrecord = {...reg};
273
426
  newrecord.index = onEventRegistrationIndex;
@@ -280,19 +433,34 @@ function parse(simulateItems, config, chainConfig, onEventRegistrations) {
280
433
  logIndex: logIndex,
281
434
  transactionIndex: 0,
282
435
  payload: {
283
- contractName: eventConfig.contractName,
284
- eventName: eventConfig.name,
436
+ contractName: eventConfig$1.contractName,
437
+ eventName: eventConfig$1.name,
285
438
  params: params,
286
439
  chainId: chainId,
287
440
  srcAddress: srcAddress,
288
441
  logIndex: logIndex,
289
- transaction: Primitive_option.some(transaction),
290
- block: Primitive_option.some(block$2)
442
+ transaction: Primitive_option.some(transaction$1),
443
+ block: Primitive_option.some(block$4)
291
444
  }
292
445
  });
293
446
  });
294
447
  });
295
- return items;
448
+ let match = config.ecosystem.name;
449
+ switch (match) {
450
+ case "evm" :
451
+ case "fuel" :
452
+ return {
453
+ items: items,
454
+ transactionStore: undefined,
455
+ blockStore: undefined
456
+ };
457
+ case "svm" :
458
+ return {
459
+ items: items,
460
+ transactionStore: Primitive_option.some(TransactionStore.fromSvmJs(svmTxs, svmActivities)),
461
+ blockStore: Primitive_option.some(BlockStore.fromJs(svmBlocks, "svm", false))
462
+ };
463
+ }
296
464
  }
297
465
 
298
466
  function patchConfig(config, processConfig, registrationsByChainId) {
@@ -329,12 +497,14 @@ function patchConfig(config, processConfig, registrationsByChainId) {
329
497
  let newrecord = {...chainConfig};
330
498
  newrecord.endBlock = endBlock;
331
499
  newrecord.startBlock = startBlock;
332
- let items = parse(simulateRaw, config, newrecord, chainRegistrations.onEventRegistrations);
500
+ let match = parse(simulateRaw, config, newrecord, chainRegistrations.onEventRegistrations);
333
501
  let newrecord$1 = {...newrecord};
334
502
  newrecord$1.sourceConfig = {
335
503
  TAG: "SimulateSourceConfig",
336
- items: items,
337
- endBlock: endBlock
504
+ items: match.items,
505
+ endBlock: endBlock,
506
+ transactionStore: match.transactionStore,
507
+ blockStore: match.blockStore
338
508
  };
339
509
  return newrecord$1;
340
510
  });
@@ -356,6 +526,7 @@ export {
356
526
  dummySrcAddress,
357
527
  firstContractAddress,
358
528
  deriveSrcAddress,
529
+ liveRegistrationsFor,
359
530
  parse,
360
531
  patchConfig,
361
532
  }
@@ -10,6 +10,12 @@ type fuelChainConfig = {
10
10
  simulate?: array<Envio.fuelSimulateItem>,
11
11
  }
12
12
 
13
+ type svmChainConfig = {
14
+ startBlock?: int,
15
+ endBlock?: int,
16
+ simulate?: array<Envio.svmSimulateItem>,
17
+ }
18
+
13
19
  // Internal type used for block range validation and state management
14
20
  type chainConfig = {
15
21
  startBlock: int,
@@ -314,22 +320,32 @@ let getSimulateEndBlock = (
314
320
  ~startBlock: int,
315
321
  ): int => {
316
322
  let maxBlock = ref(startBlock)
323
+ let blockNumberKey = switch config.ecosystem.name {
324
+ | Svm => "slot"
325
+ | _ => config.ecosystem.blockNumberName
326
+ }
327
+ let bump = (n: option<int>) =>
328
+ switch n {
329
+ | Some(v) if v > maxBlock.contents => maxBlock := v
330
+ | _ => ()
331
+ }
332
+ let getInt = (d: dict<JSON.t>, key) =>
333
+ d
334
+ ->Dict.get(key)
335
+ ->Option.flatMap(v => v->(Utils.magic: JSON.t => Nullable.t<int>)->Nullable.toOption)
317
336
  simulateItems->Array.forEach(rawJson => {
337
+ let itemDict = rawJson->(Utils.magic: JSON.t => dict<JSON.t>)
338
+ // SVM items carry the slot at the top level (`block.slot` is the override).
339
+ switch config.ecosystem.name {
340
+ | Svm => itemDict->getInt("slot")->bump
341
+ | _ => ()
342
+ }
318
343
  let blockJson: option<JSON.t> =
319
344
  (rawJson->(Utils.magic: JSON.t => {..}))["block"]
320
345
  ->(Utils.magic: 'a => Nullable.t<JSON.t>)
321
346
  ->Nullable.toOption
322
347
  switch blockJson {
323
- | Some(bj) =>
324
- let blockDict = bj->(Utils.magic: JSON.t => dict<JSON.t>)
325
- let n: option<int> =
326
- blockDict
327
- ->Dict.get(config.ecosystem.blockNumberName)
328
- ->Option.flatMap(v => v->(Utils.magic: JSON.t => Nullable.t<int>)->Nullable.toOption)
329
- switch n {
330
- | Some(v) if v > maxBlock.contents => maxBlock := v
331
- | _ => ()
332
- }
348
+ | Some(bj) => bj->(Utils.magic: JSON.t => dict<JSON.t>)->getInt(blockNumberKey)->bump
333
349
  | None => ()
334
350
  }
335
351
  })
@@ -230,22 +230,44 @@ function getSimulateEndBlock(simulateItems, config, startBlock) {
230
230
  let maxBlock = {
231
231
  contents: startBlock
232
232
  };
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
- });
233
+ let match = config.ecosystem.name;
234
+ let blockNumberKey;
235
+ switch (match) {
236
+ case "evm" :
237
+ case "fuel" :
238
+ blockNumberKey = config.ecosystem.blockNumberName;
239
+ break;
240
+ case "svm" :
241
+ blockNumberKey = "slot";
242
+ break;
243
+ }
244
+ let bump = n => {
245
245
  if (n !== undefined && n > maxBlock.contents) {
246
246
  maxBlock.contents = n;
247
247
  return;
248
248
  }
249
+ };
250
+ let getInt = (d, key) => Stdlib_Option.flatMap(d[key], v => {
251
+ if (v == null) {
252
+ return;
253
+ } else {
254
+ return Primitive_option.some(v);
255
+ }
256
+ });
257
+ simulateItems.forEach(rawJson => {
258
+ let match = config.ecosystem.name;
259
+ switch (match) {
260
+ case "evm" :
261
+ case "fuel" :
262
+ break;
263
+ case "svm" :
264
+ bump(getInt(rawJson, "slot"));
265
+ break;
266
+ }
267
+ let blockJson = rawJson.block;
268
+ if (!(blockJson == null)) {
269
+ return bump(getInt(blockJson, blockNumberKey));
270
+ }
249
271
  });
250
272
  return maxBlock.contents;
251
273
  }
@@ -14,8 +14,13 @@ type execParams = {query: string}
14
14
  @module("@clickhouse/client")
15
15
  external createClient: clientConfig => client = "createClient"
16
16
 
17
+ // `command`, not `exec`: exec hands its response stream to the caller and holds
18
+ // the socket until it is consumed, which nothing here does. The pool is 10
19
+ // sockets wide, so a schema whose DDL needs more than that — initialize issues
20
+ // 2N+3 statements for N entities — stalled every statement past the tenth until
21
+ // the 30s request timeout freed one. command destroys the stream for us.
17
22
  @send
18
- external exec: (client, execParams) => promise<unit> = "exec"
23
+ external command: (client, execParams) => promise<unit> = "command"
19
24
 
20
25
  @send
21
26
  external close: client => promise<unit> = "close"
@@ -352,6 +357,17 @@ let setUpdatesOrThrow = async (
352
357
  // The '{cluster}' macro resolves to each node's configured cluster name.
353
358
  let onClusterClause = (~onCluster: bool) => onCluster ? ` ON CLUSTER '{cluster}'` : ""
354
359
 
360
+ // ReplicatedMergeTree drops an insert whose block hash is already in Keeper, and
361
+ // mutations don't clear those hashes. Crash recovery trims the history tail past
362
+ // the committed Postgres checkpoint with ALTER ... DELETE and then replays it, so
363
+ // an identical replayed block would be discarded while still reporting success —
364
+ // a permanent gap that nothing surfaces. Trim-then-replay is what makes recovery
365
+ // correct here, and the duplicates dedup would have caught are already collapsed
366
+ // by the entity view's `LIMIT 1 BY`. Plain MergeTree goes by
367
+ // non_replicated_deduplication_window (0 by default), so it needs no clause.
368
+ let replicatedTableSettingsClause = (~replicated: bool) =>
369
+ replicated ? "\nSETTINGS replicated_deduplication_window = 0" : ""
370
+
355
371
  // Strip both engine arguments `(...)` and a trailing `SETTINGS ...` clause to
356
372
  // get the bare engine name, e.g. `Replicated('/p','{shard}','{replica}') SETTINGS x=1`
357
373
  // and `Replicated SETTINGS x=1` both yield `Replicated`.
@@ -390,9 +406,9 @@ let makeCreateHistoryTableQuery = (
390
406
  }
391
407
  })
392
408
 
393
- let (partitionBy, orderBy, ttl) = switch entityConfig.storage.clickhouseOptions {
394
- | Some(options) => (options.partitionBy, options.orderBy, options.ttl)
395
- | None => (None, None, None)
409
+ let (partitionBy, orderBy, ttl, skippingIndexes) = switch entityConfig.storage.clickhouseOptions {
410
+ | Some(options) => (options.partitionBy, options.orderBy, options.ttl, options.skippingIndexes)
411
+ | None => (None, None, None, None)
396
412
  }
397
413
 
398
414
  // Schema field name -> ClickHouse column name, so @storage(clickhouse: {...})
@@ -456,6 +472,22 @@ let makeCreateHistoryTableQuery = (
456
472
  | None => ""
457
473
  }
458
474
 
475
+ // Data skipping indexes live inside the column list, after the last column.
476
+ // GRANULARITY is omitted when unset, leaving ClickHouse's default of 1.
477
+ let skippingIndexDefinitions = switch skippingIndexes {
478
+ | Some(skippingIndexes) =>
479
+ skippingIndexes
480
+ ->Array.map(index => {
481
+ let granularityClause = switch index.granularity {
482
+ | Some(granularity) => ` GRANULARITY ${granularity->Int.toString}`
483
+ | None => ""
484
+ }
485
+ `,\n INDEX \`${index.name}\` ${index.expr->resolveExpressionColumns} TYPE ${index.type_}${granularityClause}`
486
+ })
487
+ ->Array.joinUnsafe("")
488
+ | None => ""
489
+ }
490
+
459
491
  `CREATE TABLE IF NOT EXISTS ${database}.\`${EntityHistory.historyTableName(
460
492
  ~entityName=entityConfig.name,
461
493
  ~entityIndex=entityConfig.index,
@@ -470,10 +502,10 @@ let makeCreateHistoryTableQuery = (
470
502
  ~fieldType=Enum({config: EntityHistory.RowAction.config->Table.fromGenericEnumConfig}),
471
503
  ~isNullable=false,
472
504
  ~isArray=false,
473
- )}
505
+ )}${skippingIndexDefinitions}
474
506
  )
475
507
  ENGINE = ${tableEngine}${partitionByClause}
476
- ORDER BY (${orderByColumns})${ttlClause}`
508
+ ORDER BY (${orderByColumns})${ttlClause}${replicatedTableSettingsClause(~replicated)}`
477
509
  }
478
510
 
479
511
  // Generate CREATE TABLE query for checkpoints
@@ -517,7 +549,7 @@ let makeCreateCheckpointsTableQuery = (
517
549
  )}
518
550
  )
519
551
  ENGINE = ${tableEngine}
520
- ORDER BY (${idField})`
552
+ ORDER BY (${idField})${replicatedTableSettingsClause(~replicated)}`
521
553
  }
522
554
 
523
555
  // Generate CREATE VIEW query for entity current state
@@ -628,21 +660,21 @@ let initialize = async (
628
660
  // CLUSTER removes the database from every node — the engine's own log
629
661
  // can't replicate the drop of the database it lives in — and SYNC waits
630
662
  // for the drop to finish before the CREATE below.
631
- await client->exec({
663
+ await client->command({
632
664
  query: `DROP DATABASE IF EXISTS ${database} ON CLUSTER '{cluster}' SYNC`,
633
665
  })
634
666
  } else {
635
- await client->exec({
667
+ await client->command({
636
668
  query: `TRUNCATE DATABASE IF EXISTS ${database}${onClusterClause(~onCluster=ddlOnCluster)}`,
637
669
  })
638
670
  }
639
- await client->exec({
671
+ await client->command({
640
672
  query: `CREATE DATABASE IF NOT EXISTS ${database}${databaseOnClusterClause}${databaseEngineClause}`,
641
673
  })
642
674
 
643
675
  await Promise.all(
644
676
  entities->Array.map(entityConfig =>
645
- client->exec({
677
+ client->command({
646
678
  query: makeCreateHistoryTableQuery(
647
679
  ~entityConfig,
648
680
  ~database,
@@ -653,7 +685,7 @@ let initialize = async (
653
685
  })
654
686
  ),
655
687
  )->Utils.Promise.ignoreValue
656
- await client->exec({
688
+ await client->command({
657
689
  query: makeCreateCheckpointsTableQuery(
658
690
  ~database,
659
691
  ~replicated,
@@ -670,14 +702,14 @@ let initialize = async (
670
702
  // caught up before creating the views. ON CLUSTER must precede the
671
703
  // database name in this command's grammar.
672
704
  if hasReplicatedDatabaseEngine {
673
- await client->exec({
705
+ await client->command({
674
706
  query: `SYSTEM SYNC DATABASE REPLICA ON CLUSTER '{cluster}' ${database}`,
675
707
  })
676
708
  }
677
709
 
678
710
  await Promise.all(
679
711
  entities->Array.map(entityConfig =>
680
- client->exec({
712
+ client->command({
681
713
  query: makeCreateViewQuery(~entityConfig, ~database, ~onCluster=ddlOnCluster),
682
714
  })
683
715
  ),
@@ -697,7 +729,7 @@ let resume = async (client, ~database: string, ~checkpointId: Internal.checkpoin
697
729
  try {
698
730
  // Try to use the database - will throw if it doesn't exist
699
731
  try {
700
- await client->exec({query: `USE ${database}`})
732
+ await client->command({query: `USE ${database}`})
701
733
  } catch {
702
734
  | exn =>
703
735
  Logging.errorWithExn(
@@ -717,14 +749,14 @@ let resume = async (client, ~database: string, ~checkpointId: Internal.checkpoin
717
749
  await Promise.all(
718
750
  tables->Array.map(table => {
719
751
  let tableName = table["name"]
720
- client->exec({
752
+ client->command({
721
753
  query: `ALTER TABLE ${database}.\`${tableName}\` DELETE WHERE \`${EntityHistory.checkpointIdFieldName}\` > ${checkpointId->BigInt.toString}`,
722
754
  })
723
755
  }),
724
756
  )->Utils.Promise.ignoreValue
725
757
 
726
758
  // Delete stale checkpoints
727
- await client->exec({
759
+ await client->command({
728
760
  query: `DELETE FROM ${database}.\`${InternalTable.Checkpoints.table.tableName}\` WHERE \`${Table.idFieldName}\` > ${checkpointId->BigInt.toString}`,
729
761
  })
730
762
  } catch {