@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
|
@@ -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 = (
|
|
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 _,
|
|
44
|
+
~owner as _,
|
|
45
|
+
~opts as _,
|
|
41
46
|
) => makeStorage(~pool=P.pool, ~name, ~indexes, ~subIdField)
|
|
42
47
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
Binary file
|
|
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 = (
|
|
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": {
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-postgres",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
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/
|
|
28
|
-
"@reventlessdev/
|
|
29
|
-
"@reventlessdev/
|
|
30
|
-
"@reventlessdev/reventless-
|
|
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(
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
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
|
-
(
|
|
110
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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(
|
|
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(() =>
|
|
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")
|