envio 2.26.0-alpha.9 → 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
- ~reuseExistingPgSchema=false,
61
+ ~cleanRun=false,
62
62
  ) => {
63
63
  let allTables = generalTables->Array.copy
64
64
  let allEntityTables = []
@@ -71,15 +71,12 @@ let makeInitializeTransaction = (
71
71
 
72
72
  let query = ref(
73
73
  (
74
- reuseExistingPgSchema
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 an existing schema when it's empty (our case)
78
- ? ""
79
- : `DROP SCHEMA IF EXISTS "${pgSchema}" CASCADE;
80
- CREATE SCHEMA "${pgSchema}";\n`
74
+ cleanRun
75
+ ? `DROP SCHEMA IF EXISTS "${pgSchema}" CASCADE;
76
+ CREATE SCHEMA "${pgSchema}";`
77
+ : `CREATE SCHEMA IF NOT EXISTS "${pgSchema}";`
81
78
  ) ++
82
- `GRANT ALL ON SCHEMA "${pgSchema}" TO "${pgUser}";
79
+ `GRANT ALL ON SCHEMA "${pgSchema}" TO ${pgUser};
83
80
  GRANT ALL ON SCHEMA "${pgSchema}" TO public;`,
84
81
  )
85
82
 
@@ -90,17 +87,31 @@ GRANT ALL ON SCHEMA "${pgSchema}" TO public;`,
90
87
  ->Js.Array2.map(v => `'${v->(Utils.magic: Internal.enum => string)}'`)
91
88
  ->Js.Array2.joinWith(", ")});`
92
89
 
93
- 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
+ }
94
105
  })
95
106
 
96
107
  // Batch all table creation first (optimal for PostgreSQL)
97
108
  allTables->Js.Array2.forEach((table: Table.table) => {
98
- query := query.contents ++ "\n" ++ makeCreateTableQuery(table, ~pgSchema)
109
+ query := query.contents ++ "\n" ++ makeCreateTableSql(table, ~pgSchema)
99
110
  })
100
111
 
101
112
  // Then batch all indices (better performance when tables exist)
102
113
  allTables->Js.Array2.forEach((table: Table.table) => {
103
- let indices = makeCreateTableIndicesQuery(table, ~pgSchema)
114
+ let indices = makeCreateTableIndicesSql(table, ~pgSchema)
104
115
  if indices !== "" {
105
116
  query := query.contents ++ "\n" ++ indices
106
117
  }
@@ -120,7 +131,7 @@ GRANT ALL ON SCHEMA "${pgSchema}" TO public;`,
120
131
  query :=
121
132
  query.contents ++
122
133
  "\n" ++
123
- makeCreateIndexQuery(
134
+ makeCreateIndexSql(
124
135
  ~tableName=derivedFromField.derivedFromEntity,
125
136
  ~indexFields=[indexField],
126
137
  ~pgSchema,
@@ -128,26 +139,28 @@ GRANT ALL ON SCHEMA "${pgSchema}" TO public;`,
128
139
  })
129
140
  })
130
141
 
131
- [query.contents]->Js.Array2.concat(
132
- functionsQuery.contents !== "" ? [functionsQuery.contents] : [],
133
- )
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] : [])
134
151
  }
135
152
 
136
- let makeLoadByIdQuery = (~pgSchema, ~tableName) => {
153
+ let makeLoadByIdSql = (~pgSchema, ~tableName) => {
137
154
  `SELECT * FROM "${pgSchema}"."${tableName}" WHERE id = $1 LIMIT 1;`
138
155
  }
139
156
 
140
- let makeLoadByFieldQuery = (~pgSchema, ~tableName, ~fieldName, ~operator) => {
141
- `SELECT * FROM "${pgSchema}"."${tableName}" WHERE "${fieldName}" ${operator} $1;`
142
- }
143
-
144
- let makeLoadByIdsQuery = (~pgSchema, ~tableName) => {
157
+ let makeLoadByIdsSql = (~pgSchema, ~tableName) => {
145
158
  `SELECT * FROM "${pgSchema}"."${tableName}" WHERE id = ANY($1::text[]);`
146
159
  }
147
160
 
148
- let makeInsertUnnestSetQuery = (~pgSchema, ~table: Table.table, ~itemSchema, ~isRawEvents) => {
161
+ let makeInsertUnnestSetSql = (~pgSchema, ~table: Table.table, ~itemSchema, ~isRawEvents) => {
149
162
  let {quotedFieldNames, quotedNonPrimaryFieldNames, arrayFieldTypes} =
150
- table->Table.toSqlParams(~schema=itemSchema, ~pgSchema)
163
+ table->Table.toSqlParams(~schema=itemSchema)
151
164
 
152
165
  let primaryKeyFieldNames = Table.getPrimaryKeyFieldNames(table)
153
166
 
@@ -175,9 +188,8 @@ SELECT * FROM unnest(${arrayFieldTypes
175
188
  } ++ ";"
176
189
  }
177
190
 
178
- let makeInsertValuesSetQuery = (~pgSchema, ~table: Table.table, ~itemSchema, ~itemsCount) => {
179
- let {quotedFieldNames, quotedNonPrimaryFieldNames} =
180
- table->Table.toSqlParams(~schema=itemSchema, ~pgSchema)
191
+ let makeInsertValuesSetSql = (~pgSchema, ~table: Table.table, ~itemSchema, ~itemsCount) => {
192
+ let {quotedFieldNames, quotedNonPrimaryFieldNames} = table->Table.toSqlParams(~schema=itemSchema)
181
193
 
182
194
  let primaryKeyFieldNames = Table.getPrimaryKeyFieldNames(table)
183
195
  let fieldsCount = quotedFieldNames->Array.length
@@ -224,13 +236,12 @@ VALUES${placeholders.contents}` ++
224
236
  // they are always guaranteed to be an object.
225
237
  // FIXME what about Fuel params?
226
238
  let rawEventsTableName = "raw_events"
227
- let eventSyncStateTableName = "event_sync_state"
228
239
 
229
240
  // Constants for chunking
230
241
  let maxItemsPerQuery = 500
231
242
 
232
243
  let makeTableBatchSetQuery = (~pgSchema, ~table: Table.table, ~itemSchema: S.t<'item>) => {
233
- let {dbSchema, hasArrayField} = table->Table.toSqlParams(~schema=itemSchema, ~pgSchema)
244
+ let {dbSchema, hasArrayField} = table->Table.toSqlParams(~schema=itemSchema)
234
245
  let isRawEvents = table.tableName === rawEventsTableName
235
246
 
236
247
  // Should experiment how much it'll affect performance
@@ -242,7 +253,7 @@ let makeTableBatchSetQuery = (~pgSchema, ~table: Table.table, ~itemSchema: S.t<'
242
253
 
243
254
  if isRawEvents || !hasArrayField {
244
255
  {
245
- "query": makeInsertUnnestSetQuery(~pgSchema, ~table, ~itemSchema, ~isRawEvents),
256
+ "sql": makeInsertUnnestSetSql(~pgSchema, ~table, ~itemSchema, ~isRawEvents),
246
257
  "convertOrThrow": S.compile(
247
258
  S.unnest(dbSchema),
248
259
  ~input=Value,
@@ -254,12 +265,7 @@ let makeTableBatchSetQuery = (~pgSchema, ~table: Table.table, ~itemSchema: S.t<'
254
265
  }
255
266
  } else {
256
267
  {
257
- "query": makeInsertValuesSetQuery(
258
- ~pgSchema,
259
- ~table,
260
- ~itemSchema,
261
- ~itemsCount=maxItemsPerQuery,
262
- ),
268
+ "sql": makeInsertValuesSetSql(~pgSchema, ~table, ~itemSchema, ~itemsCount=maxItemsPerQuery),
263
269
  "convertOrThrow": S.compile(
264
270
  S.unnest(itemSchema)->S.preprocess(_ => {
265
271
  serializer: Utils.Array.flatten->Utils.magic,
@@ -285,35 +291,6 @@ let chunkArray = (arr: array<'a>, ~chunkSize) => {
285
291
  chunks
286
292
  }
287
293
 
288
- let removeInvalidUtf8InPlace = entities =>
289
- entities->Js.Array2.forEach(item => {
290
- let dict = item->(Utils.magic: 'a => dict<unknown>)
291
- dict->Utils.Dict.forEachWithKey((key, value) => {
292
- if value->Js.typeof === "string" {
293
- let value = value->(Utils.magic: unknown => string)
294
- // We mutate here, since we don't care
295
- // about the original value with \x00 anyways.
296
- //
297
- // This is unsafe, but we rely that it'll use
298
- // the mutated reference on retry.
299
- // TODO: Test it properly after we start using
300
- // in-memory PGLite for indexer test framework.
301
- dict->Js.Dict.set(
302
- key,
303
- value
304
- ->Utils.String.replaceAll("\x00", "")
305
- ->(Utils.magic: string => unknown),
306
- )
307
- }
308
- })
309
- })
310
-
311
- let pgEncodingErrorSchema = S.object(s =>
312
- s.tag("message", `invalid byte sequence for encoding "UTF8": 0x00`)
313
- )
314
-
315
- exception PgEncodingError({table: Table.table})
316
-
317
294
  // WeakMap for caching table batch set queries
318
295
  let setQueryCache = Utils.WeakMap.make()
319
296
  let setOrThrow = async (sql, ~items, ~table: Table.table, ~itemSchema, ~pgSchema) => {
@@ -321,7 +298,7 @@ let setOrThrow = async (sql, ~items, ~table: Table.table, ~itemSchema, ~pgSchema
321
298
  ()
322
299
  } else {
323
300
  // Get or create cached query for this table
324
- let data = switch setQueryCache->Utils.WeakMap.get(table) {
301
+ let query = switch setQueryCache->Utils.WeakMap.get(table) {
325
302
  | Some(cached) => cached
326
303
  | None => {
327
304
  let newQuery = makeTableBatchSetQuery(
@@ -334,31 +311,50 @@ let setOrThrow = async (sql, ~items, ~table: Table.table, ~itemSchema, ~pgSchema
334
311
  }
335
312
  }
336
313
 
314
+ let sqlQuery = query["sql"]
315
+
337
316
  try {
338
- if data["isInsertValues"] {
339
- 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)
340
334
  let responses = []
341
335
  chunks->Js.Array2.forEach(chunk => {
342
336
  let chunkSize = chunk->Array.length
343
- let isFullChunk = chunkSize === maxItemsPerQuery
337
+ let isFullChunk = chunkSize === maxChunkSize
344
338
 
345
339
  let response = sql->Postgres.preparedUnsafe(
346
340
  // Either use the sql query for full chunks from cache
347
341
  // or create a new one for partial chunks on the fly.
348
342
  isFullChunk
349
- ? data["query"]
350
- : makeInsertValuesSetQuery(~pgSchema, ~table, ~itemSchema, ~itemsCount=chunkSize),
351
- 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,
352
351
  )
353
352
  responses->Js.Array2.push(response)->ignore
354
353
  })
355
354
  let _ = await Promise.all(responses)
356
355
  } else {
357
356
  // Use UNNEST approach for single query
358
- await sql->Postgres.preparedUnsafe(
359
- data["query"],
360
- data["convertOrThrow"](items->(Utils.magic: array<'item> => array<unknown>)),
361
- )
357
+ await sql->Postgres.preparedUnsafe(sqlQuery, payload->Obj.magic)
362
358
  }
363
359
  } catch {
364
360
  | S.Raised(_) as exn =>
@@ -379,81 +375,23 @@ let setOrThrow = async (sql, ~items, ~table: Table.table, ~itemSchema, ~pgSchema
379
375
  }
380
376
  }
381
377
 
382
- let setEntityHistoryOrThrow = (
383
- sql,
384
- ~entityHistory: EntityHistory.t<'entity>,
385
- ~rows: array<EntityHistory.historyRow<'entity>>,
386
- ~shouldCopyCurrentEntity=?,
387
- ~shouldRemoveInvalidUtf8=false,
388
- ) => {
389
- rows
390
- ->Belt.Array.map(historyRow => {
391
- let row = historyRow->S.reverseConvertToJsonOrThrow(entityHistory.schema)
392
- if shouldRemoveInvalidUtf8 {
393
- [row]->removeInvalidUtf8InPlace
394
- }
395
- entityHistory.insertFn(
396
- sql,
397
- row,
398
- ~shouldCopyCurrentEntity=switch shouldCopyCurrentEntity {
399
- | Some(v) => v
400
- | None => {
401
- let containsRollbackDiffChange =
402
- historyRow.containsRollbackDiffChange->Belt.Option.getWithDefault(false)
403
- !containsRollbackDiffChange
404
- }
405
- },
406
- )
407
- })
408
- ->Promise.all
409
- ->(Utils.magic: promise<array<unit>> => promise<unit>)
410
- }
411
-
412
- type schemaTableName = {
413
- @as("table_name")
414
- tableName: string,
415
- }
416
-
417
- let makeSchemaTableNamesQuery = (~pgSchema) => {
418
- `SELECT table_name FROM information_schema.tables WHERE table_schema = '${pgSchema}';`
419
- }
420
-
421
378
  let make = (~sql: Postgres.sql, ~pgSchema, ~pgUser): Persistence.storage => {
422
379
  let isInitialized = async () => {
423
- let envioTables =
380
+ let schemas =
424
381
  await sql->Postgres.unsafe(
425
- `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}';`,
426
383
  )
427
- envioTables->Utils.Array.notEmpty
384
+ schemas->Utils.Array.notEmpty
428
385
  }
429
386
 
430
- let initialize = async (~entities=[], ~generalTables=[], ~enums=[]) => {
431
- let schemaTableNames: array<schemaTableName> =
432
- await sql->Postgres.unsafe(makeSchemaTableNamesQuery(~pgSchema))
433
-
434
- // The initialization query will completely drop the schema and recreate it from scratch.
435
- // So we need to check if the schema is not used for anything else than envio.
436
- if (
437
- // Should pass with existing schema with no tables
438
- // This might happen when used with public schema
439
- // which is automatically created by postgres.
440
- schemaTableNames->Utils.Array.notEmpty &&
441
- // Otherwise should throw if there's a table, but no envio specific one
442
- // This means that the schema is used for something else than envio.
443
- !(schemaTableNames->Js.Array2.some(table => table.tableName === eventSyncStateTableName))
444
- ) {
445
- Js.Exn.raiseError(
446
- `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.`,
447
- )
448
- }
449
-
387
+ let initialize = async (~entities=[], ~generalTables=[], ~enums=[], ~cleanRun=false) => {
450
388
  let queries = makeInitializeTransaction(
451
389
  ~pgSchema,
452
390
  ~pgUser,
453
391
  ~generalTables,
454
392
  ~entities,
455
393
  ~enums,
456
- ~reuseExistingPgSchema=schemaTableNames->Utils.Array.isEmpty,
394
+ ~cleanRun,
457
395
  )
458
396
  // Execute all queries within a single transaction for integrity
459
397
  let _ = await sql->Postgres.beginSql(sql => {
@@ -461,37 +399,17 @@ let make = (~sql: Postgres.sql, ~pgSchema, ~pgUser): Persistence.storage => {
461
399
  })
462
400
  }
463
401
 
464
- let loadEffectCaches = async () => {
465
- let schemaTableNames: array<schemaTableName> =
466
- await sql->Postgres.unsafe(makeSchemaTableNamesQuery(~pgSchema))
467
- schemaTableNames->Belt.Array.keepMapU(schemaTableName => {
468
- if schemaTableName.tableName->Js.String2.startsWith("effect_cache_") {
469
- Some(
470
- (
471
- {
472
- name: schemaTableName.tableName,
473
- size: 0,
474
- table: None,
475
- }: Persistence.effectCache
476
- ),
477
- )
478
- } else {
479
- None
480
- }
481
- })
482
- }
483
-
484
402
  let loadByIdsOrThrow = async (~ids, ~table: Table.table, ~rowsSchema) => {
485
403
  switch await (
486
404
  switch ids {
487
405
  | [_] =>
488
406
  sql->Postgres.preparedUnsafe(
489
- makeLoadByIdQuery(~pgSchema, ~tableName=table.tableName),
407
+ makeLoadByIdSql(~pgSchema, ~tableName=table.tableName),
490
408
  ids->Obj.magic,
491
409
  )
492
410
  | _ =>
493
411
  sql->Postgres.preparedUnsafe(
494
- makeLoadByIdsQuery(~pgSchema, ~tableName=table.tableName),
412
+ makeLoadByIdsSql(~pgSchema, ~tableName=table.tableName),
495
413
  [ids]->Obj.magic,
496
414
  )
497
415
  }
@@ -516,52 +434,6 @@ let make = (~sql: Postgres.sql, ~pgSchema, ~pgUser): Persistence.storage => {
516
434
  }
517
435
  }
518
436
 
519
- let loadByFieldOrThrow = async (
520
- ~fieldName: string,
521
- ~fieldSchema,
522
- ~fieldValue,
523
- ~operator: Persistence.operator,
524
- ~table: Table.table,
525
- ~rowsSchema,
526
- ) => {
527
- let params = try [fieldValue->S.reverseConvertToJsonOrThrow(fieldSchema)]->Obj.magic catch {
528
- | exn =>
529
- raise(
530
- Persistence.StorageError({
531
- message: `Failed loading "${table.tableName}" from storage by field "${fieldName}". Couldn't serialize provided value.`,
532
- reason: exn,
533
- }),
534
- )
535
- }
536
- switch await sql->Postgres.preparedUnsafe(
537
- makeLoadByFieldQuery(
538
- ~pgSchema,
539
- ~tableName=table.tableName,
540
- ~fieldName,
541
- ~operator=(operator :> string),
542
- ),
543
- params,
544
- ) {
545
- | exception exn =>
546
- raise(
547
- Persistence.StorageError({
548
- message: `Failed loading "${table.tableName}" from storage by field "${fieldName}"`,
549
- reason: exn,
550
- }),
551
- )
552
- | rows =>
553
- try rows->S.parseOrThrow(rowsSchema) catch {
554
- | exn =>
555
- raise(
556
- Persistence.StorageError({
557
- message: `Failed to parse "${table.tableName}" loaded from storage by ids`,
558
- reason: exn,
559
- }),
560
- )
561
- }
562
- }
563
- }
564
-
565
437
  let setOrThrow = (
566
438
  type item,
567
439
  ~items: array<item>,
@@ -580,8 +452,6 @@ let make = (~sql: Postgres.sql, ~pgSchema, ~pgUser): Persistence.storage => {
580
452
  {
581
453
  isInitialized,
582
454
  initialize,
583
- loadByFieldOrThrow,
584
- loadEffectCaches,
585
455
  loadByIdsOrThrow,
586
456
  setOrThrow,
587
457
  }