envio 3.12.0 → 3.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/package.json +6 -6
  2. package/src/ChainFetching.res +7 -3
  3. package/src/ChainFetching.res.mjs +1 -1
  4. package/src/ChainState.res +12 -3
  5. package/src/ChainState.res.mjs +3 -3
  6. package/src/ChainState.resi +1 -1
  7. package/src/CrossChainState.res +1 -1
  8. package/src/CrossChainState.res.mjs +1 -1
  9. package/src/FetchState.res +51 -46
  10. package/src/FetchState.res.mjs +54 -36
  11. package/src/InMemoryTable.res +155 -67
  12. package/src/InMemoryTable.res.mjs +151 -60
  13. package/src/LoadLayer.res +57 -34
  14. package/src/LoadLayer.res.mjs +45 -62
  15. package/src/LoadLayer.resi +1 -1
  16. package/src/PgStorage.res +92 -62
  17. package/src/PgStorage.res.mjs +77 -50
  18. package/src/TestIndexer.res +9 -25
  19. package/src/TestIndexer.res.mjs +4 -3
  20. package/src/UserContext.res +13 -32
  21. package/src/UserContext.res.mjs +1 -7
  22. package/src/Utils.res +1 -4
  23. package/src/Utils.res.mjs +7 -16
  24. package/src/db/EntityFilter.res +487 -275
  25. package/src/db/EntityFilter.res.mjs +557 -309
  26. package/src/db/Table.res +21 -6
  27. package/src/db/Table.res.mjs +13 -4
  28. package/src/sources/BlockStore.res +7 -2
  29. package/src/sources/EvmHyperSyncSource.res +2 -0
  30. package/src/sources/EvmHyperSyncSource.res.mjs +2 -2
  31. package/src/sources/FuelHyperSyncSource.res +1 -0
  32. package/src/sources/FuelHyperSyncSource.res.mjs +1 -1
  33. package/src/sources/HyperSync.res +4 -0
  34. package/src/sources/HyperSync.res.mjs +4 -2
  35. package/src/sources/HyperSync.resi +1 -0
  36. package/src/sources/HyperSyncClient.res +3 -0
  37. package/src/sources/HyperSyncSSE.res +1 -1
  38. package/src/sources/HyperSyncSSE.res.mjs +4 -10
  39. package/src/sources/RpcSource.res +1 -0
  40. package/src/sources/RpcSource.res.mjs +1 -1
  41. package/src/sources/SimulateSource.res +1 -0
  42. package/src/sources/SimulateSource.res.mjs +1 -1
  43. package/src/sources/Source.res +7 -0
  44. package/src/sources/SourceManager.res +4 -3
  45. package/src/sources/SourceManager.res.mjs +2 -2
  46. package/src/sources/SvmHyperSyncClient.res +5 -0
  47. package/src/sources/SvmHyperSyncSource.res +3 -0
  48. package/src/sources/SvmHyperSyncSource.res.mjs +4 -2
package/src/LoadLayer.res CHANGED
@@ -6,23 +6,6 @@ let scopeKeySuffix = (scope: Internal.chainScope) =>
6
6
  | Chain(chainId) => `.${chainId->ChainId.toString}`
7
7
  }
8
8
 
9
- // Narrows a query to the scope's chain. Cross-chain entities have no chain-id
10
- // column, so their filter is left untouched.
11
- let scopeFilter = (filter: EntityFilter.t, ~table: Table.table, ~scope: Internal.chainScope) =>
12
- switch (scope, table->Table.getChainIdField) {
13
- | (Chain(chainId), Some(field)) =>
14
- EntityFilter.And({
15
- filters: [
16
- filter,
17
- Eq({
18
- fieldName: field.fieldName,
19
- fieldValue: chainId->(Utils.magic: ChainId.t => unknown),
20
- }),
21
- ],
22
- })
23
- | _ => filter
24
- }
25
-
26
9
  let loadById = (
27
10
  ~loadManager,
28
11
  ~persistence: Persistence.t,
@@ -48,10 +31,10 @@ let loadById = (
48
31
  (
49
32
  await storage.loadOrThrow(
50
33
  ~table=entityConfig.table,
51
- ~filter=EntityFilter.In({
52
- fieldName: Table.idFieldName,
53
- fieldValue: idsToLoad->(Utils.magic: array<string> => array<unknown>),
54
- })->scopeFilter(~table=entityConfig.table, ~scope),
34
+ ~filter=EntityFilter.byIds(idsToLoad)->EntityFilter.scoped(
35
+ ~table=entityConfig.table,
36
+ ~scope,
37
+ ),
55
38
  )
56
39
  )->(Utils.magic: array<unknown> => array<Internal.entity>)
57
40
  } catch {
@@ -273,15 +256,9 @@ let loadEffect = (
273
256
  let {outputSchema} = effect.storageMeta
274
257
 
275
258
  let dbEntities = try {
276
- (
277
- await storage.loadOrThrow(
278
- ~table,
279
- ~filter=EntityFilter.In({
280
- fieldName: Table.idFieldName,
281
- fieldValue: idsToLoad->(Utils.magic: array<string> => array<unknown>),
282
- }),
283
- )
284
- )->(Utils.magic: array<unknown> => array<Internal.effectCacheItem>)
259
+ (await storage.loadOrThrow(~table, ~filter=EntityFilter.byIds(idsToLoad)))->(
260
+ Utils.magic: array<unknown> => array<Internal.effectCacheItem>
261
+ )
285
262
  } catch {
286
263
  | exn =>
287
264
  Ecosystem.getItemLogger(item, ~ecosystem)->Logging.childWarn({
@@ -355,7 +332,7 @@ let loadEffect = (
355
332
  )
356
333
  }
357
334
 
358
- let loadByFilter = (
335
+ let loadByParsedFilter = (
359
336
  ~loadManager,
360
337
  ~persistence: Persistence.t,
361
338
  ~entityConfig: Internal.entityConfig,
@@ -378,7 +355,9 @@ let loadByFilter = (
378
355
 
379
356
  let size = ref(0)
380
357
 
381
- filters->Array.forEach(filter => inMemTable->InMemoryTable.Entity.addEmptyIndex(~filter))
358
+ filters->Array.forEach(filter =>
359
+ inMemTable->InMemoryTable.Entity.addEmptyIndex(~filter, ~table=entityConfig.table)
360
+ )
382
361
 
383
362
  // Any non-derived field can be filtered on, so the columns this query reads
384
363
  // are indexed on demand before it runs rather than promised by the schema.
@@ -399,7 +378,7 @@ let loadByFilter = (
399
378
  (
400
379
  await storage.loadOrThrow(
401
380
  ~table=entityConfig.table,
402
- ~filter=filter->scopeFilter(~table=entityConfig.table, ~scope),
381
+ ~filter=filter->EntityFilter.scoped(~table=entityConfig.table, ~scope),
403
382
  )
404
383
  )->(Utils.magic: array<unknown> => array<Internal.entity>)
405
384
 
@@ -412,6 +391,10 @@ let loadByFilter = (
412
391
  )
413
392
  })
414
393
 
394
+ // Every row this filter's values could match is now in the table, so a
395
+ // later getWhere naming any of them needs no round trip of its own.
396
+ inMemTable->InMemoryTable.Entity.recordLoadedValues(~filter, ~table=entityConfig.table)
397
+
415
398
  size := size.contents + entities->Array.length
416
399
  } catch {
417
400
  | Persistence.StorageError({message, reason}) =>
@@ -442,13 +425,53 @@ let loadByFilter = (
442
425
  )
443
426
  }
444
427
 
428
+ // Keying an _in walks every value, so it's computed once here and handed to
429
+ // the load manager rather than recomputed by the hasher.
430
+ let filterKey = filter->EntityFilter.toString(~table=entityConfig.table)
431
+
432
+ if !(inMemTable->InMemoryTable.Entity.hasIndex)(filterKey) {
433
+ inMemTable->InMemoryTable.Entity.tryIndexFromLoadedValues(~filter, ~table=entityConfig.table)
434
+ }
435
+
445
436
  loadManager->LoadManager.call(
446
437
  ~key,
447
438
  ~load,
448
439
  ~input=filter,
449
440
  ~shouldGroup,
450
- ~hasher=EntityFilter.toString,
441
+ ~hasher=_ => filterKey,
451
442
  ~getUnsafeInMemory=inMemTable->InMemoryTable.Entity.getUnsafeOnIndex,
452
443
  ~hasInMemory=inMemTable->InMemoryTable.Entity.hasIndex,
453
444
  )
454
445
  }
446
+
447
+ let loadByFilter = (
448
+ ~loadManager,
449
+ ~persistence,
450
+ ~entityConfig: Internal.entityConfig,
451
+ ~scope,
452
+ ~indexerState,
453
+ ~shouldGroup,
454
+ ~item,
455
+ ~ecosystem,
456
+ ~filter: dict<dict<unknown>>,
457
+ ) =>
458
+ // Rejecting rather than throwing keeps a bad filter failing only its own
459
+ // call, even when the caller batches several with Promise.all.
460
+ try {
461
+ loadByParsedFilter(
462
+ ~loadManager,
463
+ ~persistence,
464
+ ~entityConfig,
465
+ ~scope,
466
+ ~indexerState,
467
+ ~shouldGroup,
468
+ ~item,
469
+ ~ecosystem,
470
+ ~filter=filter->EntityFilter.parseOrThrow(
471
+ ~entityName=entityConfig.name,
472
+ ~table=entityConfig.table,
473
+ ),
474
+ )
475
+ } catch {
476
+ | exn => Promise.reject(exn->Utils.prettifyExn)
477
+ }
@@ -1,6 +1,5 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
- import * as Table from "./db/Table.res.mjs";
4
3
  import * as Utils from "./Utils.res.mjs";
5
4
  import * as ChainId from "./ChainId.res.mjs";
6
5
  import * as Logging from "./Logging.res.mjs";
@@ -27,25 +26,6 @@ function scopeKeySuffix(scope) {
27
26
  }
28
27
  }
29
28
 
30
- function scopeFilter(filter, table, scope) {
31
- let match = Table.getChainIdField(table);
32
- if (scope === "crossChain" || match === undefined) {
33
- return filter;
34
- } else {
35
- return {
36
- operator: "and",
37
- filters: [
38
- filter,
39
- {
40
- operator: "=",
41
- fieldName: match.fieldName,
42
- fieldValue: scope
43
- }
44
- ]
45
- };
46
- }
47
- }
48
-
49
29
  function loadById(loadManager, persistence, entityConfig, scope, indexerState, shouldGroup, item, ecosystem, entityId) {
50
30
  let key = entityConfig.name + `.get` + scopeKeySuffix(scope);
51
31
  let inMemTable = InMemoryStore.getInMemTable(indexerState, entityConfig, scope);
@@ -54,11 +34,7 @@ function loadById(loadManager, persistence, entityConfig, scope, indexerState, s
54
34
  let timerRef = IndexerState.startStorageLoad(indexerState, storage.name, key);
55
35
  let dbEntities;
56
36
  try {
57
- dbEntities = await storage.loadOrThrow(scopeFilter({
58
- operator: "in",
59
- fieldName: Table.idFieldName,
60
- fieldValue: idsToLoad
61
- }, entityConfig.table, scope), entityConfig.table);
37
+ dbEntities = await storage.loadOrThrow(EntityFilter.scoped(EntityFilter.byIds(idsToLoad), entityConfig.table, scope), entityConfig.table);
62
38
  } catch (raw_exn) {
63
39
  let exn = Primitive_exceptions.internalToException(raw_exn);
64
40
  if (exn.RE_EXN_ID === Persistence.StorageError) {
@@ -157,11 +133,7 @@ function loadEffect(loadManager, persistence, effect, effectArgs, scope, indexer
157
133
  let outputSchema = match$1.outputSchema;
158
134
  let dbEntities;
159
135
  try {
160
- dbEntities = await storage.loadOrThrow({
161
- operator: "in",
162
- fieldName: Table.idFieldName,
163
- fieldValue: idsToLoad
164
- }, table);
136
+ dbEntities = await storage.loadOrThrow(EntityFilter.byIds(idsToLoad), table);
165
137
  } catch (raw_exn) {
166
138
  let exn = Primitive_exceptions.internalToException(raw_exn);
167
139
  Logging.childWarn(Ecosystem.getItemLogger(item, ecosystem), {
@@ -211,38 +183,49 @@ function loadEffect(loadManager, persistence, effect, effectArgs, scope, indexer
211
183
  }
212
184
 
213
185
  function loadByFilter(loadManager, persistence, entityConfig, scope, indexerState, shouldGroup, item, ecosystem, filter) {
214
- let key = EntityFilter.toOperationKey(filter, entityConfig.name) + scopeKeySuffix(scope);
215
- let inMemTable = InMemoryStore.getInMemTable(indexerState, entityConfig, scope);
216
- let load = async (filters, param) => {
217
- let storage = Persistence.getInitializedStorageOrThrow(persistence);
218
- let timerRef = IndexerState.startStorageLoad(indexerState, storage.name, key);
219
- let size = {
220
- contents: 0
221
- };
222
- filters.forEach(filter => InMemoryTable.Entity.addEmptyIndex(inMemTable, filter));
223
- await storage.ensureQueryIndexes(entityConfig, scope, filters);
224
- let queries = EntityFilter.merge(filters);
225
- await Promise.all(queries.map(async filter => {
226
- try {
227
- let entities = await storage.loadOrThrow(scopeFilter(filter, entityConfig.table, scope), entityConfig.table);
228
- let committedCheckpointId = IndexerState.committedCheckpointIdFor(indexerState, scope);
229
- entities.forEach(entity => InMemoryTable.Entity.initValue(inMemTable, committedCheckpointId, entity.id, entity));
230
- size.contents = size.contents + entities.length | 0;
231
- return;
232
- } catch (raw_exn) {
233
- let exn = Primitive_exceptions.internalToException(raw_exn);
234
- if (exn.RE_EXN_ID === Persistence.StorageError) {
235
- return ErrorHandling.mkLogAndRaise(Logging.createChildFrom(Ecosystem.getItemLogger(item, ecosystem), {
236
- operation: key,
237
- params: EntityFilter.getParams(filter)
238
- }), exn.message, exn.reason);
186
+ try {
187
+ let filter$1 = EntityFilter.parseOrThrow(filter, entityConfig.name, entityConfig.table);
188
+ let key = EntityFilter.toOperationKey(filter$1, entityConfig.name) + scopeKeySuffix(scope);
189
+ let inMemTable = InMemoryStore.getInMemTable(indexerState, entityConfig, scope);
190
+ let load = async (filters, param) => {
191
+ let storage = Persistence.getInitializedStorageOrThrow(persistence);
192
+ let timerRef = IndexerState.startStorageLoad(indexerState, storage.name, key);
193
+ let size = {
194
+ contents: 0
195
+ };
196
+ filters.forEach(filter => InMemoryTable.Entity.addEmptyIndex(inMemTable, filter, entityConfig.table));
197
+ await storage.ensureQueryIndexes(entityConfig, scope, filters);
198
+ let queries = EntityFilter.merge(filters);
199
+ await Promise.all(queries.map(async filter => {
200
+ try {
201
+ let entities = await storage.loadOrThrow(EntityFilter.scoped(filter, entityConfig.table, scope), entityConfig.table);
202
+ let committedCheckpointId = IndexerState.committedCheckpointIdFor(indexerState, scope);
203
+ entities.forEach(entity => InMemoryTable.Entity.initValue(inMemTable, committedCheckpointId, entity.id, entity));
204
+ InMemoryTable.Entity.recordLoadedValues(inMemTable, filter, entityConfig.table);
205
+ size.contents = size.contents + entities.length | 0;
206
+ return;
207
+ } catch (raw_exn) {
208
+ let exn = Primitive_exceptions.internalToException(raw_exn);
209
+ if (exn.RE_EXN_ID === Persistence.StorageError) {
210
+ return ErrorHandling.mkLogAndRaise(Logging.createChildFrom(Ecosystem.getItemLogger(item, ecosystem), {
211
+ operation: key,
212
+ params: EntityFilter.getParams(filter)
213
+ }), exn.message, exn.reason);
214
+ }
215
+ throw exn;
239
216
  }
240
- throw exn;
241
- }
242
- }));
243
- return IndexerState.endStorageLoad(indexerState, timerRef, storage.name, key, Stdlib_Array.reduce(queries, 0, (acc, query) => acc + EntityFilter.valuesCount(query) | 0), size.contents);
244
- };
245
- return LoadManager.call(loadManager, filter, key, load, EntityFilter.toString, shouldGroup, InMemoryTable.Entity.hasIndex(inMemTable), InMemoryTable.Entity.getUnsafeOnIndex(inMemTable));
217
+ }));
218
+ return IndexerState.endStorageLoad(indexerState, timerRef, storage.name, key, Stdlib_Array.reduce(queries, 0, (acc, query) => acc + EntityFilter.valuesCount(query) | 0), size.contents);
219
+ };
220
+ let filterKey = EntityFilter.toString(filter$1, entityConfig.table);
221
+ if (!InMemoryTable.Entity.hasIndex(inMemTable)(filterKey)) {
222
+ InMemoryTable.Entity.tryIndexFromLoadedValues(inMemTable, filter$1, entityConfig.table);
223
+ }
224
+ return LoadManager.call(loadManager, filter$1, key, load, param => filterKey, shouldGroup, InMemoryTable.Entity.hasIndex(inMemTable), InMemoryTable.Entity.getUnsafeOnIndex(inMemTable));
225
+ } catch (raw_exn) {
226
+ let exn = Primitive_exceptions.internalToException(raw_exn);
227
+ return Promise.reject(Utils.prettifyExn(exn));
228
+ }
246
229
  }
247
230
 
248
231
  export {
@@ -250,4 +233,4 @@ export {
250
233
  loadByFilter,
251
234
  loadEffect,
252
235
  }
253
- /* Table Not a pure module */
236
+ /* Utils Not a pure module */
@@ -19,7 +19,7 @@ let loadByFilter: (
19
19
  ~shouldGroup: bool,
20
20
  ~item: Internal.item,
21
21
  ~ecosystem: Ecosystem.t,
22
- ~filter: EntityFilter.t,
22
+ ~filter: dict<dict<unknown>>,
23
23
  ) => promise<array<Internal.entity>>
24
24
 
25
25
  let loadEffect: (
package/src/PgStorage.res CHANGED
@@ -409,9 +409,10 @@ let makeLoadQuery = (~pgSchema, ~tableName, ~condition) => {
409
409
  // Field names are spliced as quoted identifiers only after the queryFields
410
410
  // lookup proves they exist on the table (and they originate from
411
411
  // codegen-validated schemas), so the interpolation can't be abused.
412
- let rec makeFilterCondition = (
412
+ let makeFilterCondition = (
413
413
  ~filter: EntityFilter.t,
414
414
  ~table: Table.table,
415
+ ~pgSchema,
415
416
  ~params: array<unknown>,
416
417
  ) => {
417
418
  // Filters reference fields by API name, while the SQL references columns
@@ -447,60 +448,89 @@ let rec makeFilterCondition = (
447
448
  params->Array.push(param)->ignore
448
449
  `$${params->Array.length->Int.toString}`
449
450
  }
450
- let scalarCondition = (~fieldName, ~fieldValue, ~op) => {
451
+
452
+ let condition = ref("")
453
+ filter
454
+ ->EntityFilter.entries
455
+ ->Utils.Dict.forEachWithKey((operators, fieldName) => {
451
456
  let queryField = getQueryFieldOrThrow(fieldName)
452
- `"${queryField.pgDbFieldName}" ${op} ${serializeParamOrThrow(
453
- ~queryField,
454
- ~fieldName,
455
- ~fieldValue,
456
- ~isArray=false,
457
- )}`
458
- }
459
- switch filter {
460
- // A per-chain entity's table is partitioned by its chain-id column, and
461
- // Postgres can only prune a plan it caches when that column is a constant in
462
- // the SQL. Bound, the cached plan has to keep every partition, and the
463
- // planner ends up throwing it away and re-planning on every execution
464
- // instead — measured at 315us per load against 218us with the id written in,
465
- // on 30 chains.
466
- //
467
- // The cost is that each chain gets its own query text, so Postgres caches a
468
- // prepared statement per (entity, chain, filter shape) rather than per
469
- // (entity, filter shape). Measured at ~8KB of plan cache each, which is ~10MB
470
- // per connection for 40 entities across 30 chains — accepted, since the
471
- // alternative is a cached plan that can't prune.
472
- //
473
- // `LoadLayer.scopeFilter` is what puts this filter here, and the value is
474
- // range-checked to a non-negative safe integer, so it can carry nothing but
475
- // digits.
476
- | Eq({fieldName, fieldValue}) if getQueryFieldOrThrow(fieldName).isChainId =>
477
- `"${getQueryFieldOrThrow(fieldName).pgDbFieldName}" = ${fieldValue
478
- ->ChainId.normalizeOrThrow
479
- ->ChainId.toString}`
480
- | Eq({fieldName, fieldValue}) => scalarCondition(~fieldName, ~fieldValue, ~op="=")
481
- | Gt({fieldName, fieldValue}) => scalarCondition(~fieldName, ~fieldValue, ~op=">")
482
- | Lt({fieldName, fieldValue}) => scalarCondition(~fieldName, ~fieldValue, ~op="<")
483
- | In({fieldName, fieldValue}) => {
484
- let queryField = getQueryFieldOrThrow(fieldName)
485
- `"${queryField.pgDbFieldName}" = ANY(${serializeParamOrThrow(
486
- ~queryField,
487
- ~fieldName,
488
- ~fieldValue=fieldValue->(Utils.magic: array<unknown> => unknown),
489
- ~isArray=true,
490
- )})`
491
- }
492
- | And({filters: []}) =>
493
- throw(
494
- Persistence.StorageError({
495
- message: `Failed loading "${table.tableName}" from storage. The "and" filter must contain at least one nested filter.`,
496
- reason: Utils.Error.make(`Empty "and" filter`),
497
- }),
498
- )
499
- | And({filters}) =>
500
- `(${filters
501
- ->Array.map(filter => makeFilterCondition(~filter, ~table, ~params))
502
- ->Array.join(" AND ")})`
503
- }
457
+ operators->Utils.Dict.forEachWithKey((fieldValue, operator) => {
458
+ let column = `"${queryField.pgDbFieldName}"`
459
+ let part = switch operator {
460
+ // A per-chain entity's table is partitioned by its chain-id column, and
461
+ // Postgres can only prune a plan it caches when that column is a constant
462
+ // in the SQL. Bound, the cached plan has to keep every partition, and the
463
+ // planner ends up throwing it away and re-planning on every execution
464
+ // instead — measured at 315us per load against 218us with the id written
465
+ // in, on 30 chains.
466
+ //
467
+ // The cost is that each chain gets its own query text, so Postgres caches
468
+ // a prepared statement per (entity, chain, filter shape) rather than per
469
+ // (entity, filter shape). Measured at ~8KB of plan cache each, which is
470
+ // ~10MB per connection for 40 entities across 30 chains — accepted, since
471
+ // the alternative is a cached plan that can't prune.
472
+ //
473
+ // `EntityFilter.scoped` is what puts this filter here, and the value is
474
+ // range-checked to a non-negative safe integer, so it can carry nothing
475
+ // but digits.
476
+ | "_eq" if queryField.isChainId =>
477
+ `${column} = ${fieldValue->ChainId.normalizeOrThrow->ChainId.toString}`
478
+ // Postgres arrays are rectangular, so candidates for a list column can't
479
+ // be bound as one array unless they all have the same length, and
480
+ // postgres.js can't bind a boolean array at all
481
+ // (https://github.com/porsager/postgres/issues/471). One equality per
482
+ // candidate has neither problem.
483
+ | "_in" if queryField.isArray || queryField.fieldType === Boolean =>
484
+ switch fieldValue->EntityFilter.asArray {
485
+ | [] => "FALSE"
486
+ | candidates =>
487
+ `(${candidates
488
+ ->Array.map(
489
+ candidate =>
490
+ `${column} = ${serializeParamOrThrow(
491
+ ~queryField,
492
+ ~fieldName,
493
+ ~fieldValue=candidate,
494
+ ~isArray=false,
495
+ )}`,
496
+ )
497
+ ->Array.join(" OR ")})`
498
+ }
499
+ | "_in" =>
500
+ let param = serializeParamOrThrow(~queryField, ~fieldName, ~fieldValue, ~isArray=true)
501
+ switch queryField.fieldType {
502
+ // A bound array of strings is text[], which has no equality with an
503
+ // enum. The insert casts the same way.
504
+ | Enum({config}) => `${column} = ANY(${param}::TEXT[]::"${pgSchema}".${config.name}[])`
505
+ | _ => `${column} = ANY(${param})`
506
+ }
507
+ | _ =>
508
+ let sqlOperator = switch operator {
509
+ | "_eq" => "="
510
+ | "_gt" => ">"
511
+ | "_lt" => "<"
512
+ | "_gte" => ">="
513
+ | "_lte" => "<="
514
+ | _ =>
515
+ throw(
516
+ Persistence.StorageError({
517
+ message: `Failed loading "${table.tableName}" from storage. Unknown filter operator "${operator}".`,
518
+ reason: Utils.Error.make(`Unknown filter operator "${operator}"`),
519
+ }),
520
+ )
521
+ }
522
+ `${column} ${sqlOperator} ${serializeParamOrThrow(
523
+ ~queryField,
524
+ ~fieldName,
525
+ ~fieldValue,
526
+ ~isArray=false,
527
+ )}`
528
+ }
529
+ condition := (condition.contents === "" ? part : condition.contents ++ " AND " ++ part)
530
+ })
531
+ })
532
+
533
+ condition.contents
504
534
  }
505
535
 
506
536
  // The chain-id predicate a per-chain entity's row-level SQL needs, already
@@ -1709,7 +1739,7 @@ let make = (
1709
1739
  // Must match PG_CONTAINER in packages/cli/src/docker_env.rs
1710
1740
  let containerName = "envio-postgres"
1711
1741
  let psqlExecOptions: NodeJs.ChildProcess.execOptions = {
1712
- env: Dict.fromArray([("PGPASSWORD", pgPassword), ("PATH", %raw(`process.env.PATH`))]),
1742
+ env: dict{"PGPASSWORD": pgPassword, "PATH": %raw(`process.env.PATH`)},
1713
1743
  }
1714
1744
 
1715
1745
  let cacheDirPath = NodeJs.Path.resolve([
@@ -2002,7 +2032,7 @@ let make = (
2002
2032
 
2003
2033
  let loadOrThrow = async (~filter: EntityFilter.t, ~table: Table.table) => {
2004
2034
  let params = []
2005
- let condition = makeFilterCondition(~filter, ~table, ~params)
2035
+ let condition = makeFilterCondition(~filter, ~table, ~pgSchema, ~params)
2006
2036
  switch await sql->Postgres.preparedUnsafe(
2007
2037
  makeLoadQuery(~pgSchema, ~tableName=table.tableName, ~condition),
2008
2038
  params->Obj.magic,
@@ -2033,9 +2063,10 @@ let make = (
2033
2063
  let queryFields = table->Table.queryFields
2034
2064
  let columns = []
2035
2065
  let seen = Utils.Set.make()
2036
- let rec collect = (filter: EntityFilter.t) =>
2037
- switch filter {
2038
- | Eq({fieldName}) | Gt({fieldName}) | Lt({fieldName}) | In({fieldName}) =>
2066
+ filters->Array.forEach(filter =>
2067
+ filter
2068
+ ->EntityFilter.entries
2069
+ ->Utils.Dict.forEachWithKey((_, fieldName) =>
2039
2070
  switch queryFields->Utils.Dict.dangerouslyGetNonOption(fieldName) {
2040
2071
  | Some({pgDbFieldName}) =>
2041
2072
  if !(seen->Utils.Set.has(pgDbFieldName)) {
@@ -2044,9 +2075,8 @@ let make = (
2044
2075
  }
2045
2076
  | None => ()
2046
2077
  }
2047
- | And({filters}) => filters->Array.forEach(collect)
2048
- }
2049
- filters->Array.forEach(collect)
2078
+ )
2079
+ )
2050
2080
  columns
2051
2081
  }
2052
2082