@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.
- package/CHANGELOG.md +7 -0
- package/lib/bs/.compiler.log +2 -2
- package/lib/bs/compiler-info.json +1 -1
- package/lib/bs/src/DcbEventLogStorage_Postgres-ReventlessPostgres.cmi +0 -0
- package/lib/bs/src/DcbEventLogStorage_Postgres-ReventlessPostgres.cmt +0 -0
- package/lib/bs/src/DcbEventLogStorage_Postgres_Ops-ReventlessPostgres.cmi +0 -0
- package/lib/bs/src/DcbEventLogStorage_Postgres_Ops-ReventlessPostgres.cmt +0 -0
- package/lib/bs/src/DcbEventLogStorage_Postgres_Ops.ast +0 -0
- package/lib/bs/src/DcbEventLogStorage_Postgres_Ops.res +41 -40
- package/lib/bs/src/EventLogChangeFeed-ReventlessPostgres.cmi +0 -0
- package/lib/bs/src/EventLogChangeFeed-ReventlessPostgres.cmt +0 -0
- package/lib/bs/src/EventLogChangeFeed.ast +0 -0
- package/lib/bs/src/EventLogChangeFeed.res +10 -4
- package/lib/bs/src/EventLogStorage_Postgres-ReventlessPostgres.cmi +0 -0
- package/lib/bs/src/EventLogStorage_Postgres-ReventlessPostgres.cmt +0 -0
- package/lib/bs/src/EventLogStorage_Postgres_Ops-ReventlessPostgres.cmi +0 -0
- package/lib/bs/src/EventLogStorage_Postgres_Ops-ReventlessPostgres.cmt +0 -0
- package/lib/bs/src/EventLogStorage_Postgres_Ops.ast +0 -0
- package/lib/bs/src/EventLogStorage_Postgres_Ops.res +23 -13
- package/lib/bs/src/PgChangeFeed-ReventlessPostgres.cmi +0 -0
- package/lib/bs/src/PgChangeFeed-ReventlessPostgres.cmt +0 -0
- package/lib/bs/src/PgChangeFeed.ast +0 -0
- package/lib/bs/src/PgChangeFeed.res +15 -8
- package/lib/bs/src/PgDriver-ReventlessPostgres.cmt +0 -0
- package/lib/bs/src/PgDriver.ast +0 -0
- package/lib/bs/src/PgDriver.res +1 -2
- package/lib/bs/src/QueryDbStorage_Postgres-ReventlessPostgres.cmi +0 -0
- package/lib/bs/src/QueryDbStorage_Postgres-ReventlessPostgres.cmt +0 -0
- package/lib/bs/src/QueryDbStorage_Postgres.ast +0 -0
- package/lib/bs/src/QueryDbStorage_Postgres.res +7 -2
- package/lib/bs/src/QueryDbStorage_Postgres_Ops-ReventlessPostgres.cmi +0 -0
- package/lib/bs/src/QueryDbStorage_Postgres_Ops-ReventlessPostgres.cmt +0 -0
- package/lib/bs/src/QueryDbStorage_Postgres_Ops.ast +0 -0
- package/lib/bs/src/QueryDbStorage_Postgres_Ops.res +13 -13
- package/lib/bs/src/QueryEnginePostgres-ReventlessPostgres.cmi +0 -0
- package/lib/bs/src/QueryEnginePostgres-ReventlessPostgres.cmt +0 -0
- package/lib/bs/src/QueryEnginePostgres.ast +0 -0
- package/lib/bs/src/QueryEnginePostgres.res +72 -30
- package/lib/bs/src/QueryEnginePostgres.res.mjs +4 -4
- package/lib/bs/tests/PostgresIntegrationTest-ReventlessPostgres.cmi +0 -0
- package/lib/bs/tests/PostgresIntegrationTest-ReventlessPostgres.cmt +0 -0
- package/lib/bs/tests/PostgresIntegrationTest.ast +0 -0
- package/lib/bs/tests/PostgresIntegrationTest.res +157 -104
- package/lib/bs/tests/QueryEnginePostgresListPageTest-ReventlessPostgres.cmi +0 -0
- package/lib/bs/tests/QueryEnginePostgresListPageTest-ReventlessPostgres.cmt +0 -0
- package/lib/bs/tests/QueryEnginePostgresListPageTest.ast +0 -0
- package/lib/bs/tests/QueryEnginePostgresListPageTest.res +29 -6
- package/lib/ocaml/.compiler.log +2 -2
- package/lib/ocaml/DcbEventLogStorage_Postgres-ReventlessPostgres.cmi +0 -0
- package/lib/ocaml/DcbEventLogStorage_Postgres-ReventlessPostgres.cmt +0 -0
- package/lib/ocaml/DcbEventLogStorage_Postgres_Ops-ReventlessPostgres.cmi +0 -0
- package/lib/ocaml/DcbEventLogStorage_Postgres_Ops-ReventlessPostgres.cmt +0 -0
- package/lib/ocaml/DcbEventLogStorage_Postgres_Ops.ast +0 -0
- package/lib/ocaml/DcbEventLogStorage_Postgres_Ops.res +41 -40
- package/lib/ocaml/EventLogChangeFeed-ReventlessPostgres.cmi +0 -0
- package/lib/ocaml/EventLogChangeFeed-ReventlessPostgres.cmt +0 -0
- package/lib/ocaml/EventLogChangeFeed.ast +0 -0
- package/lib/ocaml/EventLogChangeFeed.res +10 -4
- package/lib/ocaml/EventLogStorage_Postgres-ReventlessPostgres.cmi +0 -0
- package/lib/ocaml/EventLogStorage_Postgres-ReventlessPostgres.cmt +0 -0
- package/lib/ocaml/EventLogStorage_Postgres_Ops-ReventlessPostgres.cmi +0 -0
- package/lib/ocaml/EventLogStorage_Postgres_Ops-ReventlessPostgres.cmt +0 -0
- package/lib/ocaml/EventLogStorage_Postgres_Ops.ast +0 -0
- package/lib/ocaml/EventLogStorage_Postgres_Ops.res +23 -13
- package/lib/ocaml/PgChangeFeed-ReventlessPostgres.cmi +0 -0
- package/lib/ocaml/PgChangeFeed-ReventlessPostgres.cmt +0 -0
- package/lib/ocaml/PgChangeFeed.ast +0 -0
- package/lib/ocaml/PgChangeFeed.res +15 -8
- package/lib/ocaml/PgDriver-ReventlessPostgres.cmt +0 -0
- package/lib/ocaml/PgDriver.ast +0 -0
- package/lib/ocaml/PgDriver.res +1 -2
- package/lib/ocaml/PostgresIntegrationTest.res +157 -104
- package/lib/ocaml/QueryDbStorage_Postgres-ReventlessPostgres.cmi +0 -0
- package/lib/ocaml/QueryDbStorage_Postgres-ReventlessPostgres.cmt +0 -0
- package/lib/ocaml/QueryDbStorage_Postgres.ast +0 -0
- package/lib/ocaml/QueryDbStorage_Postgres.res +7 -2
- package/lib/ocaml/QueryDbStorage_Postgres_Ops-ReventlessPostgres.cmi +0 -0
- package/lib/ocaml/QueryDbStorage_Postgres_Ops-ReventlessPostgres.cmt +0 -0
- package/lib/ocaml/QueryDbStorage_Postgres_Ops.ast +0 -0
- package/lib/ocaml/QueryDbStorage_Postgres_Ops.res +13 -13
- package/lib/ocaml/QueryEnginePostgres-ReventlessPostgres.cmi +0 -0
- package/lib/ocaml/QueryEnginePostgres-ReventlessPostgres.cmt +0 -0
- package/lib/ocaml/QueryEnginePostgres.ast +0 -0
- package/lib/ocaml/QueryEnginePostgres.res +72 -30
- package/lib/ocaml/QueryEnginePostgresListPageTest.res +29 -6
- package/package.json +6 -6
- package/src/DcbEventLogStorage_Postgres_Ops.res +41 -40
- package/src/EventLogChangeFeed.res +10 -4
- package/src/EventLogStorage_Postgres_Ops.res +23 -13
- package/src/PgChangeFeed.res +15 -8
- package/src/PgDriver.res +1 -2
- package/src/QueryDbStorage_Postgres.res +7 -2
- package/src/QueryDbStorage_Postgres_Ops.res +13 -13
- package/src/QueryEnginePostgres.res +72 -30
- package/src/QueryEnginePostgres.res.mjs +4 -4
- package/tests/PostgresIntegrationTest.res +157 -104
- package/tests/QueryEnginePostgresListPageTest.res +29 -6
|
@@ -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 = (
|
|
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 = (
|
|
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 =>
|
|
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 = (
|
|
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(
|
|
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)) =>
|
|
121
|
-
|
|
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)
|
|
289
|
+
| Some(v) if v != JSON.Encode.null =>
|
|
269
290
|
whereParts->Array.push(
|
|
270
|
-
`${jsonTextC(f.name)} = ${b->param(
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
369
|
-
|
|
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 =>
|
|
391
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
|
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": {
|
|
@@ -235,11 +235,11 @@ function Make(P) {
|
|
|
235
235
|
let orderByField = Stdlib_Option.flatMap(Stdlib_Option.flatMap(orderByDict, ob => ob["field"]), Stdlib_JSON.Decode.string);
|
|
236
236
|
let isDesc = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(orderByDict, ob => ob["direction"]), Stdlib_JSON.Decode.string), "ASC") === "DESC";
|
|
237
237
|
let cursorExpr = orderByField !== undefined ? jsonTextC(orderByField) : idExprC;
|
|
238
|
-
let
|
|
239
|
-
if (
|
|
238
|
+
let afterCursor = Stdlib_Option.flatMap(argsDict["after"], Stdlib_JSON.Decode.string);
|
|
239
|
+
if (afterCursor !== undefined) {
|
|
240
240
|
whereParts.push(cursorExpr + ` ` + (
|
|
241
241
|
isDesc ? "<" : ">"
|
|
242
|
-
) + ` ` + param(b, QueryDbListQuery$ReventlessCore.decodeCursor(
|
|
242
|
+
) + ` ` + param(b, QueryDbListQuery$ReventlessCore.decodeCursor(afterCursor)));
|
|
243
243
|
}
|
|
244
244
|
let orderClause = orderByField !== undefined ? jsonTextC(orderByField) + ` ` + (
|
|
245
245
|
isDesc ? "DESC" : "ASC"
|
|
@@ -251,7 +251,7 @@ function Make(P) {
|
|
|
251
251
|
let pageItems = rows.slice(0, pageSize);
|
|
252
252
|
let cursorField = Stdlib_Option.getOr(orderByField, "id");
|
|
253
253
|
let cursorValueOf = item => Stdlib_Option.getOr(QueryDbListQuery$ReventlessCore.getFieldString(item, cursorField), QueryDbListQuery$ReventlessCore.getId(item));
|
|
254
|
-
return QueryDbListQuery$ReventlessCore.buildConnection(pageItems, hasMore,
|
|
254
|
+
return QueryDbListQuery$ReventlessCore.buildConnection(pageItems, hasMore, Stdlib_Option.isSome(afterCursor), cursorValueOf);
|
|
255
255
|
};
|
|
256
256
|
let itemsPage = async (readModelName, param$1, id, argsDict, ownerScope, retiredScope) => {
|
|
257
257
|
let table = QueryDbStorage_Postgres_Ops$ReventlessPostgres.tableName(readModelName);
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -100,7 +100,11 @@ switch processEnv->Dict.get("PG_URL") {
|
|
|
100
100
|
|
|
101
101
|
testPromise("append + read round-trips events with tags", async () => {
|
|
102
102
|
let (_n, ops, _s) = makeLog("dcb-rt")
|
|
103
|
-
let e1 = stored(
|
|
103
|
+
let e1 = stored(
|
|
104
|
+
"ItemAdded",
|
|
105
|
+
[{key: "itemId", value: "x1"}],
|
|
106
|
+
jsonObj([("n", JSON.Encode.string("w"))]),
|
|
107
|
+
)
|
|
104
108
|
let r = await ops.append([e1])
|
|
105
109
|
switch r {
|
|
106
110
|
| Ok(_) => expect(true)->toBe(true)
|
|
@@ -116,7 +120,12 @@ switch processEnv->Dict.get("PG_URL") {
|
|
|
116
120
|
|
|
117
121
|
testPromise("conditional append conflicts when the boundary already has an event", async () => {
|
|
118
122
|
let (_n, ops, _s) = makeLog("dcb-cond")
|
|
119
|
-
let mk = who =>
|
|
123
|
+
let mk = who =>
|
|
124
|
+
stored(
|
|
125
|
+
"Reserved",
|
|
126
|
+
[{key: "seatId", value: "s1"}],
|
|
127
|
+
jsonObj([("who", JSON.Encode.string(who))]),
|
|
128
|
+
)
|
|
120
129
|
let r1 = await ops.append([mk("a")])
|
|
121
130
|
switch r1 {
|
|
122
131
|
| Ok(_) => expect(true)->toBe(true)
|
|
@@ -134,37 +143,46 @@ switch processEnv->Dict.get("PG_URL") {
|
|
|
134
143
|
})
|
|
135
144
|
|
|
136
145
|
let writeSkew = (strategy, logName) =>
|
|
137
|
-
testPromise(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
146
|
+
testPromise(
|
|
147
|
+
`write-skew: exactly one of two concurrent appends wins (${logName})`,
|
|
148
|
+
async () => {
|
|
149
|
+
let (_n, ops, _s) = DcbEventLogStorage_Postgres.makeStorage(
|
|
150
|
+
~pool,
|
|
151
|
+
~name=logName,
|
|
152
|
+
~indexes=[],
|
|
153
|
+
~partitionTag=DcbTag.Simple({key: "k"}),
|
|
154
|
+
~opts,
|
|
155
|
+
~lockStrategy=strategy,
|
|
156
|
+
)
|
|
157
|
+
let cond: DcbTag.appendCondition = {
|
|
158
|
+
query: [{eventTypes: ["Reserved"], tags: [{key: "seatId", value: "hot"}]}],
|
|
159
|
+
}
|
|
160
|
+
let mk = who =>
|
|
161
|
+
stored(
|
|
162
|
+
"Reserved",
|
|
163
|
+
[{key: "seatId", value: "hot"}],
|
|
164
|
+
jsonObj([("who", JSON.Encode.string(who))]),
|
|
165
|
+
)
|
|
166
|
+
// Dispatch BOTH before awaiting either — genuinely concurrent on the pool.
|
|
167
|
+
let p1 = ops.append([mk("a")], ~condition=cond)
|
|
168
|
+
let p2 = ops.append([mk("b")], ~condition=cond)
|
|
169
|
+
let r1 = await p1
|
|
170
|
+
let r2 = await p2
|
|
171
|
+
let isOk = r =>
|
|
172
|
+
switch r {
|
|
173
|
+
| Ok(_) => true
|
|
174
|
+
| _ => false
|
|
175
|
+
}
|
|
176
|
+
let isConflict = r => r == Error(ReventlessInfra.DcbEventLog.Conflict)
|
|
177
|
+
let oks = [r1, r2]->Array.filter(isOk)->Array.length
|
|
178
|
+
let conflicts = [r1, r2]->Array.filter(isConflict)->Array.length
|
|
179
|
+
expect(oks)->toBe(1)
|
|
180
|
+
expect(conflicts)->toBe(1)
|
|
181
|
+
// And only one event actually landed on the boundary.
|
|
182
|
+
let read = await ops.read(~query=[{tags: [{key: "seatId", value: "hot"}]}])
|
|
183
|
+
expect(read.events->Array.length)->toBe(1)
|
|
184
|
+
},
|
|
185
|
+
)
|
|
168
186
|
|
|
169
187
|
writeSkew(#AdvisoryLocks, "dcb-skew-advisory")
|
|
170
188
|
writeSkew(#RowLocks, "dcb-skew-rows")
|
|
@@ -176,17 +194,31 @@ switch processEnv->Dict.get("PG_URL") {
|
|
|
176
194
|
}
|
|
177
195
|
let n = 12
|
|
178
196
|
// Dispatch all N before awaiting any — genuinely concurrent on the pool.
|
|
179
|
-
let promises =
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
197
|
+
let promises =
|
|
198
|
+
Array.make(~length=n, 0)->Array.mapWithIndex(
|
|
199
|
+
(_, i) =>
|
|
200
|
+
ops.append(
|
|
201
|
+
[
|
|
202
|
+
stored(
|
|
203
|
+
"Claimed",
|
|
204
|
+
[{key: "prizeId", value: "p1"}],
|
|
205
|
+
jsonObj([("who", JSON.Encode.int(i))]),
|
|
206
|
+
),
|
|
207
|
+
],
|
|
208
|
+
~condition=cond,
|
|
209
|
+
),
|
|
183
210
|
)
|
|
184
|
-
)
|
|
185
211
|
let results = await Promise.all(promises)
|
|
186
|
-
let oks =
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
212
|
+
let oks =
|
|
213
|
+
results
|
|
214
|
+
->Array.filter(
|
|
215
|
+
r =>
|
|
216
|
+
switch r {
|
|
217
|
+
| Ok(_) => true
|
|
218
|
+
| _ => false
|
|
219
|
+
},
|
|
220
|
+
)
|
|
221
|
+
->Array.length
|
|
190
222
|
expect(oks)->toBe(1)
|
|
191
223
|
// No lost updates: exactly one Claimed landed on the boundary.
|
|
192
224
|
let read = await ops.read(~query=[{tags: [{key: "prizeId", value: "p1"}]}])
|
|
@@ -195,7 +227,8 @@ switch processEnv->Dict.get("PG_URL") {
|
|
|
195
227
|
|
|
196
228
|
testPromise("cursors are monotonic and usable as `after`", async () => {
|
|
197
229
|
let (_n, ops, _s) = makeLog("dcb-cursor")
|
|
198
|
-
let mk = i =>
|
|
230
|
+
let mk = i =>
|
|
231
|
+
stored("Tick", [{key: "streamId", value: "c1"}], jsonObj([("i", JSON.Encode.int(i))]))
|
|
199
232
|
let _ = await ops.append([mk(0)])
|
|
200
233
|
let mid = switch await ops.append([mk(1)]) {
|
|
201
234
|
| Ok(pos) => pos
|
|
@@ -222,83 +255,103 @@ switch processEnv->Dict.get("PG_URL") {
|
|
|
222
255
|
let _ = await ops.append([mk(0), mk(1), mk(2)])
|
|
223
256
|
|
|
224
257
|
let collected = ref(0)
|
|
225
|
-
let processed = await PgChangeFeed.drain(
|
|
226
|
-
|
|
227
|
-
|
|
258
|
+
let processed = await PgChangeFeed.drain(
|
|
259
|
+
pool,
|
|
260
|
+
~logName="feed-log",
|
|
261
|
+
~subscriber="sub-1",
|
|
262
|
+
~handle=async evs => {
|
|
263
|
+
collected := collected.contents + evs->Array.length
|
|
264
|
+
},
|
|
265
|
+
)
|
|
228
266
|
expect(processed)->toBe(3)
|
|
229
267
|
expect(collected.contents)->toBe(3)
|
|
230
268
|
|
|
231
269
|
// A second drain from the saved checkpoint sees nothing new.
|
|
232
|
-
let again = await PgChangeFeed.drain(
|
|
270
|
+
let again = await PgChangeFeed.drain(
|
|
271
|
+
pool,
|
|
272
|
+
~logName="feed-log",
|
|
273
|
+
~subscriber="sub-1",
|
|
274
|
+
~handle=async _ => (),
|
|
275
|
+
)
|
|
233
276
|
expect(again)->toBe(0)
|
|
234
277
|
})
|
|
235
278
|
})
|
|
236
279
|
|
|
237
280
|
describe("classic change feed (event_log, B2.5)", () => {
|
|
238
|
-
testPromise(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
281
|
+
testPromise(
|
|
282
|
+
"drains classic events in commit order, with fields, then checkpoints",
|
|
283
|
+
async () => {
|
|
284
|
+
let (_n, ops, _s) = EventLogStorage_Postgres.makeStorage(~pool, ~name="cf-orders", ~opts)
|
|
285
|
+
let _ = await ops.append(0, "agg-1", [evJson("m0", 1), evJson("m1", 2)])
|
|
286
|
+
let _ = await ops.append(0, "agg-2", [evJson("m2", 3)])
|
|
242
287
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
288
|
+
let collected = ref([])
|
|
289
|
+
let processed = await EventLogChangeFeed.drain(
|
|
290
|
+
pool,
|
|
291
|
+
~logName="cf-orders",
|
|
292
|
+
~subscriber="cf-sub",
|
|
293
|
+
~handle=async evs => collected := collected.contents->Array.concat(evs),
|
|
294
|
+
)
|
|
295
|
+
expect(processed)->toBe(3)
|
|
296
|
+
expect(collected.contents->Array.length)->toBe(3)
|
|
252
297
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
298
|
+
// Fields round-trip from the stored row.
|
|
299
|
+
let first = collected.contents->Array.getUnsafe(0)
|
|
300
|
+
expect(first.aggregateId)->toBe("agg-1")
|
|
301
|
+
expect(first.seqNr)->toBe(0)
|
|
302
|
+
expect(first.msgId)->toEqual(Some("m0"))
|
|
258
303
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
304
|
+
// Cursors are strictly increasing in drain order.
|
|
305
|
+
let cursors = collected.contents->Array.map(e => e.cursor)
|
|
306
|
+
let increasing = ref(true)
|
|
307
|
+
for i in 1 to cursors->Array.length - 1 {
|
|
308
|
+
if cursors->Array.getUnsafe(i) <= cursors->Array.getUnsafe(i - 1) {
|
|
309
|
+
increasing := false
|
|
310
|
+
}
|
|
265
311
|
}
|
|
266
|
-
|
|
267
|
-
expect(increasing.contents)->toBe(true)
|
|
312
|
+
expect(increasing.contents)->toBe(true)
|
|
268
313
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
314
|
+
// Second drain from the saved checkpoint sees nothing new.
|
|
315
|
+
let again = await EventLogChangeFeed.drain(
|
|
316
|
+
pool,
|
|
317
|
+
~logName="cf-orders",
|
|
318
|
+
~subscriber="cf-sub",
|
|
319
|
+
~handle=async _ => (),
|
|
320
|
+
)
|
|
321
|
+
expect(again)->toBe(0)
|
|
322
|
+
},
|
|
323
|
+
)
|
|
278
324
|
|
|
279
|
-
testPromise(
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
Array.make(~length=n, 0)->Array.mapWithIndex((_, i) =>
|
|
287
|
-
ops.append(0, "agg-" ++ Int.toString(i), [evJson("m" ++ Int.toString(i), i)])
|
|
325
|
+
testPromise(
|
|
326
|
+
"concurrent appends across aggregates: every event drained exactly once",
|
|
327
|
+
async () => {
|
|
328
|
+
let (_n, ops, _s) = EventLogStorage_Postgres.makeStorage(
|
|
329
|
+
~pool,
|
|
330
|
+
~name="cf-concurrent",
|
|
331
|
+
~opts,
|
|
288
332
|
)
|
|
289
|
-
|
|
333
|
+
let n = 20
|
|
334
|
+
// One event per aggregate, all dispatched before awaiting — genuinely
|
|
335
|
+
// concurrent on the pool, so global_seq is assigned in interleaved/undefined
|
|
336
|
+
// commit order. The xmin fence must still drain every event exactly once.
|
|
337
|
+
let promises =
|
|
338
|
+
Array.make(~length=n, 0)->Array.mapWithIndex(
|
|
339
|
+
(_, i) => ops.append(0, "agg-" ++ Int.toString(i), [evJson("m" ++ Int.toString(i), i)]),
|
|
340
|
+
)
|
|
341
|
+
let _ = await Promise.all(promises)
|
|
290
342
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
343
|
+
let collected = ref([])
|
|
344
|
+
let _ = await EventLogChangeFeed.drain(
|
|
345
|
+
pool,
|
|
346
|
+
~logName="cf-concurrent",
|
|
347
|
+
~subscriber="cf-conc-sub",
|
|
348
|
+
~handle=async evs => collected := collected.contents->Array.concat(evs),
|
|
349
|
+
)
|
|
350
|
+
// No skips: all N events present. No dupes: N distinct cursors.
|
|
351
|
+
expect(collected.contents->Array.length)->toBe(n)
|
|
352
|
+
let distinctCursors = collected.contents->Array.map(e => e.cursor)->Set.fromArray
|
|
353
|
+
expect(distinctCursors->Set.size)->toBe(n)
|
|
354
|
+
},
|
|
355
|
+
)
|
|
303
356
|
})
|
|
304
357
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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(
|
|
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([
|
|
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
|
|
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(
|
|
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([
|
|
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([
|
|
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/lib/ocaml/.compiler.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
#Start(
|
|
2
|
-
#Done(
|
|
1
|
+
#Start(1788992674550)
|
|
2
|
+
#Done(1788992682364)
|