envio 2.26.0-alpha.8 → 2.26.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.
- package/evm.schema.json +0 -7
- package/fuel.schema.json +0 -7
- package/index.d.ts +22 -6
- package/index.js +1 -2
- package/package.json +5 -5
- package/src/Envio.gen.ts +1 -3
- package/src/Envio.res +0 -13
- package/src/Envio.res.js +0 -9
- package/src/FetchState.res +4 -1
- package/src/Internal.res +0 -3
- package/src/Logging.res +0 -8
- package/src/Logging.res.js +0 -29
- package/src/Persistence.res +47 -110
- package/src/Persistence.res.js +17 -61
- package/src/PgStorage.res +86 -519
- package/src/PgStorage.res.js +57 -386
- package/src/Prometheus.res +0 -12
- package/src/Prometheus.res.js +0 -12
- package/src/Utils.res +9 -39
- package/src/Utils.res.js +6 -17
- package/src/bindings/BigInt.res +0 -1
- package/src/bindings/NodeJs.res +26 -27
- package/src/bindings/NodeJs.res.js +13 -5
- package/src/db/EntityHistory.res +28 -5
- package/src/db/EntityHistory.res.js +23 -4
- package/src/db/Table.res +61 -3
- package/src/db/Table.res.js +42 -3
- package/src/bindings/BigInt.gen.ts +0 -10
package/src/PgStorage.res
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
let
|
|
1
|
+
let makeCreateIndexSql = (~tableName, ~indexFields, ~pgSchema) => {
|
|
2
2
|
let indexName = tableName ++ "_" ++ indexFields->Js.Array2.joinWith("_")
|
|
3
3
|
let index = indexFields->Belt.Array.map(idx => `"${idx}"`)->Js.Array2.joinWith(", ")
|
|
4
4
|
`CREATE INDEX IF NOT EXISTS "${indexName}" ON "${pgSchema}"."${tableName}"(${index});`
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
let
|
|
7
|
+
let makeCreateTableIndicesSql = (table: Table.table, ~pgSchema) => {
|
|
8
8
|
open Belt
|
|
9
9
|
let tableName = table.tableName
|
|
10
10
|
let createIndex = indexField =>
|
|
11
|
-
|
|
11
|
+
makeCreateIndexSql(~tableName, ~indexFields=[indexField], ~pgSchema)
|
|
12
12
|
let createCompositeIndex = indexFields => {
|
|
13
|
-
|
|
13
|
+
makeCreateIndexSql(~tableName, ~indexFields, ~pgSchema)
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
let singleIndices = table->Table.getSingleIndices
|
|
@@ -20,7 +20,7 @@ let makeCreateTableIndicesQuery = (table: Table.table, ~pgSchema) => {
|
|
|
20
20
|
compositeIndices->Array.map(createCompositeIndex)->Js.Array2.joinWith("\n")
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
let
|
|
23
|
+
let makeCreateTableSql = (table: Table.table, ~pgSchema) => {
|
|
24
24
|
open Belt
|
|
25
25
|
let fieldsMapped =
|
|
26
26
|
table
|
|
@@ -58,7 +58,7 @@ let makeInitializeTransaction = (
|
|
|
58
58
|
~generalTables=[],
|
|
59
59
|
~entities=[],
|
|
60
60
|
~enums=[],
|
|
61
|
-
~
|
|
61
|
+
~cleanRun=false,
|
|
62
62
|
) => {
|
|
63
63
|
let allTables = generalTables->Array.copy
|
|
64
64
|
let allEntityTables = []
|
|
@@ -71,16 +71,12 @@ let makeInitializeTransaction = (
|
|
|
71
71
|
|
|
72
72
|
let query = ref(
|
|
73
73
|
(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// (but only for public, since it's usually always exists)
|
|
79
|
-
? ""
|
|
80
|
-
: `DROP SCHEMA IF EXISTS "${pgSchema}" CASCADE;
|
|
81
|
-
CREATE SCHEMA "${pgSchema}";\n`
|
|
74
|
+
cleanRun
|
|
75
|
+
? `DROP SCHEMA IF EXISTS "${pgSchema}" CASCADE;
|
|
76
|
+
CREATE SCHEMA "${pgSchema}";`
|
|
77
|
+
: `CREATE SCHEMA IF NOT EXISTS "${pgSchema}";`
|
|
82
78
|
) ++
|
|
83
|
-
`GRANT ALL ON SCHEMA "${pgSchema}" TO
|
|
79
|
+
`GRANT ALL ON SCHEMA "${pgSchema}" TO ${pgUser};
|
|
84
80
|
GRANT ALL ON SCHEMA "${pgSchema}" TO public;`,
|
|
85
81
|
)
|
|
86
82
|
|
|
@@ -91,17 +87,31 @@ GRANT ALL ON SCHEMA "${pgSchema}" TO public;`,
|
|
|
91
87
|
->Js.Array2.map(v => `'${v->(Utils.magic: Internal.enum => string)}'`)
|
|
92
88
|
->Js.Array2.joinWith(", ")});`
|
|
93
89
|
|
|
94
|
-
query :=
|
|
90
|
+
query :=
|
|
91
|
+
query.contents ++
|
|
92
|
+
"\n" ++ if cleanRun {
|
|
93
|
+
// Direct creation when cleanRunting (faster)
|
|
94
|
+
enumCreateQuery
|
|
95
|
+
} else {
|
|
96
|
+
// Wrap with conditional check only when not cleanRunting
|
|
97
|
+
`IF NOT EXISTS (
|
|
98
|
+
SELECT 1 FROM pg_type
|
|
99
|
+
WHERE typname = '${enumConfig.name->Js.String2.toLowerCase}'
|
|
100
|
+
AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = '${pgSchema}')
|
|
101
|
+
) THEN
|
|
102
|
+
${enumCreateQuery}
|
|
103
|
+
END IF;`
|
|
104
|
+
}
|
|
95
105
|
})
|
|
96
106
|
|
|
97
107
|
// Batch all table creation first (optimal for PostgreSQL)
|
|
98
108
|
allTables->Js.Array2.forEach((table: Table.table) => {
|
|
99
|
-
query := query.contents ++ "\n" ++
|
|
109
|
+
query := query.contents ++ "\n" ++ makeCreateTableSql(table, ~pgSchema)
|
|
100
110
|
})
|
|
101
111
|
|
|
102
112
|
// Then batch all indices (better performance when tables exist)
|
|
103
113
|
allTables->Js.Array2.forEach((table: Table.table) => {
|
|
104
|
-
let indices =
|
|
114
|
+
let indices = makeCreateTableIndicesSql(table, ~pgSchema)
|
|
105
115
|
if indices !== "" {
|
|
106
116
|
query := query.contents ++ "\n" ++ indices
|
|
107
117
|
}
|
|
@@ -121,7 +131,7 @@ GRANT ALL ON SCHEMA "${pgSchema}" TO public;`,
|
|
|
121
131
|
query :=
|
|
122
132
|
query.contents ++
|
|
123
133
|
"\n" ++
|
|
124
|
-
|
|
134
|
+
makeCreateIndexSql(
|
|
125
135
|
~tableName=derivedFromField.derivedFromEntity,
|
|
126
136
|
~indexFields=[indexField],
|
|
127
137
|
~pgSchema,
|
|
@@ -129,40 +139,28 @@ GRANT ALL ON SCHEMA "${pgSchema}" TO public;`,
|
|
|
129
139
|
})
|
|
130
140
|
})
|
|
131
141
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
EXECUTE format('SELECT COUNT(*) FROM "${pgSchema}".%I', table_name) INTO result;
|
|
142
|
-
RETURN result;
|
|
143
|
-
END;
|
|
144
|
-
$$ LANGUAGE plpgsql;`
|
|
145
|
-
|
|
146
|
-
[query.contents]->Js.Array2.concat(
|
|
147
|
-
functionsQuery.contents !== "" ? [functionsQuery.contents] : [],
|
|
148
|
-
)
|
|
142
|
+
[
|
|
143
|
+
// Return optimized queries - main DDL in DO block, functions separate
|
|
144
|
+
// Note: DO $$ BEGIN wrapper is only needed for PL/pgSQL conditionals (IF NOT EXISTS)
|
|
145
|
+
// Reset case uses direct DDL (faster), non-cleanRun case uses conditionals (safer)
|
|
146
|
+
cleanRun || enums->Utils.Array.isEmpty
|
|
147
|
+
? query.contents
|
|
148
|
+
: `DO $$ BEGIN ${query.contents} END $$;`,
|
|
149
|
+
// Functions query (separate as they can't be in DO block)
|
|
150
|
+
]->Js.Array2.concat(functionsQuery.contents !== "" ? [functionsQuery.contents] : [])
|
|
149
151
|
}
|
|
150
152
|
|
|
151
|
-
let
|
|
153
|
+
let makeLoadByIdSql = (~pgSchema, ~tableName) => {
|
|
152
154
|
`SELECT * FROM "${pgSchema}"."${tableName}" WHERE id = $1 LIMIT 1;`
|
|
153
155
|
}
|
|
154
156
|
|
|
155
|
-
let
|
|
156
|
-
`SELECT * FROM "${pgSchema}"."${tableName}" WHERE "${fieldName}" ${operator} $1;`
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
let makeLoadByIdsQuery = (~pgSchema, ~tableName) => {
|
|
157
|
+
let makeLoadByIdsSql = (~pgSchema, ~tableName) => {
|
|
160
158
|
`SELECT * FROM "${pgSchema}"."${tableName}" WHERE id = ANY($1::text[]);`
|
|
161
159
|
}
|
|
162
160
|
|
|
163
|
-
let
|
|
161
|
+
let makeInsertUnnestSetSql = (~pgSchema, ~table: Table.table, ~itemSchema, ~isRawEvents) => {
|
|
164
162
|
let {quotedFieldNames, quotedNonPrimaryFieldNames, arrayFieldTypes} =
|
|
165
|
-
table->Table.toSqlParams(~schema=itemSchema
|
|
163
|
+
table->Table.toSqlParams(~schema=itemSchema)
|
|
166
164
|
|
|
167
165
|
let primaryKeyFieldNames = Table.getPrimaryKeyFieldNames(table)
|
|
168
166
|
|
|
@@ -190,9 +188,8 @@ SELECT * FROM unnest(${arrayFieldTypes
|
|
|
190
188
|
} ++ ";"
|
|
191
189
|
}
|
|
192
190
|
|
|
193
|
-
let
|
|
194
|
-
let {quotedFieldNames, quotedNonPrimaryFieldNames} =
|
|
195
|
-
table->Table.toSqlParams(~schema=itemSchema, ~pgSchema)
|
|
191
|
+
let makeInsertValuesSetSql = (~pgSchema, ~table: Table.table, ~itemSchema, ~itemsCount) => {
|
|
192
|
+
let {quotedFieldNames, quotedNonPrimaryFieldNames} = table->Table.toSqlParams(~schema=itemSchema)
|
|
196
193
|
|
|
197
194
|
let primaryKeyFieldNames = Table.getPrimaryKeyFieldNames(table)
|
|
198
195
|
let fieldsCount = quotedFieldNames->Array.length
|
|
@@ -239,13 +236,12 @@ VALUES${placeholders.contents}` ++
|
|
|
239
236
|
// they are always guaranteed to be an object.
|
|
240
237
|
// FIXME what about Fuel params?
|
|
241
238
|
let rawEventsTableName = "raw_events"
|
|
242
|
-
let eventSyncStateTableName = "event_sync_state"
|
|
243
239
|
|
|
244
240
|
// Constants for chunking
|
|
245
241
|
let maxItemsPerQuery = 500
|
|
246
242
|
|
|
247
243
|
let makeTableBatchSetQuery = (~pgSchema, ~table: Table.table, ~itemSchema: S.t<'item>) => {
|
|
248
|
-
let {dbSchema, hasArrayField} = table->Table.toSqlParams(~schema=itemSchema
|
|
244
|
+
let {dbSchema, hasArrayField} = table->Table.toSqlParams(~schema=itemSchema)
|
|
249
245
|
let isRawEvents = table.tableName === rawEventsTableName
|
|
250
246
|
|
|
251
247
|
// Should experiment how much it'll affect performance
|
|
@@ -257,7 +253,7 @@ let makeTableBatchSetQuery = (~pgSchema, ~table: Table.table, ~itemSchema: S.t<'
|
|
|
257
253
|
|
|
258
254
|
if isRawEvents || !hasArrayField {
|
|
259
255
|
{
|
|
260
|
-
"
|
|
256
|
+
"sql": makeInsertUnnestSetSql(~pgSchema, ~table, ~itemSchema, ~isRawEvents),
|
|
261
257
|
"convertOrThrow": S.compile(
|
|
262
258
|
S.unnest(dbSchema),
|
|
263
259
|
~input=Value,
|
|
@@ -269,12 +265,7 @@ let makeTableBatchSetQuery = (~pgSchema, ~table: Table.table, ~itemSchema: S.t<'
|
|
|
269
265
|
}
|
|
270
266
|
} else {
|
|
271
267
|
{
|
|
272
|
-
"
|
|
273
|
-
~pgSchema,
|
|
274
|
-
~table,
|
|
275
|
-
~itemSchema,
|
|
276
|
-
~itemsCount=maxItemsPerQuery,
|
|
277
|
-
),
|
|
268
|
+
"sql": makeInsertValuesSetSql(~pgSchema, ~table, ~itemSchema, ~itemsCount=maxItemsPerQuery),
|
|
278
269
|
"convertOrThrow": S.compile(
|
|
279
270
|
S.unnest(itemSchema)->S.preprocess(_ => {
|
|
280
271
|
serializer: Utils.Array.flatten->Utils.magic,
|
|
@@ -300,35 +291,6 @@ let chunkArray = (arr: array<'a>, ~chunkSize) => {
|
|
|
300
291
|
chunks
|
|
301
292
|
}
|
|
302
293
|
|
|
303
|
-
let removeInvalidUtf8InPlace = entities =>
|
|
304
|
-
entities->Js.Array2.forEach(item => {
|
|
305
|
-
let dict = item->(Utils.magic: 'a => dict<unknown>)
|
|
306
|
-
dict->Utils.Dict.forEachWithKey((key, value) => {
|
|
307
|
-
if value->Js.typeof === "string" {
|
|
308
|
-
let value = value->(Utils.magic: unknown => string)
|
|
309
|
-
// We mutate here, since we don't care
|
|
310
|
-
// about the original value with \x00 anyways.
|
|
311
|
-
//
|
|
312
|
-
// This is unsafe, but we rely that it'll use
|
|
313
|
-
// the mutated reference on retry.
|
|
314
|
-
// TODO: Test it properly after we start using
|
|
315
|
-
// in-memory PGLite for indexer test framework.
|
|
316
|
-
dict->Js.Dict.set(
|
|
317
|
-
key,
|
|
318
|
-
value
|
|
319
|
-
->Utils.String.replaceAll("\x00", "")
|
|
320
|
-
->(Utils.magic: string => unknown),
|
|
321
|
-
)
|
|
322
|
-
}
|
|
323
|
-
})
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
let pgEncodingErrorSchema = S.object(s =>
|
|
327
|
-
s.tag("message", `invalid byte sequence for encoding "UTF8": 0x00`)
|
|
328
|
-
)
|
|
329
|
-
|
|
330
|
-
exception PgEncodingError({table: Table.table})
|
|
331
|
-
|
|
332
294
|
// WeakMap for caching table batch set queries
|
|
333
295
|
let setQueryCache = Utils.WeakMap.make()
|
|
334
296
|
let setOrThrow = async (sql, ~items, ~table: Table.table, ~itemSchema, ~pgSchema) => {
|
|
@@ -336,7 +298,7 @@ let setOrThrow = async (sql, ~items, ~table: Table.table, ~itemSchema, ~pgSchema
|
|
|
336
298
|
()
|
|
337
299
|
} else {
|
|
338
300
|
// Get or create cached query for this table
|
|
339
|
-
let
|
|
301
|
+
let query = switch setQueryCache->Utils.WeakMap.get(table) {
|
|
340
302
|
| Some(cached) => cached
|
|
341
303
|
| None => {
|
|
342
304
|
let newQuery = makeTableBatchSetQuery(
|
|
@@ -349,31 +311,50 @@ let setOrThrow = async (sql, ~items, ~table: Table.table, ~itemSchema, ~pgSchema
|
|
|
349
311
|
}
|
|
350
312
|
}
|
|
351
313
|
|
|
314
|
+
let sqlQuery = query["sql"]
|
|
315
|
+
|
|
352
316
|
try {
|
|
353
|
-
|
|
354
|
-
|
|
317
|
+
let payload =
|
|
318
|
+
query["convertOrThrow"](items->(Utils.magic: array<'item> => array<unknown>))->(
|
|
319
|
+
Utils.magic: unknown => array<unknown>
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
if query["isInsertValues"] {
|
|
323
|
+
let fieldsCount = switch itemSchema->S.classify {
|
|
324
|
+
| S.Object({items}) => items->Array.length
|
|
325
|
+
| _ => Js.Exn.raiseError("Expected an object schema for table")
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Chunk the items for VALUES-based queries
|
|
329
|
+
// We need to multiply by fields count,
|
|
330
|
+
// because we flattened our entity values with S.unnest
|
|
331
|
+
// to optimize the query execution.
|
|
332
|
+
let maxChunkSize = maxItemsPerQuery * fieldsCount
|
|
333
|
+
let chunks = chunkArray(payload, ~chunkSize=maxChunkSize)
|
|
355
334
|
let responses = []
|
|
356
335
|
chunks->Js.Array2.forEach(chunk => {
|
|
357
336
|
let chunkSize = chunk->Array.length
|
|
358
|
-
let isFullChunk = chunkSize ===
|
|
337
|
+
let isFullChunk = chunkSize === maxChunkSize
|
|
359
338
|
|
|
360
339
|
let response = sql->Postgres.preparedUnsafe(
|
|
361
340
|
// Either use the sql query for full chunks from cache
|
|
362
341
|
// or create a new one for partial chunks on the fly.
|
|
363
342
|
isFullChunk
|
|
364
|
-
?
|
|
365
|
-
:
|
|
366
|
-
|
|
343
|
+
? sqlQuery
|
|
344
|
+
: makeInsertValuesSetSql(
|
|
345
|
+
~pgSchema,
|
|
346
|
+
~table,
|
|
347
|
+
~itemSchema,
|
|
348
|
+
~itemsCount=chunkSize / fieldsCount,
|
|
349
|
+
),
|
|
350
|
+
chunk->Utils.magic,
|
|
367
351
|
)
|
|
368
352
|
responses->Js.Array2.push(response)->ignore
|
|
369
353
|
})
|
|
370
354
|
let _ = await Promise.all(responses)
|
|
371
355
|
} else {
|
|
372
356
|
// Use UNNEST approach for single query
|
|
373
|
-
await sql->Postgres.preparedUnsafe(
|
|
374
|
-
data["query"],
|
|
375
|
-
data["convertOrThrow"](items->(Utils.magic: array<'item> => array<unknown>)),
|
|
376
|
-
)
|
|
357
|
+
await sql->Postgres.preparedUnsafe(sqlQuery, payload->Obj.magic)
|
|
377
358
|
}
|
|
378
359
|
} catch {
|
|
379
360
|
| S.Raised(_) as exn =>
|
|
@@ -387,201 +368,35 @@ let setOrThrow = async (sql, ~items, ~table: Table.table, ~itemSchema, ~pgSchema
|
|
|
387
368
|
raise(
|
|
388
369
|
Persistence.StorageError({
|
|
389
370
|
message: `Failed to insert items into table "${table.tableName}"`,
|
|
390
|
-
reason: exn
|
|
371
|
+
reason: exn,
|
|
391
372
|
}),
|
|
392
373
|
)
|
|
393
374
|
}
|
|
394
375
|
}
|
|
395
376
|
}
|
|
396
377
|
|
|
397
|
-
let
|
|
398
|
-
sql,
|
|
399
|
-
~entityHistory: EntityHistory.t<'entity>,
|
|
400
|
-
~rows: array<EntityHistory.historyRow<'entity>>,
|
|
401
|
-
~shouldCopyCurrentEntity=?,
|
|
402
|
-
~shouldRemoveInvalidUtf8=false,
|
|
403
|
-
) => {
|
|
404
|
-
rows
|
|
405
|
-
->Belt.Array.map(historyRow => {
|
|
406
|
-
let row = historyRow->S.reverseConvertToJsonOrThrow(entityHistory.schema)
|
|
407
|
-
if shouldRemoveInvalidUtf8 {
|
|
408
|
-
[row]->removeInvalidUtf8InPlace
|
|
409
|
-
}
|
|
410
|
-
entityHistory.insertFn(
|
|
411
|
-
sql,
|
|
412
|
-
row,
|
|
413
|
-
~shouldCopyCurrentEntity=switch shouldCopyCurrentEntity {
|
|
414
|
-
| Some(v) => v
|
|
415
|
-
| None => {
|
|
416
|
-
let containsRollbackDiffChange =
|
|
417
|
-
historyRow.containsRollbackDiffChange->Belt.Option.getWithDefault(false)
|
|
418
|
-
!containsRollbackDiffChange
|
|
419
|
-
}
|
|
420
|
-
},
|
|
421
|
-
)
|
|
422
|
-
})
|
|
423
|
-
->Promise.all
|
|
424
|
-
->(Utils.magic: promise<array<unit>> => promise<unit>)
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
type schemaTableName = {
|
|
428
|
-
@as("table_name")
|
|
429
|
-
tableName: string,
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
let makeSchemaTableNamesQuery = (~pgSchema) => {
|
|
433
|
-
`SELECT table_name FROM information_schema.tables WHERE table_schema = '${pgSchema}';`
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
let cacheTablePrefix = "envio_effect_"
|
|
437
|
-
let cacheTablePrefixLength = cacheTablePrefix->String.length
|
|
438
|
-
|
|
439
|
-
type schemaCacheTableInfo = {
|
|
440
|
-
@as("table_name")
|
|
441
|
-
tableName: string,
|
|
442
|
-
@as("count")
|
|
443
|
-
count: int,
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
let makeSchemaCacheTableInfoQuery = (~pgSchema) => {
|
|
447
|
-
`SELECT
|
|
448
|
-
t.table_name,
|
|
449
|
-
get_cache_row_count(t.table_name) as count
|
|
450
|
-
FROM information_schema.tables t
|
|
451
|
-
WHERE t.table_schema = '${pgSchema}'
|
|
452
|
-
AND t.table_name LIKE '${cacheTablePrefix}%';`
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
type psqlExecState =
|
|
456
|
-
Unknown | Pending(promise<result<string, string>>) | Resolved(result<string, string>)
|
|
457
|
-
|
|
458
|
-
let getConnectedPsqlExec = {
|
|
459
|
-
let pgDockerServiceName = "envio-postgres"
|
|
460
|
-
// Should use the default port, since we're executing the command
|
|
461
|
-
// from the postgres container's network.
|
|
462
|
-
let pgDockerServicePort = 5432
|
|
463
|
-
|
|
464
|
-
// For development: We run the indexer process locally,
|
|
465
|
-
// and there might not be psql installed on the user's machine.
|
|
466
|
-
// So we use docker-compose to run psql existing in the postgres container.
|
|
467
|
-
// For production: We expect indexer to be running in a container,
|
|
468
|
-
// with psql installed. So we can call it directly.
|
|
469
|
-
let psqlExecState = ref(Unknown)
|
|
470
|
-
async (~pgUser, ~pgHost, ~pgDatabase, ~pgPort) => {
|
|
471
|
-
switch psqlExecState.contents {
|
|
472
|
-
| Unknown => {
|
|
473
|
-
let promise = Promise.make((resolve, _reject) => {
|
|
474
|
-
let binary = "psql"
|
|
475
|
-
NodeJs.ChildProcess.exec(`${binary} --version`, (~error, ~stdout as _, ~stderr as _) => {
|
|
476
|
-
switch error {
|
|
477
|
-
| Value(_) => {
|
|
478
|
-
let binary = `docker-compose exec -T -u ${pgUser} ${pgDockerServiceName} psql`
|
|
479
|
-
NodeJs.ChildProcess.exec(
|
|
480
|
-
`${binary} --version`,
|
|
481
|
-
(~error, ~stdout as _, ~stderr as _) => {
|
|
482
|
-
switch error {
|
|
483
|
-
| Value(_) =>
|
|
484
|
-
resolve(
|
|
485
|
-
Error(`Please check if "psql" binary is installed or docker-compose is running for the local indexer.`),
|
|
486
|
-
)
|
|
487
|
-
| Null =>
|
|
488
|
-
resolve(
|
|
489
|
-
Ok(
|
|
490
|
-
`${binary} -h ${pgHost} -p ${pgDockerServicePort->Js.Int.toString} -U ${pgUser} -d ${pgDatabase}`,
|
|
491
|
-
),
|
|
492
|
-
)
|
|
493
|
-
}
|
|
494
|
-
},
|
|
495
|
-
)
|
|
496
|
-
}
|
|
497
|
-
| Null =>
|
|
498
|
-
resolve(
|
|
499
|
-
Ok(
|
|
500
|
-
`${binary} -h ${pgHost} -p ${pgPort->Js.Int.toString} -U ${pgUser} -d ${pgDatabase}`,
|
|
501
|
-
),
|
|
502
|
-
)
|
|
503
|
-
}
|
|
504
|
-
})
|
|
505
|
-
})
|
|
506
|
-
|
|
507
|
-
psqlExecState := Pending(promise)
|
|
508
|
-
let result = await promise
|
|
509
|
-
psqlExecState := Resolved(result)
|
|
510
|
-
result
|
|
511
|
-
}
|
|
512
|
-
| Pending(promise) => await promise
|
|
513
|
-
| Resolved(result) => result
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
let make = (
|
|
519
|
-
~sql: Postgres.sql,
|
|
520
|
-
~pgHost,
|
|
521
|
-
~pgSchema,
|
|
522
|
-
~pgPort,
|
|
523
|
-
~pgUser,
|
|
524
|
-
~pgDatabase,
|
|
525
|
-
~pgPassword,
|
|
526
|
-
~onInitialize=?,
|
|
527
|
-
~onNewTables=?,
|
|
528
|
-
): Persistence.storage => {
|
|
529
|
-
let psqlExecOptions: NodeJs.ChildProcess.execOptions = {
|
|
530
|
-
env: Js.Dict.fromArray([("PGPASSWORD", pgPassword), ("PATH", %raw(`process.env.PATH`))]),
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
let cacheDirPath = NodeJs.Path.resolve([
|
|
534
|
-
// Right outside of the generated directory
|
|
535
|
-
"..",
|
|
536
|
-
".envio",
|
|
537
|
-
"cache",
|
|
538
|
-
])
|
|
539
|
-
|
|
378
|
+
let make = (~sql: Postgres.sql, ~pgSchema, ~pgUser): Persistence.storage => {
|
|
540
379
|
let isInitialized = async () => {
|
|
541
|
-
let
|
|
380
|
+
let schemas =
|
|
542
381
|
await sql->Postgres.unsafe(
|
|
543
|
-
`SELECT
|
|
382
|
+
`SELECT schema_name FROM information_schema.schemata WHERE schema_name = '${pgSchema}';`,
|
|
544
383
|
)
|
|
545
|
-
|
|
384
|
+
schemas->Utils.Array.notEmpty
|
|
546
385
|
}
|
|
547
386
|
|
|
548
|
-
let initialize = async (~entities=[], ~generalTables=[], ~enums=[]) => {
|
|
549
|
-
let schemaTableNames: array<schemaTableName> =
|
|
550
|
-
await sql->Postgres.unsafe(makeSchemaTableNamesQuery(~pgSchema))
|
|
551
|
-
|
|
552
|
-
// The initialization query will completely drop the schema and recreate it from scratch.
|
|
553
|
-
// So we need to check if the schema is not used for anything else than envio.
|
|
554
|
-
if (
|
|
555
|
-
// Should pass with existing schema with no tables
|
|
556
|
-
// This might happen when used with public schema
|
|
557
|
-
// which is automatically created by postgres.
|
|
558
|
-
schemaTableNames->Utils.Array.notEmpty &&
|
|
559
|
-
// Otherwise should throw if there's a table, but no envio specific one
|
|
560
|
-
// This means that the schema is used for something else than envio.
|
|
561
|
-
!(schemaTableNames->Js.Array2.some(table => table.tableName === eventSyncStateTableName))
|
|
562
|
-
) {
|
|
563
|
-
Js.Exn.raiseError(
|
|
564
|
-
`Cannot run Envio migrations on PostgreSQL schema "${pgSchema}" because it contains non-Envio tables. Running migrations would delete all data in this schema.\n\nTo resolve this:\n1. If you want to use this schema, first backup any important data, then drop it with: "pnpm envio local db-migrate down"\n2. Or specify a different schema name by setting the "ENVIO_PG_PUBLIC_SCHEMA" environment variable\n3. Or manually drop the schema in your database if you're certain the data is not needed.`,
|
|
565
|
-
)
|
|
566
|
-
}
|
|
567
|
-
|
|
387
|
+
let initialize = async (~entities=[], ~generalTables=[], ~enums=[], ~cleanRun=false) => {
|
|
568
388
|
let queries = makeInitializeTransaction(
|
|
569
389
|
~pgSchema,
|
|
570
390
|
~pgUser,
|
|
571
391
|
~generalTables,
|
|
572
392
|
~entities,
|
|
573
393
|
~enums,
|
|
574
|
-
~
|
|
394
|
+
~cleanRun,
|
|
575
395
|
)
|
|
576
396
|
// Execute all queries within a single transaction for integrity
|
|
577
397
|
let _ = await sql->Postgres.beginSql(sql => {
|
|
578
398
|
queries->Js.Array2.map(query => sql->Postgres.unsafe(query))
|
|
579
399
|
})
|
|
580
|
-
|
|
581
|
-
switch onInitialize {
|
|
582
|
-
| Some(onInitialize) => await onInitialize()
|
|
583
|
-
| None => ()
|
|
584
|
-
}
|
|
585
400
|
}
|
|
586
401
|
|
|
587
402
|
let loadByIdsOrThrow = async (~ids, ~table: Table.table, ~rowsSchema) => {
|
|
@@ -589,12 +404,12 @@ let make = (
|
|
|
589
404
|
switch ids {
|
|
590
405
|
| [_] =>
|
|
591
406
|
sql->Postgres.preparedUnsafe(
|
|
592
|
-
|
|
407
|
+
makeLoadByIdSql(~pgSchema, ~tableName=table.tableName),
|
|
593
408
|
ids->Obj.magic,
|
|
594
409
|
)
|
|
595
410
|
| _ =>
|
|
596
411
|
sql->Postgres.preparedUnsafe(
|
|
597
|
-
|
|
412
|
+
makeLoadByIdsSql(~pgSchema, ~tableName=table.tableName),
|
|
598
413
|
[ids]->Obj.magic,
|
|
599
414
|
)
|
|
600
415
|
}
|
|
@@ -619,52 +434,6 @@ let make = (
|
|
|
619
434
|
}
|
|
620
435
|
}
|
|
621
436
|
|
|
622
|
-
let loadByFieldOrThrow = async (
|
|
623
|
-
~fieldName: string,
|
|
624
|
-
~fieldSchema,
|
|
625
|
-
~fieldValue,
|
|
626
|
-
~operator: Persistence.operator,
|
|
627
|
-
~table: Table.table,
|
|
628
|
-
~rowsSchema,
|
|
629
|
-
) => {
|
|
630
|
-
let params = try [fieldValue->S.reverseConvertToJsonOrThrow(fieldSchema)]->Obj.magic catch {
|
|
631
|
-
| exn =>
|
|
632
|
-
raise(
|
|
633
|
-
Persistence.StorageError({
|
|
634
|
-
message: `Failed loading "${table.tableName}" from storage by field "${fieldName}". Couldn't serialize provided value.`,
|
|
635
|
-
reason: exn,
|
|
636
|
-
}),
|
|
637
|
-
)
|
|
638
|
-
}
|
|
639
|
-
switch await sql->Postgres.preparedUnsafe(
|
|
640
|
-
makeLoadByFieldQuery(
|
|
641
|
-
~pgSchema,
|
|
642
|
-
~tableName=table.tableName,
|
|
643
|
-
~fieldName,
|
|
644
|
-
~operator=(operator :> string),
|
|
645
|
-
),
|
|
646
|
-
params,
|
|
647
|
-
) {
|
|
648
|
-
| exception exn =>
|
|
649
|
-
raise(
|
|
650
|
-
Persistence.StorageError({
|
|
651
|
-
message: `Failed loading "${table.tableName}" from storage by field "${fieldName}"`,
|
|
652
|
-
reason: exn,
|
|
653
|
-
}),
|
|
654
|
-
)
|
|
655
|
-
| rows =>
|
|
656
|
-
try rows->S.parseOrThrow(rowsSchema) catch {
|
|
657
|
-
| exn =>
|
|
658
|
-
raise(
|
|
659
|
-
Persistence.StorageError({
|
|
660
|
-
message: `Failed to parse "${table.tableName}" loaded from storage by ids`,
|
|
661
|
-
reason: exn,
|
|
662
|
-
}),
|
|
663
|
-
)
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
|
|
668
437
|
let setOrThrow = (
|
|
669
438
|
type item,
|
|
670
439
|
~items: array<item>,
|
|
@@ -680,212 +449,10 @@ let make = (
|
|
|
680
449
|
)
|
|
681
450
|
}
|
|
682
451
|
|
|
683
|
-
let setEffectCacheOrThrow = async (
|
|
684
|
-
~effectName: string,
|
|
685
|
-
~ids: array<string>,
|
|
686
|
-
~outputs: array<Internal.effectOutput>,
|
|
687
|
-
~outputSchema: S.t<Internal.effectOutput>,
|
|
688
|
-
~initialize: bool,
|
|
689
|
-
) => {
|
|
690
|
-
let table = Table.mkTable(
|
|
691
|
-
cacheTablePrefix ++ effectName,
|
|
692
|
-
~fields=[
|
|
693
|
-
Table.mkField("id", Text, ~fieldSchema=S.string, ~isPrimaryKey=true),
|
|
694
|
-
Table.mkField("output", JsonB, ~fieldSchema=S.json(~validate=false)),
|
|
695
|
-
],
|
|
696
|
-
~compositeIndices=[],
|
|
697
|
-
)
|
|
698
|
-
|
|
699
|
-
if initialize {
|
|
700
|
-
let _ = await sql->Postgres.unsafe(makeCreateTableQuery(table, ~pgSchema))
|
|
701
|
-
switch onNewTables {
|
|
702
|
-
| Some(onNewTables) => await onNewTables(~tableNames=[table.tableName])
|
|
703
|
-
| None => ()
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
let items = []
|
|
708
|
-
for idx in 0 to outputs->Array.length - 1 {
|
|
709
|
-
items
|
|
710
|
-
->Js.Array2.push({
|
|
711
|
-
"id": ids[idx],
|
|
712
|
-
"output": outputs[idx],
|
|
713
|
-
})
|
|
714
|
-
->ignore
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
await setOrThrow(
|
|
718
|
-
~items,
|
|
719
|
-
~table,
|
|
720
|
-
~itemSchema=S.schema(s =>
|
|
721
|
-
{
|
|
722
|
-
"id": s.matches(S.string),
|
|
723
|
-
"output": s.matches(outputSchema),
|
|
724
|
-
}
|
|
725
|
-
),
|
|
726
|
-
)
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
let dumpEffectCache = async () => {
|
|
730
|
-
try {
|
|
731
|
-
let cacheTableInfo: array<schemaCacheTableInfo> =
|
|
732
|
-
(await sql
|
|
733
|
-
->Postgres.unsafe(makeSchemaCacheTableInfoQuery(~pgSchema)))
|
|
734
|
-
->Js.Array2.filter(i => i.count > 0)
|
|
735
|
-
|
|
736
|
-
if cacheTableInfo->Utils.Array.notEmpty {
|
|
737
|
-
// Create .envio/cache directory if it doesn't exist
|
|
738
|
-
try {
|
|
739
|
-
await NodeJs.Fs.Promises.access(cacheDirPath)
|
|
740
|
-
} catch {
|
|
741
|
-
| _ =>
|
|
742
|
-
// Create directory if it doesn't exist
|
|
743
|
-
await NodeJs.Fs.Promises.mkdir(~path=cacheDirPath, ~options={recursive: true})
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
// Command for testing. Run from generated
|
|
747
|
-
// docker-compose exec -T -u postgres envio-postgres psql -d envio-dev -c 'COPY "public"."envio_effect_getTokenMetadata" TO STDOUT (FORMAT text, HEADER);' > ../.envio/cache/getTokenMetadata.tsv
|
|
748
|
-
|
|
749
|
-
switch await getConnectedPsqlExec(~pgUser, ~pgHost, ~pgDatabase, ~pgPort) {
|
|
750
|
-
| Ok(psqlExec) => {
|
|
751
|
-
Logging.info(
|
|
752
|
-
`Dumping cache: ${cacheTableInfo
|
|
753
|
-
->Js.Array2.map(({tableName, count}) =>
|
|
754
|
-
tableName ++ " (" ++ count->Belt.Int.toString ++ " rows)"
|
|
755
|
-
)
|
|
756
|
-
->Js.Array2.joinWith(", ")}`,
|
|
757
|
-
)
|
|
758
|
-
|
|
759
|
-
let promises = cacheTableInfo->Js.Array2.map(async ({tableName}) => {
|
|
760
|
-
let cacheName = tableName->Js.String2.sliceToEnd(~from=cacheTablePrefixLength)
|
|
761
|
-
let outputFile =
|
|
762
|
-
NodeJs.Path.join(cacheDirPath, cacheName ++ ".tsv")->NodeJs.Path.toString
|
|
763
|
-
|
|
764
|
-
let command = `${psqlExec} -c 'COPY "${pgSchema}"."${tableName}" TO STDOUT WITH (FORMAT text, HEADER);' > ${outputFile}`
|
|
765
|
-
|
|
766
|
-
Promise.make((resolve, reject) => {
|
|
767
|
-
NodeJs.ChildProcess.execWithOptions(
|
|
768
|
-
command,
|
|
769
|
-
psqlExecOptions,
|
|
770
|
-
(~error, ~stdout, ~stderr as _) => {
|
|
771
|
-
switch error {
|
|
772
|
-
| Value(error) => reject(error)
|
|
773
|
-
| Null => resolve(stdout)
|
|
774
|
-
}
|
|
775
|
-
},
|
|
776
|
-
)
|
|
777
|
-
})
|
|
778
|
-
})
|
|
779
|
-
|
|
780
|
-
let _ = await promises->Promise.all
|
|
781
|
-
Logging.info(`Successfully dumped cache to ${cacheDirPath->NodeJs.Path.toString}`)
|
|
782
|
-
}
|
|
783
|
-
| Error(message) => Logging.error(`Failed to dump cache. ${message}`)
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
} catch {
|
|
787
|
-
| exn => Logging.errorWithExn(exn->Internal.prettifyExn, `Failed to dump cache.`)
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
let restoreEffectCache = async (~withUpload) => {
|
|
792
|
-
if withUpload {
|
|
793
|
-
// Try to restore cache tables from binary files
|
|
794
|
-
let nothingToUploadErrorMessage = "Nothing to upload."
|
|
795
|
-
|
|
796
|
-
switch await Promise.all2((
|
|
797
|
-
NodeJs.Fs.Promises.readdir(cacheDirPath)
|
|
798
|
-
->Promise.thenResolve(e => Ok(e))
|
|
799
|
-
->Promise.catch(_ => Promise.resolve(Error(nothingToUploadErrorMessage))),
|
|
800
|
-
getConnectedPsqlExec(~pgUser, ~pgHost, ~pgDatabase, ~pgPort),
|
|
801
|
-
)) {
|
|
802
|
-
| (Ok(entries), Ok(psqlExec)) => {
|
|
803
|
-
let cacheFiles = entries->Js.Array2.filter(entry => {
|
|
804
|
-
entry->Js.String2.endsWith(".tsv")
|
|
805
|
-
})
|
|
806
|
-
|
|
807
|
-
let _ =
|
|
808
|
-
await cacheFiles
|
|
809
|
-
->Js.Array2.map(entry => {
|
|
810
|
-
let cacheName = entry->Js.String2.slice(~from=0, ~to_=-4) // Remove .tsv extension
|
|
811
|
-
let tableName = cacheTablePrefix ++ cacheName
|
|
812
|
-
let table = Table.mkTable(
|
|
813
|
-
tableName,
|
|
814
|
-
~fields=[
|
|
815
|
-
Table.mkField("id", Text, ~fieldSchema=S.string, ~isPrimaryKey=true),
|
|
816
|
-
Table.mkField("output", JsonB, ~fieldSchema=S.json(~validate=false)),
|
|
817
|
-
],
|
|
818
|
-
~compositeIndices=[],
|
|
819
|
-
)
|
|
820
|
-
|
|
821
|
-
sql
|
|
822
|
-
->Postgres.unsafe(makeCreateTableQuery(table, ~pgSchema))
|
|
823
|
-
->Promise.then(() => {
|
|
824
|
-
let inputFile = NodeJs.Path.join(cacheDirPath, entry)->NodeJs.Path.toString
|
|
825
|
-
|
|
826
|
-
let command = `${psqlExec} -c 'COPY "${pgSchema}"."${tableName}" FROM STDIN WITH (FORMAT text, HEADER);' < ${inputFile}`
|
|
827
|
-
|
|
828
|
-
Promise.make(
|
|
829
|
-
(resolve, reject) => {
|
|
830
|
-
NodeJs.ChildProcess.execWithOptions(
|
|
831
|
-
command,
|
|
832
|
-
psqlExecOptions,
|
|
833
|
-
(~error, ~stdout, ~stderr as _) => {
|
|
834
|
-
switch error {
|
|
835
|
-
| Value(error) => reject(error)
|
|
836
|
-
| Null => resolve(stdout)
|
|
837
|
-
}
|
|
838
|
-
},
|
|
839
|
-
)
|
|
840
|
-
},
|
|
841
|
-
)
|
|
842
|
-
})
|
|
843
|
-
})
|
|
844
|
-
->Promise.all
|
|
845
|
-
|
|
846
|
-
Logging.info("Successfully uploaded cache.")
|
|
847
|
-
}
|
|
848
|
-
| (Error(message), _)
|
|
849
|
-
| (_, Error(message)) =>
|
|
850
|
-
if message === nothingToUploadErrorMessage {
|
|
851
|
-
Logging.info("No cache found to upload.")
|
|
852
|
-
} else {
|
|
853
|
-
Logging.error(`Failed to upload cache, continuing without it. ${message}`)
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
let cacheTableInfo: array<schemaCacheTableInfo> =
|
|
859
|
-
await sql->Postgres.unsafe(makeSchemaCacheTableInfoQuery(~pgSchema))
|
|
860
|
-
|
|
861
|
-
if withUpload && cacheTableInfo->Utils.Array.notEmpty {
|
|
862
|
-
switch onNewTables {
|
|
863
|
-
| Some(onNewTables) =>
|
|
864
|
-
await onNewTables(
|
|
865
|
-
~tableNames=cacheTableInfo->Js.Array2.map(info => {
|
|
866
|
-
info.tableName
|
|
867
|
-
}),
|
|
868
|
-
)
|
|
869
|
-
| None => ()
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
cacheTableInfo->Js.Array2.map((info): Persistence.effectCacheRecord => {
|
|
874
|
-
{
|
|
875
|
-
effectName: info.tableName->Js.String2.sliceToEnd(~from=cacheTablePrefixLength),
|
|
876
|
-
count: info.count,
|
|
877
|
-
}
|
|
878
|
-
})
|
|
879
|
-
}
|
|
880
|
-
|
|
881
452
|
{
|
|
882
453
|
isInitialized,
|
|
883
454
|
initialize,
|
|
884
|
-
loadByFieldOrThrow,
|
|
885
455
|
loadByIdsOrThrow,
|
|
886
456
|
setOrThrow,
|
|
887
|
-
setEffectCacheOrThrow,
|
|
888
|
-
dumpEffectCache,
|
|
889
|
-
restoreEffectCache,
|
|
890
457
|
}
|
|
891
458
|
}
|