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/src/PgStorage.res CHANGED
@@ -1,16 +1,16 @@
1
- let makeCreateIndexQuery = (~tableName, ~indexFields, ~pgSchema) => {
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 makeCreateTableIndicesQuery = (table: Table.table, ~pgSchema) => {
7
+ let makeCreateTableIndicesSql = (table: Table.table, ~pgSchema) => {
8
8
  open Belt
9
9
  let tableName = table.tableName
10
10
  let createIndex = indexField =>
11
- makeCreateIndexQuery(~tableName, ~indexFields=[indexField], ~pgSchema)
11
+ makeCreateIndexSql(~tableName, ~indexFields=[indexField], ~pgSchema)
12
12
  let createCompositeIndex = indexFields => {
13
- makeCreateIndexQuery(~tableName, ~indexFields, ~pgSchema)
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 makeCreateTableQuery = (table: Table.table, ~pgSchema) => {
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
- ~isEmptyPgSchema=false,
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
- isEmptyPgSchema && pgSchema === "public"
75
- // Hosted Service already have a DB with the created public schema
76
- // It also doesn't allow to simply drop it,
77
- // so we reuse the existing schema when it's empty
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 "${pgUser}";
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 := query.contents ++ "\n" ++ enumCreateQuery
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" ++ makeCreateTableQuery(table, ~pgSchema)
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 = makeCreateTableIndicesQuery(table, ~pgSchema)
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
- makeCreateIndexQuery(
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
- // Add cache row count function
133
- functionsQuery :=
134
- functionsQuery.contents ++
135
- "\n" ++
136
- `CREATE OR REPLACE FUNCTION get_cache_row_count(table_name text)
137
- RETURNS integer AS $$
138
- DECLARE
139
- result integer;
140
- BEGIN
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 makeLoadByIdQuery = (~pgSchema, ~tableName) => {
153
+ let makeLoadByIdSql = (~pgSchema, ~tableName) => {
152
154
  `SELECT * FROM "${pgSchema}"."${tableName}" WHERE id = $1 LIMIT 1;`
153
155
  }
154
156
 
155
- let makeLoadByFieldQuery = (~pgSchema, ~tableName, ~fieldName, ~operator) => {
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 makeInsertUnnestSetQuery = (~pgSchema, ~table: Table.table, ~itemSchema, ~isRawEvents) => {
161
+ let makeInsertUnnestSetSql = (~pgSchema, ~table: Table.table, ~itemSchema, ~isRawEvents) => {
164
162
  let {quotedFieldNames, quotedNonPrimaryFieldNames, arrayFieldTypes} =
165
- table->Table.toSqlParams(~schema=itemSchema, ~pgSchema)
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 makeInsertValuesSetQuery = (~pgSchema, ~table: Table.table, ~itemSchema, ~itemsCount) => {
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, ~pgSchema)
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
- "query": makeInsertUnnestSetQuery(~pgSchema, ~table, ~itemSchema, ~isRawEvents),
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
- "query": makeInsertValuesSetQuery(
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 data = switch setQueryCache->Utils.WeakMap.get(table) {
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
- if data["isInsertValues"] {
354
- let chunks = chunkArray(items, ~chunkSize=maxItemsPerQuery)
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 === maxItemsPerQuery
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
- ? data["query"]
365
- : makeInsertValuesSetQuery(~pgSchema, ~table, ~itemSchema, ~itemsCount=chunkSize),
366
- data["convertOrThrow"](chunk->(Utils.magic: array<'item> => array<unknown>)),
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->Internal.prettifyExn,
371
+ reason: exn,
391
372
  }),
392
373
  )
393
374
  }
394
375
  }
395
376
  }
396
377
 
397
- let setEntityHistoryOrThrow = (
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 envioTables =
380
+ let schemas =
542
381
  await sql->Postgres.unsafe(
543
- `SELECT table_schema FROM information_schema.tables WHERE table_schema = '${pgSchema}' AND table_name = '${eventSyncStateTableName}';`,
382
+ `SELECT schema_name FROM information_schema.schemata WHERE schema_name = '${pgSchema}';`,
544
383
  )
545
- envioTables->Utils.Array.notEmpty
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
- ~isEmptyPgSchema=schemaTableNames->Utils.Array.isEmpty,
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
- makeLoadByIdQuery(~pgSchema, ~tableName=table.tableName),
407
+ makeLoadByIdSql(~pgSchema, ~tableName=table.tableName),
593
408
  ids->Obj.magic,
594
409
  )
595
410
  | _ =>
596
411
  sql->Postgres.preparedUnsafe(
597
- makeLoadByIdsQuery(~pgSchema, ~tableName=table.tableName),
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
  }