@reventlessdev/reventless-postgres 3.0.0-alpha.124 → 3.0.0-alpha.125

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 (97) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/lib/bs/.compiler.log +2 -2
  3. package/lib/bs/compiler-info.json +1 -1
  4. package/lib/bs/src/DcbEventLogStorage_Postgres-ReventlessPostgres.cmi +0 -0
  5. package/lib/bs/src/DcbEventLogStorage_Postgres-ReventlessPostgres.cmt +0 -0
  6. package/lib/bs/src/DcbEventLogStorage_Postgres_Ops-ReventlessPostgres.cmi +0 -0
  7. package/lib/bs/src/DcbEventLogStorage_Postgres_Ops-ReventlessPostgres.cmt +0 -0
  8. package/lib/bs/src/DcbEventLogStorage_Postgres_Ops.ast +0 -0
  9. package/lib/bs/src/DcbEventLogStorage_Postgres_Ops.res +41 -40
  10. package/lib/bs/src/EventLogChangeFeed-ReventlessPostgres.cmi +0 -0
  11. package/lib/bs/src/EventLogChangeFeed-ReventlessPostgres.cmt +0 -0
  12. package/lib/bs/src/EventLogChangeFeed.ast +0 -0
  13. package/lib/bs/src/EventLogChangeFeed.res +10 -4
  14. package/lib/bs/src/EventLogStorage_Postgres-ReventlessPostgres.cmi +0 -0
  15. package/lib/bs/src/EventLogStorage_Postgres-ReventlessPostgres.cmt +0 -0
  16. package/lib/bs/src/EventLogStorage_Postgres_Ops-ReventlessPostgres.cmi +0 -0
  17. package/lib/bs/src/EventLogStorage_Postgres_Ops-ReventlessPostgres.cmt +0 -0
  18. package/lib/bs/src/EventLogStorage_Postgres_Ops.ast +0 -0
  19. package/lib/bs/src/EventLogStorage_Postgres_Ops.res +23 -13
  20. package/lib/bs/src/PgChangeFeed-ReventlessPostgres.cmi +0 -0
  21. package/lib/bs/src/PgChangeFeed-ReventlessPostgres.cmt +0 -0
  22. package/lib/bs/src/PgChangeFeed.ast +0 -0
  23. package/lib/bs/src/PgChangeFeed.res +15 -8
  24. package/lib/bs/src/PgDriver-ReventlessPostgres.cmt +0 -0
  25. package/lib/bs/src/PgDriver.ast +0 -0
  26. package/lib/bs/src/PgDriver.res +1 -2
  27. package/lib/bs/src/QueryDbStorage_Postgres-ReventlessPostgres.cmi +0 -0
  28. package/lib/bs/src/QueryDbStorage_Postgres-ReventlessPostgres.cmt +0 -0
  29. package/lib/bs/src/QueryDbStorage_Postgres.ast +0 -0
  30. package/lib/bs/src/QueryDbStorage_Postgres.res +7 -2
  31. package/lib/bs/src/QueryDbStorage_Postgres_Ops-ReventlessPostgres.cmi +0 -0
  32. package/lib/bs/src/QueryDbStorage_Postgres_Ops-ReventlessPostgres.cmt +0 -0
  33. package/lib/bs/src/QueryDbStorage_Postgres_Ops.ast +0 -0
  34. package/lib/bs/src/QueryDbStorage_Postgres_Ops.res +13 -13
  35. package/lib/bs/src/QueryEnginePostgres-ReventlessPostgres.cmi +0 -0
  36. package/lib/bs/src/QueryEnginePostgres-ReventlessPostgres.cmt +0 -0
  37. package/lib/bs/src/QueryEnginePostgres.ast +0 -0
  38. package/lib/bs/src/QueryEnginePostgres.res +72 -30
  39. package/lib/bs/src/QueryEnginePostgres.res.mjs +4 -4
  40. package/lib/bs/tests/PostgresIntegrationTest-ReventlessPostgres.cmi +0 -0
  41. package/lib/bs/tests/PostgresIntegrationTest-ReventlessPostgres.cmt +0 -0
  42. package/lib/bs/tests/PostgresIntegrationTest.ast +0 -0
  43. package/lib/bs/tests/PostgresIntegrationTest.res +157 -104
  44. package/lib/bs/tests/QueryEnginePostgresListPageTest-ReventlessPostgres.cmi +0 -0
  45. package/lib/bs/tests/QueryEnginePostgresListPageTest-ReventlessPostgres.cmt +0 -0
  46. package/lib/bs/tests/QueryEnginePostgresListPageTest.ast +0 -0
  47. package/lib/bs/tests/QueryEnginePostgresListPageTest.res +29 -6
  48. package/lib/ocaml/.compiler.log +2 -2
  49. package/lib/ocaml/DcbEventLogStorage_Postgres-ReventlessPostgres.cmi +0 -0
  50. package/lib/ocaml/DcbEventLogStorage_Postgres-ReventlessPostgres.cmt +0 -0
  51. package/lib/ocaml/DcbEventLogStorage_Postgres_Ops-ReventlessPostgres.cmi +0 -0
  52. package/lib/ocaml/DcbEventLogStorage_Postgres_Ops-ReventlessPostgres.cmt +0 -0
  53. package/lib/ocaml/DcbEventLogStorage_Postgres_Ops.ast +0 -0
  54. package/lib/ocaml/DcbEventLogStorage_Postgres_Ops.res +41 -40
  55. package/lib/ocaml/EventLogChangeFeed-ReventlessPostgres.cmi +0 -0
  56. package/lib/ocaml/EventLogChangeFeed-ReventlessPostgres.cmt +0 -0
  57. package/lib/ocaml/EventLogChangeFeed.ast +0 -0
  58. package/lib/ocaml/EventLogChangeFeed.res +10 -4
  59. package/lib/ocaml/EventLogStorage_Postgres-ReventlessPostgres.cmi +0 -0
  60. package/lib/ocaml/EventLogStorage_Postgres-ReventlessPostgres.cmt +0 -0
  61. package/lib/ocaml/EventLogStorage_Postgres_Ops-ReventlessPostgres.cmi +0 -0
  62. package/lib/ocaml/EventLogStorage_Postgres_Ops-ReventlessPostgres.cmt +0 -0
  63. package/lib/ocaml/EventLogStorage_Postgres_Ops.ast +0 -0
  64. package/lib/ocaml/EventLogStorage_Postgres_Ops.res +23 -13
  65. package/lib/ocaml/PgChangeFeed-ReventlessPostgres.cmi +0 -0
  66. package/lib/ocaml/PgChangeFeed-ReventlessPostgres.cmt +0 -0
  67. package/lib/ocaml/PgChangeFeed.ast +0 -0
  68. package/lib/ocaml/PgChangeFeed.res +15 -8
  69. package/lib/ocaml/PgDriver-ReventlessPostgres.cmt +0 -0
  70. package/lib/ocaml/PgDriver.ast +0 -0
  71. package/lib/ocaml/PgDriver.res +1 -2
  72. package/lib/ocaml/PostgresIntegrationTest.res +157 -104
  73. package/lib/ocaml/QueryDbStorage_Postgres-ReventlessPostgres.cmi +0 -0
  74. package/lib/ocaml/QueryDbStorage_Postgres-ReventlessPostgres.cmt +0 -0
  75. package/lib/ocaml/QueryDbStorage_Postgres.ast +0 -0
  76. package/lib/ocaml/QueryDbStorage_Postgres.res +7 -2
  77. package/lib/ocaml/QueryDbStorage_Postgres_Ops-ReventlessPostgres.cmi +0 -0
  78. package/lib/ocaml/QueryDbStorage_Postgres_Ops-ReventlessPostgres.cmt +0 -0
  79. package/lib/ocaml/QueryDbStorage_Postgres_Ops.ast +0 -0
  80. package/lib/ocaml/QueryDbStorage_Postgres_Ops.res +13 -13
  81. package/lib/ocaml/QueryEnginePostgres-ReventlessPostgres.cmi +0 -0
  82. package/lib/ocaml/QueryEnginePostgres-ReventlessPostgres.cmt +0 -0
  83. package/lib/ocaml/QueryEnginePostgres.ast +0 -0
  84. package/lib/ocaml/QueryEnginePostgres.res +72 -30
  85. package/lib/ocaml/QueryEnginePostgresListPageTest.res +29 -6
  86. package/package.json +6 -6
  87. package/src/DcbEventLogStorage_Postgres_Ops.res +41 -40
  88. package/src/EventLogChangeFeed.res +10 -4
  89. package/src/EventLogStorage_Postgres_Ops.res +23 -13
  90. package/src/PgChangeFeed.res +15 -8
  91. package/src/PgDriver.res +1 -2
  92. package/src/QueryDbStorage_Postgres.res +7 -2
  93. package/src/QueryDbStorage_Postgres_Ops.res +13 -13
  94. package/src/QueryEnginePostgres.res +72 -30
  95. package/src/QueryEnginePostgres.res.mjs +4 -4
  96. package/tests/PostgresIntegrationTest.res +157 -104
  97. package/tests/QueryEnginePostgresListPageTest.res +29 -6
@@ -26,7 +26,11 @@ let makeStorage = (
26
26
  }
27
27
 
28
28
  // Standalone/deploy storage: inject the pool via a provider module.
29
- module Make = (P: {let pool: PgDriver.pool}) => {
29
+ module Make = (
30
+ P: {
31
+ let pool: PgDriver.pool
32
+ },
33
+ ) => {
30
34
  type api = unit
31
35
  type role = unit
32
36
 
@@ -37,6 +41,7 @@ module Make = (P: {let pool: PgDriver.pool}) => {
37
41
  ~ttl as _=?,
38
42
  ~api as _,
39
43
  ~apiRole as _,
40
- ~owner as _, ~opts as _,
44
+ ~owner as _,
45
+ ~opts as _,
41
46
  ) => makeStorage(~pool=P.pool, ~name, ~indexes, ~subIdField)
42
47
  }
@@ -14,14 +14,12 @@ open Reventless.ReadModel
14
14
  let tableName = (name: string): string => "qdb_" ++ name->String.replaceAll("-", "_")
15
15
 
16
16
  // Collapse anything outside [A-Za-z0-9_] to _ — predictable, injection-safe idents.
17
- let sanitizeIdent = (s: string): string =>
18
- s->String.replaceRegExp(%re("/[^A-Za-z0-9_]/g"), "_")
17
+ let sanitizeIdent = (s: string): string => s->String.replaceRegExp(/[^A-Za-z0-9_]/g, "_")
19
18
 
20
19
  let notExpiredClause = "(expires_at IS NULL OR expires_at > extract(epoch from now()))"
21
20
 
22
21
  // JSONB text extraction for a single top-level field.
23
- let jsonField = (field: string): string =>
24
- `(item->>'${field->String.replaceAll("'", "''")}')`
22
+ let jsonField = (field: string): string => `(item->>'${field->String.replaceAll("'", "''")}')`
25
23
 
26
24
  let compositeExpr = (fields: array<string>, sep: string): string => {
27
25
  let escapedSep = sep->String.replaceAll("'", "''")
@@ -120,10 +118,13 @@ let makeOperations = (
120
118
 
121
119
  let rowsFor = async (id: string): array<JSON.t> => {
122
120
  await ready()
123
- (await pool->PgDriver.query(
124
- `SELECT item FROM ${table} WHERE partition_key = $1 AND ${notExpiredClause} ORDER BY sub_key ASC`,
125
- [JSON.Encode.string(id)],
126
- ))->Array.map(decodeItem)
121
+
122
+ (
123
+ await pool->PgDriver.query(
124
+ `SELECT item FROM ${table} WHERE partition_key = $1 AND ${notExpiredClause} ORDER BY sub_key ASC`,
125
+ [JSON.Encode.string(id)],
126
+ )
127
+ )->Array.map(decodeItem)
127
128
  }
128
129
 
129
130
  let saveOne = async (id: string, state: JSON.t, ttl: option<int>) => {
@@ -149,11 +150,10 @@ let makeOperations = (
149
150
 
150
151
  let load: QueryDb.load<string, JSON.t> = async id => Ok(await rowsFor(id))
151
152
 
152
- let loadStream: QueryDb.loadStream<string, JSON.t> =
153
- id =>
154
- Effect.promise(() => rowsFor(id))
155
- ->Stream.fromEffect
156
- ->Stream.flatMap(arr => Stream.fromIterable(arr))
153
+ let loadStream: QueryDb.loadStream<string, JSON.t> = id =>
154
+ Effect.promise(() => rowsFor(id))
155
+ ->Stream.fromEffect
156
+ ->Stream.flatMap(arr => Stream.fromIterable(arr))
157
157
 
158
158
  let save: QueryDb.save<string, JSON.t> = async (id, state, _saveMode, ttl) => {
159
159
  await saveOne(id, state, ttl)
Binary file
@@ -28,7 +28,12 @@ let param = (b: paramBuilder, v: JSON.t): string => {
28
28
  let col = (field: string): string => `(item->>'${field->String.replaceAll("'", "''")}')`
29
29
 
30
30
  // One Filter.config → SQL predicate, appending any bound params.
31
- let filterSql = (b: paramBuilder, field: string, cmp: QueryEngine.Filter.comparator, v: QueryEngine.value): string => {
31
+ let filterSql = (
32
+ b: paramBuilder,
33
+ field: string,
34
+ cmp: QueryEngine.Filter.comparator,
35
+ v: QueryEngine.value,
36
+ ): string => {
32
37
  let c = col(field)
33
38
  let bind = () => b->param(JSON.Encode.string(valueToText(v)))
34
39
  let cmpSql = op =>
@@ -48,10 +53,16 @@ let filterSql = (b: paramBuilder, field: string, cmp: QueryEngine.Filter.compara
48
53
  }
49
54
  }
50
55
 
51
- let subIdSql = (b: paramBuilder, field: string, cmp: QueryEngine.SubId.comparator, v: QueryEngine.value): string => {
56
+ let subIdSql = (
57
+ b: paramBuilder,
58
+ field: string,
59
+ cmp: QueryEngine.SubId.comparator,
60
+ v: QueryEngine.value,
61
+ ): string => {
52
62
  let c = col(field)
53
63
  let bind = () => b->param(JSON.Encode.string(valueToText(v)))
54
- let cmpSql = op => isNumeric(v) ? `${c}::numeric ${op} ${bind()}::numeric` : `${c} ${op} ${bind()}`
64
+ let cmpSql = op =>
65
+ isNumeric(v) ? `${c}::numeric ${op} ${bind()}::numeric` : `${c} ${op} ${bind()}`
55
66
  switch cmp {
56
67
  | QueryEngine.SubId.Equal => cmpSql("=")
57
68
  | Unequal => cmpSql("<>")
@@ -80,7 +91,11 @@ let decodeRow = (~withId, row: dict<JSON.t>): JSON.t => {
80
91
  }
81
92
  }
82
93
 
83
- module Make = (P: {let pool: PgDriver.pool}) => {
94
+ module Make = (
95
+ P: {
96
+ let pool: PgDriver.pool
97
+ },
98
+ ) => {
84
99
  let query = async (
85
100
  ~readModelName,
86
101
  ~key=?,
@@ -109,7 +124,9 @@ module Make = (P: {let pool: PgDriver.pool}) => {
109
124
  | Some(n) => ` LIMIT ${b->param(JSON.Encode.int(n))}`
110
125
  | None => ""
111
126
  }
112
- let sql = `SELECT partition_key, item FROM ${table} WHERE ${where->Array.join(" AND ")} ORDER BY sub_key ${dir}${limitClause}`
127
+ let sql = `SELECT partition_key, item FROM ${table} WHERE ${where->Array.join(
128
+ " AND ",
129
+ )} ORDER BY sub_key ${dir}${limitClause}`
113
130
  (await P.pool->PgDriver.query(sql, b.params))->Array.map(decodeRow(~withId=false, ...))
114
131
  }
115
132
 
@@ -117,8 +134,12 @@ module Make = (P: {let pool: PgDriver.pool}) => {
117
134
  let table = QueryDbStorage_Postgres_Ops.tableName(readModelName)
118
135
  let b = {params: []}
119
136
  let where = [notExpired]
120
- filterConfigs->Array.forEach(((field, cmp, v)) => where->Array.push(filterSql(b, field, cmp, v)))
121
- let sql = `SELECT partition_key, item FROM ${table} WHERE ${where->Array.join(" AND ")} ORDER BY partition_key, sub_key LIMIT ${b->param(JSON.Encode.int(limit))}`
137
+ filterConfigs->Array.forEach(((field, cmp, v)) =>
138
+ where->Array.push(filterSql(b, field, cmp, v))
139
+ )
140
+ let sql = `SELECT partition_key, item FROM ${table} WHERE ${where->Array.join(
141
+ " AND ",
142
+ )} ORDER BY partition_key, sub_key LIMIT ${b->param(JSON.Encode.int(limit))}`
122
143
  (await P.pool->PgDriver.query(sql, b.params))->Array.map(decodeRow(~withId=true, ...))
123
144
  }
124
145
 
@@ -265,15 +286,17 @@ module Make = (P: {let pool: PgDriver.pool}) => {
265
286
  }
266
287
  capability.filterFields->Array.forEach(f => {
267
288
  switch filterDict->Dict.get(f.name ++ "Eq") {
268
- | Some(v) when v != JSON.Encode.null =>
289
+ | Some(v) if v != JSON.Encode.null =>
269
290
  whereParts->Array.push(
270
- `${jsonTextC(f.name)} = ${b->param(JSON.Encode.string(valString(v)->Option.getOr("")))}`,
291
+ `${jsonTextC(f.name)} = ${b->param(
292
+ JSON.Encode.string(valString(v)->Option.getOr("")),
293
+ )}`,
271
294
  )
272
295
  | _ => ()
273
296
  }
274
297
  if f.range {
275
298
  switch filterDict->Dict.get(f.name ++ "From") {
276
- | Some(v) when v != JSON.Encode.null =>
299
+ | Some(v) if v != JSON.Encode.null =>
277
300
  whereParts->Array.push(
278
301
  `${jsonTextC(f.name)} >= ${b->param(
279
302
  JSON.Encode.string(valString(v)->Option.getOr("")),
@@ -282,7 +305,7 @@ module Make = (P: {let pool: PgDriver.pool}) => {
282
305
  | _ => ()
283
306
  }
284
307
  switch filterDict->Dict.get(f.name ++ "To") {
285
- | Some(v) when v != JSON.Encode.null =>
308
+ | Some(v) if v != JSON.Encode.null =>
286
309
  whereParts->Array.push(
287
310
  `${jsonTextC(f.name)} <= ${b->param(
288
311
  JSON.Encode.string(valString(v)->Option.getOr("")),
@@ -304,7 +327,8 @@ module Make = (P: {let pool: PgDriver.pool}) => {
304
327
  | Some(f) => jsonTextC(f)
305
328
  | None => idExprC
306
329
  }
307
- switch argsDict->Dict.get("after")->Option.flatMap(JSON.Decode.string) {
330
+ let afterCursor = argsDict->Dict.get("after")->Option.flatMap(JSON.Decode.string)
331
+ switch afterCursor {
308
332
  | Some(c) =>
309
333
  whereParts->Array.push(
310
334
  `${cursorExpr} ${isDesc ? "<" : ">"} ${b->param(
@@ -326,7 +350,8 @@ module Make = (P: {let pool: PgDriver.pool}) => {
326
350
  let sql = `SELECT partition_key, item FROM ${table} WHERE ${whereParts->Array.join(
327
351
  " AND ",
328
352
  )} ORDER BY ${orderClause} LIMIT ${b->param(JSON.Encode.int(pageSize + 1))}`
329
- let rows = (await P.pool->PgDriver.query(sql, b.params))->Array.map(decodeRow(~withId=true, ...))
353
+ let rows =
354
+ (await P.pool->PgDriver.query(sql, b.params))->Array.map(decodeRow(~withId=true, ...))
330
355
  let hasMore = rows->Array.length > pageSize
331
356
  let pageItems = rows->Array.slice(~start=0, ~end=pageSize)
332
357
  let cursorField = orderByField->Option.getOr("id")
@@ -338,7 +363,10 @@ module Make = (P: {let pool: PgDriver.pool}) => {
338
363
  ReventlessCore.QueryDbListQuery.buildConnection(
339
364
  ~pageItems,
340
365
  ~hasNextPage=hasMore,
341
- ~hasPreviousPage=false,
366
+ // Same rule the spec uses, because the parity suite compares them: a
367
+ // forward page reached by a cursor has one behind it. Hardcoding false
368
+ // made Prev disappear on exactly the pages that have a previous one.
369
+ ~hasPreviousPage=afterCursor->Option.isSome,
342
370
  ~cursorValueOf,
343
371
  ),
344
372
  )
@@ -365,8 +393,10 @@ module Make = (P: {let pool: PgDriver.pool}) => {
365
393
  argsDict->Dict.get("filter")->Option.flatMap(JSON.Decode.object)->Option.getOr(Dict.make())
366
394
  let fstr = k => filterDict->Dict.get(k)->Option.flatMap(JSON.Decode.string)
367
395
  let orderDesc = fstr("order")->Option.mapOr(false, o => o == "DESC")
368
- let last = argsDict->Dict.get("last")->Option.flatMap(JSON.Decode.float)->Option.map(Float.toInt)
369
- let first = argsDict->Dict.get("first")->Option.flatMap(JSON.Decode.float)->Option.map(Float.toInt)
396
+ let last =
397
+ argsDict->Dict.get("last")->Option.flatMap(JSON.Decode.float)->Option.map(Float.toInt)
398
+ let first =
399
+ argsDict->Dict.get("first")->Option.flatMap(JSON.Decode.float)->Option.map(Float.toInt)
370
400
  let after = argsDict->Dict.get("after")->Option.flatMap(JSON.Decode.string)
371
401
  let before = argsDict->Dict.get("before")->Option.flatMap(JSON.Decode.string)
372
402
  let isBackward = last->Option.isSome
@@ -387,25 +417,36 @@ module Make = (P: {let pool: PgDriver.pool}) => {
387
417
  fstr("prefix")->Option.forEach(p =>
388
418
  where->Array.push(`starts_with(sub_key, ${b->param(JSON.Encode.string(p))})`)
389
419
  )
390
- fstr("from")->Option.forEach(f => where->Array.push(`${sk} >= ${b->param(JSON.Encode.string(f))}`))
391
- fstr("to")->Option.forEach(t => where->Array.push(`${sk} <= ${b->param(JSON.Encode.string(t))}`))
420
+ fstr("from")->Option.forEach(f =>
421
+ where->Array.push(`${sk} >= ${b->param(JSON.Encode.string(f))}`)
422
+ )
423
+ fstr("to")->Option.forEach(t =>
424
+ where->Array.push(`${sk} <= ${b->param(JSON.Encode.string(t))}`)
425
+ )
392
426
  fstr("eq")->Option.forEach(e => where->Array.push(`${sk} = ${b->param(JSON.Encode.string(e))}`))
393
427
  // Keyset: forward excludes ≤ after; backward excludes ≥ before.
394
428
  switch (isBackward, after, before) {
395
429
  | (false, Some(c), _) =>
396
- where->Array.push(`${sk} > ${b->param(JSON.Encode.string(ReventlessCore.QueryDbListQuery.decodeCursor(c)))}`)
430
+ where->Array.push(
431
+ `${sk} > ${b->param(JSON.Encode.string(ReventlessCore.QueryDbListQuery.decodeCursor(c)))}`,
432
+ )
397
433
  | (true, _, Some(c)) =>
398
- where->Array.push(`${sk} < ${b->param(JSON.Encode.string(ReventlessCore.QueryDbListQuery.decodeCursor(c)))}`)
434
+ where->Array.push(
435
+ `${sk} < ${b->param(JSON.Encode.string(ReventlessCore.QueryDbListQuery.decodeCursor(c)))}`,
436
+ )
399
437
  | _ => ()
400
438
  }
401
439
  // Page-fetch direction is the LOGICAL order forward, and its OPPOSITE
402
440
  // backward (take the N nearest `before`, then reverse to logical) — mirrors
403
441
  // the resolver's `reverse = isBackward ? !orderDesc : orderDesc`.
404
442
  let fetchDesc = isBackward ? !orderDesc : orderDesc
405
- let pageSize = (isBackward ? last : first)->Option.getOr(ReventlessCore.QueryDbListQuery.defaultListPageSize)
443
+ let pageSize =
444
+ (isBackward ? last : first)->Option.getOr(ReventlessCore.QueryDbListQuery.defaultListPageSize)
406
445
  let sql = `SELECT partition_key, sub_key, item FROM ${table} WHERE ${where->Array.join(
407
446
  " AND ",
408
- )} ORDER BY ${sk} ${fetchDesc ? "DESC" : "ASC"} LIMIT ${b->param(JSON.Encode.int(pageSize + 1))}`
447
+ )} ORDER BY ${sk} ${fetchDesc ? "DESC" : "ASC"} LIMIT ${b->param(
448
+ JSON.Encode.int(pageSize + 1),
449
+ )}`
409
450
  let rows = await P.pool->PgDriver.query(sql, b.params)
410
451
  let hasMore = rows->Array.length > pageSize
411
452
  let taken = rows->Array.slice(~start=0, ~end=pageSize)
@@ -419,17 +460,18 @@ module Make = (P: {let pool: PgDriver.pool}) => {
419
460
  | _ => ""
420
461
  }
421
462
  let cursors = logical->Array.map(subKeyOf)
422
- let edges =
423
- pageItems->Array.mapWithIndex((item, i) =>
424
- Obj.magic({
425
- "node": item,
426
- "cursor": ReventlessCore.QueryDbListQuery.encodeCursor(cursors->Array.getUnsafe(i)),
427
- })
428
- )
463
+ let edges = pageItems->Array.mapWithIndex((item, i) =>
464
+ Obj.magic({
465
+ "node": item,
466
+ "cursor": ReventlessCore.QueryDbListQuery.encodeCursor(cursors->Array.getUnsafe(i)),
467
+ })
468
+ )
429
469
  let startCursor =
430
470
  cursors->Array.get(0)->Option.map(ReventlessCore.QueryDbListQuery.encodeCursor)
431
471
  let endCursor =
432
- cursors->Array.get(cursors->Array.length - 1)->Option.map(ReventlessCore.QueryDbListQuery.encodeCursor)
472
+ cursors
473
+ ->Array.get(cursors->Array.length - 1)
474
+ ->Option.map(ReventlessCore.QueryDbListQuery.encodeCursor)
433
475
  Obj.magic({
434
476
  "edges": edges,
435
477
  "pageInfo": {
@@ -135,7 +135,12 @@ switch processEnv->Dict.get("PG_URL") {
135
135
 
136
136
  // Assert the push-down declines this shape (resolver falls back to the spec).
137
137
  let checkFallback = async args => {
138
- let r = await Eng.listPage(~readModelName=name, ~argsDict=argsOf(args), ~capability, ~labelField="name")
138
+ let r = await Eng.listPage(
139
+ ~readModelName=name,
140
+ ~argsDict=argsOf(args),
141
+ ~capability,
142
+ ~labelField="name",
143
+ )
139
144
  expect(r->Option.isSome)->toBe(false)
140
145
  }
141
146
 
@@ -204,7 +209,9 @@ switch processEnv->Dict.get("PG_URL") {
204
209
  checkFallback([("filter", filterOf([("searchPrefix", JSON.Encode.string("A"))]))])
205
210
  )
206
211
  testPromise("ids → fallback", () =>
207
- checkFallback([("filter", filterOf([("ids", JSON.Encode.array([JSON.Encode.string("p-1")]))]))])
212
+ checkFallback([
213
+ ("filter", filterOf([("ids", JSON.Encode.array([JSON.Encode.string("p-1")]))])),
214
+ ])
208
215
  )
209
216
  testPromise("backward (last/before) → fallback", () =>
210
217
  checkFallback([("last", JSON.Encode.int(2)), ("before", JSON.Encode.string(cur("p-4")))])
@@ -265,10 +272,20 @@ switch processEnv->Dict.get("PG_URL") {
265
272
  ->Array.filterMap(e => e->field("node")->Option.flatMap(n => n->str("seq")))
266
273
 
267
274
  let pageInfoBool = (conn, key) =>
268
- conn->field("pageInfo")->Option.getOr(JSON.Encode.null)->field(key)->Option.flatMap(JSON.Decode.bool)->Option.getOr(false)
275
+ conn
276
+ ->field("pageInfo")
277
+ ->Option.getOr(JSON.Encode.null)
278
+ ->field(key)
279
+ ->Option.flatMap(JSON.Decode.bool)
280
+ ->Option.getOr(false)
269
281
 
270
282
  let itemsPage = args =>
271
- Eng.itemsPage(~readModelName=itemsName, ~subIdField="seq", ~id="order-1", ~argsDict=argsOf(args))
283
+ Eng.itemsPage(
284
+ ~readModelName=itemsName,
285
+ ~subIdField="seq",
286
+ ~id="order-1",
287
+ ~argsDict=argsOf(args),
288
+ )
272
289
 
273
290
  describe("QueryEnginePostgres itemsPage (sub-id keyset)", () => {
274
291
  testPromise("bare → all lines ASC, no next page", async () => {
@@ -282,7 +299,10 @@ switch processEnv->Dict.get("PG_URL") {
282
299
  expect(pageInfoBool(r, "hasNextPage"))->toBe(true)
283
300
  })
284
301
  testPromise("first:2 after b → [c,d]", async () => {
285
- let r = await itemsPage([("first", JSON.Encode.int(2)), ("after", JSON.Encode.string(cur("b")))])
302
+ let r = await itemsPage([
303
+ ("first", JSON.Encode.int(2)),
304
+ ("after", JSON.Encode.string(cur("b"))),
305
+ ])
286
306
  expect(r->itemSeqs)->toEqual(["c", "d"])
287
307
  })
288
308
  testPromise("order DESC → [e..a]", async () => {
@@ -304,7 +324,10 @@ switch processEnv->Dict.get("PG_URL") {
304
324
  expect(r->itemSeqs)->toEqual(["c"])
305
325
  })
306
326
  testPromise("backward last:2 before d → [b,c] (logical ASC)", async () => {
307
- let r = await itemsPage([("last", JSON.Encode.int(2)), ("before", JSON.Encode.string(cur("d")))])
327
+ let r = await itemsPage([
328
+ ("last", JSON.Encode.int(2)),
329
+ ("before", JSON.Encode.string(cur("d"))),
330
+ ])
308
331
  expect(r->itemSeqs)->toEqual(["b", "c"])
309
332
  expect(pageInfoBool(r, "hasPreviousPage"))->toBe(true)
310
333
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-postgres",
3
- "version": "3.0.0-alpha.124",
3
+ "version": "3.0.0-alpha.125",
4
4
  "description": "Postgres storage backend for Reventless (classic + DCB event logs, query DB, change feed). Connection-string only — RDS/Aurora, any managed provider, or a local container.",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -22,12 +22,12 @@
22
22
  "dependencies": {
23
23
  "pg": "^8.13.1",
24
24
  "sury": "11.0.0-rc.2",
25
- "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
26
25
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
27
- "@reventlessdev/reventless-core": "3.0.0-alpha.260",
28
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
29
- "@reventlessdev/reventless-infra": "3.0.0-alpha.161",
30
- "@reventlessdev/reventless-spec": "3.0.0-alpha.133"
26
+ "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.20",
27
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.162",
28
+ "@reventlessdev/rescript-effect": "0.1.0-alpha.33",
29
+ "@reventlessdev/reventless-core": "3.0.0-alpha.261",
30
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.134"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/pg": "^8.11.10",
@@ -143,7 +143,7 @@ let buildReadWhere = (
143
143
  // Strip the zero-padding a cursor carries for string-sortability, leaving a
144
144
  // plain decimal both `::xid8` and `::bigint` parse unambiguously ("0" if empty).
145
145
  let stripZeros = (s: string): string => {
146
- let t = s->String.replaceRegExp(%re("/^0+/"), "")
146
+ let t = s->String.replaceRegExp(/^0+/, "")
147
147
  t == "" ? "0" : t
148
148
  }
149
149
 
@@ -222,49 +222,45 @@ let makeOps = (
222
222
  }
223
223
  }
224
224
 
225
- let append = async (
226
- newEvents: array<DcbEventLog_Adapter.rawStoredEvent>,
227
- ~condition=?,
228
- ): result<DcbTag.sequencePosition, ReventlessInfra.DcbEventLog.appendError> => {
225
+ let append = async (newEvents: array<DcbEventLog_Adapter.rawStoredEvent>, ~condition=?): result<
226
+ DcbTag.sequencePosition,
227
+ ReventlessInfra.DcbEventLog.appendError,
228
+ > => {
229
229
  let eventsJson = JSON.Array(
230
- newEvents->Array.map(ev =>
231
- JSON.Object(
230
+ newEvents->Array.map(ev => JSON.Object(
231
+ Dict.fromArray([
232
+ ("event_type", JSON.Encode.string(ev.eventType)),
233
+ (
234
+ "tags",
235
+ JSON.Array(ev.tags->Array.map(t => JSON.Encode.string(t.key ++ "=" ++ t.value))),
236
+ ),
237
+ ("data", ev.data),
238
+ ("meta", ev.meta->Reventless.Util_Sury.toJson(Reventless.Message.metaSchema)),
239
+ ]),
240
+ )),
241
+ )
242
+ let conditionParam = switch condition {
243
+ | Some(cond: DcbTag.appendCondition) =>
244
+ let queryJson = JSON.Array(
245
+ cond.query->Array.map(clause => JSON.Object(
232
246
  Dict.fromArray([
233
- ("event_type", JSON.Encode.string(ev.eventType)),
247
+ (
248
+ "eventTypes",
249
+ switch clause.eventTypes {
250
+ | Some(types) => JSON.Array(types->Array.map(JSON.Encode.string))
251
+ | None => JSON.Encode.null
252
+ },
253
+ ),
234
254
  (
235
255
  "tags",
236
- JSON.Array(ev.tags->Array.map(t => JSON.Encode.string(t.key ++ "=" ++ t.value))),
256
+ switch clause.tags {
257
+ | Some(tags) =>
258
+ JSON.Array(tags->Array.map(t => JSON.Encode.string(t.key ++ "=" ++ t.value)))
259
+ | None => JSON.Encode.null
260
+ },
237
261
  ),
238
- ("data", ev.data),
239
- ("meta", ev.meta->Reventless.Util_Sury.toJson(Reventless.Message.metaSchema)),
240
262
  ]),
241
- )
242
- ),
243
- )
244
- let conditionParam = switch condition {
245
- | Some(cond: DcbTag.appendCondition) =>
246
- let queryJson = JSON.Array(
247
- cond.query->Array.map(clause =>
248
- JSON.Object(
249
- Dict.fromArray([
250
- (
251
- "eventTypes",
252
- switch clause.eventTypes {
253
- | Some(types) => JSON.Array(types->Array.map(JSON.Encode.string))
254
- | None => JSON.Encode.null
255
- },
256
- ),
257
- (
258
- "tags",
259
- switch clause.tags {
260
- | Some(tags) =>
261
- JSON.Array(tags->Array.map(t => JSON.Encode.string(t.key ++ "=" ++ t.value)))
262
- | None => JSON.Encode.null
263
- },
264
- ),
265
- ]),
266
- )
267
- ),
263
+ )),
268
264
  )
269
265
  let fields = [("query", queryJson)]
270
266
  switch cond.after {
@@ -306,7 +302,10 @@ let makeOps = (
306
302
  ? Error(ReventlessInfra.DcbEventLog.Conflict)
307
303
  : Error(
308
304
  ReventlessInfra.DcbEventLog.StorageFailure(
309
- exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("storage error"),
305
+ exn
306
+ ->JsExn.fromException
307
+ ->Option.flatMap(JsExn.message)
308
+ ->Option.getOr("storage error"),
310
309
  ),
311
310
  )
312
311
  }
@@ -324,7 +323,9 @@ let makeOps = (
324
323
  Effect.promise(() => {
325
324
  let b = mkBuilder()
326
325
  let where = buildReadWhere(b, ~name, ~query, ~after=cursor, ~applyFence=true)
327
- let sql = `SELECT ${selectColumns}, transaction_id::text AS tx_raw, position::text AS pos_raw FROM dcb_event WHERE ${where} ORDER BY transaction_id ASC, position ASC LIMIT ${Int.toString(pageSize)}`
326
+ let sql = `SELECT ${selectColumns}, transaction_id::text AS tx_raw, position::text AS pos_raw FROM dcb_event WHERE ${where} ORDER BY transaction_id ASC, position ASC LIMIT ${Int.toString(
327
+ pageSize,
328
+ )}`
328
329
  pool->PgDriver.query(sql, b.params)
329
330
  })->Effect.map(rows => {
330
331
  let events = rows->Array.map(rowToEvent)
@@ -90,11 +90,12 @@ let readBatch = async (
90
90
  params->Array.push(JSON.Encode.string(tx))
91
91
  params->Array.push(JSON.Encode.string(pos))
92
92
  where :=
93
- where.contents ++
94
- " AND (transaction_id > $2::xid8 OR (transaction_id = $2::xid8 AND global_seq > $3::bigint))"
93
+ where.contents ++ " AND (transaction_id > $2::xid8 OR (transaction_id = $2::xid8 AND global_seq > $3::bigint))"
95
94
  | None => ()
96
95
  }
97
- let sql = `SELECT ${selectColumns} FROM event_log WHERE ${where.contents} ORDER BY transaction_id ASC, global_seq ASC LIMIT ${Int.toString(limit)}`
96
+ let sql = `SELECT ${selectColumns} FROM event_log WHERE ${where.contents} ORDER BY transaction_id ASC, global_seq ASC LIMIT ${Int.toString(
97
+ limit,
98
+ )}`
98
99
  let rows = await pool->PgDriver.query(sql, params)
99
100
  let events = rows->Array.map(rowToEvent)
100
101
  let cursor = switch events->Array.get(events->Array.length - 1) {
@@ -166,7 +167,12 @@ let drain = async (
166
167
  let cursor = ref(await loadCheckpoint(pool, ~subscriber))
167
168
  let continue = ref(true)
168
169
  while continue.contents {
169
- let {events, cursor: newCursor} = await readBatch(pool, ~logName, ~after=?cursor.contents, ~limit)
170
+ let {events, cursor: newCursor} = await readBatch(
171
+ pool,
172
+ ~logName,
173
+ ~after=?cursor.contents,
174
+ ~limit,
175
+ )
170
176
  if events->Array.length == 0 {
171
177
  continue := false
172
178
  } else {
@@ -44,12 +44,10 @@ let isUniqueViolation = (exn: exn): bool => {
44
44
 
45
45
  // Builds the classic runtime op set bound to `pool`. Returns `(name, ops)`; the
46
46
  // deploy-time `EventLog_Adapter` wrapper lives in `EventLogStorage_Postgres`.
47
- let makeOps = (
48
- ~pool: PgDriver.pool,
49
- ~name: string,
50
- ~opts as _,
51
- ~onAppended=noTracking,
52
- ): (string, EventLog_Adapter.operations) => {
47
+ let makeOps = (~pool: PgDriver.pool, ~name: string, ~opts as _, ~onAppended=noTracking): (
48
+ string,
49
+ EventLog_Adapter.operations,
50
+ ) => {
53
51
  let appendTracked = async (~track, seqNr: int, id: string, jsons: array<JSON.t>) => {
54
52
  // Empty append is a legitimate no-op (at-least-once retries, idempotent
55
53
  // commands), NOT a conflict — return before the guarded insert whose 0-row
@@ -95,7 +93,10 @@ let makeOps = (
95
93
  ? Error(EventLog.Conflict)
96
94
  : Error(
97
95
  EventLog.StorageFailure(
98
- exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("storage error"),
96
+ exn
97
+ ->JsExn.fromException
98
+ ->Option.flatMap(JsExn.message)
99
+ ->Option.getOr("storage error"),
99
100
  ),
100
101
  )
101
102
  }
@@ -106,12 +107,14 @@ let makeOps = (
106
107
  appendTracked(~track=true, seqNr, id, jsons)
107
108
 
108
109
  let replayArray = async (id: string, ~fromSeq=0): array<JSON.t> =>
109
- (await pool->PgDriver.query(
110
- "SELECT payload FROM event_log
110
+ (
111
+ await pool->PgDriver.query(
112
+ "SELECT payload FROM event_log
111
113
  WHERE log_name = $1 AND aggregate_id = $2 AND seq_nr >= $3::bigint
112
114
  ORDER BY seq_nr ASC",
113
- [JSON.Encode.string(name), JSON.Encode.string(id), JSON.Encode.int(fromSeq)],
114
- ))->Array.map(decodePayload)
115
+ [JSON.Encode.string(name), JSON.Encode.string(id), JSON.Encode.int(fromSeq)],
116
+ )
117
+ )->Array.map(decodePayload)
115
118
 
116
119
  let replay: EventLog.replay<string, JSON.t> = id => replayArray(id)
117
120
 
@@ -152,14 +155,21 @@ let makeOps = (
152
155
  Ok()
153
156
  } catch {
154
157
  | exn =>
155
- Error(exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("snapshot write error"))
158
+ Error(
159
+ exn
160
+ ->JsExn.fromException
161
+ ->Option.flatMap(JsExn.message)
162
+ ->Option.getOr("snapshot write error"),
163
+ )
156
164
  }
157
165
 
158
166
  let appendStream: EventLog.appendStream<string, JSON.t> = (startingSeqNr, id, stream) =>
159
167
  stream
160
168
  ->Stream.runCollect
161
169
  ->Effect.flatMap(jsons =>
162
- Effect.promise(() => appendTracked(~track=false, startingSeqNr, id, jsons))->Effect.flatMap(result =>
170
+ Effect.promise(() =>
171
+ appendTracked(~track=false, startingSeqNr, id, jsons)
172
+ )->Effect.flatMap(result =>
163
173
  switch result {
164
174
  | Ok() => Effect.succeed()
165
175
  | Error(EventLog.Conflict) => Effect.fail("conflict")