@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
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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 {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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")
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -49,7 +49,9 @@ let readBatch = async (
|
|
|
49
49
|
~after=after->Option.flatMap(Dcb.decodeCursor),
|
|
50
50
|
~applyFence=true,
|
|
51
51
|
)
|
|
52
|
-
let sql = `SELECT ${Dcb.selectColumns} FROM dcb_event WHERE ${where} ORDER BY transaction_id ASC, position ASC LIMIT ${Int.toString(
|
|
52
|
+
let sql = `SELECT ${Dcb.selectColumns} FROM dcb_event WHERE ${where} ORDER BY transaction_id ASC, position ASC LIMIT ${Int.toString(
|
|
53
|
+
limit,
|
|
54
|
+
)}`
|
|
53
55
|
let rows = await pool->PgDriver.query(sql, b.params)
|
|
54
56
|
let events = rows->Array.map(Dcb.rowToEvent)
|
|
55
57
|
let cursor = switch events->Array.get(events->Array.length - 1) {
|
|
@@ -63,10 +65,9 @@ let readBatch = async (
|
|
|
63
65
|
|
|
64
66
|
// The persisted checkpoint for `subscriber`, encoded as a cursor, or None if the
|
|
65
67
|
// subscriber has never committed (→ replay from the beginning).
|
|
66
|
-
let loadCheckpoint = async (
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
): option<DcbTag.sequencePosition> =>
|
|
68
|
+
let loadCheckpoint = async (pool: PgDriver.pool, ~subscriber: string): option<
|
|
69
|
+
DcbTag.sequencePosition,
|
|
70
|
+
> =>
|
|
70
71
|
switch await pool->PgDriver.queryOne(
|
|
71
72
|
"SELECT lpad(last_tx::text, 20, '0') || ':' || lpad(last_position::text, 20, '0') AS cursor
|
|
72
73
|
FROM dcb_subscription WHERE subscriber = $1 AND last_position > 0",
|
|
@@ -105,8 +106,9 @@ let saveCheckpoint = async (
|
|
|
105
106
|
// dedicated client (caller owns it — release via PgDriver.unlisten). Pair with a
|
|
106
107
|
// low-frequency fallback tick in the consumer loop so a missed NOTIFY only adds
|
|
107
108
|
// latency, never a permanent stall.
|
|
108
|
-
let listen = (pool: PgDriver.pool, ~logName: string, ~onWake: unit => unit): promise<
|
|
109
|
-
|
|
109
|
+
let listen = (pool: PgDriver.pool, ~logName: string, ~onWake: unit => unit): promise<
|
|
110
|
+
PgDriver.client,
|
|
111
|
+
> => pool->PgDriver.listen(~channel="dcb_" ++ logName, ~onNotify=_ => onWake())
|
|
110
112
|
|
|
111
113
|
let unlisten = (client: PgDriver.client, ~logName: string): promise<unit> =>
|
|
112
114
|
client->PgDriver.unlisten(~channel="dcb_" ++ logName)
|
|
@@ -129,7 +131,12 @@ let drain = async (
|
|
|
129
131
|
let cursor = ref(await loadCheckpoint(pool, ~subscriber))
|
|
130
132
|
let continue = ref(true)
|
|
131
133
|
while continue.contents {
|
|
132
|
-
let {events, cursor: newCursor} = await readBatch(
|
|
134
|
+
let {events, cursor: newCursor} = await readBatch(
|
|
135
|
+
pool,
|
|
136
|
+
~logName,
|
|
137
|
+
~after=?cursor.contents,
|
|
138
|
+
~limit,
|
|
139
|
+
)
|
|
133
140
|
if events->Array.length == 0 {
|
|
134
141
|
continue := false
|
|
135
142
|
} else {
|
|
Binary file
|
package/lib/ocaml/PgDriver.ast
CHANGED
|
Binary file
|
package/lib/ocaml/PgDriver.res
CHANGED
|
@@ -47,8 +47,7 @@ type queryResult = {rows: array<dict<JSON.t>>}
|
|
|
47
47
|
|
|
48
48
|
let query = async (pool, sql, params) => (await _query(pool, sql, params)).rows
|
|
49
49
|
|
|
50
|
-
let queryOne = async (pool, sql, params) =>
|
|
51
|
-
(await _query(pool, sql, params)).rows->Array.get(0)
|
|
50
|
+
let queryOne = async (pool, sql, params) => (await _query(pool, sql, params)).rows->Array.get(0)
|
|
52
51
|
|
|
53
52
|
let exec = async (pool, sql) => {
|
|
54
53
|
let _ = await _queryText(pool, sql)
|
|
@@ -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
|