envio 3.0.2 → 3.1.0-rc.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 (91) hide show
  1. package/README.md +0 -1
  2. package/evm.schema.json +15 -8
  3. package/fuel.schema.json +19 -12
  4. package/index.d.ts +0 -2
  5. package/package.json +6 -7
  6. package/rescript.json +1 -1
  7. package/src/Batch.res +4 -214
  8. package/src/Batch.res.mjs +6 -165
  9. package/src/ChainFetcher.res +4 -5
  10. package/src/ChainFetcher.res.mjs +6 -7
  11. package/src/ChainManager.res +10 -9
  12. package/src/ChainManager.res.mjs +6 -10
  13. package/src/Config.res +9 -25
  14. package/src/Config.res.mjs +17 -27
  15. package/src/Core.res +7 -0
  16. package/src/Ctx.res +1 -0
  17. package/src/Env.res +0 -1
  18. package/src/Env.res.mjs +0 -3
  19. package/src/EventConfigBuilder.res +13 -123
  20. package/src/EventConfigBuilder.res.mjs +6 -73
  21. package/src/EventProcessing.res +5 -29
  22. package/src/EventProcessing.res.mjs +11 -20
  23. package/src/EventUtils.res +0 -27
  24. package/src/EventUtils.res.mjs +0 -24
  25. package/src/FetchState.res +1 -11
  26. package/src/FetchState.res.mjs +2 -16
  27. package/src/GlobalState.res +23 -37
  28. package/src/GlobalState.res.mjs +10 -38
  29. package/src/HandlerLoader.res +6 -5
  30. package/src/HandlerLoader.res.mjs +27 -9
  31. package/src/HandlerRegister.res +1 -12
  32. package/src/HandlerRegister.res.mjs +1 -6
  33. package/src/HandlerRegister.resi +1 -1
  34. package/src/Hasura.res +96 -32
  35. package/src/Hasura.res.mjs +93 -38
  36. package/src/InMemoryStore.res +181 -45
  37. package/src/InMemoryStore.res.mjs +143 -40
  38. package/src/InMemoryTable.res +147 -247
  39. package/src/InMemoryTable.res.mjs +131 -230
  40. package/src/Internal.res +10 -34
  41. package/src/Internal.res.mjs +9 -3
  42. package/src/LoadLayer.res +5 -5
  43. package/src/LoadLayer.res.mjs +5 -5
  44. package/src/Main.res +4 -6
  45. package/src/Main.res.mjs +26 -15
  46. package/src/Persistence.res +7 -132
  47. package/src/Persistence.res.mjs +1 -102
  48. package/src/PgStorage.res +57 -40
  49. package/src/PgStorage.res.mjs +60 -34
  50. package/src/ReorgDetection.res +35 -58
  51. package/src/ReorgDetection.res.mjs +21 -29
  52. package/src/SimulateItems.res.mjs +21 -3
  53. package/src/Sink.res +2 -2
  54. package/src/Sink.res.mjs +1 -1
  55. package/src/TableIndices.res +9 -2
  56. package/src/TableIndices.res.mjs +7 -1
  57. package/src/TestIndexer.res +53 -60
  58. package/src/TestIndexer.res.mjs +77 -63
  59. package/src/TestIndexerProxyStorage.res +4 -14
  60. package/src/TestIndexerProxyStorage.res.mjs +1 -5
  61. package/src/UserContext.res +2 -4
  62. package/src/UserContext.res.mjs +4 -5
  63. package/src/Utils.res +0 -2
  64. package/src/Utils.res.mjs +0 -3
  65. package/src/bindings/ClickHouse.res +45 -38
  66. package/src/bindings/ClickHouse.res.mjs +16 -17
  67. package/src/bindings/Vitest.res +3 -0
  68. package/src/db/InternalTable.res +59 -18
  69. package/src/db/InternalTable.res.mjs +82 -51
  70. package/src/db/Table.res +9 -2
  71. package/src/db/Table.res.mjs +10 -7
  72. package/src/sources/EvmChain.res +32 -9
  73. package/src/sources/EvmChain.res.mjs +31 -4
  74. package/src/sources/HyperFuelSource.res +14 -57
  75. package/src/sources/HyperFuelSource.res.mjs +18 -38
  76. package/src/sources/HyperSync.res +36 -101
  77. package/src/sources/HyperSync.res.mjs +42 -96
  78. package/src/sources/HyperSync.resi +4 -22
  79. package/src/sources/HyperSyncClient.res +67 -245
  80. package/src/sources/HyperSyncClient.res.mjs +47 -46
  81. package/src/sources/HyperSyncSource.res +76 -147
  82. package/src/sources/HyperSyncSource.res.mjs +61 -114
  83. package/src/sources/RpcSource.res +43 -22
  84. package/src/sources/RpcSource.res.mjs +50 -35
  85. package/src/sources/SimulateSource.res +1 -7
  86. package/src/sources/SimulateSource.res.mjs +1 -7
  87. package/src/sources/Source.res +8 -1
  88. package/src/sources/SourceManager.res +9 -0
  89. package/src/sources/SourceManager.res.mjs +10 -0
  90. package/src/sources/SourceManager.resi +2 -0
  91. package/svm.schema.json +11 -4
@@ -95,71 +95,66 @@ function handleWriteBatch(state, updatedEntities, checkpointIds, checkpointChain
95
95
  entityDict = dict$1;
96
96
  }
97
97
  let entityConfig = state.entityConfigs[entityName];
98
- param.updates.forEach(update => {
99
- let processChange = change => {
100
- if (change.type === "SET") {
101
- let parsedEntity = S$RescriptSchema.parseOrThrow(change.entity, entityConfig.schema);
102
- entityDict[change.entityId] = parsedEntity;
103
- let checkpointKey = change.checkpointId.toString();
104
- let changes = changesByCheckpoint[checkpointKey];
105
- let entityChanges;
106
- if (changes !== undefined) {
107
- entityChanges = changes;
108
- } else {
109
- let changes$1 = {};
110
- changesByCheckpoint[checkpointKey] = changes$1;
111
- entityChanges = changes$1;
112
- }
113
- let change$1 = entityChanges[entityName];
114
- let entityChange;
115
- if (change$1 !== undefined) {
116
- entityChange = change$1;
117
- } else {
118
- let change_sets = [];
119
- let change_deleted = [];
120
- let change$2 = {
121
- sets: change_sets,
122
- deleted: change_deleted
123
- };
124
- entityChanges[entityName] = change$2;
125
- entityChange = change$2;
126
- }
127
- entityChange.sets.push(parsedEntity);
128
- return;
129
- }
130
- let entityId = change.entityId;
131
- Stdlib_Dict.$$delete(entityDict, entityId);
132
- let checkpointKey$1 = change.checkpointId.toString();
133
- let changes$2 = changesByCheckpoint[checkpointKey$1];
134
- let entityChanges$1;
135
- if (changes$2 !== undefined) {
136
- entityChanges$1 = changes$2;
98
+ let processChange = change => {
99
+ if (change.type === "SET") {
100
+ let parsedEntity = S$RescriptSchema.parseOrThrow(change.entity, entityConfig.schema);
101
+ entityDict[change.entityId] = parsedEntity;
102
+ let checkpointKey = change.checkpointId.toString();
103
+ let changes = changesByCheckpoint[checkpointKey];
104
+ let entityChanges;
105
+ if (changes !== undefined) {
106
+ entityChanges = changes;
137
107
  } else {
138
- let changes$3 = {};
139
- changesByCheckpoint[checkpointKey$1] = changes$3;
140
- entityChanges$1 = changes$3;
108
+ let changes$1 = {};
109
+ changesByCheckpoint[checkpointKey] = changes$1;
110
+ entityChanges = changes$1;
141
111
  }
142
- let change$3 = entityChanges$1[entityName];
143
- let entityChange$1;
144
- if (change$3 !== undefined) {
145
- entityChange$1 = change$3;
112
+ let change$1 = entityChanges[entityName];
113
+ let entityChange;
114
+ if (change$1 !== undefined) {
115
+ entityChange = change$1;
146
116
  } else {
147
- let change_sets$1 = [];
148
- let change_deleted$1 = [];
149
- let change$4 = {
150
- sets: change_sets$1,
151
- deleted: change_deleted$1
117
+ let change_sets = [];
118
+ let change_deleted = [];
119
+ let change$2 = {
120
+ sets: change_sets,
121
+ deleted: change_deleted
152
122
  };
153
- entityChanges$1[entityName] = change$4;
154
- entityChange$1 = change$4;
123
+ entityChanges[entityName] = change$2;
124
+ entityChange = change$2;
155
125
  }
156
- entityChange$1.deleted.push(entityId);
157
- };
158
- update.history.forEach(processChange);
159
- if (update.history.length === 0) {
160
- return processChange(update.latestChange);
126
+ entityChange.sets.push(parsedEntity);
127
+ return;
161
128
  }
162
- });
129
+ let entityId = change.entityId;
130
+ Stdlib_Dict.$$delete(entityDict, entityId);
131
+ let checkpointKey$1 = change.checkpointId.toString();
132
+ let changes$2 = changesByCheckpoint[checkpointKey$1];
133
+ let entityChanges$1;
134
+ if (changes$2 !== undefined) {
135
+ entityChanges$1 = changes$2;
136
+ } else {
137
+ let changes$3 = {};
138
+ changesByCheckpoint[checkpointKey$1] = changes$3;
139
+ entityChanges$1 = changes$3;
140
+ }
141
+ let change$3 = entityChanges$1[entityName];
142
+ let entityChange$1;
143
+ if (change$3 !== undefined) {
144
+ entityChange$1 = change$3;
145
+ } else {
146
+ let change_sets$1 = [];
147
+ let change_deleted$1 = [];
148
+ let change$4 = {
149
+ sets: change_sets$1,
150
+ deleted: change_deleted$1
151
+ };
152
+ entityChanges$1[entityName] = change$4;
153
+ entityChange$1 = change$4;
154
+ }
155
+ entityChange$1.deleted.push(entityId);
156
+ };
157
+ param.changes.forEach(processChange);
163
158
  });
164
159
  for (let i = 0, i_finish = checkpointIds.length; i < i_finish; ++i) {
165
160
  let checkpointId = checkpointIds[i];
@@ -649,12 +644,31 @@ function initTestWorker() {
649
644
  }
650
645
  let patchConfig = (config, _registrations) => {
651
646
  let config$1 = SimulateItems.patchConfig(config, processConfig);
652
- if (!exitAfterFirstEventBlock) {
647
+ if (exitAfterFirstEventBlock) {
648
+ return {
649
+ name: config$1.name,
650
+ description: config$1.description,
651
+ handlers: config$1.handlers,
652
+ contractHandlers: config$1.contractHandlers,
653
+ shouldRollbackOnReorg: config$1.shouldRollbackOnReorg,
654
+ shouldSaveFullHistory: config$1.shouldSaveFullHistory,
655
+ storage: config$1.storage,
656
+ chainMap: config$1.chainMap,
657
+ defaultChain: config$1.defaultChain,
658
+ ecosystem: config$1.ecosystem,
659
+ enableRawEvents: config$1.enableRawEvents,
660
+ maxAddrInPartition: config$1.maxAddrInPartition,
661
+ batchSize: 1,
662
+ lowercaseAddresses: config$1.lowercaseAddresses,
663
+ isDev: config$1.isDev,
664
+ userEntitiesByName: config$1.userEntitiesByName,
665
+ userEntities: config$1.userEntities,
666
+ allEntities: config$1.allEntities,
667
+ allEnums: config$1.allEnums
668
+ };
669
+ } else {
653
670
  return config$1;
654
671
  }
655
- let newrecord = {...config$1};
656
- newrecord.batchSize = 1;
657
- return newrecord;
658
672
  };
659
673
  Main.start(persistence, undefined, true, exitAfterFirstEventBlock, patchConfig);
660
674
  }
@@ -7,15 +7,9 @@ type serializableChange =
7
7
  | @as("SET") Set({entityId: string, entity: JSON.t, checkpointId: bigint})
8
8
  | @as("DELETE") Delete({entityId: string, checkpointId: bigint})
9
9
 
10
- type serializableEntityUpdate = {
11
- latestChange: serializableChange,
12
- history: array<serializableChange>,
13
- containsRollbackDiffChange: bool,
14
- }
15
-
16
10
  type serializableUpdatedEntity = {
17
11
  entityName: string,
18
- updates: array<serializableEntityUpdate>,
12
+ changes: array<serializableChange>,
19
13
  }
20
14
 
21
15
  // Worker -> Main thread payloads
@@ -138,7 +132,7 @@ let makeStorage = (proxy: t): Persistence.storage => {
138
132
  writeBatch: async (
139
133
  ~batch,
140
134
  ~rawEvents as _,
141
- ~rollbackTargetCheckpointId as _,
135
+ ~rollback as _,
142
136
  ~isInReorgThreshold as _,
143
137
  ~config as _,
144
138
  ~allEntities as _,
@@ -147,7 +141,7 @@ let makeStorage = (proxy: t): Persistence.storage => {
147
141
  ) => {
148
142
  // Encode entities to JSON for serialization across worker boundary
149
143
  let serializableEntities = updatedEntities->Array.map((
150
- {entityConfig, updates}: Persistence.updatedEntity,
144
+ {entityConfig, changes}: Persistence.updatedEntity,
151
145
  ) => {
152
146
  let encodeChange = (change: Change.t<Internal.entity>): serializableChange => {
153
147
  switch change {
@@ -162,11 +156,7 @@ let makeStorage = (proxy: t): Persistence.storage => {
162
156
  }
163
157
  {
164
158
  entityName: entityConfig.name,
165
- updates: updates->Array.map(update => {
166
- latestChange: encodeChange(update.latestChange),
167
- history: update.history->Array.map(encodeChange),
168
- containsRollbackDiffChange: update.containsRollbackDiffChange,
169
- }),
159
+ changes: changes->Array.map(encodeChange),
170
160
  }
171
161
  })
172
162
  let _ = await proxy->sendRequest(
@@ -98,11 +98,7 @@ function makeStorage(proxy) {
98
98
  };
99
99
  return {
100
100
  entityName: entityConfig.name,
101
- updates: param.updates.map(update => ({
102
- latestChange: encodeChange(update.latestChange),
103
- history: update.history.map(encodeChange),
104
- containsRollbackDiffChange: update.containsRollbackDiffChange
105
- }))
101
+ changes: param.changes.map(encodeChange)
106
102
  };
107
103
  });
108
104
  await sendRequest(proxy, {
@@ -7,7 +7,6 @@ type contextParams = {
7
7
  loadManager: LoadManager.t,
8
8
  persistence: Persistence.t,
9
9
  isPreload: bool,
10
- shouldSaveHistory: bool,
11
10
  chains: Internal.chains,
12
11
  config: Config.t,
13
12
  mutable isResolved: bool,
@@ -223,12 +222,12 @@ let entityTraps: Utils.Proxy.traps<entityContextParams> = {
223
222
  params.inMemoryStore
224
223
  ->InMemoryStore.getInMemTable(~entityConfig=params.entityConfig)
225
224
  ->InMemoryTable.Entity.set(
225
+ ~committedCheckpointId=params.inMemoryStore.committedCheckpointId,
226
226
  Set({
227
227
  entityId: entity.id,
228
228
  checkpointId: params.checkpointId,
229
229
  entity,
230
230
  }),
231
- ~shouldSaveHistory=params.shouldSaveHistory,
232
231
  )
233
232
  }
234
233
 
@@ -329,11 +328,11 @@ let entityTraps: Utils.Proxy.traps<entityContextParams> = {
329
328
  params.inMemoryStore
330
329
  ->InMemoryStore.getInMemTable(~entityConfig=params.entityConfig)
331
330
  ->InMemoryTable.Entity.set(
331
+ ~committedCheckpointId=params.inMemoryStore.committedCheckpointId,
332
332
  Delete({
333
333
  entityId,
334
334
  checkpointId: params.checkpointId,
335
335
  }),
336
- ~shouldSaveHistory=params.shouldSaveHistory,
337
336
  )
338
337
  }
339
338
  }->(Utils.magic: (string => unit) => unknown)
@@ -379,7 +378,6 @@ let handlerTraps: Utils.Proxy.traps<contextParams> = {
379
378
  inMemoryStore: params.inMemoryStore,
380
379
  loadManager: params.loadManager,
381
380
  persistence: params.persistence,
382
- shouldSaveHistory: params.shouldSaveHistory,
383
381
  checkpointId: params.checkpointId,
384
382
  chains: params.chains,
385
383
  isResolved: params.isResolved,
@@ -125,22 +125,22 @@ function throwClickHouseReadOnly(entityConfig, op) {
125
125
 
126
126
  let entityTraps_get = (params, prop) => {
127
127
  let isClickHouseOnly = !params.entityConfig.storage.postgres;
128
- let set = params.isPreload ? noopSet : entity => InMemoryTable.Entity.set(InMemoryStore.getInMemTable(params.inMemoryStore, params.entityConfig), {
128
+ let set = params.isPreload ? noopSet : entity => InMemoryTable.Entity.set(InMemoryStore.getInMemTable(params.inMemoryStore, params.entityConfig), params.inMemoryStore.committedCheckpointId, {
129
129
  type: "SET",
130
130
  entityId: entity.id,
131
131
  entity: entity,
132
132
  checkpointId: params.checkpointId
133
- }, params.shouldSaveHistory, undefined);
133
+ });
134
134
  switch (prop) {
135
135
  case "deleteUnsafe" :
136
136
  if (params.isPreload) {
137
137
  return noopDeleteUnsafe;
138
138
  } else {
139
- return entityId => InMemoryTable.Entity.set(InMemoryStore.getInMemTable(params.inMemoryStore, params.entityConfig), {
139
+ return entityId => InMemoryTable.Entity.set(InMemoryStore.getInMemTable(params.inMemoryStore, params.entityConfig), params.inMemoryStore.committedCheckpointId, {
140
140
  type: "DELETE",
141
141
  entityId: entityId,
142
142
  checkpointId: params.checkpointId
143
- }, params.shouldSaveHistory, undefined);
143
+ });
144
144
  }
145
145
  case "get" :
146
146
  if (isClickHouseOnly) {
@@ -218,7 +218,6 @@ let handlerTraps_get = (params, prop) => {
218
218
  loadManager: params.loadManager,
219
219
  persistence: params.persistence,
220
220
  isPreload: params.isPreload,
221
- shouldSaveHistory: params.shouldSaveHistory,
222
221
  chains: params.chains,
223
222
  config: params.config,
224
223
  isResolved: params.isResolved,
package/src/Utils.res CHANGED
@@ -114,8 +114,6 @@ module Dict = {
114
114
  @get_index
115
115
  external dangerouslyGetByIntNonOption: (dict<'a>, int) => option<'a> = ""
116
116
 
117
- let has: (dict<'a>, string) => bool = %raw(`(dict, key) => key in dict`)
118
-
119
117
  let push = (dict, key, value) => {
120
118
  switch dict->dangerouslyGetNonOption(key) {
121
119
  | Some(arr) => arr->Array.push(value)
package/src/Utils.res.mjs CHANGED
@@ -72,8 +72,6 @@ function getOrInsertEmptyDict(dict, key) {
72
72
  return d$1;
73
73
  }
74
74
 
75
- let has = ((dict, key) => key in dict);
76
-
77
75
  function push(dict, key, value) {
78
76
  let arr = dict[key];
79
77
  if (arr !== undefined) {
@@ -171,7 +169,6 @@ let incrementByInt = ((dict, key) => {
171
169
  let Dict = {
172
170
  $$delete: Stdlib_Dict.$$delete,
173
171
  getOrInsertEmptyDict: getOrInsertEmptyDict,
174
- has: has,
175
172
  push: push,
176
173
  pushMany: pushMany,
177
174
  merge: merge,
@@ -35,8 +35,10 @@ type queryResult<'a>
35
35
  @send
36
36
  external query: (client, queryParams) => promise<queryResult<'a>> = "query"
37
37
 
38
+ // The default `JSON` query format resolves to a `ResponseJSON` wrapper whose
39
+ // rows live under `data`, not at the top level.
38
40
  @send
39
- external json: queryResult<'a> => promise<'a> = "json"
41
+ external json: queryResult<'a> => promise<{"data": array<'a>}> = "json"
40
42
 
41
43
  let getClickHouseFieldType = (
42
44
  ~fieldType: Table.fieldType,
@@ -76,13 +78,13 @@ let getClickHouseFieldType = (
76
78
  | Json => "String"
77
79
  | Date => "DateTime64(3, 'UTC')"
78
80
  | Enum({config}) => {
79
- let variantsLength = config.variants->Belt.Array.length
81
+ let variantsLength = config.variants->Array.length
80
82
  // Theoretically we can store 256 variants in Enum8,
81
83
  // but it'd require to explicitly start with a negative index (probably)
82
84
  let enumType = variantsLength <= 127 ? "Enum8" : "Enum16"
83
85
  let enumValues =
84
86
  config.variants
85
- ->Belt.Array.map(variant => {
87
+ ->Array.map(variant => {
86
88
  let variantStr = variant->(Utils.magic: 'a => string)
87
89
  `'${variantStr}'`
88
90
  })
@@ -105,7 +107,7 @@ let getClickHouseFieldType = (
105
107
  let makeClickHouseEntitySchema = (table: Table.table): S.t<Internal.entity> => {
106
108
  S.schema(s => {
107
109
  let dict = Dict.make()
108
- table.fields->Belt.Array.forEach(field => {
110
+ table.fields->Array.forEach(field => {
109
111
  switch field {
110
112
  | Field(f) => {
111
113
  let fieldName = f->Table.getDbFieldName
@@ -203,11 +205,11 @@ let setCheckpointsOrThrow = async (client, ~batch: Batch.t, ~database: string) =
203
205
  for idx in 0 to checkpointsCount - 1 {
204
206
  checkpointRows
205
207
  ->Array.push((
206
- batch.checkpointIds->Belt.Array.getUnsafe(idx)->BigInt.toString,
207
- batch.checkpointChainIds->Belt.Array.getUnsafe(idx),
208
- batch.checkpointBlockNumbers->Belt.Array.getUnsafe(idx),
209
- batch.checkpointBlockHashes->Belt.Array.getUnsafe(idx),
210
- batch.checkpointEventsProcessed->Belt.Array.getUnsafe(idx),
208
+ batch.checkpointIds->Array.getUnsafe(idx)->BigInt.toString,
209
+ batch.checkpointChainIds->Array.getUnsafe(idx),
210
+ batch.checkpointBlockNumbers->Array.getUnsafe(idx),
211
+ batch.checkpointBlockHashes->Array.getUnsafe(idx),
212
+ batch.checkpointEventsProcessed->Array.getUnsafe(idx),
211
213
  ))
212
214
  ->ignore
213
215
  }
@@ -233,17 +235,17 @@ let setCheckpointsOrThrow = async (client, ~batch: Batch.t, ~database: string) =
233
235
 
234
236
  type setUpdatesCache = {
235
237
  tableName: string,
236
- convertOrThrow: Change.t<Internal.entity> => JSON.t,
238
+ convertOrThrow: array<Change.t<Internal.entity>> => array<JSON.t>,
237
239
  }
238
240
 
239
241
  let setUpdatesOrThrow = async (
240
242
  client,
241
243
  ~cache: Utils.WeakMap.t<Internal.entityConfig, setUpdatesCache>,
242
- ~updates: array<Internal.inMemoryStoreEntityUpdate<Internal.entity>>,
244
+ ~changes: array<Change.t<Internal.entity>>,
243
245
  ~entityConfig: Internal.entityConfig,
244
246
  ~database: string,
245
247
  ) => {
246
- if updates->Array.length === 0 {
248
+ if changes->Array.length === 0 {
247
249
  ()
248
250
  } else {
249
251
  let {convertOrThrow, tableName} = switch cache->Utils.WeakMap.get(entityConfig) {
@@ -255,23 +257,29 @@ let setUpdatesOrThrow = async (
255
257
  ~entityIndex=entityConfig.index,
256
258
  )}\``,
257
259
  convertOrThrow: S.compile(
258
- S.union([
259
- EntityHistory.makeSetUpdateSchema(makeClickHouseEntitySchema(entityConfig.table)),
260
- S.object(s => {
261
- s.tag(EntityHistory.changeFieldName, EntityHistory.RowAction.DELETE)
262
- Change.Delete({
263
- entityId: s.field(Table.idFieldName, S.string),
264
- checkpointId: s.field(
265
- EntityHistory.checkpointIdFieldName,
266
- EntityHistory.unsafeCheckpointIdSchema,
267
- ),
268
- })
269
- }),
270
- ]),
260
+ S.array(
261
+ S.union([
262
+ EntityHistory.makeSetUpdateSchema(makeClickHouseEntitySchema(entityConfig.table)),
263
+ S.object(s => {
264
+ s.tag(EntityHistory.changeFieldName, EntityHistory.RowAction.DELETE)
265
+ Change.Delete({
266
+ entityId: s.field(Table.idFieldName, S.string),
267
+ checkpointId: s.field(
268
+ EntityHistory.checkpointIdFieldName,
269
+ EntityHistory.unsafeCheckpointIdSchema,
270
+ ),
271
+ })
272
+ }),
273
+ ]),
274
+ ),
271
275
  ~input=Value,
272
276
  ~output=Json,
273
277
  ~typeValidation=false,
274
278
  ~mode=Sync,
279
+ )->(
280
+ Utils.magic: (array<Change.t<Internal.entity>> => JSON.t) => array<
281
+ Change.t<Internal.entity>,
282
+ > => array<JSON.t>
275
283
  ),
276
284
  }
277
285
 
@@ -280,10 +288,9 @@ let setUpdatesOrThrow = async (
280
288
  }
281
289
 
282
290
  try {
283
- // Convert entity updates to ClickHouse row format
284
- let values = updates->Array.map(update => {
285
- update.latestChange->convertOrThrow
286
- })
291
+ // The entity history table is the source of truth for ClickHouse, so every
292
+ // intermediate change must be persisted, not only the current value.
293
+ let values = changes->convertOrThrow
287
294
 
288
295
  await insertWithRetry(client, ~table=tableName, ~values, ~format="JSONEachRow")
289
296
  } catch {
@@ -305,7 +312,7 @@ let makeCreateHistoryTableQuery = (
305
312
  ~replicated: bool=false,
306
313
  ) => {
307
314
  let tableEngine = replicated ? "ReplicatedMergeTree" : "MergeTree()"
308
- let fieldDefinitions = entityConfig.table.fields->Belt.Array.keepMap(field => {
315
+ let fieldDefinitions = entityConfig.table.fields->Array.filterMap(field => {
309
316
  switch field {
310
317
  | Field(field) =>
311
318
  Some({
@@ -389,7 +396,7 @@ let makeCreateViewQuery = (~entityConfig: Internal.entityConfig, ~database: stri
389
396
 
390
397
  let entityFields =
391
398
  entityConfig.table.fields
392
- ->Belt.Array.keepMap(field => {
399
+ ->Array.filterMap(field => {
393
400
  switch field {
394
401
  | Field(field) => {
395
402
  let fieldName = field->Table.getDbFieldName
@@ -433,12 +440,12 @@ let initialize = async (
433
440
 
434
441
  switch databaseEngine {
435
442
  | Some(engineSpec) => {
436
- let expectedEngineName = engineSpec->String.split("(")->Belt.Array.getUnsafe(0)->String.trim
443
+ let expectedEngineName = engineSpec->String.split("(")->Array.getUnsafe(0)->String.trim
437
444
  let existingResult = await client->query({
438
445
  query: `SELECT engine FROM system.databases WHERE name = '${database}'`,
439
446
  })
440
- let rows: array<{"engine": string}> = await existingResult->json
441
- switch rows->Belt.Array.get(0) {
447
+ let rows = (await existingResult->json)["data"]
448
+ switch rows->Array.get(0) {
442
449
  | Some(row) if row["engine"] !== expectedEngineName =>
443
450
  JsError.throwWithMessage(
444
451
  `ClickHouse database "${database}" exists with engine "${row["engine"]}" but ENVIO_CLICKHOUSE_DATABASE_ENGINE specifies "${expectedEngineName}". Drop the database manually to change its engine.`,
@@ -456,14 +463,14 @@ let initialize = async (
456
463
  await client->exec({query: `USE ${database}`})
457
464
 
458
465
  await Promise.all(
459
- entities->Belt.Array.map(entityConfig =>
466
+ entities->Array.map(entityConfig =>
460
467
  client->exec({query: makeCreateHistoryTableQuery(~entityConfig, ~database, ~replicated)})
461
468
  ),
462
469
  )->Utils.Promise.ignoreValue
463
470
  await client->exec({query: makeCreateCheckpointsTableQuery(~database, ~replicated)})
464
471
 
465
472
  await Promise.all(
466
- entities->Belt.Array.map(entityConfig =>
473
+ entities->Array.map(entityConfig =>
467
474
  client->exec({query: makeCreateViewQuery(~entityConfig, ~database)})
468
475
  ),
469
476
  )->Utils.Promise.ignoreValue
@@ -496,11 +503,11 @@ let resume = async (client, ~database: string, ~checkpointId: Internal.checkpoin
496
503
  let tablesResult = await client->query({
497
504
  query: `SHOW TABLES FROM ${database} LIKE '${EntityHistory.historyTablePrefix}%'`,
498
505
  })
499
- let tables: array<{"name": string}> = await tablesResult->json
506
+ let tables = (await tablesResult->json)["data"]
500
507
 
501
508
  // Delete rows with checkpoint IDs higher than the target for each history table
502
509
  await Promise.all(
503
- tables->Belt.Array.map(table => {
510
+ tables->Array.map(table => {
504
511
  let tableName = table["name"]
505
512
  client->exec({
506
513
  query: `ALTER TABLE ${database}.\`${tableName}\` DELETE WHERE \`${EntityHistory.checkpointIdFieldName}\` > ${checkpointId->BigInt.toString}`,
@@ -4,8 +4,8 @@ import * as Env from "../Env.res.mjs";
4
4
  import * as Table from "../db/Table.res.mjs";
5
5
  import * as Utils from "../Utils.res.mjs";
6
6
  import * as Logging from "../Logging.res.mjs";
7
- import * as Belt_Array from "@rescript/runtime/lib/es6/Belt_Array.js";
8
7
  import * as Persistence from "../Persistence.res.mjs";
8
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
9
9
  import * as EntityHistory from "../db/EntityHistory.res.mjs";
10
10
  import * as InternalTable from "../db/InternalTable.res.mjs";
11
11
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
@@ -65,7 +65,7 @@ function getClickHouseFieldType(fieldType, isNullable, isArray) {
65
65
  let config$1 = fieldType.config;
66
66
  let variantsLength = config$1.variants.length;
67
67
  let enumType = variantsLength <= 127 ? "Enum8" : "Enum16";
68
- let enumValues = Belt_Array.map(config$1.variants, variant => `'` + variant + `'`).join(", ");
68
+ let enumValues = config$1.variants.map(variant => `'` + variant + `'`).join(", ");
69
69
  baseType = enumType + `(` + enumValues + `)`;
70
70
  break;
71
71
  case "Entity" :
@@ -84,7 +84,7 @@ function getClickHouseFieldType(fieldType, isNullable, isArray) {
84
84
  function makeClickHouseEntitySchema(table) {
85
85
  return S$RescriptSchema.schema(s => {
86
86
  let dict = {};
87
- Belt_Array.forEach(table.fields, field => {
87
+ table.fields.forEach(field => {
88
88
  if (field.TAG !== "Field") {
89
89
  return;
90
90
  }
@@ -192,8 +192,8 @@ async function setCheckpointsOrThrow(client, batch, database) {
192
192
  }
193
193
  }
194
194
 
195
- async function setUpdatesOrThrow(client, cache, updates, entityConfig, database) {
196
- if (updates.length === 0) {
195
+ async function setUpdatesOrThrow(client, cache, changes, entityConfig, database) {
196
+ if (changes.length === 0) {
197
197
  return;
198
198
  }
199
199
  let cached = cache.get(entityConfig);
@@ -202,7 +202,7 @@ async function setUpdatesOrThrow(client, cache, updates, entityConfig, database)
202
202
  match = cached;
203
203
  } else {
204
204
  let cached_tableName = database + `.\`` + EntityHistory.historyTableName(entityConfig.name, entityConfig.index) + `\``;
205
- let cached_convertOrThrow = S$RescriptSchema.compile(S$RescriptSchema.union([
205
+ let cached_convertOrThrow = S$RescriptSchema.compile(S$RescriptSchema.array(S$RescriptSchema.union([
206
206
  EntityHistory.makeSetUpdateSchema(makeClickHouseEntitySchema(entityConfig.table)),
207
207
  S$RescriptSchema.object(s => {
208
208
  s.tag(EntityHistory.changeFieldName, "DELETE");
@@ -212,7 +212,7 @@ async function setUpdatesOrThrow(client, cache, updates, entityConfig, database)
212
212
  checkpointId: s.f(EntityHistory.checkpointIdFieldName, EntityHistory.unsafeCheckpointIdSchema)
213
213
  };
214
214
  })
215
- ]), "Output", "Json", "Sync", false);
215
+ ])), "Output", "Json", "Sync", false);
216
216
  let cached$1 = {
217
217
  tableName: cached_tableName,
218
218
  convertOrThrow: cached_convertOrThrow
@@ -220,10 +220,9 @@ async function setUpdatesOrThrow(client, cache, updates, entityConfig, database)
220
220
  cache.set(entityConfig, cached$1);
221
221
  match = cached$1;
222
222
  }
223
- let convertOrThrow = match.convertOrThrow;
224
223
  let tableName = match.tableName;
225
224
  try {
226
- let values = updates.map(update => convertOrThrow(update.latestChange));
225
+ let values = match.convertOrThrow(changes);
227
226
  return await insertWithRetry(client, tableName, values, "JSONEachRow", undefined);
228
227
  } catch (raw_exn) {
229
228
  let exn = Primitive_exceptions.internalToException(raw_exn);
@@ -239,7 +238,7 @@ async function setUpdatesOrThrow(client, cache, updates, entityConfig, database)
239
238
  function makeCreateHistoryTableQuery(entityConfig, database, replicatedOpt) {
240
239
  let replicated = replicatedOpt !== undefined ? replicatedOpt : false;
241
240
  let tableEngine = replicated ? "ReplicatedMergeTree" : "MergeTree()";
242
- let fieldDefinitions = Belt_Array.keepMap(entityConfig.table.fields, field => {
241
+ let fieldDefinitions = Stdlib_Array.filterMap(entityConfig.table.fields, field => {
243
242
  if (field.TAG !== "Field") {
244
243
  return;
245
244
  }
@@ -277,7 +276,7 @@ ORDER BY (` + "id" + `)`;
277
276
  function makeCreateViewQuery(entityConfig, database) {
278
277
  let historyTableName = EntityHistory.historyTableName(entityConfig.name, entityConfig.index);
279
278
  let checkpointsTableName = InternalTable.Checkpoints.table.tableName;
280
- let entityFields = Belt_Array.keepMap(entityConfig.table.fields, field => {
279
+ let entityFields = Stdlib_Array.filterMap(entityConfig.table.fields, field => {
281
280
  if (field.TAG !== "Field") {
282
281
  return;
283
282
  }
@@ -307,8 +306,8 @@ async function initialize(client, database, entities, param) {
307
306
  let existingResult = await client.query({
308
307
  query: `SELECT engine FROM system.databases WHERE name = '` + database + `'`
309
308
  });
310
- let rows = await existingResult.json();
311
- let row = Belt_Array.get(rows, 0);
309
+ let rows = (await existingResult.json()).data;
310
+ let row = rows[0];
312
311
  if (row !== undefined) {
313
312
  let row$1 = Primitive_option.valFromOption(row);
314
313
  if (row$1.engine !== expectedEngineName) {
@@ -325,13 +324,13 @@ async function initialize(client, database, entities, param) {
325
324
  await client.exec({
326
325
  query: `USE ` + database
327
326
  });
328
- await Promise.all(Belt_Array.map(entities, entityConfig => client.exec({
327
+ await Promise.all(entities.map(entityConfig => client.exec({
329
328
  query: makeCreateHistoryTableQuery(entityConfig, database, replicated)
330
329
  })));
331
330
  await client.exec({
332
331
  query: makeCreateCheckpointsTableQuery(database, replicated)
333
332
  });
334
- await Promise.all(Belt_Array.map(entities, entityConfig => client.exec({
333
+ await Promise.all(entities.map(entityConfig => client.exec({
335
334
  query: makeCreateViewQuery(entityConfig, database)
336
335
  })));
337
336
  return Logging.trace("ClickHouse storage initialization completed successfully");
@@ -356,8 +355,8 @@ async function resume(client, database, checkpointId) {
356
355
  let tablesResult = await client.query({
357
356
  query: `SHOW TABLES FROM ` + database + ` LIKE '` + EntityHistory.historyTablePrefix + `%'`
358
357
  });
359
- let tables = await tablesResult.json();
360
- await Promise.all(Belt_Array.map(tables, table => {
358
+ let tables = (await tablesResult.json()).data;
359
+ await Promise.all(tables.map(table => {
361
360
  let tableName = table.name;
362
361
  return client.exec({
363
362
  query: `ALTER TABLE ` + database + `.\`` + tableName + `\` DELETE WHERE \`` + EntityHistory.checkpointIdFieldName + `\` > ` + checkpointId.toString()
@@ -117,6 +117,9 @@ module Async = {
117
117
  @module("vitest") @scope("it")
118
118
  external it_skipIf: bool => (string, testContext => promise<unit>) => unit = "skipIf"
119
119
 
120
+ @module("vitest") @scope("it")
121
+ external it_fails: (string, testContext => promise<unit>) => unit = "fails"
122
+
120
123
  let isClaudeCloud: bool = %raw(`process.env.CLAUDE_CODE_CONTAINER_ID != null`)
121
124
  let itSkipInClaudeCloud = (name, fn) => it_skipIf(isClaudeCloud)(name, fn)
122
125