@reventlessdev/reventless-local 3.0.0-alpha.114 → 3.0.0-alpha.116
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 +30 -0
- package/package.json +8 -8
- package/src/Platform.res +116 -67
- package/src/Platform.res.mjs +114 -190
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +102 -17
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs +105 -22
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +123 -40
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +96 -24
- package/src/adapter/EventCollector/LocalEventCollectorChannel.res +33 -0
- package/src/adapter/EventCollector/LocalEventCollectorChannel.res.mjs +44 -0
- package/src/adapter/EventLog/EventLogStorage_InMemory.res +24 -3
- package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +21 -4
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res +148 -27
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res.mjs +144 -34
- package/src/adapter/LocalBus.res +85 -3
- package/src/adapter/LocalBus.res.mjs +220 -66
- package/src/adapter/ProjectionCheckpoint.res +350 -0
- package/src/adapter/ProjectionCheckpoint.res.mjs +327 -0
- package/src/adapter/ProjectionPending.res +60 -0
- package/src/adapter/ProjectionPending.res.mjs +64 -0
- package/src/adapter/QueryDb/QueryDbListQuery.res +261 -0
- package/src/adapter/QueryDb/QueryDbListQuery.res.mjs +212 -0
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +54 -286
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +28 -190
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +128 -10
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res.mjs +123 -23
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +187 -12
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res.mjs +120 -7
- package/src/adapter/SqliteDriver.res +17 -2
- package/src/adapter/SqliteDriver.res.mjs +10 -2
- package/src/components/DcbEventLog_Builder.res +7 -1
- package/src/components/DcbEventLog_Builder.res.mjs +3 -3
- package/src/components/ReadModel_Builder.res +3 -1
- package/src/components/ReadModel_Builder.res.mjs +13 -4
- package/src/components/StateViewSlice_Builder.res +4 -1
- package/src/components/StateViewSlice_Builder.res.mjs +9 -3
- package/src/test/Mocks/MockEventLogStorage.res +20 -3
- package/src/test/Mocks/MockEventLogStorage.res.mjs +25 -3
- package/tests/PluginEventDecodeTest.res +51 -0
- package/tests/PluginEventDecodeTest.res.mjs +63 -0
- package/tests/adapter/BackendParityTest.res +64 -0
- package/tests/adapter/BackendParityTest.res.mjs +45 -0
- package/tests/adapter/DcbEventLogStorageSqliteTest.res +73 -0
- package/tests/adapter/DcbEventLogStorageSqliteTest.res.mjs +86 -0
- package/tests/adapter/DcbEventLogStorageTest.res +46 -0
- package/tests/adapter/DcbEventLogStorageTest.res.mjs +63 -0
- package/tests/adapter/EventLogSnapshotParityTest.res +81 -0
- package/tests/adapter/EventLogSnapshotParityTest.res.mjs +141 -0
- package/tests/adapter/EventLogStorageSqliteTest.res +132 -2
- package/tests/adapter/EventLogStorageSqliteTest.res.mjs +195 -1
- package/tests/adapter/LocalBusPubSubTest.res +31 -0
- package/tests/adapter/LocalBusPubSubTest.res.mjs +28 -0
- package/tests/adapter/ProjectionCheckpointTest.res +422 -0
- package/tests/adapter/ProjectionCheckpointTest.res.mjs +401 -0
- package/tests/adapter/QueryDbGsiTtlTest.res +78 -0
- package/tests/adapter/QueryDbGsiTtlTest.res.mjs +77 -0
- package/tests/adapter/QueryDbListPushdownParityTest.res +197 -0
- package/tests/adapter/QueryDbListPushdownParityTest.res.mjs +292 -0
- package/tests/adapter/QueryDbListResolverTest.res +76 -0
- package/tests/adapter/QueryDbListResolverTest.res.mjs +84 -0
- package/tests/components/aggregate/AggregateFixtures.res +4 -0
- package/tests/components/aggregate/AggregateFixtures.res.mjs +1 -0
- package/tests/components/eventlog/EventLogAppendStreamTest.res.mjs +4 -4
- package/tests/components/eventlog/EventLogStreamTest.res.mjs +8 -8
- package/tests/plugin/PluginBehavior_GWT.res.mjs +1 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
|
|
4
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
6
|
import * as Effect from "effect/Effect";
|
|
6
7
|
import * as Stream from "effect/Stream";
|
|
@@ -38,6 +39,30 @@ function matchesQuery(event, query) {
|
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
function tagPostingKey(key, value) {
|
|
43
|
+
return key + "\x00" + value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function intersectSorted(a, b) {
|
|
47
|
+
let out = [];
|
|
48
|
+
let i = 0;
|
|
49
|
+
let j = 0;
|
|
50
|
+
while (i < a.length && j < b.length) {
|
|
51
|
+
let av = a[i];
|
|
52
|
+
let bv = b[j];
|
|
53
|
+
if (av === bv) {
|
|
54
|
+
out.push(av);
|
|
55
|
+
i = i + 1 | 0;
|
|
56
|
+
j = j + 1 | 0;
|
|
57
|
+
} else if (av < bv) {
|
|
58
|
+
i = i + 1 | 0;
|
|
59
|
+
} else {
|
|
60
|
+
j = j + 1 | 0;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
41
66
|
function makeStorage(_name, param, param$1, param$2) {
|
|
42
67
|
let events = {
|
|
43
68
|
contents: []
|
|
@@ -45,15 +70,71 @@ function makeStorage(_name, param, param$1, param$2) {
|
|
|
45
70
|
let position = {
|
|
46
71
|
contents: 0
|
|
47
72
|
};
|
|
48
|
-
let
|
|
49
|
-
|
|
50
|
-
|
|
73
|
+
let byTag = {};
|
|
74
|
+
let byType = {};
|
|
75
|
+
let pushInto = (dict, key, idx) => {
|
|
76
|
+
let list = dict[key];
|
|
77
|
+
if (list !== undefined) {
|
|
78
|
+
list.push(idx);
|
|
79
|
+
} else {
|
|
80
|
+
dict[key] = [idx];
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
let indexEvent = (idx, ev) => {
|
|
84
|
+
pushInto(byType, ev.eventType, idx);
|
|
85
|
+
ev.tags.forEach(t => pushInto(byTag, tagPostingKey(t.key, t.value), idx));
|
|
86
|
+
};
|
|
87
|
+
let candidateIndices = query => {
|
|
88
|
+
let acc = {};
|
|
89
|
+
let addAll = arr => {
|
|
90
|
+
arr.forEach(i => {
|
|
91
|
+
acc[i.toString()] = i;
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
query.forEach(clause => {
|
|
95
|
+
let tags = clause.tags;
|
|
96
|
+
if (tags !== undefined && tags.length !== 0) {
|
|
97
|
+
let lists = tags.map(t => Stdlib_Option.getOr(byTag[tagPostingKey(t.key, t.value)], []));
|
|
98
|
+
if (lists.some(l => l.length === 0)) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
let sorted = lists.toSorted((a, b) => a.length - b.length | 0);
|
|
102
|
+
let acc0 = sorted[0];
|
|
103
|
+
return addAll(Stdlib_Array.reduce(sorted.slice(1, sorted.length), acc0, intersectSorted));
|
|
104
|
+
}
|
|
105
|
+
let types = clause.eventTypes;
|
|
106
|
+
if (types !== undefined) {
|
|
107
|
+
types.forEach(t => addAll(Stdlib_Option.getOr(byType[t], [])));
|
|
108
|
+
} else {
|
|
109
|
+
events.contents.forEach((param, i) => {
|
|
110
|
+
acc[i.toString()] = i;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
return Object.values(acc).toSorted((a, b) => a - b | 0);
|
|
115
|
+
};
|
|
116
|
+
let matchingEvents = (query, after) => {
|
|
117
|
+
let pass = ev => {
|
|
118
|
+
let afterMatch = after !== undefined ? posToInt(ev.position) > posToInt(after) : true;
|
|
51
119
|
if (afterMatch) {
|
|
52
|
-
return matchesQuery(
|
|
120
|
+
return matchesQuery(ev, query);
|
|
53
121
|
} else {
|
|
54
122
|
return false;
|
|
55
123
|
}
|
|
56
|
-
}
|
|
124
|
+
};
|
|
125
|
+
if (query.length === 0) {
|
|
126
|
+
return events.contents.filter(pass);
|
|
127
|
+
} else {
|
|
128
|
+
return Stdlib_Array.filterMap(candidateIndices(query), i => {
|
|
129
|
+
let ev = events.contents[i];
|
|
130
|
+
if (pass(ev)) {
|
|
131
|
+
return ev;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
let read = async (query, after) => {
|
|
137
|
+
let filtered = matchingEvents(query, after);
|
|
57
138
|
let headPosition = Stdlib_Option.map(events.contents[events.contents.length - 1 | 0], e => e.position);
|
|
58
139
|
return {
|
|
59
140
|
events: filtered,
|
|
@@ -61,32 +142,32 @@ function makeStorage(_name, param, param$1, param$2) {
|
|
|
61
142
|
};
|
|
62
143
|
};
|
|
63
144
|
let append = async (newEvents, condition) => {
|
|
64
|
-
let conflictDetected = condition !== undefined ?
|
|
65
|
-
let pos = condition.after;
|
|
66
|
-
let afterMatch = pos !== undefined ? posToInt(event.position) > posToInt(pos) : true;
|
|
67
|
-
if (afterMatch) {
|
|
68
|
-
return matchesQuery(event, condition.query);
|
|
69
|
-
} else {
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
}) : false;
|
|
145
|
+
let conflictDetected = condition !== undefined ? matchingEvents(condition.query, condition.after).length !== 0 : false;
|
|
73
146
|
if (conflictDetected) {
|
|
74
147
|
return {
|
|
75
148
|
TAG: "Error",
|
|
76
149
|
_0: "conflict: condition check failed"
|
|
77
150
|
};
|
|
78
151
|
}
|
|
79
|
-
let
|
|
152
|
+
let base = events.contents.length;
|
|
153
|
+
let storedEvents = newEvents.map((event, j) => {
|
|
80
154
|
position.contents = position.contents + 1 | 0;
|
|
81
155
|
let pos = position.contents.toString();
|
|
82
|
-
|
|
156
|
+
let stored_eventType = event.eventType;
|
|
157
|
+
let stored_data = event.data;
|
|
158
|
+
let stored_tags = event.tags;
|
|
159
|
+
let stored_meta = event.meta;
|
|
160
|
+
let stored_recordedAt = Message$ReventlessCore.nowAsISOString();
|
|
161
|
+
let stored = {
|
|
83
162
|
position: pos,
|
|
84
|
-
eventType:
|
|
85
|
-
data:
|
|
86
|
-
tags:
|
|
87
|
-
meta:
|
|
88
|
-
recordedAt:
|
|
163
|
+
eventType: stored_eventType,
|
|
164
|
+
data: stored_data,
|
|
165
|
+
tags: stored_tags,
|
|
166
|
+
meta: stored_meta,
|
|
167
|
+
recordedAt: stored_recordedAt
|
|
89
168
|
};
|
|
169
|
+
indexEvent(base + j | 0, stored);
|
|
170
|
+
return stored;
|
|
90
171
|
});
|
|
91
172
|
events.contents = events.contents.concat(storedEvents);
|
|
92
173
|
return {
|
|
@@ -117,7 +198,7 @@ function Make(Bus) {
|
|
|
117
198
|
let make = (name, indexes, partitionTag, param, opts) => {
|
|
118
199
|
let db = BackendState$ReventlessLocal.getDb();
|
|
119
200
|
if (db !== undefined) {
|
|
120
|
-
let match = DcbEventLogStorage_Sqlite$ReventlessLocal.makeStorage(Primitive_option.valFromOption(db),
|
|
201
|
+
let match = DcbEventLogStorage_Sqlite$ReventlessLocal.makeStorage(Primitive_option.valFromOption(db), name, indexes, partitionTag, opts);
|
|
121
202
|
Bus.registerDcbEventLogRead(match[0], match[1]);
|
|
122
203
|
return match[2];
|
|
123
204
|
}
|
|
@@ -133,6 +214,8 @@ function Make(Bus) {
|
|
|
133
214
|
export {
|
|
134
215
|
posToInt,
|
|
135
216
|
matchesQuery,
|
|
217
|
+
tagPostingKey,
|
|
218
|
+
intersectSorted,
|
|
136
219
|
makeStorage,
|
|
137
220
|
make,
|
|
138
221
|
Make,
|
|
@@ -34,6 +34,12 @@ let ensureSchema = (db: SqliteDriver.t) => {
|
|
|
34
34
|
db->SqliteDriver.exec(
|
|
35
35
|
"CREATE INDEX IF NOT EXISTS dcb_tag_by_kv ON dcb_tag(log_name, tag_key, tag_value, position)",
|
|
36
36
|
)
|
|
37
|
+
// Serves the per-position tag hydration (the `WHERE log_name = ? AND position
|
|
38
|
+
// IN (...)` batch). The `_by_kv` index leads with tag_key/tag_value, so it
|
|
39
|
+
// cannot answer a position lookup — without this the batch scanned dcb_tag.
|
|
40
|
+
db->SqliteDriver.exec(
|
|
41
|
+
"CREATE INDEX IF NOT EXISTS dcb_tag_by_pos ON dcb_tag(log_name, position)",
|
|
42
|
+
)
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
let posToInt = (pos: string) => pos->Int.fromString->Option.getOr(0)
|
|
@@ -117,7 +123,10 @@ let buildClauseSql = (
|
|
|
117
123
|
("(" ++ parts.contents->Array.join(" AND ") ++ ")", params.contents)
|
|
118
124
|
}
|
|
119
125
|
|
|
120
|
-
|
|
126
|
+
// The WHERE expression (+ bound params) for a query — shared by the row-fetching
|
|
127
|
+
// read path and the LIMIT-1 existence check the append-condition uses, so both
|
|
128
|
+
// match the exact same rows.
|
|
129
|
+
let buildQueryWhere = (
|
|
121
130
|
~logName: string,
|
|
122
131
|
~query: DcbTag.query,
|
|
123
132
|
~after: option<int>,
|
|
@@ -132,26 +141,29 @@ let buildQuerySql = (
|
|
|
132
141
|
| None => [JSON.Encode.string(logName)]
|
|
133
142
|
}
|
|
134
143
|
let _ = sqlEscape
|
|
135
|
-
(
|
|
136
|
-
`SELECT position, event_type, data, meta, recorded_at FROM dcb_event WHERE ${baseWhere} ORDER BY position ASC`,
|
|
137
|
-
baseParams,
|
|
138
|
-
)
|
|
144
|
+
(baseWhere, baseParams)
|
|
139
145
|
} else {
|
|
140
146
|
let clauses = query->Array.map(c => buildClauseSql(~logName, ~clause=c, ~after))
|
|
141
|
-
let combinedWhere =
|
|
142
|
-
clauses->Array.map(((sql, _)) => sql)->Array.join(" OR ")
|
|
147
|
+
let combinedWhere = clauses->Array.map(((sql, _)) => sql)->Array.join(" OR ")
|
|
143
148
|
let combinedParams = clauses->Array.flatMap(((_, p)) => p)
|
|
144
|
-
(
|
|
145
|
-
`SELECT position, event_type, data, meta, recorded_at FROM dcb_event WHERE ${combinedWhere} ORDER BY position ASC`,
|
|
146
|
-
combinedParams,
|
|
147
|
-
)
|
|
149
|
+
(combinedWhere, combinedParams)
|
|
148
150
|
}
|
|
149
151
|
}
|
|
150
152
|
|
|
153
|
+
let buildQuerySql = (
|
|
154
|
+
~logName: string,
|
|
155
|
+
~query: DcbTag.query,
|
|
156
|
+
~after: option<int>,
|
|
157
|
+
): (string, array<JSON.t>) => {
|
|
158
|
+
let (where, params) = buildQueryWhere(~logName, ~query, ~after)
|
|
159
|
+
(
|
|
160
|
+
`SELECT position, event_type, data, meta, recorded_at FROM dcb_event WHERE ${where} ORDER BY position ASC`,
|
|
161
|
+
params,
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
151
165
|
let makeStorage = (
|
|
152
166
|
~db: SqliteDriver.t,
|
|
153
|
-
~bus: ('a, JSON.t) => unit,
|
|
154
|
-
~publishToTopic: (~topicName: string, ~json: JSON.t) => unit,
|
|
155
167
|
~name: string,
|
|
156
168
|
~indexes as _,
|
|
157
169
|
~partitionTag as _,
|
|
@@ -162,6 +174,7 @@ let makeStorage = (
|
|
|
162
174
|
let insertEventStmt = db->SqliteDriver.prepare(
|
|
163
175
|
"INSERT INTO dcb_event(log_name, position, event_type, data, meta, recorded_at) VALUES(?,?,?,?,?,?)",
|
|
164
176
|
)
|
|
177
|
+
let lastRowidStmt = db->SqliteDriver.prepare("SELECT last_insert_rowid() AS r")
|
|
165
178
|
let insertTagStmt = db->SqliteDriver.prepare(
|
|
166
179
|
"INSERT INTO dcb_tag(log_name, position, tag_key, tag_value) VALUES(?,?,?,?)",
|
|
167
180
|
)
|
|
@@ -171,12 +184,21 @@ let makeStorage = (
|
|
|
171
184
|
let headPositionStmt = db->SqliteDriver.prepare(
|
|
172
185
|
"SELECT MAX(position) AS m FROM dcb_event WHERE log_name = ?",
|
|
173
186
|
)
|
|
174
|
-
let tagsForPositionsStmt = db->SqliteDriver.prepare(
|
|
175
|
-
"SELECT position, tag_key, tag_value FROM dcb_tag WHERE log_name = ? AND position = ?",
|
|
176
|
-
)
|
|
177
187
|
|
|
178
|
-
|
|
179
|
-
|
|
188
|
+
// Prepared-statement cache keyed on SQL text. `runQuery`, the batched tag
|
|
189
|
+
// hydration, and the conflict check all build SQL whose shape varies with the
|
|
190
|
+
// clause/position count; preparing per call (the old `runQuery`) recompiled the
|
|
191
|
+
// same handful of shapes on every read. node:sqlite statements are re-runnable
|
|
192
|
+
// with fresh params, so one prepare per distinct shape suffices.
|
|
193
|
+
let stmtCache: Dict.t<SqliteDriver.statement> = Dict.make()
|
|
194
|
+
let preparedFor = (sql: string): SqliteDriver.statement =>
|
|
195
|
+
switch stmtCache->Dict.get(sql) {
|
|
196
|
+
| Some(s) => s
|
|
197
|
+
| None =>
|
|
198
|
+
let s = db->SqliteDriver.prepare(sql)
|
|
199
|
+
stmtCache->Dict.set(sql, s)
|
|
200
|
+
s
|
|
201
|
+
}
|
|
180
202
|
|
|
181
203
|
let currentMaxPosition = (): int =>
|
|
182
204
|
switch maxPositionStmt->SqliteDriver.get([JSON.Encode.string(name)]) {
|
|
@@ -188,27 +210,56 @@ let makeStorage = (
|
|
|
188
210
|
| None => 0
|
|
189
211
|
}
|
|
190
212
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
let
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
(
|
|
204
|
-
|
|
213
|
+
// Hydrate tags for a batch of positions in ONE query, grouped by position, in
|
|
214
|
+
// insertion order (`rowid`) within each position — the same order the old
|
|
215
|
+
// per-position query returned. Replaces the N+1 `tagsForPosition` per event.
|
|
216
|
+
let tagsForPositions = (positions: array<int>): Dict.t<array<DcbTag.tag>> => {
|
|
217
|
+
let byPos: Dict.t<array<DcbTag.tag>> = Dict.make()
|
|
218
|
+
if positions->Array.length > 0 {
|
|
219
|
+
let placeholders = positions->Array.map(_ => "?")->Array.join(",")
|
|
220
|
+
let sql = `SELECT position, tag_key, tag_value FROM dcb_tag WHERE log_name = ? AND position IN (${placeholders}) ORDER BY position ASC, rowid ASC`
|
|
221
|
+
let params =
|
|
222
|
+
[JSON.Encode.string(name)]->Array.concat(positions->Array.map(JSON.Encode.int))
|
|
223
|
+
preparedFor(sql)
|
|
224
|
+
->SqliteDriver.all(params)
|
|
225
|
+
->Array.forEach(row => {
|
|
226
|
+
let pos = switch row->Dict.get("position") {
|
|
227
|
+
| Some(v) => coercePosition(v)
|
|
228
|
+
| None => 0
|
|
229
|
+
}
|
|
230
|
+
let key = switch row->Dict.get("tag_key") {
|
|
231
|
+
| Some(JSON.String(s)) => s
|
|
232
|
+
| _ => ""
|
|
233
|
+
}
|
|
234
|
+
let value = switch row->Dict.get("tag_value") {
|
|
235
|
+
| Some(JSON.String(s)) => s
|
|
236
|
+
| _ => ""
|
|
237
|
+
}
|
|
238
|
+
let k = Int.toString(pos)
|
|
239
|
+
let arr = switch byPos->Dict.get(k) {
|
|
240
|
+
| Some(a) => a
|
|
241
|
+
| None =>
|
|
242
|
+
let a = []
|
|
243
|
+
byPos->Dict.set(k, a)
|
|
244
|
+
a
|
|
245
|
+
}
|
|
246
|
+
arr->Array.push(({key, value}: DcbTag.tag))
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
byPos
|
|
250
|
+
}
|
|
205
251
|
|
|
206
252
|
let runQuery = (~query: DcbTag.query, ~after: option<int>): array<DcbEventLog_Adapter.rawSequencedEvent> => {
|
|
207
253
|
let (sql, params) = buildQuerySql(~logName=name, ~query, ~after)
|
|
208
|
-
let
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
254
|
+
let rows = preparedFor(sql)->SqliteDriver.all(params)
|
|
255
|
+
let positions = rows->Array.map(row =>
|
|
256
|
+
switch row->Dict.get("position") {
|
|
257
|
+
| Some(v) => coercePosition(v)
|
|
258
|
+
| None => 0
|
|
259
|
+
}
|
|
260
|
+
)
|
|
261
|
+
let tagsByPos = tagsForPositions(positions)
|
|
262
|
+
rows->Array.map(row => {
|
|
212
263
|
let position = switch row->Dict.get("position") {
|
|
213
264
|
| Some(v) => coercePosition(v)
|
|
214
265
|
| None => 0
|
|
@@ -237,13 +288,23 @@ let makeStorage = (
|
|
|
237
288
|
DcbEventLog_Adapter.position: Int.toString(position),
|
|
238
289
|
eventType,
|
|
239
290
|
data,
|
|
240
|
-
tags:
|
|
291
|
+
tags: tagsByPos->Dict.get(Int.toString(position))->Option.getOr([]),
|
|
241
292
|
meta,
|
|
242
293
|
recordedAt,
|
|
243
294
|
}: DcbEventLog_Adapter.rawSequencedEvent)
|
|
244
295
|
})
|
|
245
296
|
}
|
|
246
297
|
|
|
298
|
+
// Does any event match the query? The append-condition only needs existence,
|
|
299
|
+
// so avoid materialising + tag-hydrating every conflicting row (the old
|
|
300
|
+
// `runQuery(...)->Array.length > 0`): a `SELECT 1 … LIMIT 1` short-circuits.
|
|
301
|
+
let anyMatch = (~query: DcbTag.query, ~after: option<int>): bool => {
|
|
302
|
+
let (where, params) = buildQueryWhere(~logName=name, ~query, ~after)
|
|
303
|
+
preparedFor(`SELECT 1 FROM dcb_event WHERE ${where} LIMIT 1`)
|
|
304
|
+
->SqliteDriver.get(params)
|
|
305
|
+
->Option.isSome
|
|
306
|
+
}
|
|
307
|
+
|
|
247
308
|
let read = async (
|
|
248
309
|
~query: DcbTag.query,
|
|
249
310
|
~after=?,
|
|
@@ -263,13 +324,17 @@ let makeStorage = (
|
|
|
263
324
|
~condition=?,
|
|
264
325
|
): result<DcbTag.sequencePosition, string> => {
|
|
265
326
|
let result = ref(Ok(""))
|
|
327
|
+
// (msgId, dcb_event rowid) per inserted event, for the projection
|
|
328
|
+
// checkpoint's DCB-axis pending set. The rowid is read immediately after
|
|
329
|
+
// each event insert — before the tag inserts move last_insert_rowid() to
|
|
330
|
+
// the dcb_tag table. Registered only after the transaction commits.
|
|
331
|
+
let appended: array<(string, int)> = []
|
|
266
332
|
try {
|
|
267
333
|
db->SqliteDriver.transaction(() => {
|
|
268
334
|
switch condition {
|
|
269
335
|
| Some(cond: DcbTag.appendCondition) =>
|
|
270
336
|
let afterInt = cond.after->Option.map(posToInt)
|
|
271
|
-
|
|
272
|
-
if conflicting->Array.length > 0 {
|
|
337
|
+
if anyMatch(~query=cond.query, ~after=afterInt) {
|
|
273
338
|
throw(Failure("conflict: condition check failed"))
|
|
274
339
|
}
|
|
275
340
|
| None => ()
|
|
@@ -290,6 +355,14 @@ let makeStorage = (
|
|
|
290
355
|
JSON.Encode.string(JSON.stringify(metaJson)),
|
|
291
356
|
JSON.Encode.string(recordedAt),
|
|
292
357
|
])
|
|
358
|
+
switch lastRowidStmt->SqliteDriver.get([]) {
|
|
359
|
+
| Some(row) =>
|
|
360
|
+
switch row->Dict.get("r") {
|
|
361
|
+
| Some(JSON.Number(n)) => appended->Array.push((event.meta.msgId, Float.toInt(n)))
|
|
362
|
+
| _ => ()
|
|
363
|
+
}
|
|
364
|
+
| None => ()
|
|
365
|
+
}
|
|
293
366
|
event.tags->Array.forEach(tag => {
|
|
294
367
|
insertTagStmt->SqliteDriver.run([
|
|
295
368
|
JSON.Encode.string(name),
|
|
@@ -303,10 +376,20 @@ let makeStorage = (
|
|
|
303
376
|
|
|
304
377
|
result := Ok(Int.toString(lastPos.contents))
|
|
305
378
|
})
|
|
379
|
+
ProjectionPending.trackAppended(~axis=ProjectionPending.Dcb, appended)
|
|
306
380
|
result.contents
|
|
307
381
|
} catch {
|
|
308
382
|
| Failure(msg) => Error(msg)
|
|
309
|
-
|
|
|
383
|
+
| exn =>
|
|
384
|
+
// The deliberate append-condition failure above throws
|
|
385
|
+
// Failure("conflict: …"); a lost race on the computed position shows up here
|
|
386
|
+
// as a PRIMARY KEY / UNIQUE violation — also a genuine conflict. Every other
|
|
387
|
+
// failure (disk full, SQL error, locked db) must surface as a real error,
|
|
388
|
+
// not the retryable "conflict" sentinel that would be retried forever.
|
|
389
|
+
let msg = exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("")
|
|
390
|
+
msg->String.includes("constraint failed")
|
|
391
|
+
? Error("conflict")
|
|
392
|
+
: Error(msg == "" ? "storage error" : msg)
|
|
310
393
|
}
|
|
311
394
|
}
|
|
312
395
|
|
|
@@ -2,20 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
import * as S from "sury/src/S.res.mjs";
|
|
4
4
|
import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
|
|
5
|
+
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
5
6
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
7
|
import * as Effect from "effect/Effect";
|
|
7
8
|
import * as Stream from "effect/Stream";
|
|
8
9
|
import * as Pulumi from "@pulumi/pulumi";
|
|
9
10
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
11
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
10
12
|
import * as Message$Reventless from "@reventlessdev/reventless-spec/src/types/Message.res.mjs";
|
|
11
13
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
12
14
|
import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
13
15
|
import * as SqliteDriver$ReventlessLocal from "../SqliteDriver.res.mjs";
|
|
16
|
+
import * as ProjectionPending$ReventlessLocal from "../ProjectionPending.res.mjs";
|
|
14
17
|
|
|
15
18
|
function ensureSchema(db) {
|
|
16
19
|
SqliteDriver$ReventlessLocal.exec(db, "CREATE TABLE IF NOT EXISTS dcb_event (log_name TEXT NOT NULL, position INTEGER NOT NULL, event_type TEXT NOT NULL, data TEXT NOT NULL, meta TEXT NOT NULL, recorded_at TEXT NOT NULL, PRIMARY KEY (log_name, position))");
|
|
17
20
|
SqliteDriver$ReventlessLocal.exec(db, "CREATE TABLE IF NOT EXISTS dcb_tag (log_name TEXT NOT NULL, position INTEGER NOT NULL, tag_key TEXT NOT NULL, tag_value TEXT NOT NULL)");
|
|
18
21
|
SqliteDriver$ReventlessLocal.exec(db, "CREATE INDEX IF NOT EXISTS dcb_tag_by_kv ON dcb_tag(log_name, tag_key, tag_value, position)");
|
|
22
|
+
SqliteDriver$ReventlessLocal.exec(db, "CREATE INDEX IF NOT EXISTS dcb_tag_by_pos ON dcb_tag(log_name, position)");
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
function posToInt(pos) {
|
|
@@ -95,7 +99,7 @@ function buildClauseSql(logName, clause, after) {
|
|
|
95
99
|
];
|
|
96
100
|
}
|
|
97
101
|
|
|
98
|
-
function
|
|
102
|
+
function buildQueryWhere(logName, query, after) {
|
|
99
103
|
if (query.length === 0) {
|
|
100
104
|
let baseWhere = after !== undefined ? "log_name = ? AND position > ?" : "log_name = ?";
|
|
101
105
|
let baseParams = after !== undefined ? [
|
|
@@ -103,7 +107,7 @@ function buildQuerySql(logName, query, after) {
|
|
|
103
107
|
after
|
|
104
108
|
] : [logName];
|
|
105
109
|
return [
|
|
106
|
-
|
|
110
|
+
baseWhere,
|
|
107
111
|
baseParams
|
|
108
112
|
];
|
|
109
113
|
}
|
|
@@ -111,18 +115,36 @@ function buildQuerySql(logName, query, after) {
|
|
|
111
115
|
let combinedWhere = clauses.map(param => param[0]).join(" OR ");
|
|
112
116
|
let combinedParams = clauses.flatMap(param => param[1]);
|
|
113
117
|
return [
|
|
114
|
-
|
|
118
|
+
combinedWhere,
|
|
115
119
|
combinedParams
|
|
116
120
|
];
|
|
117
121
|
}
|
|
118
122
|
|
|
119
|
-
function
|
|
123
|
+
function buildQuerySql(logName, query, after) {
|
|
124
|
+
let match = buildQueryWhere(logName, query, after);
|
|
125
|
+
return [
|
|
126
|
+
`SELECT position, event_type, data, meta, recorded_at FROM dcb_event WHERE ` + match[0] + ` ORDER BY position ASC`,
|
|
127
|
+
match[1]
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function makeStorage(db, name, param, param$1, param$2) {
|
|
120
132
|
ensureSchema(db);
|
|
121
133
|
let insertEventStmt = SqliteDriver$ReventlessLocal.prepare(db, "INSERT INTO dcb_event(log_name, position, event_type, data, meta, recorded_at) VALUES(?,?,?,?,?,?)");
|
|
134
|
+
let lastRowidStmt = SqliteDriver$ReventlessLocal.prepare(db, "SELECT last_insert_rowid() AS r");
|
|
122
135
|
let insertTagStmt = SqliteDriver$ReventlessLocal.prepare(db, "INSERT INTO dcb_tag(log_name, position, tag_key, tag_value) VALUES(?,?,?,?)");
|
|
123
136
|
let maxPositionStmt = SqliteDriver$ReventlessLocal.prepare(db, "SELECT COALESCE(MAX(position), 0) AS m FROM dcb_event WHERE log_name = ?");
|
|
124
137
|
SqliteDriver$ReventlessLocal.prepare(db, "SELECT MAX(position) AS m FROM dcb_event WHERE log_name = ?");
|
|
125
|
-
let
|
|
138
|
+
let stmtCache = {};
|
|
139
|
+
let preparedFor = sql => {
|
|
140
|
+
let s = stmtCache[sql];
|
|
141
|
+
if (s !== undefined) {
|
|
142
|
+
return Primitive_option.valFromOption(s);
|
|
143
|
+
}
|
|
144
|
+
let s$1 = SqliteDriver$ReventlessLocal.prepare(db, sql);
|
|
145
|
+
stmtCache[sql] = s$1;
|
|
146
|
+
return s$1;
|
|
147
|
+
};
|
|
126
148
|
let currentMaxPosition = () => {
|
|
127
149
|
let row = SqliteDriver$ReventlessLocal.get(maxPositionStmt, [name]);
|
|
128
150
|
if (row === undefined) {
|
|
@@ -135,23 +157,50 @@ function makeStorage(db, bus, publishToTopic, name, param, param$1, param$2) {
|
|
|
135
157
|
return 0;
|
|
136
158
|
}
|
|
137
159
|
};
|
|
138
|
-
let
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
160
|
+
let tagsForPositions = positions => {
|
|
161
|
+
let byPos = {};
|
|
162
|
+
if (positions.length !== 0) {
|
|
163
|
+
let placeholders = positions.map(param => "?").join(",");
|
|
164
|
+
let sql = `SELECT position, tag_key, tag_value FROM dcb_tag WHERE log_name = ? AND position IN (` + placeholders + `) ORDER BY position ASC, rowid ASC`;
|
|
165
|
+
let params = [name].concat(positions.map(prim => prim));
|
|
166
|
+
SqliteDriver$ReventlessLocal.all(preparedFor(sql), params).forEach(row => {
|
|
167
|
+
let v = row["position"];
|
|
168
|
+
let pos = v !== undefined ? coercePosition(v) : 0;
|
|
169
|
+
let match = row["tag_key"];
|
|
170
|
+
let key = typeof match === "string" ? match : "";
|
|
171
|
+
let match$1 = row["tag_value"];
|
|
172
|
+
let value = typeof match$1 === "string" ? match$1 : "";
|
|
173
|
+
let k = pos.toString();
|
|
174
|
+
let a = byPos[k];
|
|
175
|
+
let arr;
|
|
176
|
+
if (a !== undefined) {
|
|
177
|
+
arr = a;
|
|
178
|
+
} else {
|
|
179
|
+
let a$1 = [];
|
|
180
|
+
byPos[k] = a$1;
|
|
181
|
+
arr = a$1;
|
|
182
|
+
}
|
|
183
|
+
arr.push({
|
|
184
|
+
key: key,
|
|
185
|
+
value: value
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return byPos;
|
|
190
|
+
};
|
|
151
191
|
let runQuery = (query, after) => {
|
|
152
192
|
let match = buildQuerySql(name, query, after);
|
|
153
|
-
let
|
|
154
|
-
|
|
193
|
+
let rows = SqliteDriver$ReventlessLocal.all(preparedFor(match[0]), match[1]);
|
|
194
|
+
let positions = rows.map(row => {
|
|
195
|
+
let v = row["position"];
|
|
196
|
+
if (v !== undefined) {
|
|
197
|
+
return coercePosition(v);
|
|
198
|
+
} else {
|
|
199
|
+
return 0;
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
let tagsByPos = tagsForPositions(positions);
|
|
203
|
+
return rows.map(row => {
|
|
155
204
|
let v = row["position"];
|
|
156
205
|
let position = v !== undefined ? coercePosition(v) : 0;
|
|
157
206
|
let match = row["event_type"];
|
|
@@ -181,12 +230,16 @@ function makeStorage(db, bus, publishToTopic, name, param, param$1, param$2) {
|
|
|
181
230
|
position: position.toString(),
|
|
182
231
|
eventType: eventType,
|
|
183
232
|
data: data,
|
|
184
|
-
tags:
|
|
233
|
+
tags: Stdlib_Option.getOr(tagsByPos[position.toString()], []),
|
|
185
234
|
meta: meta,
|
|
186
235
|
recordedAt: recordedAt
|
|
187
236
|
};
|
|
188
237
|
});
|
|
189
238
|
};
|
|
239
|
+
let anyMatch = (query, after) => {
|
|
240
|
+
let match = buildQueryWhere(name, query, after);
|
|
241
|
+
return Stdlib_Option.isSome(SqliteDriver$ReventlessLocal.get(preparedFor(`SELECT 1 FROM dcb_event WHERE ` + match[0] + ` LIMIT 1`), match[1]));
|
|
242
|
+
};
|
|
190
243
|
let read = async (query, after) => {
|
|
191
244
|
let afterInt = Stdlib_Option.map(after, posToInt);
|
|
192
245
|
let events = runQuery(query, afterInt);
|
|
@@ -209,12 +262,12 @@ function makeStorage(db, bus, publishToTopic, name, param, param$1, param$2) {
|
|
|
209
262
|
_0: ""
|
|
210
263
|
}
|
|
211
264
|
};
|
|
265
|
+
let appended = [];
|
|
212
266
|
try {
|
|
213
267
|
SqliteDriver$ReventlessLocal.transaction(db, () => {
|
|
214
268
|
if (condition !== undefined) {
|
|
215
269
|
let afterInt = Stdlib_Option.map(condition.after, posToInt);
|
|
216
|
-
|
|
217
|
-
if (conflicting.length !== 0) {
|
|
270
|
+
if (anyMatch(condition.query, afterInt)) {
|
|
218
271
|
throw {
|
|
219
272
|
RE_EXN_ID: "Failure",
|
|
220
273
|
_1: "conflict: condition check failed",
|
|
@@ -241,6 +294,16 @@ function makeStorage(db, bus, publishToTopic, name, param, param$1, param$2) {
|
|
|
241
294
|
JSON.stringify(metaJson),
|
|
242
295
|
recordedAt
|
|
243
296
|
]);
|
|
297
|
+
let row = SqliteDriver$ReventlessLocal.get(lastRowidStmt, []);
|
|
298
|
+
if (row !== undefined) {
|
|
299
|
+
let match = row["r"];
|
|
300
|
+
if (typeof match === "number") {
|
|
301
|
+
appended.push([
|
|
302
|
+
event.meta.msgId,
|
|
303
|
+
match | 0
|
|
304
|
+
]);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
244
307
|
event.tags.forEach(tag => SqliteDriver$ReventlessLocal.run(insertTagStmt, [
|
|
245
308
|
name,
|
|
246
309
|
pos,
|
|
@@ -254,6 +317,7 @@ function makeStorage(db, bus, publishToTopic, name, param, param$1, param$2) {
|
|
|
254
317
|
_0: lastPos.contents.toString()
|
|
255
318
|
};
|
|
256
319
|
});
|
|
320
|
+
ProjectionPending$ReventlessLocal.trackAppended("Dcb", appended);
|
|
257
321
|
return result.contents;
|
|
258
322
|
} catch (raw_msg) {
|
|
259
323
|
let msg = Primitive_exceptions.internalToException(raw_msg);
|
|
@@ -262,11 +326,18 @@ function makeStorage(db, bus, publishToTopic, name, param, param$1, param$2) {
|
|
|
262
326
|
TAG: "Error",
|
|
263
327
|
_0: msg._1
|
|
264
328
|
};
|
|
265
|
-
}
|
|
329
|
+
}
|
|
330
|
+
let msg$1 = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(msg), Stdlib_JsExn.message), "");
|
|
331
|
+
if (msg$1.includes("constraint failed")) {
|
|
266
332
|
return {
|
|
267
333
|
TAG: "Error",
|
|
268
334
|
_0: "conflict"
|
|
269
335
|
};
|
|
336
|
+
} else {
|
|
337
|
+
return {
|
|
338
|
+
TAG: "Error",
|
|
339
|
+
_0: msg$1 === "" ? "storage error" : msg$1
|
|
340
|
+
};
|
|
270
341
|
}
|
|
271
342
|
}
|
|
272
343
|
};
|
|
@@ -293,6 +364,7 @@ export {
|
|
|
293
364
|
coercePosition,
|
|
294
365
|
sqlEscape,
|
|
295
366
|
buildClauseSql,
|
|
367
|
+
buildQueryWhere,
|
|
296
368
|
buildQuerySql,
|
|
297
369
|
makeStorage,
|
|
298
370
|
}
|