envio 3.9.0 → 3.10.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 (110) hide show
  1. package/evm.schema.json +26 -0
  2. package/fuel.schema.json +26 -0
  3. package/index.d.ts +35 -3
  4. package/package.json +6 -7
  5. package/src/Batch.res +9 -0
  6. package/src/ChainState.res +35 -39
  7. package/src/ChainState.res.mjs +14 -11
  8. package/src/ChainState.resi +15 -7
  9. package/src/Config.res +52 -44
  10. package/src/Config.res.mjs +51 -17
  11. package/src/Core.res +4 -0
  12. package/src/CrossChainState.res +15 -23
  13. package/src/CrossChainState.res.mjs +10 -18
  14. package/src/CrossChainState.resi +4 -1
  15. package/src/Ecosystem.res +7 -2
  16. package/src/Envio.res +6 -4
  17. package/src/EventConfigBuilder.res +169 -26
  18. package/src/EventConfigBuilder.res.mjs +124 -15
  19. package/src/HandlerRegister.res +21 -4
  20. package/src/HandlerRegister.res.mjs +24 -5
  21. package/src/InMemoryStore.res +9 -7
  22. package/src/InMemoryStore.res.mjs +3 -3
  23. package/src/IndexerState.res +43 -6
  24. package/src/IndexerState.res.mjs +36 -7
  25. package/src/IndexerState.resi +4 -4
  26. package/src/Internal.res +16 -17
  27. package/src/Metrics.res +72 -6
  28. package/src/Metrics.res.mjs +30 -2
  29. package/src/Persistence.res +36 -30
  30. package/src/Persistence.res.mjs +1 -20
  31. package/src/PgStorage.res +104 -56
  32. package/src/PgStorage.res.mjs +57 -35
  33. package/src/PruneStaleHistory.res +85 -52
  34. package/src/PruneStaleHistory.res.mjs +63 -37
  35. package/src/Rollback.res +68 -45
  36. package/src/Rollback.res.mjs +38 -19
  37. package/src/RollbackCommit.res +7 -9
  38. package/src/RollbackCommit.res.mjs +5 -6
  39. package/src/SafeCheckpointTracking.res +11 -11
  40. package/src/SafeCheckpointTracking.res.mjs +7 -6
  41. package/src/SimulateItems.res +33 -18
  42. package/src/SimulateItems.res.mjs +26 -22
  43. package/src/Sink.res +38 -26
  44. package/src/Sink.res.mjs +21 -16
  45. package/src/TestIndexer.res +5 -5
  46. package/src/TestIndexer.res.mjs +1 -1
  47. package/src/Utils.res +170 -0
  48. package/src/Utils.res.mjs +153 -0
  49. package/src/Writing.res +2 -2
  50. package/src/Writing.res.mjs +1 -1
  51. package/src/bindings/ClickHouse.res +366 -698
  52. package/src/bindings/ClickHouse.res.mjs +371 -421
  53. package/src/bindings/ClickHouseSink.res +337 -0
  54. package/src/bindings/ClickHouseSink.res.mjs +246 -0
  55. package/src/bindings/EventSource.res +8 -2
  56. package/src/bindings/NodeJs.res +6 -0
  57. package/src/bindings/Vitest.res +23 -0
  58. package/src/bindings/Vitest.res.mjs +3 -0
  59. package/src/bindings/WebSocket.res +9 -10
  60. package/src/db/CheckpointBounds.res +53 -0
  61. package/src/db/CheckpointBounds.res.mjs +44 -0
  62. package/src/db/EntityFilter.res +36 -17
  63. package/src/db/EntityFilter.res.mjs +25 -14
  64. package/src/db/EntityHistory.res +47 -22
  65. package/src/db/EntityHistory.res.mjs +19 -12
  66. package/src/db/InternalTable.res +84 -35
  67. package/src/db/InternalTable.res.mjs +56 -23
  68. package/src/db/RollbackFloors.res +55 -0
  69. package/src/db/RollbackFloors.res.mjs +92 -0
  70. package/src/db/Table.res +22 -4
  71. package/src/db/Table.res.mjs +30 -9
  72. package/src/sources/Evm.res +2 -0
  73. package/src/sources/Evm.res.mjs +2 -0
  74. package/src/sources/EvmHyperSyncSource.res +3 -3
  75. package/src/sources/EvmHyperSyncSource.res.mjs +3 -3
  76. package/src/sources/EvmRpcWs.res +124 -0
  77. package/src/sources/EvmRpcWs.res.mjs +117 -0
  78. package/src/sources/Fuel.res +2 -0
  79. package/src/sources/Fuel.res.mjs +2 -0
  80. package/src/sources/FuelHyperSyncSource.res +1 -1
  81. package/src/sources/FuelHyperSyncSource.res.mjs +2 -1
  82. package/src/sources/HeightFeed.res +568 -0
  83. package/src/sources/HeightFeed.res.mjs +452 -0
  84. package/src/sources/HeightStream.res +257 -0
  85. package/src/sources/HeightStream.res.mjs +194 -0
  86. package/src/sources/HyperSync.res +5 -0
  87. package/src/sources/HyperSync.res.mjs +3 -0
  88. package/src/sources/HyperSync.resi +4 -0
  89. package/src/sources/HyperSyncSSE.res +49 -0
  90. package/src/sources/HyperSyncSSE.res.mjs +79 -0
  91. package/src/sources/RpcSource.res +1 -1
  92. package/src/sources/RpcSource.res.mjs +2 -2
  93. package/src/sources/Source.res +54 -1
  94. package/src/sources/Source.res.mjs +25 -0
  95. package/src/sources/SourceManager.res +220 -227
  96. package/src/sources/SourceManager.res.mjs +139 -164
  97. package/src/sources/SourceManager.resi +8 -0
  98. package/src/sources/Svm.res +2 -0
  99. package/src/sources/Svm.res.mjs +2 -0
  100. package/src/sources/SvmHyperSyncClient.res +70 -127
  101. package/src/sources/SvmHyperSyncClient.res.mjs +51 -25
  102. package/src/sources/SvmHyperSyncSource.res +18 -15
  103. package/src/sources/SvmHyperSyncSource.res.mjs +5 -4
  104. package/svm.schema.json +1 -75
  105. package/src/MemoryStorage.res +0 -729
  106. package/src/MemoryStorage.res.mjs +0 -586
  107. package/src/sources/HyperSyncHeightStream.res +0 -129
  108. package/src/sources/HyperSyncHeightStream.res.mjs +0 -110
  109. package/src/sources/RpcWebSocketHeightStream.res +0 -175
  110. package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -180
@@ -7,533 +7,483 @@ import * as ChainId from "../ChainId.res.mjs";
7
7
  import * as Logging from "../Logging.res.mjs";
8
8
  import * as Internal from "../Internal.res.mjs";
9
9
  import * as Persistence from "../Persistence.res.mjs";
10
+ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
10
11
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
11
12
  import * as EntityHistory from "../db/EntityHistory.res.mjs";
12
13
  import * as InternalTable from "../db/InternalTable.res.mjs";
14
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
15
+ import * as ClickHouseSink from "./ClickHouseSink.res.mjs";
13
16
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
14
17
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
15
18
  import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
16
19
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
17
20
 
18
- function getClickHouseFieldType(fieldType, isNullable, isArray, chainIdModeOpt) {
19
- let chainIdMode = chainIdModeOpt !== undefined ? chainIdModeOpt : "int32";
20
- let baseType;
21
+ function clickHouseFieldSchema(f) {
22
+ let shaped = baseSchema => {
23
+ if (f.isNullable) {
24
+ return Utils.Schema.nullTolerant(baseSchema);
25
+ } else if (f.isArray) {
26
+ return S$RescriptSchema.array(baseSchema);
27
+ } else {
28
+ return baseSchema;
29
+ }
30
+ };
31
+ let match = f.fieldType;
32
+ if (typeof match === "object") {
33
+ return f.fieldSchema;
34
+ }
35
+ switch (match) {
36
+ case "Bytea" :
37
+ return shaped(S$RescriptSchema.json(false));
38
+ case "ChainId" :
39
+ return ChainId.schema;
40
+ case "Json" :
41
+ if (f.isArray) {
42
+ return f.fieldSchema;
43
+ } else {
44
+ return Utils.Schema.clickHouseJson;
45
+ }
46
+ case "Date" :
47
+ return shaped(Utils.Schema.clickHouseDate);
48
+ default:
49
+ return f.fieldSchema;
50
+ }
51
+ }
52
+
53
+ function makeClickHouseEntitySchema(table) {
54
+ return S$RescriptSchema.object(s => {
55
+ let dict = {};
56
+ table.fields.forEach(field => {
57
+ if (field.TAG !== "Field") {
58
+ return;
59
+ }
60
+ let f = field._0;
61
+ dict[Table.getApiFieldName(f)] = s.f(Table.getClickHouseDbFieldName(f), clickHouseFieldSchema(f));
62
+ });
63
+ return dict;
64
+ });
65
+ }
66
+
67
+ let logger = Logging.createChild({
68
+ context: "ClickHouse"
69
+ });
70
+
71
+ function makeSink(host, username, password, database, chainIdMode) {
72
+ return ClickHouseSink.make(host, username, password, database, chainIdMode, msg => Logging.childWarn(logger, {
73
+ msg: msg
74
+ }));
75
+ }
76
+
77
+ function makeColumnSpec(name, fieldNameOpt, fieldType, isNullableOpt, isArrayOpt) {
78
+ let fieldName = fieldNameOpt !== undefined ? fieldNameOpt : name;
79
+ let isNullable = isNullableOpt !== undefined ? isNullableOpt : false;
80
+ let isArray = isArrayOpt !== undefined ? isArrayOpt : false;
81
+ let tmp;
21
82
  if (typeof fieldType !== "object") {
22
83
  switch (fieldType) {
84
+ case "String" :
85
+ tmp = "String";
86
+ break;
23
87
  case "Boolean" :
24
- baseType = "Bool";
88
+ tmp = "Boolean";
25
89
  break;
26
90
  case "Uint32" :
27
- baseType = "UInt32";
91
+ tmp = "Uint32";
92
+ break;
93
+ case "UInt52" :
94
+ tmp = "UInt52";
28
95
  break;
29
96
  case "SmallInt" :
97
+ tmp = Stdlib_JsError.throwWithMessage("ClickHouse doesn't support the internal SmallInt column type");
98
+ break;
30
99
  case "Bytea" :
31
- baseType = Stdlib_JsError.throwWithMessage("ClickHouse doesn't support the internal SmallInt and Bytea column types");
100
+ tmp = "Bytea";
32
101
  break;
33
- case "UInt52" :
34
102
  case "UInt64" :
35
- baseType = "UInt64";
103
+ tmp = "UInt64";
104
+ break;
105
+ case "Int32" :
106
+ tmp = "Int32";
36
107
  break;
37
108
  case "ChainId" :
38
- baseType = chainIdMode === "int32" ? "Int32" : "UInt64";
109
+ tmp = "ChainId";
39
110
  break;
40
111
  case "Number" :
41
- baseType = "Float64";
112
+ tmp = "Number";
42
113
  break;
43
- case "Int32" :
44
114
  case "Serial" :
45
- baseType = "Int32";
115
+ tmp = "Serial";
46
116
  break;
47
117
  case "BigSerial" :
48
- baseType = "Int64";
118
+ tmp = "BigSerial";
49
119
  break;
50
- case "String" :
51
120
  case "Json" :
52
- baseType = "String";
121
+ tmp = "Json";
53
122
  break;
54
123
  case "Date" :
55
- baseType = "DateTime64(3, 'UTC')";
124
+ tmp = "Date";
56
125
  break;
57
126
  }
58
127
  } else {
59
128
  switch (fieldType.type) {
60
129
  case "BigInt" :
61
- let precision = fieldType.precision;
62
- baseType = precision !== undefined && precision <= 38 ? `Decimal(` + precision.toString() + `,0)` : "String";
130
+ tmp = "BigInt";
63
131
  break;
64
132
  case "BigDecimal" :
65
- let config = fieldType.config;
66
- if (config !== undefined) {
67
- let scale = config[1];
68
- let precision$1 = config[0];
69
- baseType = precision$1 > 38 || scale > precision$1 ? "String" : `Decimal(` + precision$1.toString() + `,` + scale.toString() + `)`;
70
- } else {
71
- baseType = "String";
72
- }
133
+ tmp = "BigDecimal";
73
134
  break;
74
135
  case "Enum" :
75
- let config$1 = fieldType.config;
76
- let variantsLength = config$1.variants.length;
77
- let enumType = variantsLength <= 127 ? "Enum8" : "Enum16";
78
- let enumValues = config$1.variants.map(variant => `'` + variant + `'`).join(", ");
79
- baseType = enumType + `(` + enumValues + `)`;
136
+ tmp = "Enum";
80
137
  break;
81
138
  }
82
139
  }
83
- let baseType$1 = isArray ? `Array(` + baseType + `)` : baseType;
84
- if (isNullable) {
85
- return `Nullable(` + baseType$1 + `)`;
140
+ let tmp$1;
141
+ if (typeof fieldType !== "object") {
142
+ tmp$1 = undefined;
86
143
  } else {
87
- return baseType$1;
144
+ switch (fieldType.type) {
145
+ case "BigInt" :
146
+ tmp$1 = fieldType.precision;
147
+ break;
148
+ case "BigDecimal" :
149
+ tmp$1 = Stdlib_Option.map(fieldType.config, param => param[0]);
150
+ break;
151
+ default:
152
+ tmp$1 = undefined;
153
+ }
88
154
  }
155
+ let tmp$2;
156
+ tmp$2 = typeof fieldType !== "object" || fieldType.type !== "BigDecimal" ? undefined : Stdlib_Option.map(fieldType.config, param => param[1]);
157
+ let tmp$3;
158
+ tmp$3 = typeof fieldType !== "object" || fieldType.type !== "Enum" ? undefined : fieldType.config.variants.map(variant => variant);
159
+ return {
160
+ name: name,
161
+ fieldName: fieldName,
162
+ fieldType: tmp,
163
+ isNullable: isNullable,
164
+ isArray: isArray,
165
+ precision: tmp$1,
166
+ scale: tmp$2,
167
+ enumVariants: tmp$3
168
+ };
89
169
  }
90
170
 
91
- function makeClickHouseEntitySchema(table) {
92
- return S$RescriptSchema.object(s => {
93
- let dict = {};
94
- table.fields.forEach(field => {
171
+ let checkpointColumns = Stdlib_Array.filterMap(InternalTable.Checkpoints.columns, param => {
172
+ let field = param.field;
173
+ if (field.TAG !== "Field") {
174
+ return;
175
+ }
176
+ let match = field._0;
177
+ return {
178
+ spec: makeColumnSpec(match.fieldName, undefined, param.clickHouseFieldType, match.isNullable, undefined),
179
+ valuesOf: param.valuesOf
180
+ };
181
+ });
182
+
183
+ let checkpointColumnSpecs = checkpointColumns.map(param => param.spec);
184
+
185
+ function entitySpec(entityConfig) {
186
+ let options = entityConfig.storage.clickhouseOptions;
187
+ return {
188
+ name: entityConfig.name,
189
+ historyTable: EntityHistory.historyTableName(entityConfig.name, entityConfig.index),
190
+ columns: Stdlib_Array.filterMap(entityConfig.table.fields, field => {
95
191
  if (field.TAG !== "Field") {
96
192
  return;
97
193
  }
98
194
  let f = field._0;
99
- let fieldName = Table.getClickHouseDbFieldName(f);
100
- let match = f.fieldType;
101
- let fieldSchema;
102
- if (typeof match !== "object") {
103
- switch (match) {
104
- case "UInt52" :
105
- let uint52Schema = S$RescriptSchema.preprocess(S$RescriptSchema.float, param => ({
106
- p: unknown => parseFloat(unknown)
107
- }));
108
- fieldSchema = f.isNullable ? S$RescriptSchema.$$null(uint52Schema) : (
109
- f.isArray ? S$RescriptSchema.array(uint52Schema) : uint52Schema
110
- );
111
- break;
112
- case "ChainId" :
113
- fieldSchema = ChainId.schema;
114
- break;
115
- case "Date" :
116
- let dateSchema = Utils.Schema.clickHouseDate;
117
- fieldSchema = f.isNullable ? S$RescriptSchema.$$null(dateSchema) : (
118
- f.isArray ? S$RescriptSchema.array(dateSchema) : dateSchema
119
- );
120
- break;
121
- default:
122
- fieldSchema = f.fieldSchema;
123
- }
195
+ return makeColumnSpec(Table.getClickHouseDbFieldName(f), f.fieldName, f.fieldType, f.isNullable, f.isArray);
196
+ }),
197
+ chainIdColumn: Stdlib_Option.map(Table.getChainIdField(entityConfig.table), Table.getClickHouseDbFieldName),
198
+ partitionBy: Stdlib_Option.flatMap(options, options => options.partitionBy),
199
+ orderBy: Stdlib_Option.flatMap(options, options => options.orderBy),
200
+ ttl: Stdlib_Option.flatMap(options, options => options.ttl),
201
+ skippingIndexes: Stdlib_Option.flatMap(options, options => Stdlib_Option.map(options.skippingIndexes, indexes => indexes.map(index => ({
202
+ name: index.name,
203
+ expr: index.expr,
204
+ indexType: index.type,
205
+ granularity: index.granularity
206
+ }))))
207
+ };
208
+ }
209
+
210
+ function makeRegistry() {
211
+ return {
212
+ entities: {},
213
+ checkpoints: undefined,
214
+ converters: {}
215
+ };
216
+ }
217
+
218
+ let absent = undefined;
219
+
220
+ function compileSerializer(schema) {
221
+ return S$RescriptSchema.compile(schema, "Output", "Json", "Sync", false);
222
+ }
223
+
224
+ function makeConverters(entityConfig, scope, table) {
225
+ let columns = table.columns;
226
+ let tableName = table.name;
227
+ let sourceTable = entityConfig.table;
228
+ let match = Table.getChainIdField(sourceTable);
229
+ let match$1 = Internal.chainScopeChainId(scope);
230
+ let chainIdTag = match !== undefined && match$1 !== undefined ? [
231
+ Table.getClickHouseDbFieldName(match),
232
+ Primitive_option.valFromOption(match$1)
233
+ ] : undefined;
234
+ let fieldsByColumn = {};
235
+ sourceTable.fields.forEach(field => {
236
+ if (field.TAG !== "Field") {
237
+ return;
238
+ }
239
+ let f = field._0;
240
+ fieldsByColumn[Table.getClickHouseDbFieldName(f)] = f;
241
+ });
242
+ let setCells = [];
243
+ let deleteCells = [];
244
+ let registeredNames = new Set(columns.map(column => column.name));
245
+ columns.forEach(param => {
246
+ let name = param.name;
247
+ let match;
248
+ if (name === EntityHistory.checkpointIdFieldName) {
249
+ let cell = change => change.checkpointId;
250
+ match = [
251
+ cell,
252
+ cell
253
+ ];
254
+ } else if (name === EntityHistory.changeFieldName) {
255
+ match = [
256
+ param => "SET",
257
+ param => "DELETE"
258
+ ];
259
+ } else {
260
+ let match$1 = fieldsByColumn[name];
261
+ let exit = 0;
262
+ if (chainIdTag !== undefined && chainIdTag[0] === name) {
263
+ let chainId = chainIdTag[1];
264
+ match = [
265
+ param => chainId,
266
+ param => chainId
267
+ ];
124
268
  } else {
125
- fieldSchema = f.fieldSchema;
269
+ exit = 1;
126
270
  }
127
- dict[Table.getApiFieldName(f)] = s.f(fieldName, fieldSchema);
128
- });
129
- return dict;
271
+ if (exit === 1) {
272
+ if (match$1 !== undefined) {
273
+ let serialize = compileSerializer(clickHouseFieldSchema(match$1));
274
+ let apiName = Table.getApiFieldName(match$1);
275
+ if (apiName === Table.idFieldName) {
276
+ let cell$1 = change => serialize(change.entityId);
277
+ match = [
278
+ cell$1,
279
+ cell$1
280
+ ];
281
+ } else {
282
+ match = [
283
+ change => serialize(change.entity[apiName]),
284
+ param => absent
285
+ ];
286
+ }
287
+ } else {
288
+ match = Stdlib_JsError.throwWithMessage(`ClickHouse table "` + tableName + `" registered a column "` + name + `" that entity "` + entityConfig.name + `" has no value for`);
289
+ }
290
+ }
291
+ }
292
+ setCells.push(match[0]);
293
+ deleteCells.push(match[1]);
130
294
  });
295
+ Stdlib_Dict.forEachWithKey(fieldsByColumn, (f, column) => {
296
+ if (!registeredNames.has(column)) {
297
+ return Stdlib_JsError.throwWithMessage(`ClickHouse table "` + tableName + `" registered no column for field "` + f.fieldName + `" of entity "` + entityConfig.name + `"`);
298
+ }
299
+ });
300
+ return {
301
+ setCells: setCells,
302
+ deleteCells: deleteCells
303
+ };
131
304
  }
132
305
 
133
- let logger = Logging.createChild({
134
- context: "ClickHouse"
135
- });
136
-
137
- async function insertWithRetry(client, table, values, format, retriesOpt) {
138
- let retries = retriesOpt !== undefined ? retriesOpt : 8;
139
- try {
140
- return await client.insert({
141
- table: table,
142
- values: values,
143
- format: format
144
- });
145
- } catch (raw_exn) {
146
- let exn = Primitive_exceptions.internalToException(raw_exn);
147
- if (retries > 0) {
148
- let delayMs = Math.min(1000, 100 + ((900 * (8 - retries | 0) | 0) / 7 | 0) | 0);
149
- if (values.length > 1) {
150
- Logging.childWarn(logger, {
151
- msg: "ClickHouse insert failed, splitting batch in half and retrying",
152
- table: table,
153
- batchSize: values.length,
154
- retriesLeft: retries,
155
- err: Utils.prettifyExn(exn)
156
- });
157
- await Utils.delay(delayMs);
158
- let mid = (values.length >> 1);
159
- let first = values.slice(0, mid);
160
- let second = values.slice(mid);
161
- await insertWithRetry(client, table, first, format, retries - 1 | 0);
162
- return await insertWithRetry(client, table, second, format, retries - 1 | 0);
163
- }
164
- Logging.childWarn(logger, {
165
- msg: "ClickHouse insert failed, retrying after delay",
166
- table: table,
167
- retriesLeft: retries,
168
- err: Utils.prettifyExn(exn)
169
- });
170
- await Utils.delay(delayMs);
171
- return await insertWithRetry(client, table, values, format, retries - 1 | 0);
306
+ function fillBuilders(table, converters, changes) {
307
+ let rows = changes.length;
308
+ let builders = table.columns.map(__x => ClickHouseSink.makeBuilder(__x, rows));
309
+ let columns = builders.length;
310
+ for (let row = 0; row < rows; ++row) {
311
+ let change = changes[row];
312
+ let match;
313
+ match = change.type === "SET" ? [
314
+ converters.setCells,
315
+ ClickHouseSink.writeValue
316
+ ] : [
317
+ converters.deleteCells,
318
+ ClickHouseSink.writeDeletedValue
319
+ ];
320
+ let write = match[1];
321
+ let cells = match[0];
322
+ for (let column = 0; column < columns; ++column) {
323
+ write(builders[column], row, cells[column](change));
172
324
  }
173
- throw exn;
174
325
  }
326
+ return builders;
175
327
  }
176
328
 
177
- async function setCheckpointsOrThrow(client, batch, database) {
178
- let checkpointsCount = batch.checkpointIds.length;
179
- if (checkpointsCount === 0) {
180
- return;
329
+ function entityTable(sink, registry, entityConfig) {
330
+ let table = registry.entities[entityConfig.name];
331
+ if (table !== undefined) {
332
+ return table;
181
333
  }
182
- let checkpointRows = [];
183
- for (let idx = 0; idx < checkpointsCount; ++idx) {
184
- checkpointRows.push([
185
- batch.checkpointIds[idx].toString(),
186
- batch.checkpointChainIds[idx],
187
- batch.checkpointBlockNumbers[idx],
188
- batch.checkpointBlockHashes[idx],
189
- batch.checkpointEventsProcessed[idx]
190
- ]);
334
+ let spec = entitySpec(entityConfig);
335
+ let table$1 = ClickHouseSink.makeTable(spec.historyTable, sink.registerEntityTable(spec));
336
+ registry.entities[entityConfig.name] = table$1;
337
+ return table$1;
338
+ }
339
+
340
+ function checkpointsTable(sink, registry) {
341
+ let table = registry.checkpoints;
342
+ if (table !== undefined) {
343
+ return table;
191
344
  }
345
+ let table$1 = ClickHouseSink.makeTable(InternalTable.Checkpoints.table.tableName, sink.registerCheckpointsTable(checkpointColumnSpecs));
346
+ registry.checkpoints = table$1;
347
+ return table$1;
348
+ }
349
+
350
+ function stageBuilders(sink, table, builders, rows) {
351
+ return sink.stage(table.handle, rows, builders.map(ClickHouseSink.builderPayload));
352
+ }
353
+
354
+ function stageCheckpointsOrThrow(sink, registry, batch) {
355
+ let rows = batch.checkpointIds.length;
356
+ if (rows === 0) {
357
+ return null;
358
+ }
359
+ let table = checkpointsTable(sink, registry);
192
360
  try {
193
- return await insertWithRetry(client, database + `.\`` + InternalTable.Checkpoints.table.tableName + `\``, checkpointRows, "JSONCompactEachRow", undefined);
361
+ let builders = table.columns.map(__x => ClickHouseSink.makeBuilder(__x, rows));
362
+ builders.forEach((builder, index) => {
363
+ let columnValues = checkpointColumns[index].valuesOf(batch);
364
+ for (let row = 0; row < rows; ++row) {
365
+ ClickHouseSink.writeValue(builder, row, columnValues[row]);
366
+ }
367
+ });
368
+ return stageBuilders(sink, table, builders, rows);
194
369
  } catch (raw_exn) {
195
370
  let exn = Primitive_exceptions.internalToException(raw_exn);
196
371
  throw {
197
372
  RE_EXN_ID: Persistence.StorageError,
198
- message: `Failed to insert checkpoints into ClickHouse table "` + InternalTable.Checkpoints.table.tableName + `"`,
373
+ message: `Failed to convert checkpoints for ClickHouse table "` + table.name + `"`,
199
374
  reason: Utils.prettifyExn(exn),
200
375
  Error: new Error()
201
376
  };
202
377
  }
203
378
  }
204
379
 
205
- async function setUpdatesOrThrow(client, cache, changes, entityConfig, scope, database) {
206
- if (changes.length === 0) {
380
+ function stageUpdatesOrThrow(sink, registry, changes, entityConfig, scope) {
381
+ let rows = changes.length;
382
+ if (rows === 0) {
207
383
  return;
208
384
  }
209
- let chainIdField = Table.getChainIdField(entityConfig.table);
210
- let scopeChainId = Internal.chainScopeChainId(scope);
385
+ let table = entityTable(sink, registry, entityConfig);
386
+ let tableName = table.name;
211
387
  let cacheKey = entityConfig.name + `|` + Internal.chainScopeToString(scope);
212
- let cached = cache[cacheKey];
213
- let match;
214
- if (cached !== undefined) {
215
- match = cached;
216
- } else {
217
- let cached_tableName = database + `.\`` + EntityHistory.historyTableName(entityConfig.name, entityConfig.index) + `\``;
218
- let cached_convertOrThrow = S$RescriptSchema.compile(S$RescriptSchema.array(S$RescriptSchema.union([
219
- EntityHistory.makeSetUpdateSchema(Table.getIdSchema(entityConfig.table), makeClickHouseEntitySchema(entityConfig.table)),
220
- S$RescriptSchema.object(s => {
221
- s.tag(EntityHistory.changeFieldName, "DELETE");
222
- if (chainIdField !== undefined && scopeChainId !== undefined) {
223
- s.tag(Table.getClickHouseDbFieldName(chainIdField), Primitive_option.valFromOption(scopeChainId));
224
- }
225
- return {
226
- type: "DELETE",
227
- entityId: s.f(Table.idFieldName, Table.getIdSchema(entityConfig.table)),
228
- checkpointId: s.f(EntityHistory.checkpointIdFieldName, EntityHistory.unsafeCheckpointIdSchema)
229
- };
230
- })
231
- ])), "Output", "Json", "Sync", false);
232
- let cached$1 = {
233
- tableName: cached_tableName,
234
- convertOrThrow: cached_convertOrThrow
235
- };
236
- cache[cacheKey] = cached$1;
237
- match = cached$1;
238
- }
239
- let tableName = match.tableName;
240
- let changes$1;
241
- if (chainIdField !== undefined && scopeChainId !== undefined) {
242
- let chainId = Primitive_option.valFromOption(scopeChainId);
243
- changes$1 = changes.map(change => {
244
- if (change.type === "SET") {
245
- return {
246
- type: "SET",
247
- entityId: change.entityId,
248
- entity: Internal.stampChainId(change.entity, chainIdField.fieldName, chainId),
249
- checkpointId: change.checkpointId
250
- };
251
- } else {
252
- return change;
253
- }
254
- });
255
- } else {
256
- changes$1 = changes;
257
- }
258
388
  try {
259
- let values = match.convertOrThrow(changes$1);
260
- return await insertWithRetry(client, tableName, values, "JSONEachRow", undefined);
389
+ let cached = registry.converters[cacheKey];
390
+ let converters;
391
+ if (cached !== undefined) {
392
+ converters = cached;
393
+ } else {
394
+ let cached$1 = makeConverters(entityConfig, scope, table);
395
+ registry.converters[cacheKey] = cached$1;
396
+ converters = cached$1;
397
+ }
398
+ let builders = fillBuilders(table, converters, changes);
399
+ return stageBuilders(sink, table, builders, rows);
261
400
  } catch (raw_exn) {
262
401
  let exn = Primitive_exceptions.internalToException(raw_exn);
263
402
  throw {
264
403
  RE_EXN_ID: Persistence.StorageError,
265
- message: `Failed to insert items into ClickHouse table "` + tableName + `"`,
404
+ message: `Failed to convert items for ClickHouse table "` + tableName + `"`,
266
405
  reason: Utils.prettifyExn(exn),
267
406
  Error: new Error()
268
407
  };
269
408
  }
270
409
  }
271
410
 
272
- function onClusterClause(onCluster) {
273
- if (onCluster) {
274
- return ` ON CLUSTER '{cluster}'`;
275
- } else {
276
- return "";
277
- }
278
- }
279
-
280
- function replicatedTableSettingsClause(replicated) {
281
- if (replicated) {
282
- return "\nSETTINGS replicated_deduplication_window = 0";
283
- } else {
284
- return "";
285
- }
286
- }
287
-
288
- function databaseEngineName(engineSpec) {
289
- return engineSpec.trim().split("(")[0].split(" ")[0].trim();
290
- }
291
-
292
- function makeCreateHistoryTableQuery(entityConfig, database, replicatedOpt, onClusterOpt, chainIdModeOpt) {
293
- let replicated = replicatedOpt !== undefined ? replicatedOpt : false;
294
- let onCluster = onClusterOpt !== undefined ? onClusterOpt : false;
295
- let chainIdMode = chainIdModeOpt !== undefined ? chainIdModeOpt : "int32";
296
- let tableEngine = replicated ? "ReplicatedMergeTree" : "MergeTree()";
297
- let fieldDefinitions = Stdlib_Array.filterMap(entityConfig.table.fields, field => {
298
- if (field.TAG !== "Field") {
299
- return;
300
- }
301
- let field$1 = field._0;
302
- let fieldName = Table.getClickHouseDbFieldName(field$1);
303
- let clickHouseType = getClickHouseFieldType(field$1.fieldType, field$1.isNullable, field$1.isArray, chainIdMode);
304
- return `\`` + fieldName + `\` ` + clickHouseType;
305
- });
306
- let options = entityConfig.storage.clickhouseOptions;
307
- let match = options !== undefined ? [
308
- options.partitionBy,
309
- options.orderBy,
310
- options.ttl,
311
- options.skippingIndexes
312
- ] : [
313
- undefined,
314
- undefined,
315
- undefined,
316
- undefined
317
- ];
318
- let skippingIndexes = match[3];
319
- let ttl = match[2];
320
- let orderBy = match[1];
321
- let partitionBy = match[0];
322
- let columnByFieldName = {};
323
- entityConfig.table.fields.forEach(field => {
324
- if (field.TAG !== "Field") {
325
- return;
326
- }
327
- let f = field._0;
328
- columnByFieldName[f.fieldName] = Table.getClickHouseDbFieldName(f);
329
- });
330
- let orderByColumns;
331
- if (orderBy !== undefined) {
332
- let userColumns = orderBy.map(fieldName => {
333
- let column = columnByFieldName[fieldName];
334
- if (column !== undefined) {
335
- return `\`` + column + `\``;
336
- } else {
337
- return Stdlib_JsError.throwWithMessage(`ClickHouse orderBy field "` + fieldName + `" is not defined on entity "` + entityConfig.name + `"`);
338
- }
339
- }).join(", ");
340
- orderByColumns = userColumns + `, ` + EntityHistory.checkpointIdFieldName;
341
- } else {
342
- orderByColumns = Table.idFieldName + `, ` + EntityHistory.checkpointIdFieldName;
411
+ async function writeStagedOrThrow(sink, entities, checkpoints) {
412
+ try {
413
+ return await sink.writeBatch(entities, checkpoints);
414
+ } catch (raw_exn) {
415
+ let exn = Primitive_exceptions.internalToException(raw_exn);
416
+ throw {
417
+ RE_EXN_ID: Persistence.StorageError,
418
+ message: "Failed to write a batch to ClickHouse",
419
+ reason: Utils.prettifyExn(exn),
420
+ Error: new Error()
421
+ };
343
422
  }
344
- let resolveExpressionColumns = expression => expression.replace(/'[^']*'|`[^`]*`|[A-Za-z_][A-Za-z0-9_]*/g, (match, param, param$1) => {
345
- let column = columnByFieldName[match];
346
- if (column !== undefined) {
347
- return `\`` + column + `\``;
348
- } else {
349
- return match;
350
- }
351
- });
352
- let partitionByClause = partitionBy !== undefined ? `\nPARTITION BY ` + resolveExpressionColumns(partitionBy) : "";
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("") : "";
359
- return `CREATE TABLE IF NOT EXISTS ` + database + `.\`` + EntityHistory.historyTableName(entityConfig.name, entityConfig.index) + `\`` + (
360
- onCluster ? ` ON CLUSTER '{cluster}'` : ""
361
- ) + ` (
362
- ` + fieldDefinitions.join(",\n ") + `,
363
- \`` + EntityHistory.checkpointIdFieldName + `\` ` + getClickHouseFieldType("UInt64", false, false, undefined) + `,
364
- \`` + EntityHistory.changeFieldName + `\` ` + getClickHouseFieldType({
365
- type: "Enum",
366
- config: EntityHistory.RowAction.config
367
- }, false, false, undefined) + skippingIndexDefinitions + `
368
- )
369
- ENGINE = ` + tableEngine + partitionByClause + `
370
- ORDER BY (` + orderByColumns + `)` + ttlClause + (
371
- replicated ? "\nSETTINGS replicated_deduplication_window = 0" : ""
372
- );
373
- }
374
-
375
- function makeCreateCheckpointsTableQuery(database, replicatedOpt, onClusterOpt, chainIdModeOpt) {
376
- let replicated = replicatedOpt !== undefined ? replicatedOpt : false;
377
- let onCluster = onClusterOpt !== undefined ? onClusterOpt : false;
378
- let chainIdMode = chainIdModeOpt !== undefined ? chainIdModeOpt : "int32";
379
- let tableEngine = replicated ? "ReplicatedMergeTree" : "MergeTree()";
380
- return `CREATE TABLE IF NOT EXISTS ` + database + `.\`` + InternalTable.Checkpoints.table.tableName + `\`` + (
381
- onCluster ? ` ON CLUSTER '{cluster}'` : ""
382
- ) + ` (
383
- \`` + "id" + `\` ` + getClickHouseFieldType("UInt64", false, false, undefined) + `,
384
- \`` + "chain_id" + `\` ` + getClickHouseFieldType("ChainId", false, false, chainIdMode) + `,
385
- \`` + "block_number" + `\` ` + getClickHouseFieldType("Int32", false, false, undefined) + `,
386
- \`` + "block_hash" + `\` ` + getClickHouseFieldType("String", true, false, undefined) + `,
387
- \`` + "events_processed" + `\` ` + getClickHouseFieldType("UInt64", false, false, undefined) + `
388
- )
389
- ENGINE = ` + tableEngine + `
390
- ORDER BY (` + "id" + `)` + (
391
- replicated ? "\nSETTINGS replicated_deduplication_window = 0" : ""
392
- );
393
423
  }
394
424
 
395
- function makeCreateViewQuery(entityConfig, database, onClusterOpt) {
396
- let onCluster = onClusterOpt !== undefined ? onClusterOpt : false;
397
- let historyTableName = EntityHistory.historyTableName(entityConfig.name, entityConfig.index);
398
- let field = Table.getChainIdField(entityConfig.table);
399
- let dedupKey = (
400
- field !== undefined ? [
401
- Table.idFieldName,
402
- Table.getClickHouseDbFieldName(field)
403
- ] : [Table.idFieldName]
404
- ).map(name => `\`` + name + `\``).join(", ");
405
- let checkpointsTableName = InternalTable.Checkpoints.table.tableName;
406
- let entityFields = Stdlib_Array.filterMap(entityConfig.table.fields, field => {
407
- if (field.TAG !== "Field") {
408
- return;
409
- }
410
- let fieldName = Table.getClickHouseDbFieldName(field._0);
411
- return `\`` + fieldName + `\``;
412
- }).join(", ");
413
- return `CREATE VIEW IF NOT EXISTS ` + database + `.\`` + entityConfig.name + `\`` + (
414
- onCluster ? ` ON CLUSTER '{cluster}'` : ""
415
- ) + ` AS
416
- SELECT ` + entityFields + `
417
- FROM (
418
- SELECT ` + entityFields + `, \`` + EntityHistory.changeFieldName + `\`
419
- FROM ` + database + `.\`` + historyTableName + `\`
420
- WHERE \`` + EntityHistory.checkpointIdFieldName + `\` <= (SELECT max(` + "id" + `) FROM ` + database + `.\`` + checkpointsTableName + `\`)
421
- ORDER BY \`` + EntityHistory.checkpointIdFieldName + `\` DESC
422
- LIMIT 1 BY ` + dedupKey + `
423
- )
424
- WHERE \`` + EntityHistory.changeFieldName + `\` = '` + "SET" + `'`;
425
- }
426
-
427
- async function initialize(client, database, entities, param, chainIdModeOpt) {
428
- let chainIdMode = chainIdModeOpt !== undefined ? chainIdModeOpt : "int32";
425
+ async function initialize(sink, entities) {
429
426
  try {
430
- let databaseEngine = Env.ClickHouse.databaseEngine();
431
- let databaseEngineClause = databaseEngine !== undefined ? ` ENGINE = ` + databaseEngine : "";
432
- let hasReplicatedDatabaseEngine = databaseEngine !== undefined ? databaseEngineName(databaseEngine) === "Replicated" : false;
433
- let envReplicated = Env.ClickHouse.replicated();
434
- let replicated = envReplicated || hasReplicatedDatabaseEngine;
435
- if (hasReplicatedDatabaseEngine && !envReplicated) {
436
- Logging.info("ENVIO_CLICKHOUSE_DATABASE_ENGINE is Replicated; enabling replicated mode so tables use the ReplicatedMergeTree engine.");
437
- }
438
- let databaseOnClusterClause = replicated ? ` ON CLUSTER '{cluster}'` : "";
439
- let ddlOnCluster = replicated && !hasReplicatedDatabaseEngine;
440
- if (databaseEngine !== undefined) {
441
- let expectedEngineName = databaseEngineName(databaseEngine);
442
- let existingResult = await client.query({
443
- query: `SELECT engine FROM system.databases WHERE name = '` + database + `'`
444
- });
445
- let rows = (await existingResult.json()).data;
446
- let row = rows[0];
447
- if (row !== undefined) {
448
- let row$1 = Primitive_option.valFromOption(row);
449
- if (row$1.engine !== expectedEngineName) {
450
- Stdlib_JsError.throwWithMessage(`ClickHouse database "` + database + `" exists with engine "` + row$1.engine + `" but ENVIO_CLICKHOUSE_DATABASE_ENGINE specifies "` + expectedEngineName + `". Drop the database manually to change its engine.`);
451
- }
452
- }
453
- }
454
- if (hasReplicatedDatabaseEngine) {
455
- await client.command({
456
- query: `DROP DATABASE IF EXISTS ` + database + ` ON CLUSTER '{cluster}' SYNC`
457
- });
458
- } else {
459
- await client.command({
460
- query: `TRUNCATE DATABASE IF EXISTS ` + database + (
461
- ddlOnCluster ? ` ON CLUSTER '{cluster}'` : ""
462
- )
463
- });
464
- }
465
- await client.command({
466
- query: `CREATE DATABASE IF NOT EXISTS ` + database + databaseOnClusterClause + databaseEngineClause
467
- });
468
- await Promise.all(entities.map(entityConfig => client.command({
469
- query: makeCreateHistoryTableQuery(entityConfig, database, replicated, ddlOnCluster, chainIdMode)
470
- })));
471
- await client.command({
472
- query: makeCreateCheckpointsTableQuery(database, replicated, ddlOnCluster, chainIdMode)
427
+ await sink.initialize({
428
+ entities: entities.map(entitySpec),
429
+ checkpointColumns: checkpointColumnSpecs,
430
+ replicated: Env.ClickHouse.replicated(),
431
+ databaseEngine: Env.ClickHouse.databaseEngine()
473
432
  });
474
- if (hasReplicatedDatabaseEngine) {
475
- await client.command({
476
- query: `SYSTEM SYNC DATABASE REPLICA ON CLUSTER '{cluster}' ` + database
477
- });
478
- }
479
- await Promise.all(entities.map(entityConfig => client.command({
480
- query: makeCreateViewQuery(entityConfig, database, ddlOnCluster)
481
- })));
482
433
  return Logging.trace("ClickHouse storage initialization completed successfully");
483
434
  } catch (raw_exn) {
484
435
  let exn = Primitive_exceptions.internalToException(raw_exn);
485
436
  Logging.errorWithExn(exn, "Failed to initialize ClickHouse storage");
486
- return Stdlib_JsError.throwWithMessage("ClickHouse initialization failed");
437
+ throw {
438
+ RE_EXN_ID: Persistence.StorageError,
439
+ message: "Failed to initialize ClickHouse storage",
440
+ reason: Utils.prettifyExn(exn),
441
+ Error: new Error()
442
+ };
487
443
  }
488
444
  }
489
445
 
490
- async function resume(client, database, checkpointId) {
446
+ async function resume(sink, checkpointId, chains, entities) {
447
+ let chainProgress = chains.map(chain => ({
448
+ chainId: ChainId.toString(chain.id),
449
+ progressBlockNumber: chain.progressBlockNumber
450
+ }));
491
451
  try {
492
- try {
493
- await client.command({
494
- query: `USE ` + database
495
- });
496
- } catch (raw_exn) {
497
- let exn = Primitive_exceptions.internalToException(raw_exn);
498
- Logging.errorWithExn(exn, `ClickHouse storage database "` + database + `" not found. Please run 'envio start -r' to reinitialize the indexer (it'll also drop Postgres database).`);
499
- Stdlib_JsError.throwWithMessage("ClickHouse resume failed");
500
- }
501
- let tablesResult = await client.query({
502
- query: `SHOW TABLES FROM ` + database + ` LIKE '` + EntityHistory.historyTablePrefix + `%'`
503
- });
504
- let tables = (await tablesResult.json()).data;
505
- await Promise.all(tables.map(table => {
506
- let tableName = table.name;
507
- return client.command({
508
- query: `ALTER TABLE ` + database + `.\`` + tableName + `\` DELETE WHERE \`` + EntityHistory.checkpointIdFieldName + `\` > ` + checkpointId.toString()
509
- });
510
- }));
511
- return await client.command({
512
- query: `DELETE FROM ` + database + `.\`` + InternalTable.Checkpoints.table.tableName + `\` WHERE \`` + Table.idFieldName + `\` > ` + checkpointId.toString()
452
+ return await sink.resume({
453
+ checkpointId: checkpointId.toString(),
454
+ chainProgress: chainProgress,
455
+ historyTables: entities.map(entityConfig => entitySpec(entityConfig).historyTable),
456
+ replicated: Env.ClickHouse.replicated(),
457
+ databaseEngine: Env.ClickHouse.databaseEngine()
513
458
  });
514
- } catch (raw_exn$1) {
515
- let exn$1 = Primitive_exceptions.internalToException(raw_exn$1);
516
- if (exn$1.RE_EXN_ID === Persistence.StorageError) {
517
- throw exn$1;
518
- }
519
- Logging.errorWithExn(exn$1, "Failed to resume ClickHouse storage");
520
- return Stdlib_JsError.throwWithMessage("ClickHouse resume failed");
459
+ } catch (raw_exn) {
460
+ let exn = Primitive_exceptions.internalToException(raw_exn);
461
+ Logging.errorWithExn(exn, "Failed to resume ClickHouse storage");
462
+ throw {
463
+ RE_EXN_ID: Persistence.StorageError,
464
+ message: "Failed to resume ClickHouse storage",
465
+ reason: Utils.prettifyExn(exn),
466
+ Error: new Error()
467
+ };
521
468
  }
522
469
  }
523
470
 
524
471
  export {
525
- getClickHouseFieldType,
472
+ clickHouseFieldSchema,
526
473
  makeClickHouseEntitySchema,
527
474
  logger,
528
- insertWithRetry,
529
- setCheckpointsOrThrow,
530
- setUpdatesOrThrow,
531
- onClusterClause,
532
- replicatedTableSettingsClause,
533
- databaseEngineName,
534
- makeCreateHistoryTableQuery,
535
- makeCreateCheckpointsTableQuery,
536
- makeCreateViewQuery,
475
+ makeSink,
476
+ makeColumnSpec,
477
+ checkpointColumns,
478
+ checkpointColumnSpecs,
479
+ entitySpec,
480
+ makeRegistry,
481
+ entityTable,
482
+ checkpointsTable,
483
+ stageBuilders,
484
+ stageCheckpointsOrThrow,
485
+ stageUpdatesOrThrow,
486
+ writeStagedOrThrow,
537
487
  initialize,
538
488
  resume,
539
489
  }