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.
- package/evm.schema.json +26 -0
- package/fuel.schema.json +26 -0
- package/index.d.ts +35 -3
- package/package.json +6 -7
- package/src/Batch.res +9 -0
- package/src/ChainState.res +35 -39
- package/src/ChainState.res.mjs +14 -11
- package/src/ChainState.resi +15 -7
- package/src/Config.res +52 -44
- package/src/Config.res.mjs +51 -17
- package/src/Core.res +4 -0
- package/src/CrossChainState.res +15 -23
- package/src/CrossChainState.res.mjs +10 -18
- package/src/CrossChainState.resi +4 -1
- package/src/Ecosystem.res +7 -2
- package/src/Envio.res +6 -4
- package/src/EventConfigBuilder.res +169 -26
- package/src/EventConfigBuilder.res.mjs +124 -15
- package/src/HandlerRegister.res +21 -4
- package/src/HandlerRegister.res.mjs +24 -5
- package/src/InMemoryStore.res +9 -7
- package/src/InMemoryStore.res.mjs +3 -3
- package/src/IndexerState.res +43 -6
- package/src/IndexerState.res.mjs +36 -7
- package/src/IndexerState.resi +4 -4
- package/src/Internal.res +16 -17
- package/src/Metrics.res +72 -6
- package/src/Metrics.res.mjs +30 -2
- package/src/Persistence.res +36 -30
- package/src/Persistence.res.mjs +1 -20
- package/src/PgStorage.res +104 -56
- package/src/PgStorage.res.mjs +57 -35
- package/src/PruneStaleHistory.res +85 -52
- package/src/PruneStaleHistory.res.mjs +63 -37
- package/src/Rollback.res +68 -45
- package/src/Rollback.res.mjs +38 -19
- package/src/RollbackCommit.res +7 -9
- package/src/RollbackCommit.res.mjs +5 -6
- package/src/SafeCheckpointTracking.res +11 -11
- package/src/SafeCheckpointTracking.res.mjs +7 -6
- package/src/SimulateItems.res +33 -18
- package/src/SimulateItems.res.mjs +26 -22
- package/src/Sink.res +38 -26
- package/src/Sink.res.mjs +21 -16
- package/src/TestIndexer.res +5 -5
- package/src/TestIndexer.res.mjs +1 -1
- package/src/Utils.res +170 -0
- package/src/Utils.res.mjs +153 -0
- package/src/Writing.res +2 -2
- package/src/Writing.res.mjs +1 -1
- package/src/bindings/ClickHouse.res +366 -698
- package/src/bindings/ClickHouse.res.mjs +371 -421
- package/src/bindings/ClickHouseSink.res +337 -0
- package/src/bindings/ClickHouseSink.res.mjs +246 -0
- package/src/bindings/EventSource.res +8 -2
- package/src/bindings/NodeJs.res +6 -0
- package/src/bindings/Vitest.res +23 -0
- package/src/bindings/Vitest.res.mjs +3 -0
- package/src/bindings/WebSocket.res +9 -10
- package/src/db/CheckpointBounds.res +53 -0
- package/src/db/CheckpointBounds.res.mjs +44 -0
- package/src/db/EntityFilter.res +36 -17
- package/src/db/EntityFilter.res.mjs +25 -14
- package/src/db/EntityHistory.res +47 -22
- package/src/db/EntityHistory.res.mjs +19 -12
- package/src/db/InternalTable.res +84 -35
- package/src/db/InternalTable.res.mjs +56 -23
- package/src/db/RollbackFloors.res +55 -0
- package/src/db/RollbackFloors.res.mjs +92 -0
- package/src/db/Table.res +22 -4
- package/src/db/Table.res.mjs +30 -9
- package/src/sources/Evm.res +2 -0
- package/src/sources/Evm.res.mjs +2 -0
- package/src/sources/EvmHyperSyncSource.res +3 -3
- package/src/sources/EvmHyperSyncSource.res.mjs +3 -3
- package/src/sources/EvmRpcWs.res +124 -0
- package/src/sources/EvmRpcWs.res.mjs +117 -0
- package/src/sources/Fuel.res +2 -0
- package/src/sources/Fuel.res.mjs +2 -0
- package/src/sources/FuelHyperSyncSource.res +1 -1
- package/src/sources/FuelHyperSyncSource.res.mjs +2 -1
- package/src/sources/HeightFeed.res +568 -0
- package/src/sources/HeightFeed.res.mjs +452 -0
- package/src/sources/HeightStream.res +257 -0
- package/src/sources/HeightStream.res.mjs +194 -0
- package/src/sources/HyperSync.res +5 -0
- package/src/sources/HyperSync.res.mjs +3 -0
- package/src/sources/HyperSync.resi +4 -0
- package/src/sources/HyperSyncSSE.res +49 -0
- package/src/sources/HyperSyncSSE.res.mjs +79 -0
- package/src/sources/RpcSource.res +1 -1
- package/src/sources/RpcSource.res.mjs +2 -2
- package/src/sources/Source.res +54 -1
- package/src/sources/Source.res.mjs +25 -0
- package/src/sources/SourceManager.res +220 -227
- package/src/sources/SourceManager.res.mjs +139 -164
- package/src/sources/SourceManager.resi +8 -0
- package/src/sources/Svm.res +2 -0
- package/src/sources/Svm.res.mjs +2 -0
- package/src/sources/SvmHyperSyncClient.res +70 -127
- package/src/sources/SvmHyperSyncClient.res.mjs +51 -25
- package/src/sources/SvmHyperSyncSource.res +18 -15
- package/src/sources/SvmHyperSyncSource.res.mjs +5 -4
- package/svm.schema.json +1 -75
- package/src/MemoryStorage.res +0 -729
- package/src/MemoryStorage.res.mjs +0 -586
- package/src/sources/HyperSyncHeightStream.res +0 -129
- package/src/sources/HyperSyncHeightStream.res.mjs +0 -110
- package/src/sources/RpcWebSocketHeightStream.res +0 -175
- package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -180
|
@@ -1,169 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
password?: string,
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
type execParams = {query: string}
|
|
13
|
-
|
|
14
|
-
@module("@clickhouse/client")
|
|
15
|
-
external createClient: clientConfig => client = "createClient"
|
|
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.
|
|
22
|
-
@send
|
|
23
|
-
external command: (client, execParams) => promise<unit> = "command"
|
|
24
|
-
|
|
25
|
-
@send
|
|
26
|
-
external close: client => promise<unit> = "close"
|
|
27
|
-
|
|
28
|
-
type insertParams<'a> = {
|
|
29
|
-
table: string,
|
|
30
|
-
values: array<'a>,
|
|
31
|
-
format: string,
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
@send
|
|
35
|
-
external insert: (client, insertParams<'a>) => promise<unit> = "insert"
|
|
36
|
-
|
|
37
|
-
type queryParams = {query: string}
|
|
38
|
-
type queryResult<'a>
|
|
39
|
-
|
|
40
|
-
@send
|
|
41
|
-
external query: (client, queryParams) => promise<queryResult<'a>> = "query"
|
|
42
|
-
|
|
43
|
-
// The default `JSON` query format resolves to a `ResponseJSON` wrapper whose
|
|
44
|
-
// rows live under `data`, not at the top level.
|
|
45
|
-
@send
|
|
46
|
-
external json: queryResult<'a> => promise<{"data": array<'a>}> = "json"
|
|
47
|
-
|
|
48
|
-
let getClickHouseFieldType = (
|
|
49
|
-
~fieldType: Table.fieldType,
|
|
50
|
-
~isNullable: bool,
|
|
51
|
-
~isArray: bool,
|
|
52
|
-
~chainIdMode: ChainId.mode=Int32,
|
|
53
|
-
): string => {
|
|
54
|
-
let baseType = switch fieldType {
|
|
55
|
-
| Int32 => "Int32"
|
|
56
|
-
| ChainId =>
|
|
57
|
-
switch chainIdMode {
|
|
58
|
-
| Int32 => "Int32"
|
|
59
|
-
| Int64 => "UInt64"
|
|
60
|
-
}
|
|
61
|
-
| Uint32 => "UInt32"
|
|
62
|
-
| UInt52 => "UInt64"
|
|
63
|
-
// Internal-only column types, never on an entity the sink mirrors.
|
|
64
|
-
| SmallInt
|
|
65
|
-
| Bytea =>
|
|
66
|
-
JsError.throwWithMessage(
|
|
67
|
-
"ClickHouse doesn't support the internal SmallInt and Bytea column types",
|
|
68
|
-
)
|
|
69
|
-
| UInt64 => "UInt64"
|
|
70
|
-
| Serial => "Int32"
|
|
71
|
-
| BigSerial => "Int64"
|
|
72
|
-
| BigInt({?precision}) =>
|
|
73
|
-
switch precision {
|
|
74
|
-
| None => "String" // Fallback for unbounded BigInt
|
|
75
|
-
| Some(precision) =>
|
|
76
|
-
if precision > 38 {
|
|
77
|
-
"String"
|
|
78
|
-
} else {
|
|
79
|
-
`Decimal(${precision->Int.toString},0)`
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
| BigDecimal({?config}) =>
|
|
83
|
-
switch config {
|
|
84
|
-
| None => "String" // Fallback for unbounded BigDecimal
|
|
85
|
-
| Some((precision, scale)) =>
|
|
86
|
-
if precision > 38 || scale > precision {
|
|
87
|
-
"String"
|
|
88
|
-
} else {
|
|
89
|
-
`Decimal(${precision->Int.toString},${scale->Int.toString})`
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
| Boolean => "Bool"
|
|
93
|
-
| Number => "Float64"
|
|
94
|
-
| String => "String"
|
|
95
|
-
| Json => "String"
|
|
96
|
-
| Date => "DateTime64(3, 'UTC')"
|
|
97
|
-
| Enum({config}) => {
|
|
98
|
-
let variantsLength = config.variants->Array.length
|
|
99
|
-
// Theoretically we can store 256 variants in Enum8,
|
|
100
|
-
// but it'd require to explicitly start with a negative index (probably)
|
|
101
|
-
let enumType = variantsLength <= 127 ? "Enum8" : "Enum16"
|
|
102
|
-
let enumValues =
|
|
103
|
-
config.variants
|
|
104
|
-
->Array.map(variant => {
|
|
105
|
-
let variantStr = variant->(Utils.magic: 'a => string)
|
|
106
|
-
`'${variantStr}'`
|
|
107
|
-
})
|
|
108
|
-
->Array.joinUnsafe(", ")
|
|
109
|
-
`${enumType}(${enumValues})`
|
|
1
|
+
let clickHouseFieldSchema = (f: Table.field) => {
|
|
2
|
+
let shaped = baseSchema =>
|
|
3
|
+
if f.isNullable {
|
|
4
|
+
Utils.Schema.nullTolerant(baseSchema)->S.toUnknown
|
|
5
|
+
} else if f.isArray {
|
|
6
|
+
S.array(baseSchema)->S.toUnknown
|
|
7
|
+
} else {
|
|
8
|
+
baseSchema
|
|
110
9
|
}
|
|
10
|
+
switch f.fieldType {
|
|
11
|
+
| Date => shaped(Utils.Schema.clickHouseDate->S.toUnknown)
|
|
12
|
+
| Bytea => shaped(S.json(~validate=false)->S.toUnknown)
|
|
13
|
+
| ChainId => ChainId.schema->S.toUnknown
|
|
14
|
+
| Json if !f.isArray => Utils.Schema.clickHouseJson->S.toUnknown
|
|
15
|
+
| _ => f.fieldSchema
|
|
111
16
|
}
|
|
112
|
-
|
|
113
|
-
let baseType = if isArray {
|
|
114
|
-
`Array(${baseType})`
|
|
115
|
-
} else {
|
|
116
|
-
baseType
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
isNullable ? `Nullable(${baseType})` : baseType
|
|
120
17
|
}
|
|
121
18
|
|
|
122
|
-
// Creates an entity schema from table definition, using clickHouseDate for Date fields.
|
|
123
|
-
// Serialized keys are the db column names, while the entity values are keyed
|
|
124
|
-
// by API field names (they only differ when column renaming is configured).
|
|
125
19
|
let makeClickHouseEntitySchema = (table: Table.table): S.t<Internal.entity> => {
|
|
126
20
|
S.object(s => {
|
|
127
21
|
let dict = Dict.make()
|
|
128
22
|
table.fields->Array.forEach(field => {
|
|
129
23
|
switch field {
|
|
130
|
-
| Field(f) =>
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
S.null(dateSchema)->S.toUnknown
|
|
137
|
-
} else if f.isArray {
|
|
138
|
-
S.array(dateSchema)->S.toUnknown
|
|
139
|
-
} else {
|
|
140
|
-
dateSchema
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
| ChainId => ChainId.schema->S.toUnknown
|
|
144
|
-
// ClickHouse returns UInt64 values as strings, need to parse to float
|
|
145
|
-
| UInt52 => {
|
|
146
|
-
let uint52Schema =
|
|
147
|
-
S.float
|
|
148
|
-
->S.preprocess(
|
|
149
|
-
_ => {
|
|
150
|
-
parser: unknown => unknown->(Utils.magic: unknown => string)->Float.parseFloat,
|
|
151
|
-
},
|
|
152
|
-
)
|
|
153
|
-
->S.toUnknown
|
|
154
|
-
if f.isNullable {
|
|
155
|
-
S.null(uint52Schema)->S.toUnknown
|
|
156
|
-
} else if f.isArray {
|
|
157
|
-
S.array(uint52Schema)->S.toUnknown
|
|
158
|
-
} else {
|
|
159
|
-
uint52Schema
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
| _ => f.fieldSchema
|
|
163
|
-
}
|
|
164
|
-
dict->Dict.set(f->Table.getApiFieldName, s.field(fieldName, fieldSchema))
|
|
165
|
-
}
|
|
166
|
-
| DerivedFrom(_) => () // Skip derived fields
|
|
24
|
+
| Field(f) =>
|
|
25
|
+
dict->Dict.set(
|
|
26
|
+
f->Table.getApiFieldName,
|
|
27
|
+
s.field(f->Table.getClickHouseDbFieldName, clickHouseFieldSchema(f)),
|
|
28
|
+
)
|
|
29
|
+
| DerivedFrom(_) => ()
|
|
167
30
|
}
|
|
168
31
|
})
|
|
169
32
|
dict->(Utils.magic: dict<unknown> => Internal.entity)
|
|
@@ -172,604 +35,409 @@ let makeClickHouseEntitySchema = (table: Table.table): S.t<Internal.entity> => {
|
|
|
172
35
|
|
|
173
36
|
let logger = Logging.createChild(~params={"context": "ClickHouse"})
|
|
174
37
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
~
|
|
182
|
-
~
|
|
183
|
-
~
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
38
|
+
let makeSink = (~host, ~username, ~password, ~database, ~chainIdMode) =>
|
|
39
|
+
ClickHouseSink.make(~url=host, ~username, ~password, ~database, ~chainIdMode, ~onWarning=msg =>
|
|
40
|
+
logger->Logging.childWarn({"msg": msg})
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
let makeColumnSpec = (
|
|
44
|
+
~name,
|
|
45
|
+
~fieldName=name,
|
|
46
|
+
~fieldType: Table.fieldType,
|
|
47
|
+
~isNullable=false,
|
|
48
|
+
~isArray=false,
|
|
49
|
+
): ClickHouseSink.columnSpec => {
|
|
50
|
+
name,
|
|
51
|
+
fieldName,
|
|
52
|
+
fieldType: switch fieldType {
|
|
53
|
+
| String => "String"
|
|
54
|
+
| Boolean => "Boolean"
|
|
55
|
+
| Uint32 => "Uint32"
|
|
56
|
+
| UInt52 => "UInt52"
|
|
57
|
+
| SmallInt =>
|
|
58
|
+
JsError.throwWithMessage("ClickHouse doesn't support the internal SmallInt column type")
|
|
59
|
+
| Bytea => "Bytea"
|
|
60
|
+
| UInt64 => "UInt64"
|
|
61
|
+
| Int32 => "Int32"
|
|
62
|
+
| ChainId => "ChainId"
|
|
63
|
+
| Number => "Number"
|
|
64
|
+
| Serial => "Serial"
|
|
65
|
+
| BigSerial => "BigSerial"
|
|
66
|
+
| Json => "Json"
|
|
67
|
+
| Date => "Date"
|
|
68
|
+
| BigInt(_) => "BigInt"
|
|
69
|
+
| BigDecimal(_) => "BigDecimal"
|
|
70
|
+
| Enum(_) => "Enum"
|
|
71
|
+
},
|
|
72
|
+
isNullable,
|
|
73
|
+
isArray,
|
|
74
|
+
precision: ?switch fieldType {
|
|
75
|
+
| BigInt({?precision}) => precision
|
|
76
|
+
| BigDecimal({?config}) => config->Option.map(((precision, _)) => precision)
|
|
77
|
+
| _ => None
|
|
78
|
+
},
|
|
79
|
+
scale: ?switch fieldType {
|
|
80
|
+
| BigDecimal({?config}) => config->Option.map(((_, scale)) => scale)
|
|
81
|
+
| _ => None
|
|
82
|
+
},
|
|
83
|
+
enumVariants: ?switch fieldType {
|
|
84
|
+
| Enum({config}) =>
|
|
85
|
+
Some(config.variants->Array.map(variant => variant->(Utils.magic: Table.enum => string)))
|
|
86
|
+
| _ => None
|
|
87
|
+
},
|
|
215
88
|
}
|
|
216
89
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
} else {
|
|
222
|
-
// Convert columnar data to row format for JSONCompactEachRow
|
|
223
|
-
let checkpointRows = []
|
|
224
|
-
for idx in 0 to checkpointsCount - 1 {
|
|
225
|
-
checkpointRows
|
|
226
|
-
->Array.push((
|
|
227
|
-
batch.checkpointIds->Array.getUnsafe(idx)->BigInt.toString,
|
|
228
|
-
batch.checkpointChainIds->Array.getUnsafe(idx),
|
|
229
|
-
batch.checkpointBlockNumbers->Array.getUnsafe(idx),
|
|
230
|
-
batch.checkpointBlockHashes->Array.getUnsafe(idx),
|
|
231
|
-
batch.checkpointEventsProcessed->Array.getUnsafe(idx),
|
|
232
|
-
))
|
|
233
|
-
->ignore
|
|
234
|
-
}
|
|
90
|
+
type checkpointColumn = {
|
|
91
|
+
spec: ClickHouseSink.columnSpec,
|
|
92
|
+
valuesOf: Batch.t => array<unknown>,
|
|
93
|
+
}
|
|
235
94
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
)
|
|
251
|
-
}
|
|
95
|
+
// Registration and staging both read this one list, so the column a batch's
|
|
96
|
+
// values are written to is the column that was registered in its place.
|
|
97
|
+
let checkpointColumns = InternalTable.Checkpoints.columns->Array.filterMap(({
|
|
98
|
+
field,
|
|
99
|
+
clickHouseFieldType,
|
|
100
|
+
valuesOf,
|
|
101
|
+
}) =>
|
|
102
|
+
switch field {
|
|
103
|
+
| Table.Field({fieldName, isNullable}) =>
|
|
104
|
+
Some({
|
|
105
|
+
spec: makeColumnSpec(~name=fieldName, ~fieldType=clickHouseFieldType, ~isNullable),
|
|
106
|
+
valuesOf,
|
|
107
|
+
})
|
|
108
|
+
| DerivedFrom(_) => None
|
|
252
109
|
}
|
|
253
|
-
|
|
110
|
+
)
|
|
254
111
|
|
|
255
|
-
|
|
256
|
-
tableName: string,
|
|
257
|
-
convertOrThrow: array<Change.t<Internal.entity>> => array<JSON.t>,
|
|
258
|
-
}
|
|
112
|
+
let checkpointColumnSpecs = checkpointColumns->Array.map(({spec}) => spec)
|
|
259
113
|
|
|
260
|
-
let
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
)
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
| None =>
|
|
279
|
-
let cached: setUpdatesCache = {
|
|
280
|
-
tableName: `${database}.\`${EntityHistory.historyTableName(
|
|
281
|
-
~entityName=entityConfig.name,
|
|
282
|
-
~entityIndex=entityConfig.index,
|
|
283
|
-
)}\``,
|
|
284
|
-
convertOrThrow: S.compile(
|
|
285
|
-
S.array(
|
|
286
|
-
S.union([
|
|
287
|
-
EntityHistory.makeSetUpdateSchema(
|
|
288
|
-
~idSchema=entityConfig.table->Table.getIdSchema,
|
|
289
|
-
makeClickHouseEntitySchema(entityConfig.table),
|
|
290
|
-
),
|
|
291
|
-
S.object(s => {
|
|
292
|
-
s.tag(EntityHistory.changeFieldName, EntityHistory.RowAction.DELETE)
|
|
293
|
-
switch (chainIdField, scopeChainId) {
|
|
294
|
-
| (Some(field), Some(chainId)) =>
|
|
295
|
-
s.tag(field->Table.getClickHouseDbFieldName, chainId)
|
|
296
|
-
| _ => ()
|
|
297
|
-
}
|
|
298
|
-
Change.Delete({
|
|
299
|
-
entityId: s.field(Table.idFieldName, entityConfig.table->Table.getIdSchema),
|
|
300
|
-
checkpointId: s.field(
|
|
301
|
-
EntityHistory.checkpointIdFieldName,
|
|
302
|
-
EntityHistory.unsafeCheckpointIdSchema,
|
|
303
|
-
),
|
|
304
|
-
})
|
|
305
|
-
}),
|
|
306
|
-
]),
|
|
114
|
+
let entitySpec = (~entityConfig: Internal.entityConfig): ClickHouseSink.entitySpec => {
|
|
115
|
+
let options = entityConfig.storage.clickhouseOptions
|
|
116
|
+
{
|
|
117
|
+
name: entityConfig.name,
|
|
118
|
+
historyTable: EntityHistory.historyTableName(
|
|
119
|
+
~entityName=entityConfig.name,
|
|
120
|
+
~entityIndex=entityConfig.index,
|
|
121
|
+
),
|
|
122
|
+
columns: entityConfig.table.fields->Array.filterMap(field =>
|
|
123
|
+
switch field {
|
|
124
|
+
| Table.Field(f) =>
|
|
125
|
+
Some(
|
|
126
|
+
makeColumnSpec(
|
|
127
|
+
~name=f->Table.getClickHouseDbFieldName,
|
|
128
|
+
~fieldName=f.fieldName,
|
|
129
|
+
~fieldType=f.fieldType,
|
|
130
|
+
~isNullable=f.isNullable,
|
|
131
|
+
~isArray=f.isArray,
|
|
307
132
|
),
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
~typeValidation=false,
|
|
311
|
-
~mode=Sync,
|
|
312
|
-
)->(
|
|
313
|
-
Utils.magic: (array<Change.t<Internal.entity>> => JSON.t) => array<
|
|
314
|
-
Change.t<Internal.entity>,
|
|
315
|
-
> => array<JSON.t>
|
|
316
|
-
),
|
|
133
|
+
)
|
|
134
|
+
| DerivedFrom(_) => None
|
|
317
135
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
136
|
+
),
|
|
137
|
+
chainIdColumn: ?(
|
|
138
|
+
entityConfig.table->Table.getChainIdField->Option.map(Table.getClickHouseDbFieldName)
|
|
139
|
+
),
|
|
140
|
+
partitionBy: ?(options->Option.flatMap(options => options.partitionBy)),
|
|
141
|
+
orderBy: ?(options->Option.flatMap(options => options.orderBy)),
|
|
142
|
+
ttl: ?(options->Option.flatMap(options => options.ttl)),
|
|
143
|
+
skippingIndexes: ?(
|
|
144
|
+
options->Option.flatMap(options =>
|
|
145
|
+
options.skippingIndexes->Option.map(indexes =>
|
|
146
|
+
indexes->Array.map(
|
|
147
|
+
(index): ClickHouseSink.skippingIndexSpec => {
|
|
148
|
+
name: index.name,
|
|
149
|
+
expr: index.expr,
|
|
150
|
+
indexType: index.type_,
|
|
151
|
+
granularity: ?index.granularity,
|
|
152
|
+
},
|
|
153
|
+
)
|
|
154
|
+
)
|
|
334
155
|
)
|
|
335
|
-
|
|
336
|
-
|
|
156
|
+
),
|
|
157
|
+
}
|
|
158
|
+
}
|
|
337
159
|
|
|
338
|
-
|
|
339
|
-
// The entity history table is the source of truth for ClickHouse, so every
|
|
340
|
-
// intermediate change must be persisted, not only the current value.
|
|
341
|
-
let values = changes->convertOrThrow
|
|
160
|
+
type cell = Change.t<Internal.entity> => unknown
|
|
342
161
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
throw(
|
|
347
|
-
Persistence.StorageError({
|
|
348
|
-
message: `Failed to insert items into ClickHouse table "${tableName}"`,
|
|
349
|
-
reason: exn->Utils.prettifyExn,
|
|
350
|
-
}),
|
|
351
|
-
)
|
|
352
|
-
}
|
|
353
|
-
}
|
|
162
|
+
type converters = {
|
|
163
|
+
setCells: array<cell>,
|
|
164
|
+
deleteCells: array<cell>,
|
|
354
165
|
}
|
|
355
166
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
// own log, and combining it with ON CLUSTER is rejected/double-applied — so
|
|
362
|
-
// table-level DDL must carry the clause only in the plain-database case.
|
|
363
|
-
// The '{cluster}' macro resolves to each node's configured cluster name.
|
|
364
|
-
let onClusterClause = (~onCluster: bool) => onCluster ? ` ON CLUSTER '{cluster}'` : ""
|
|
167
|
+
type registry = {
|
|
168
|
+
entities: dict<ClickHouseSink.table>,
|
|
169
|
+
mutable checkpoints: option<ClickHouseSink.table>,
|
|
170
|
+
converters: dict<converters>,
|
|
171
|
+
}
|
|
365
172
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
// correct here, and the duplicates dedup would have caught are already collapsed
|
|
372
|
-
// by the entity view's `LIMIT 1 BY`. Plain MergeTree goes by
|
|
373
|
-
// non_replicated_deduplication_window (0 by default), so it needs no clause.
|
|
374
|
-
let replicatedTableSettingsClause = (~replicated: bool) =>
|
|
375
|
-
replicated ? "\nSETTINGS replicated_deduplication_window = 0" : ""
|
|
173
|
+
let makeRegistry = () => {
|
|
174
|
+
entities: Dict.make(),
|
|
175
|
+
checkpoints: None,
|
|
176
|
+
converters: Dict.make(),
|
|
177
|
+
}
|
|
376
178
|
|
|
377
|
-
|
|
378
|
-
// get the bare engine name, e.g. `Replicated('/p','{shard}','{replica}') SETTINGS x=1`
|
|
379
|
-
// and `Replicated SETTINGS x=1` both yield `Replicated`.
|
|
380
|
-
let databaseEngineName = (engineSpec: string) =>
|
|
381
|
-
engineSpec
|
|
382
|
-
->String.trim
|
|
383
|
-
->String.split("(")
|
|
384
|
-
->Array.getUnsafe(0)
|
|
385
|
-
->String.split(" ")
|
|
386
|
-
->Array.getUnsafe(0)
|
|
387
|
-
->String.trim
|
|
179
|
+
@get external changeEntity: Change.t<Internal.entity> => dict<unknown> = "entity"
|
|
388
180
|
|
|
389
|
-
|
|
390
|
-
let makeCreateHistoryTableQuery = (
|
|
391
|
-
~entityConfig: Internal.entityConfig,
|
|
392
|
-
~database: string,
|
|
393
|
-
~replicated: bool=false,
|
|
394
|
-
~onCluster: bool=false,
|
|
395
|
-
~chainIdMode: ChainId.mode=Int32,
|
|
396
|
-
) => {
|
|
397
|
-
let tableEngine = replicated ? "ReplicatedMergeTree" : "MergeTree()"
|
|
398
|
-
let fieldDefinitions = entityConfig.table.fields->Array.filterMap(field => {
|
|
399
|
-
switch field {
|
|
400
|
-
| Field(field) =>
|
|
401
|
-
Some({
|
|
402
|
-
let fieldName = field->Table.getClickHouseDbFieldName
|
|
403
|
-
let clickHouseType = getClickHouseFieldType(
|
|
404
|
-
~fieldType=field.fieldType,
|
|
405
|
-
~isNullable=field.isNullable,
|
|
406
|
-
~isArray=field.isArray,
|
|
407
|
-
~chainIdMode,
|
|
408
|
-
)
|
|
409
|
-
`\`${fieldName}\` ${clickHouseType}`
|
|
410
|
-
})
|
|
411
|
-
| DerivedFrom(_) => None
|
|
412
|
-
}
|
|
413
|
-
})
|
|
181
|
+
%%private(let absent: unknown = %raw(`undefined`))
|
|
414
182
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
183
|
+
%%private(
|
|
184
|
+
let compileSerializer = schema =>
|
|
185
|
+
S.compile(schema, ~input=Value, ~output=Json, ~typeValidation=false, ~mode=Sync)->(
|
|
186
|
+
Utils.magic: (unknown => JSON.t) => unknown => unknown
|
|
187
|
+
)
|
|
188
|
+
)
|
|
419
189
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
190
|
+
%%private(
|
|
191
|
+
let makeConverters = (
|
|
192
|
+
~entityConfig: Internal.entityConfig,
|
|
193
|
+
~scope: Internal.chainScope,
|
|
194
|
+
~table: ClickHouseSink.table,
|
|
195
|
+
): converters => {
|
|
196
|
+
let {name: tableName, columns} = table
|
|
197
|
+
let sourceTable = entityConfig.table
|
|
198
|
+
let chainIdTag = switch (
|
|
199
|
+
sourceTable->Table.getChainIdField,
|
|
200
|
+
scope->Internal.chainScopeChainId,
|
|
201
|
+
) {
|
|
202
|
+
| (Some(field), Some(chainId)) =>
|
|
203
|
+
Some((field->Table.getClickHouseDbFieldName, chainId->(Utils.magic: ChainId.t => unknown)))
|
|
204
|
+
| _ => None
|
|
429
205
|
}
|
|
430
|
-
|
|
206
|
+
let fieldsByColumn = Dict.make()
|
|
207
|
+
sourceTable.fields->Array.forEach(field =>
|
|
208
|
+
switch field {
|
|
209
|
+
| Table.Field(f) => fieldsByColumn->Dict.set(f->Table.getClickHouseDbFieldName, f)
|
|
210
|
+
| DerivedFrom(_) => ()
|
|
211
|
+
}
|
|
212
|
+
)
|
|
431
213
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
214
|
+
let setCells = []
|
|
215
|
+
let deleteCells = []
|
|
216
|
+
let registeredNames = columns->Array.map(column => column.name)->Utils.Set.fromArray
|
|
217
|
+
columns->Array.forEach(({name}) => {
|
|
218
|
+
let (set, delete) = if name === EntityHistory.checkpointIdFieldName {
|
|
219
|
+
let cell = change => change->Change.getCheckpointId->(Utils.magic: bigint => unknown)
|
|
220
|
+
(cell, cell)
|
|
221
|
+
} else if name === EntityHistory.changeFieldName {
|
|
222
|
+
(
|
|
223
|
+
_ => (EntityHistory.RowAction.SET :> string)->(Utils.magic: string => unknown),
|
|
224
|
+
_ => (EntityHistory.RowAction.DELETE :> string)->(Utils.magic: string => unknown),
|
|
225
|
+
)
|
|
226
|
+
} else {
|
|
227
|
+
switch (chainIdTag, fieldsByColumn->Utils.Dict.dangerouslyGetNonOption(name)) {
|
|
228
|
+
| (Some((column, chainId)), _) if column === name => (_ => chainId, _ => chainId)
|
|
229
|
+
| (_, Some(f)) =>
|
|
230
|
+
let serialize = compileSerializer(clickHouseFieldSchema(f))
|
|
231
|
+
let apiName = f->Table.getApiFieldName
|
|
232
|
+
if apiName === Table.idFieldName {
|
|
233
|
+
let cell = change =>
|
|
234
|
+
serialize(change->Change.getEntityId->(Utils.magic: EntityId.t => unknown))
|
|
235
|
+
(cell, cell)
|
|
236
|
+
} else {
|
|
237
|
+
(change => serialize(change->changeEntity->Dict.getUnsafe(apiName)), _ => absent)
|
|
238
|
+
}
|
|
239
|
+
| (_, None) =>
|
|
446
240
|
JsError.throwWithMessage(
|
|
447
|
-
`ClickHouse
|
|
241
|
+
`ClickHouse table "${tableName}" registered a column "${name}" that entity "${entityConfig.name}" has no value for`,
|
|
448
242
|
)
|
|
449
243
|
}
|
|
450
|
-
|
|
451
|
-
->Array.
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
244
|
+
}
|
|
245
|
+
setCells->Array.push(set)
|
|
246
|
+
deleteCells->Array.push(delete)
|
|
247
|
+
})
|
|
455
248
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
expression->String.replaceRegExpBy0Unsafe(/'[^']*'|`[^`]*`|[A-Za-z_][A-Za-z0-9_]*/g, (
|
|
462
|
-
~match,
|
|
463
|
-
~offset as _,
|
|
464
|
-
~input as _,
|
|
465
|
-
) =>
|
|
466
|
-
switch columnByFieldName->Dict.get(match) {
|
|
467
|
-
| Some(column) => `\`${column}\``
|
|
468
|
-
| None => match
|
|
249
|
+
fieldsByColumn->Dict.forEachWithKey((f, column) =>
|
|
250
|
+
if !(registeredNames->Utils.Set.has(column)) {
|
|
251
|
+
JsError.throwWithMessage(
|
|
252
|
+
`ClickHouse table "${tableName}" registered no column for field "${f.fieldName}" of entity "${entityConfig.name}"`,
|
|
253
|
+
)
|
|
469
254
|
}
|
|
470
255
|
)
|
|
471
256
|
|
|
472
|
-
|
|
473
|
-
| Some(expression) => `\nPARTITION BY ${expression->resolveExpressionColumns}`
|
|
474
|
-
| None => ""
|
|
475
|
-
}
|
|
476
|
-
let ttlClause = switch ttl {
|
|
477
|
-
| Some(expression) => `\nTTL ${expression->resolveExpressionColumns}`
|
|
478
|
-
| None => ""
|
|
257
|
+
{setCells, deleteCells}
|
|
479
258
|
}
|
|
259
|
+
)
|
|
480
260
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
261
|
+
%%private(
|
|
262
|
+
let fillBuilders = (
|
|
263
|
+
~table: ClickHouseSink.table,
|
|
264
|
+
~converters,
|
|
265
|
+
~changes: array<Change.t<Internal.entity>>,
|
|
266
|
+
) => {
|
|
267
|
+
let rows = changes->Array.length
|
|
268
|
+
let builders = table.columns->Array.map(ClickHouseSink.makeBuilder(_, ~rows))
|
|
269
|
+
let columns = builders->Array.length
|
|
270
|
+
for row in 0 to rows - 1 {
|
|
271
|
+
let change = changes->Array.getUnsafe(row)
|
|
272
|
+
let (cells, write) = switch change {
|
|
273
|
+
| Change.Set(_) => (converters.setCells, ClickHouseSink.writeValue)
|
|
274
|
+
| Delete(_) => (converters.deleteCells, ClickHouseSink.writeDeletedValue)
|
|
490
275
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
276
|
+
for column in 0 to columns - 1 {
|
|
277
|
+
builders->Array.getUnsafe(column)->write(~row, (cells->Array.getUnsafe(column))(change))
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
builders
|
|
495
281
|
}
|
|
496
|
-
|
|
497
|
-
`CREATE TABLE IF NOT EXISTS ${database}.\`${EntityHistory.historyTableName(
|
|
498
|
-
~entityName=entityConfig.name,
|
|
499
|
-
~entityIndex=entityConfig.index,
|
|
500
|
-
)}\`${onClusterClause(~onCluster)} (
|
|
501
|
-
${fieldDefinitions->Array.joinUnsafe(",\n ")},
|
|
502
|
-
\`${EntityHistory.checkpointIdFieldName}\` ${getClickHouseFieldType(
|
|
503
|
-
~fieldType=UInt64,
|
|
504
|
-
~isNullable=false,
|
|
505
|
-
~isArray=false,
|
|
506
|
-
)},
|
|
507
|
-
\`${EntityHistory.changeFieldName}\` ${getClickHouseFieldType(
|
|
508
|
-
~fieldType=Enum({config: EntityHistory.RowAction.config->Table.fromGenericEnumConfig}),
|
|
509
|
-
~isNullable=false,
|
|
510
|
-
~isArray=false,
|
|
511
|
-
)}${skippingIndexDefinitions}
|
|
512
282
|
)
|
|
513
|
-
ENGINE = ${tableEngine}${partitionByClause}
|
|
514
|
-
ORDER BY (${orderByColumns})${ttlClause}${replicatedTableSettingsClause(~replicated)}`
|
|
515
|
-
}
|
|
516
283
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
let eventsProcessedField = (#events_processed: InternalTable.Checkpoints.field :> string)
|
|
284
|
+
let entityTable = (sink, ~registry, ~entityConfig: Internal.entityConfig) =>
|
|
285
|
+
switch registry.entities->Utils.Dict.dangerouslyGetNonOption(entityConfig.name) {
|
|
286
|
+
| Some(table) => table
|
|
287
|
+
| None =>
|
|
288
|
+
let spec = entitySpec(~entityConfig)
|
|
289
|
+
let table =
|
|
290
|
+
sink
|
|
291
|
+
->ClickHouseSink.registerEntityTable(spec)
|
|
292
|
+
->ClickHouseSink.makeTable(~name=spec.historyTable)
|
|
293
|
+
registry.entities->Dict.set(entityConfig.name, table)
|
|
294
|
+
table
|
|
295
|
+
}
|
|
530
296
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
~
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
~fieldType=Int32,
|
|
543
|
-
~isNullable=false,
|
|
544
|
-
~isArray=false,
|
|
545
|
-
)},
|
|
546
|
-
\`${blockHashField}\` ${getClickHouseFieldType(
|
|
547
|
-
~fieldType=String,
|
|
548
|
-
~isNullable=true,
|
|
549
|
-
~isArray=false,
|
|
550
|
-
)},
|
|
551
|
-
\`${eventsProcessedField}\` ${getClickHouseFieldType(
|
|
552
|
-
~fieldType=UInt64,
|
|
553
|
-
~isNullable=false,
|
|
554
|
-
~isArray=false,
|
|
555
|
-
)}
|
|
556
|
-
)
|
|
557
|
-
ENGINE = ${tableEngine}
|
|
558
|
-
ORDER BY (${idField})${replicatedTableSettingsClause(~replicated)}`
|
|
559
|
-
}
|
|
297
|
+
let checkpointsTable = (sink, ~registry) =>
|
|
298
|
+
switch registry.checkpoints {
|
|
299
|
+
| Some(table) => table
|
|
300
|
+
| None =>
|
|
301
|
+
let table =
|
|
302
|
+
sink
|
|
303
|
+
->ClickHouseSink.registerCheckpointsTable(checkpointColumnSpecs)
|
|
304
|
+
->ClickHouseSink.makeTable(~name=InternalTable.Checkpoints.table.tableName)
|
|
305
|
+
registry.checkpoints = Some(table)
|
|
306
|
+
table
|
|
307
|
+
}
|
|
560
308
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
) => {
|
|
567
|
-
let historyTableName = EntityHistory.historyTableName(
|
|
568
|
-
~entityName=entityConfig.name,
|
|
569
|
-
~entityIndex=entityConfig.index,
|
|
309
|
+
let stageBuilders = (sink, ~table: ClickHouseSink.table, ~builders, ~rows) =>
|
|
310
|
+
sink->ClickHouseSink.stage(
|
|
311
|
+
~table=table.handle,
|
|
312
|
+
~rows,
|
|
313
|
+
~columns=builders->Array.map(ClickHouseSink.builderPayload),
|
|
570
314
|
)
|
|
571
315
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
let entityFields =
|
|
586
|
-
entityConfig.table.fields
|
|
587
|
-
->Array.filterMap(field => {
|
|
588
|
-
switch field {
|
|
589
|
-
| Field(field) => {
|
|
590
|
-
let fieldName = field->Table.getClickHouseDbFieldName
|
|
591
|
-
Some(`\`${fieldName}\``)
|
|
316
|
+
let stageCheckpointsOrThrow = (sink, ~registry, ~batch: Batch.t) => {
|
|
317
|
+
let rows = batch.checkpointIds->Array.length
|
|
318
|
+
if rows === 0 {
|
|
319
|
+
Null.null
|
|
320
|
+
} else {
|
|
321
|
+
let table = sink->checkpointsTable(~registry)
|
|
322
|
+
try {
|
|
323
|
+
// The table was registered from `checkpointColumns`, in this order.
|
|
324
|
+
let builders = table.columns->Array.map(ClickHouseSink.makeBuilder(_, ~rows))
|
|
325
|
+
builders->Array.forEachWithIndex((builder, index) => {
|
|
326
|
+
let columnValues = (checkpointColumns->Array.getUnsafe(index)).valuesOf(batch)
|
|
327
|
+
for row in 0 to rows - 1 {
|
|
328
|
+
builder->ClickHouseSink.writeValue(~row, columnValues->Array.getUnsafe(row))
|
|
592
329
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
FROM ${database}.\`${historyTableName}\`
|
|
603
|
-
WHERE \`${EntityHistory.checkpointIdFieldName}\` <= (SELECT max(${checkpointIdField}) FROM ${database}.\`${checkpointsTableName}\`)
|
|
604
|
-
ORDER BY \`${EntityHistory.checkpointIdFieldName}\` DESC
|
|
605
|
-
LIMIT 1 BY ${dedupKey}
|
|
606
|
-
)
|
|
607
|
-
WHERE \`${EntityHistory.changeFieldName}\` = '${(EntityHistory.RowAction.SET :> string)}'`
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// Initialize ClickHouse tables for entities
|
|
611
|
-
let initialize = async (
|
|
612
|
-
client,
|
|
613
|
-
~database: string,
|
|
614
|
-
~entities: array<Internal.entityConfig>,
|
|
615
|
-
~enums as _: array<Table.enumConfig<Table.enum>>,
|
|
616
|
-
~chainIdMode: ChainId.mode=Int32,
|
|
617
|
-
) => {
|
|
618
|
-
try {
|
|
619
|
-
let databaseEngine = Env.ClickHouse.databaseEngine()
|
|
620
|
-
let databaseEngineClause = switch databaseEngine {
|
|
621
|
-
| Some(engine) => ` ENGINE = ${engine}`
|
|
622
|
-
| None => ""
|
|
623
|
-
}
|
|
624
|
-
let hasReplicatedDatabaseEngine = switch databaseEngine {
|
|
625
|
-
| Some(engine) => engine->databaseEngineName === "Replicated"
|
|
626
|
-
| None => false
|
|
627
|
-
}
|
|
628
|
-
let envReplicated = Env.ClickHouse.replicated()
|
|
629
|
-
// A Replicated database engine only replicates data when its tables use the
|
|
630
|
-
// ReplicatedMergeTree engine, so it implies replicated mode even when
|
|
631
|
-
// ENVIO_CLICKHOUSE_REPLICATED is unset.
|
|
632
|
-
let replicated = envReplicated || hasReplicatedDatabaseEngine
|
|
633
|
-
if hasReplicatedDatabaseEngine && !envReplicated {
|
|
634
|
-
Logging.info(
|
|
635
|
-
"ENVIO_CLICKHOUSE_DATABASE_ENGINE is Replicated; enabling replicated mode so tables use the ReplicatedMergeTree engine.",
|
|
330
|
+
})
|
|
331
|
+
Null.make(sink->stageBuilders(~table, ~builders, ~rows))
|
|
332
|
+
} catch {
|
|
333
|
+
| exn =>
|
|
334
|
+
throw(
|
|
335
|
+
Persistence.StorageError({
|
|
336
|
+
message: `Failed to convert checkpoints for ClickHouse table "${table.name}"`,
|
|
337
|
+
reason: exn->Utils.prettifyExn,
|
|
338
|
+
}),
|
|
636
339
|
)
|
|
637
340
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
// ON CLUSTER on top of it — the clause is only for the plain-database case.
|
|
641
|
-
let ddlOnCluster = replicated && !hasReplicatedDatabaseEngine
|
|
341
|
+
}
|
|
342
|
+
}
|
|
642
343
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
344
|
+
let stageUpdatesOrThrow = (
|
|
345
|
+
sink,
|
|
346
|
+
~registry,
|
|
347
|
+
~changes: array<Change.t<Internal.entity>>,
|
|
348
|
+
~entityConfig: Internal.entityConfig,
|
|
349
|
+
~scope: Internal.chainScope,
|
|
350
|
+
) => {
|
|
351
|
+
let rows = changes->Array.length
|
|
352
|
+
if rows === 0 {
|
|
353
|
+
None
|
|
354
|
+
} else {
|
|
355
|
+
let table = sink->entityTable(~registry, ~entityConfig)
|
|
356
|
+
let tableName = table.name
|
|
357
|
+
let cacheKey = `${entityConfig.name}|${scope->Internal.chainScopeToString}`
|
|
358
|
+
try {
|
|
359
|
+
let converters = switch registry.converters->Utils.Dict.dangerouslyGetNonOption(cacheKey) {
|
|
360
|
+
| Some(cached) => cached
|
|
361
|
+
| None =>
|
|
362
|
+
let cached = makeConverters(~entityConfig, ~scope, ~table)
|
|
363
|
+
registry.converters->Dict.set(cacheKey, cached)
|
|
364
|
+
cached
|
|
657
365
|
}
|
|
658
|
-
|
|
366
|
+
let builders = fillBuilders(~table, ~converters, ~changes)
|
|
367
|
+
Some(sink->stageBuilders(~table, ~builders, ~rows))
|
|
368
|
+
} catch {
|
|
369
|
+
| exn =>
|
|
370
|
+
throw(
|
|
371
|
+
Persistence.StorageError({
|
|
372
|
+
message: `Failed to convert items for ClickHouse table "${tableName}"`,
|
|
373
|
+
reason: exn->Utils.prettifyExn,
|
|
374
|
+
}),
|
|
375
|
+
)
|
|
659
376
|
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
660
379
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
})
|
|
672
|
-
} else {
|
|
673
|
-
await client->command({
|
|
674
|
-
query: `TRUNCATE DATABASE IF EXISTS ${database}${onClusterClause(~onCluster=ddlOnCluster)}`,
|
|
675
|
-
})
|
|
676
|
-
}
|
|
677
|
-
await client->command({
|
|
678
|
-
query: `CREATE DATABASE IF NOT EXISTS ${database}${databaseOnClusterClause}${databaseEngineClause}`,
|
|
679
|
-
})
|
|
380
|
+
let writeStagedOrThrow = async (sink, ~entities, ~checkpoints) =>
|
|
381
|
+
try await sink->ClickHouseSink.writeBatch(~entities, ~checkpoints) catch {
|
|
382
|
+
| exn =>
|
|
383
|
+
throw(
|
|
384
|
+
Persistence.StorageError({
|
|
385
|
+
message: "Failed to write a batch to ClickHouse",
|
|
386
|
+
reason: exn->Utils.prettifyExn,
|
|
387
|
+
}),
|
|
388
|
+
)
|
|
389
|
+
}
|
|
680
390
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
),
|
|
691
|
-
})
|
|
692
|
-
),
|
|
693
|
-
)->Utils.Promise.ignoreValue
|
|
694
|
-
await client->command({
|
|
695
|
-
query: makeCreateCheckpointsTableQuery(
|
|
696
|
-
~database,
|
|
697
|
-
~replicated,
|
|
698
|
-
~onCluster=ddlOnCluster,
|
|
699
|
-
~chainIdMode,
|
|
700
|
-
),
|
|
391
|
+
// Tables are not registered here: an indexer that finds an existing storage
|
|
392
|
+
// never runs this, so the write path registers them on first use either way.
|
|
393
|
+
let initialize = async (sink, ~entities: array<Internal.entityConfig>) => {
|
|
394
|
+
try {
|
|
395
|
+
await sink->ClickHouseSink.initialize({
|
|
396
|
+
entities: entities->Array.map(entityConfig => entitySpec(~entityConfig)),
|
|
397
|
+
checkpointColumns: checkpointColumnSpecs,
|
|
398
|
+
replicated: Env.ClickHouse.replicated(),
|
|
399
|
+
databaseEngine: ?Env.ClickHouse.databaseEngine(),
|
|
701
400
|
})
|
|
702
401
|
|
|
703
|
-
// The client pools HTTP connections, so consecutive statements may reach
|
|
704
|
-
// different replicas, while a Replicated database applies DDL from its
|
|
705
|
-
// Keeper log asynchronously. A CREATE VIEW is analyzed against the node's
|
|
706
|
-
// local metadata and can land on a replica that hasn't applied the table
|
|
707
|
-
// creates yet, failing with UNKNOWN_TABLE. Block until every replica has
|
|
708
|
-
// caught up before creating the views. ON CLUSTER must precede the
|
|
709
|
-
// database name in this command's grammar.
|
|
710
|
-
if hasReplicatedDatabaseEngine {
|
|
711
|
-
await client->command({
|
|
712
|
-
query: `SYSTEM SYNC DATABASE REPLICA ON CLUSTER '{cluster}' ${database}`,
|
|
713
|
-
})
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
await Promise.all(
|
|
717
|
-
entities->Array.map(entityConfig =>
|
|
718
|
-
client->command({
|
|
719
|
-
query: makeCreateViewQuery(~entityConfig, ~database, ~onCluster=ddlOnCluster),
|
|
720
|
-
})
|
|
721
|
-
),
|
|
722
|
-
)->Utils.Promise.ignoreValue
|
|
723
|
-
|
|
724
402
|
Logging.trace("ClickHouse storage initialization completed successfully")
|
|
725
403
|
} catch {
|
|
726
404
|
| exn => {
|
|
727
405
|
Logging.errorWithExn(exn, "Failed to initialize ClickHouse storage")
|
|
728
|
-
|
|
406
|
+
throw(
|
|
407
|
+
Persistence.StorageError({
|
|
408
|
+
message: "Failed to initialize ClickHouse storage",
|
|
409
|
+
reason: exn->Utils.prettifyExn,
|
|
410
|
+
}),
|
|
411
|
+
)
|
|
729
412
|
}
|
|
730
413
|
}
|
|
731
414
|
}
|
|
732
415
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
query: `SHOW TABLES FROM ${database} LIKE '${EntityHistory.historyTablePrefix}%'`,
|
|
751
|
-
})
|
|
752
|
-
let tables = (await tablesResult->json)["data"]
|
|
753
|
-
|
|
754
|
-
// Delete rows with checkpoint IDs higher than the target for each history table
|
|
755
|
-
await Promise.all(
|
|
756
|
-
tables->Array.map(table => {
|
|
757
|
-
let tableName = table["name"]
|
|
758
|
-
client->command({
|
|
759
|
-
query: `ALTER TABLE ${database}.\`${tableName}\` DELETE WHERE \`${EntityHistory.checkpointIdFieldName}\` > ${checkpointId->BigInt.toString}`,
|
|
760
|
-
})
|
|
761
|
-
}),
|
|
762
|
-
)->Utils.Promise.ignoreValue
|
|
763
|
-
|
|
764
|
-
// Delete stale checkpoints
|
|
765
|
-
await client->command({
|
|
766
|
-
query: `DELETE FROM ${database}.\`${InternalTable.Checkpoints.table.tableName}\` WHERE \`${Table.idFieldName}\` > ${checkpointId->BigInt.toString}`,
|
|
767
|
-
})
|
|
768
|
-
} catch {
|
|
769
|
-
| Persistence.StorageError(_) as exn => throw(exn)
|
|
416
|
+
let resume = async (
|
|
417
|
+
sink,
|
|
418
|
+
~checkpointId: Internal.checkpointId,
|
|
419
|
+
~chains: array<Persistence.initialChainState>,
|
|
420
|
+
~entities: array<Internal.entityConfig>,
|
|
421
|
+
) => {
|
|
422
|
+
let chainProgress = chains->Array.map(chain => {
|
|
423
|
+
ClickHouseSink.chainId: chain.id->ChainId.toString,
|
|
424
|
+
progressBlockNumber: chain.progressBlockNumber,
|
|
425
|
+
})
|
|
426
|
+
try await sink->ClickHouseSink.resume({
|
|
427
|
+
checkpointId: checkpointId->BigInt.toString,
|
|
428
|
+
chainProgress,
|
|
429
|
+
historyTables: entities->Array.map(entityConfig => entitySpec(~entityConfig).historyTable),
|
|
430
|
+
replicated: Env.ClickHouse.replicated(),
|
|
431
|
+
databaseEngine: ?Env.ClickHouse.databaseEngine(),
|
|
432
|
+
}) catch {
|
|
770
433
|
| exn => {
|
|
771
434
|
Logging.errorWithExn(exn, "Failed to resume ClickHouse storage")
|
|
772
|
-
|
|
435
|
+
throw(
|
|
436
|
+
Persistence.StorageError({
|
|
437
|
+
message: "Failed to resume ClickHouse storage",
|
|
438
|
+
reason: exn->Utils.prettifyExn,
|
|
439
|
+
}),
|
|
440
|
+
)
|
|
773
441
|
}
|
|
774
442
|
}
|
|
775
443
|
}
|