sequent 8.1.1 → 8.2.0
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.
- checksums.yaml +4 -4
- data/db/migrate/20250101000000_sequent_initial_schema.rb +166 -0
- data/db/migrate/20250101000001_sequent_stored_procedures.rb +48 -0
- data/db/migrate/20250312105100_sequent_store_events_v02.rb +29 -0
- data/db/migrate/sequent/aggregate_event_type_v01.sql +7 -0
- data/db/migrate/sequent/aggregates_that_need_snapshots_v01.sql +12 -0
- data/db/migrate/sequent/command_records_v01.sql +11 -0
- data/db/migrate/sequent/delete_all_snapshots_v01.sql +9 -0
- data/db/migrate/sequent/delete_snapshots_before_v01.sql +14 -0
- data/db/migrate/sequent/enrich_command_json_v01.sql +14 -0
- data/db/migrate/sequent/enrich_event_json_v01.sql +11 -0
- data/db/migrate/sequent/event_records_v01.sql +13 -0
- data/db/migrate/sequent/load_event_v01.sql +19 -0
- data/db/migrate/sequent/load_events_v01.sql +40 -0
- data/db/migrate/sequent/load_latest_snapshots_v01.sql +12 -0
- data/db/migrate/sequent/permanently_delete_commands_without_events_v01.sql +13 -0
- data/db/migrate/sequent/permanently_delete_event_streams_v01.sql +13 -0
- data/db/migrate/sequent/save_events_trigger_v01.sql +53 -0
- data/db/migrate/sequent/select_aggregates_for_snapshotting_v01.sql +19 -0
- data/db/migrate/sequent/store_aggregates_v01.sql +39 -0
- data/db/migrate/sequent/store_command_v01.sql +21 -0
- data/db/migrate/sequent/store_events_v01.sql +37 -0
- data/db/migrate/sequent/store_events_v02.sql +37 -0
- data/db/migrate/sequent/store_snapshots_v01.sql +30 -0
- data/db/migrate/sequent/stream_records_v01.sql +7 -0
- data/db/migrate/sequent/update_types_v01.sql +32 -0
- data/db/migrate/sequent/update_unique_keys_v01.sql +33 -0
- data/db/sequent_pgsql.sql +22 -440
- data/db/structure.sql +1358 -0
- data/lib/sequent/core/command_record.rb +0 -1
- data/lib/sequent/core/event_record.rb +0 -1
- data/lib/sequent/core/event_store.rb +64 -6
- data/lib/sequent/core/helpers/message_router.rb +9 -6
- data/lib/sequent/core/persistors/active_record_persistor.rb +1 -22
- data/lib/sequent/generator/template_project/db/database.yml +1 -3
- data/lib/sequent/generator/template_project/spec/spec_helper.rb +0 -1
- data/lib/sequent/migrations/sequent_schema.rb +2 -11
- data/lib/sequent/migrations/view_schema.rb +1 -1
- data/lib/sequent/rake/migration_files.rb +69 -0
- data/lib/sequent/rake/migration_tasks.rb +71 -9
- data/lib/sequent/support/database.rb +12 -31
- data/lib/sequent/test/command_handler_helpers.rb +14 -1
- data/lib/sequent/util/dry_run.rb +8 -0
- data/lib/version.rb +1 -1
- metadata +29 -2
- data/db/migrate/20250108162754_aggregate_unique_keys.rb +0 -31
@@ -0,0 +1,37 @@
|
|
1
|
+
CREATE OR REPLACE PROCEDURE store_events(_command jsonb, _aggregates_with_events jsonb)
|
2
|
+
LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
|
3
|
+
DECLARE
|
4
|
+
_command_id commands.id%TYPE;
|
5
|
+
_aggregates jsonb;
|
6
|
+
_aggregate jsonb;
|
7
|
+
_events jsonb;
|
8
|
+
_aggregate_id aggregates.aggregate_id%TYPE;
|
9
|
+
_events_partition_key aggregates.events_partition_key%TYPE;
|
10
|
+
BEGIN
|
11
|
+
CALL update_types(_command, _aggregates_with_events);
|
12
|
+
|
13
|
+
_command_id = store_command(_command);
|
14
|
+
|
15
|
+
CALL store_aggregates(_aggregates_with_events);
|
16
|
+
|
17
|
+
FOR _aggregate, _events IN SELECT row->0, row->1 FROM jsonb_array_elements(_aggregates_with_events) AS row
|
18
|
+
ORDER BY row->0->'aggregate_id', row->1->0->'event_json'->'sequence_number'
|
19
|
+
LOOP
|
20
|
+
_aggregate_id = _aggregate->>'aggregate_id';
|
21
|
+
SELECT events_partition_key INTO STRICT _events_partition_key FROM aggregates WHERE aggregate_id = _aggregate_id;
|
22
|
+
|
23
|
+
INSERT INTO events (partition_key, aggregate_id, sequence_number, created_at, command_id, event_type_id, event_json)
|
24
|
+
SELECT _events_partition_key,
|
25
|
+
_aggregate_id,
|
26
|
+
(event->'event_json'->'sequence_number')::integer,
|
27
|
+
(event->>'created_at')::timestamptz,
|
28
|
+
_command_id,
|
29
|
+
(SELECT id FROM event_types WHERE type = event->>'event_type'),
|
30
|
+
(event->'event_json') - '{aggregate_id,created_at,event_type,sequence_number}'::text[]
|
31
|
+
FROM jsonb_array_elements(_events) AS event;
|
32
|
+
END LOOP;
|
33
|
+
|
34
|
+
_aggregates = (SELECT jsonb_agg(row->0) FROM jsonb_array_elements(_aggregates_with_events) AS row);
|
35
|
+
CALL update_unique_keys(_aggregates);
|
36
|
+
END;
|
37
|
+
$$;
|
@@ -0,0 +1,30 @@
|
|
1
|
+
CREATE OR REPLACE PROCEDURE store_snapshots(_snapshots jsonb)
|
2
|
+
LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
|
3
|
+
DECLARE
|
4
|
+
_aggregate_id uuid;
|
5
|
+
_snapshot jsonb;
|
6
|
+
_sequence_number snapshot_records.sequence_number%TYPE;
|
7
|
+
BEGIN
|
8
|
+
FOR _snapshot IN SELECT * FROM jsonb_array_elements(_snapshots) LOOP
|
9
|
+
_aggregate_id = _snapshot->>'aggregate_id';
|
10
|
+
_sequence_number = _snapshot->'sequence_number';
|
11
|
+
|
12
|
+
INSERT INTO aggregates_that_need_snapshots AS row (aggregate_id, snapshot_sequence_number_high_water_mark)
|
13
|
+
VALUES (_aggregate_id, _sequence_number)
|
14
|
+
ON CONFLICT (aggregate_id) DO UPDATE
|
15
|
+
SET snapshot_sequence_number_high_water_mark =
|
16
|
+
GREATEST(row.snapshot_sequence_number_high_water_mark, EXCLUDED.snapshot_sequence_number_high_water_mark),
|
17
|
+
snapshot_outdated_at = NULL,
|
18
|
+
snapshot_scheduled_at = NULL;
|
19
|
+
|
20
|
+
INSERT INTO snapshot_records (aggregate_id, sequence_number, created_at, snapshot_type, snapshot_json)
|
21
|
+
VALUES (
|
22
|
+
_aggregate_id,
|
23
|
+
_sequence_number,
|
24
|
+
(_snapshot->>'created_at')::timestamptz,
|
25
|
+
_snapshot->>'snapshot_type',
|
26
|
+
_snapshot->'snapshot_json'
|
27
|
+
);
|
28
|
+
END LOOP;
|
29
|
+
END;
|
30
|
+
$$;
|
@@ -0,0 +1,7 @@
|
|
1
|
+
DROP VIEW IF EXISTS stream_records;
|
2
|
+
CREATE VIEW stream_records (aggregate_id, events_partition_key, aggregate_type, created_at) AS
|
3
|
+
SELECT aggregates.aggregate_id,
|
4
|
+
aggregates.events_partition_key,
|
5
|
+
aggregate_types.type,
|
6
|
+
aggregates.created_at
|
7
|
+
FROM aggregates JOIN aggregate_types ON aggregates.aggregate_type_id = aggregate_types.id;
|
@@ -0,0 +1,32 @@
|
|
1
|
+
CREATE OR REPLACE PROCEDURE update_types(_command jsonb, _aggregates_with_events jsonb)
|
2
|
+
LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
|
3
|
+
BEGIN
|
4
|
+
IF NOT EXISTS (SELECT 1 FROM command_types t WHERE t.type = _command->>'command_type') THEN
|
5
|
+
-- Only try inserting if it doesn't exist to avoid exhausting the id sequence
|
6
|
+
INSERT INTO command_types (type)
|
7
|
+
VALUES (_command->>'command_type')
|
8
|
+
ON CONFLICT DO NOTHING;
|
9
|
+
END IF;
|
10
|
+
|
11
|
+
WITH types AS (
|
12
|
+
SELECT DISTINCT row->0->>'aggregate_type' AS type
|
13
|
+
FROM jsonb_array_elements(_aggregates_with_events) AS row
|
14
|
+
)
|
15
|
+
INSERT INTO aggregate_types (type)
|
16
|
+
SELECT type FROM types
|
17
|
+
WHERE type NOT IN (SELECT type FROM aggregate_types)
|
18
|
+
ORDER BY 1
|
19
|
+
ON CONFLICT DO NOTHING;
|
20
|
+
|
21
|
+
WITH types AS (
|
22
|
+
SELECT DISTINCT events->>'event_type' AS type
|
23
|
+
FROM jsonb_array_elements(_aggregates_with_events) AS row
|
24
|
+
CROSS JOIN LATERAL jsonb_array_elements(row->1) AS events
|
25
|
+
)
|
26
|
+
INSERT INTO event_types (type)
|
27
|
+
SELECT type FROM types
|
28
|
+
WHERE type NOT IN (SELECT type FROM event_types)
|
29
|
+
ORDER BY 1
|
30
|
+
ON CONFLICT DO NOTHING;
|
31
|
+
END;
|
32
|
+
$$;
|
@@ -0,0 +1,33 @@
|
|
1
|
+
CREATE OR REPLACE PROCEDURE update_unique_keys(_stream_records jsonb)
|
2
|
+
LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
|
3
|
+
DECLARE
|
4
|
+
_aggregate jsonb;
|
5
|
+
_aggregate_id aggregates.aggregate_id%TYPE;
|
6
|
+
_unique_keys jsonb;
|
7
|
+
BEGIN
|
8
|
+
FOR _aggregate IN SELECT aggregate FROM jsonb_array_elements(_stream_records) AS aggregate LOOP
|
9
|
+
_aggregate_id = _aggregate->>'aggregate_id';
|
10
|
+
_unique_keys = COALESCE(_aggregate->'unique_keys', '{}'::jsonb);
|
11
|
+
|
12
|
+
DELETE FROM aggregate_unique_keys AS target
|
13
|
+
WHERE target.aggregate_id = _aggregate_id
|
14
|
+
AND NOT (_unique_keys ? target.scope);
|
15
|
+
END LOOP;
|
16
|
+
|
17
|
+
FOR _aggregate IN SELECT aggregate FROM jsonb_array_elements(_stream_records) AS aggregate LOOP
|
18
|
+
_aggregate_id = _aggregate->>'aggregate_id';
|
19
|
+
_unique_keys = COALESCE(_aggregate->'unique_keys', '{}'::jsonb);
|
20
|
+
|
21
|
+
INSERT INTO aggregate_unique_keys AS target (aggregate_id, scope, key)
|
22
|
+
SELECT _aggregate_id, key, value
|
23
|
+
FROM jsonb_each(_unique_keys) AS x
|
24
|
+
ON CONFLICT (aggregate_id, scope) DO UPDATE
|
25
|
+
SET key = EXCLUDED.key
|
26
|
+
WHERE target.key <> EXCLUDED.key;
|
27
|
+
END LOOP;
|
28
|
+
EXCEPTION
|
29
|
+
WHEN unique_violation THEN
|
30
|
+
RAISE unique_violation
|
31
|
+
USING MESSAGE = 'duplicate unique key value for aggregate ' || (_aggregate->>'aggregate_type') || ' ' || _aggregate_id || ' (' || SQLERRM || ')';
|
32
|
+
END;
|
33
|
+
$$;
|
data/db/sequent_pgsql.sql
CHANGED
@@ -1,440 +1,22 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
$$;
|
24
|
-
|
25
|
-
CREATE OR REPLACE FUNCTION enrich_event_json(event events) RETURNS jsonb
|
26
|
-
LANGUAGE plpgsql AS $$
|
27
|
-
BEGIN
|
28
|
-
RETURN jsonb_build_object(
|
29
|
-
'aggregate_id', event.aggregate_id,
|
30
|
-
'sequence_number', event.sequence_number,
|
31
|
-
'created_at', event.created_at
|
32
|
-
)
|
33
|
-
|| event.event_json;
|
34
|
-
END
|
35
|
-
$$;
|
36
|
-
|
37
|
-
CREATE OR REPLACE FUNCTION load_event(
|
38
|
-
_aggregate_id uuid,
|
39
|
-
_sequence_number integer
|
40
|
-
) RETURNS SETOF aggregate_event_type
|
41
|
-
LANGUAGE plpgsql AS $$
|
42
|
-
BEGIN
|
43
|
-
RETURN QUERY SELECT aggregate_types.type,
|
44
|
-
a.aggregate_id,
|
45
|
-
a.events_partition_key,
|
46
|
-
event_types.type,
|
47
|
-
enrich_event_json(e)
|
48
|
-
FROM aggregates a
|
49
|
-
INNER JOIN events e ON (a.events_partition_key, a.aggregate_id) = (e.partition_key, e.aggregate_id)
|
50
|
-
INNER JOIN aggregate_types ON a.aggregate_type_id = aggregate_types.id
|
51
|
-
INNER JOIN event_types ON e.event_type_id = event_types.id
|
52
|
-
WHERE a.aggregate_id = _aggregate_id
|
53
|
-
AND e.sequence_number = _sequence_number;
|
54
|
-
END;
|
55
|
-
$$;
|
56
|
-
|
57
|
-
CREATE OR REPLACE FUNCTION load_events(
|
58
|
-
_aggregate_ids jsonb,
|
59
|
-
_use_snapshots boolean DEFAULT TRUE,
|
60
|
-
_until timestamptz DEFAULT NULL
|
61
|
-
) RETURNS SETOF aggregate_event_type
|
62
|
-
LANGUAGE plpgsql AS $$
|
63
|
-
DECLARE
|
64
|
-
_aggregate_id aggregates.aggregate_id%TYPE;
|
65
|
-
BEGIN
|
66
|
-
FOR _aggregate_id IN SELECT * FROM jsonb_array_elements_text(_aggregate_ids) LOOP
|
67
|
-
-- Use a single query to avoid race condition with UPDATEs to the events partition key
|
68
|
-
-- in case transaction isolation level is lower than repeatable read (the default of
|
69
|
-
-- PostgreSQL is read committed).
|
70
|
-
RETURN QUERY WITH
|
71
|
-
aggregate AS (
|
72
|
-
SELECT aggregate_types.type, aggregate_id, events_partition_key
|
73
|
-
FROM aggregates
|
74
|
-
JOIN aggregate_types ON aggregate_type_id = aggregate_types.id
|
75
|
-
WHERE aggregate_id = _aggregate_id
|
76
|
-
),
|
77
|
-
snapshot AS (
|
78
|
-
SELECT *
|
79
|
-
FROM snapshot_records
|
80
|
-
WHERE _use_snapshots
|
81
|
-
AND aggregate_id = _aggregate_id
|
82
|
-
AND (_until IS NULL OR created_at < _until)
|
83
|
-
ORDER BY sequence_number DESC LIMIT 1
|
84
|
-
)
|
85
|
-
(SELECT a.*, s.snapshot_type, s.snapshot_json FROM aggregate a, snapshot s)
|
86
|
-
UNION ALL
|
87
|
-
(SELECT a.*, event_types.type, enrich_event_json(e)
|
88
|
-
FROM aggregate a
|
89
|
-
JOIN events e ON (a.events_partition_key, a.aggregate_id) = (e.partition_key, e.aggregate_id)
|
90
|
-
JOIN event_types ON e.event_type_id = event_types.id
|
91
|
-
WHERE e.sequence_number >= COALESCE((SELECT sequence_number FROM snapshot), 0)
|
92
|
-
AND (_until IS NULL OR e.created_at < _until)
|
93
|
-
ORDER BY e.sequence_number ASC);
|
94
|
-
END LOOP;
|
95
|
-
END;
|
96
|
-
$$;
|
97
|
-
|
98
|
-
CREATE OR REPLACE FUNCTION store_command(_command jsonb) RETURNS bigint
|
99
|
-
LANGUAGE plpgsql AS $$
|
100
|
-
DECLARE
|
101
|
-
_id commands.id%TYPE;
|
102
|
-
_command_json jsonb = _command->'command_json';
|
103
|
-
BEGIN
|
104
|
-
IF NOT EXISTS (SELECT 1 FROM command_types t WHERE t.type = _command->>'command_type') THEN
|
105
|
-
-- Only try inserting if it doesn't exist to avoid exhausting the id sequence
|
106
|
-
INSERT INTO command_types (type)
|
107
|
-
VALUES (_command->>'command_type')
|
108
|
-
ON CONFLICT DO NOTHING;
|
109
|
-
END IF;
|
110
|
-
|
111
|
-
INSERT INTO commands (
|
112
|
-
created_at, user_id, aggregate_id, command_type_id, command_json,
|
113
|
-
event_aggregate_id, event_sequence_number
|
114
|
-
) VALUES (
|
115
|
-
(_command->>'created_at')::timestamptz,
|
116
|
-
(_command_json->>'user_id')::uuid,
|
117
|
-
(_command_json->>'aggregate_id')::uuid,
|
118
|
-
(SELECT id FROM command_types WHERE type = _command->>'command_type'),
|
119
|
-
(_command->'command_json') - '{command_type,created_at,organization_id,user_id,aggregate_id,event_aggregate_id,event_sequence_number}'::text[],
|
120
|
-
(_command_json->>'event_aggregate_id')::uuid,
|
121
|
-
NULLIF(_command_json->'event_sequence_number', 'null'::jsonb)::integer
|
122
|
-
) RETURNING id INTO STRICT _id;
|
123
|
-
RETURN _id;
|
124
|
-
END;
|
125
|
-
$$;
|
126
|
-
|
127
|
-
CREATE OR REPLACE PROCEDURE store_events(_command jsonb, _aggregates_with_events jsonb)
|
128
|
-
LANGUAGE plpgsql AS $$
|
129
|
-
DECLARE
|
130
|
-
_command_id commands.id%TYPE;
|
131
|
-
_aggregate jsonb;
|
132
|
-
_events jsonb;
|
133
|
-
_aggregate_id aggregates.aggregate_id%TYPE;
|
134
|
-
_aggregate_row aggregates%ROWTYPE;
|
135
|
-
_provided_events_partition_key aggregates.events_partition_key%TYPE;
|
136
|
-
_events_partition_key aggregates.events_partition_key%TYPE;
|
137
|
-
_snapshot_outdated_at aggregates_that_need_snapshots.snapshot_outdated_at%TYPE;
|
138
|
-
_unique_keys jsonb;
|
139
|
-
BEGIN
|
140
|
-
_command_id = store_command(_command);
|
141
|
-
|
142
|
-
WITH types AS (
|
143
|
-
SELECT DISTINCT row->0->>'aggregate_type' AS type
|
144
|
-
FROM jsonb_array_elements(_aggregates_with_events) AS row
|
145
|
-
)
|
146
|
-
INSERT INTO aggregate_types (type)
|
147
|
-
SELECT type FROM types
|
148
|
-
WHERE type NOT IN (SELECT type FROM aggregate_types)
|
149
|
-
ORDER BY 1
|
150
|
-
ON CONFLICT DO NOTHING;
|
151
|
-
|
152
|
-
WITH types AS (
|
153
|
-
SELECT DISTINCT events->>'event_type' AS type
|
154
|
-
FROM jsonb_array_elements(_aggregates_with_events) AS row
|
155
|
-
CROSS JOIN LATERAL jsonb_array_elements(row->1) AS events
|
156
|
-
)
|
157
|
-
INSERT INTO event_types (type)
|
158
|
-
SELECT type FROM types
|
159
|
-
WHERE type NOT IN (SELECT type FROM event_types)
|
160
|
-
ORDER BY 1
|
161
|
-
ON CONFLICT DO NOTHING;
|
162
|
-
|
163
|
-
FOR _aggregate IN SELECT row->0 FROM jsonb_array_elements(_aggregates_with_events) AS row LOOP
|
164
|
-
_aggregate_id = _aggregate->>'aggregate_id';
|
165
|
-
_unique_keys = COALESCE(_aggregate->'unique_keys', '{}'::jsonb);
|
166
|
-
|
167
|
-
DELETE FROM aggregate_unique_keys AS target
|
168
|
-
WHERE target.aggregate_id = _aggregate_id
|
169
|
-
AND NOT (_unique_keys ? target.scope);
|
170
|
-
END LOOP;
|
171
|
-
|
172
|
-
FOR _aggregate, _events IN SELECT row->0, row->1 FROM jsonb_array_elements(_aggregates_with_events) AS row
|
173
|
-
ORDER BY row->0->'aggregate_id', row->1->0->'event_json'->'sequence_number'
|
174
|
-
LOOP
|
175
|
-
_aggregate_id = _aggregate->>'aggregate_id';
|
176
|
-
_provided_events_partition_key = _aggregate->>'events_partition_key';
|
177
|
-
_snapshot_outdated_at = _aggregate->>'snapshot_outdated_at';
|
178
|
-
_unique_keys = COALESCE(_aggregate->'unique_keys', '{}'::jsonb);
|
179
|
-
|
180
|
-
SELECT * INTO _aggregate_row FROM aggregates WHERE aggregate_id = _aggregate_id;
|
181
|
-
_events_partition_key = COALESCE(_provided_events_partition_key, _aggregate_row.events_partition_key, '');
|
182
|
-
|
183
|
-
INSERT INTO aggregates (aggregate_id, created_at, aggregate_type_id, events_partition_key)
|
184
|
-
VALUES (
|
185
|
-
_aggregate_id,
|
186
|
-
(_events->0->>'created_at')::timestamptz,
|
187
|
-
(SELECT id FROM aggregate_types WHERE type = _aggregate->>'aggregate_type'),
|
188
|
-
_events_partition_key
|
189
|
-
) ON CONFLICT (aggregate_id)
|
190
|
-
DO UPDATE SET events_partition_key = EXCLUDED.events_partition_key
|
191
|
-
WHERE aggregates.events_partition_key IS DISTINCT FROM EXCLUDED.events_partition_key;
|
192
|
-
|
193
|
-
BEGIN
|
194
|
-
INSERT INTO aggregate_unique_keys AS target (aggregate_id, scope, key)
|
195
|
-
SELECT _aggregate_id, key, value
|
196
|
-
FROM jsonb_each(_unique_keys) AS x
|
197
|
-
ON CONFLICT (aggregate_id, scope) DO UPDATE
|
198
|
-
SET key = EXCLUDED.key
|
199
|
-
WHERE target.key <> EXCLUDED.key;
|
200
|
-
EXCEPTION
|
201
|
-
WHEN unique_violation THEN
|
202
|
-
RAISE unique_violation
|
203
|
-
USING MESSAGE = 'duplicate unique key value for aggregate ' || (_aggregate->>'aggregate_type') || ' ' || _aggregate_id || ' (' || SQLERRM || ')';
|
204
|
-
END;
|
205
|
-
|
206
|
-
INSERT INTO events (partition_key, aggregate_id, sequence_number, created_at, command_id, event_type_id, event_json)
|
207
|
-
SELECT _events_partition_key,
|
208
|
-
_aggregate_id,
|
209
|
-
(event->'event_json'->'sequence_number')::integer,
|
210
|
-
(event->>'created_at')::timestamptz,
|
211
|
-
_command_id,
|
212
|
-
(SELECT id FROM event_types WHERE type = event->>'event_type'),
|
213
|
-
(event->'event_json') - '{aggregate_id,created_at,event_type,sequence_number}'::text[]
|
214
|
-
FROM jsonb_array_elements(_events) AS event;
|
215
|
-
|
216
|
-
IF _snapshot_outdated_at IS NOT NULL THEN
|
217
|
-
INSERT INTO aggregates_that_need_snapshots AS row (aggregate_id, snapshot_outdated_at)
|
218
|
-
VALUES (_aggregate_id, _snapshot_outdated_at)
|
219
|
-
ON CONFLICT (aggregate_id) DO UPDATE
|
220
|
-
SET snapshot_outdated_at = LEAST(row.snapshot_outdated_at, EXCLUDED.snapshot_outdated_at)
|
221
|
-
WHERE row.snapshot_outdated_at IS DISTINCT FROM EXCLUDED.snapshot_outdated_at;
|
222
|
-
END IF;
|
223
|
-
END LOOP;
|
224
|
-
END;
|
225
|
-
$$;
|
226
|
-
|
227
|
-
CREATE OR REPLACE PROCEDURE store_snapshots(_snapshots jsonb)
|
228
|
-
LANGUAGE plpgsql AS $$
|
229
|
-
DECLARE
|
230
|
-
_aggregate_id uuid;
|
231
|
-
_snapshot jsonb;
|
232
|
-
_sequence_number snapshot_records.sequence_number%TYPE;
|
233
|
-
BEGIN
|
234
|
-
FOR _snapshot IN SELECT * FROM jsonb_array_elements(_snapshots) LOOP
|
235
|
-
_aggregate_id = _snapshot->>'aggregate_id';
|
236
|
-
_sequence_number = _snapshot->'sequence_number';
|
237
|
-
|
238
|
-
INSERT INTO aggregates_that_need_snapshots AS row (aggregate_id, snapshot_sequence_number_high_water_mark)
|
239
|
-
VALUES (_aggregate_id, _sequence_number)
|
240
|
-
ON CONFLICT (aggregate_id) DO UPDATE
|
241
|
-
SET snapshot_sequence_number_high_water_mark =
|
242
|
-
GREATEST(row.snapshot_sequence_number_high_water_mark, EXCLUDED.snapshot_sequence_number_high_water_mark),
|
243
|
-
snapshot_outdated_at = NULL,
|
244
|
-
snapshot_scheduled_at = NULL;
|
245
|
-
|
246
|
-
INSERT INTO snapshot_records (aggregate_id, sequence_number, created_at, snapshot_type, snapshot_json)
|
247
|
-
VALUES (
|
248
|
-
_aggregate_id,
|
249
|
-
_sequence_number,
|
250
|
-
(_snapshot->>'created_at')::timestamptz,
|
251
|
-
_snapshot->>'snapshot_type',
|
252
|
-
_snapshot->'snapshot_json'
|
253
|
-
);
|
254
|
-
END LOOP;
|
255
|
-
END;
|
256
|
-
$$;
|
257
|
-
|
258
|
-
CREATE OR REPLACE FUNCTION load_latest_snapshot(_aggregate_id uuid) RETURNS aggregate_event_type
|
259
|
-
LANGUAGE SQL AS $$
|
260
|
-
SELECT (SELECT type FROM aggregate_types WHERE id = a.aggregate_type_id),
|
261
|
-
a.aggregate_id,
|
262
|
-
a.events_partition_key,
|
263
|
-
s.snapshot_type,
|
264
|
-
s.snapshot_json
|
265
|
-
FROM aggregates a JOIN snapshot_records s ON a.aggregate_id = s.aggregate_id
|
266
|
-
WHERE a.aggregate_id = _aggregate_id
|
267
|
-
ORDER BY s.sequence_number DESC
|
268
|
-
LIMIT 1;
|
269
|
-
$$;
|
270
|
-
|
271
|
-
CREATE OR REPLACE PROCEDURE delete_all_snapshots(_now timestamp with time zone DEFAULT NOW())
|
272
|
-
LANGUAGE plpgsql AS $$
|
273
|
-
BEGIN
|
274
|
-
UPDATE aggregates_that_need_snapshots
|
275
|
-
SET snapshot_outdated_at = _now
|
276
|
-
WHERE snapshot_outdated_at IS NULL;
|
277
|
-
DELETE FROM snapshot_records;
|
278
|
-
END;
|
279
|
-
$$;
|
280
|
-
|
281
|
-
CREATE OR REPLACE PROCEDURE delete_snapshots_before(_aggregate_id uuid, _sequence_number integer, _now timestamp with time zone DEFAULT NOW())
|
282
|
-
LANGUAGE plpgsql AS $$
|
283
|
-
BEGIN
|
284
|
-
DELETE FROM snapshot_records
|
285
|
-
WHERE aggregate_id = _aggregate_id
|
286
|
-
AND sequence_number < _sequence_number;
|
287
|
-
|
288
|
-
UPDATE aggregates_that_need_snapshots
|
289
|
-
SET snapshot_outdated_at = _now
|
290
|
-
WHERE aggregate_id = _aggregate_id
|
291
|
-
AND snapshot_outdated_at IS NULL
|
292
|
-
AND NOT EXISTS (SELECT 1 FROM snapshot_records WHERE aggregate_id = _aggregate_id);
|
293
|
-
END;
|
294
|
-
$$;
|
295
|
-
|
296
|
-
CREATE OR REPLACE FUNCTION aggregates_that_need_snapshots(_last_aggregate_id uuid, _limit integer)
|
297
|
-
RETURNS TABLE (aggregate_id uuid)
|
298
|
-
LANGUAGE plpgsql AS $$
|
299
|
-
BEGIN
|
300
|
-
RETURN QUERY SELECT a.aggregate_id
|
301
|
-
FROM aggregates_that_need_snapshots a
|
302
|
-
WHERE a.snapshot_outdated_at IS NOT NULL
|
303
|
-
AND (_last_aggregate_id IS NULL OR a.aggregate_id > _last_aggregate_id)
|
304
|
-
ORDER BY 1
|
305
|
-
LIMIT _limit;
|
306
|
-
END;
|
307
|
-
$$;
|
308
|
-
|
309
|
-
CREATE OR REPLACE FUNCTION select_aggregates_for_snapshotting(_limit integer, _reschedule_snapshot_scheduled_before timestamp with time zone, _now timestamp with time zone DEFAULT NOW())
|
310
|
-
RETURNS TABLE (aggregate_id uuid)
|
311
|
-
LANGUAGE plpgsql AS $$
|
312
|
-
BEGIN
|
313
|
-
RETURN QUERY WITH scheduled AS MATERIALIZED (
|
314
|
-
SELECT a.aggregate_id
|
315
|
-
FROM aggregates_that_need_snapshots AS a
|
316
|
-
WHERE snapshot_outdated_at IS NOT NULL
|
317
|
-
ORDER BY snapshot_outdated_at ASC, snapshot_sequence_number_high_water_mark DESC, aggregate_id ASC
|
318
|
-
LIMIT _limit
|
319
|
-
FOR UPDATE
|
320
|
-
) UPDATE aggregates_that_need_snapshots AS row
|
321
|
-
SET snapshot_scheduled_at = _now
|
322
|
-
FROM scheduled
|
323
|
-
WHERE row.aggregate_id = scheduled.aggregate_id
|
324
|
-
AND (row.snapshot_scheduled_at IS NULL OR row.snapshot_scheduled_at < _reschedule_snapshot_scheduled_before)
|
325
|
-
RETURNING row.aggregate_id;
|
326
|
-
END;
|
327
|
-
$$;
|
328
|
-
|
329
|
-
CREATE OR REPLACE PROCEDURE permanently_delete_commands_without_events(_aggregate_id uuid, _organization_id uuid)
|
330
|
-
LANGUAGE plpgsql AS $$
|
331
|
-
BEGIN
|
332
|
-
IF _aggregate_id IS NULL AND _organization_id IS NULL THEN
|
333
|
-
RAISE EXCEPTION 'aggregate_id or organization_id must be specified to delete commands';
|
334
|
-
END IF;
|
335
|
-
|
336
|
-
DELETE FROM commands
|
337
|
-
WHERE (_aggregate_id IS NULL OR aggregate_id = _aggregate_id)
|
338
|
-
AND NOT EXISTS (SELECT 1 FROM events WHERE command_id = commands.id);
|
339
|
-
END;
|
340
|
-
$$;
|
341
|
-
|
342
|
-
CREATE OR REPLACE PROCEDURE permanently_delete_event_streams(_aggregate_ids jsonb)
|
343
|
-
LANGUAGE plpgsql AS $$
|
344
|
-
BEGIN
|
345
|
-
DELETE FROM events
|
346
|
-
USING jsonb_array_elements_text(_aggregate_ids) AS ids (id)
|
347
|
-
JOIN aggregates ON ids.id::uuid = aggregates.aggregate_id
|
348
|
-
WHERE events.partition_key = aggregates.events_partition_key
|
349
|
-
AND events.aggregate_id = aggregates.aggregate_id;
|
350
|
-
DELETE FROM aggregates
|
351
|
-
USING jsonb_array_elements_text(_aggregate_ids) AS ids (id)
|
352
|
-
WHERE aggregates.aggregate_id = ids.id::uuid;
|
353
|
-
END;
|
354
|
-
$$;
|
355
|
-
|
356
|
-
DROP VIEW IF EXISTS command_records;
|
357
|
-
CREATE VIEW command_records (id, user_id, aggregate_id, command_type, command_json, created_at, event_aggregate_id, event_sequence_number) AS
|
358
|
-
SELECT id,
|
359
|
-
user_id,
|
360
|
-
aggregate_id,
|
361
|
-
(SELECT type FROM command_types WHERE command_types.id = command.command_type_id),
|
362
|
-
enrich_command_json(command),
|
363
|
-
created_at,
|
364
|
-
event_aggregate_id,
|
365
|
-
event_sequence_number
|
366
|
-
FROM commands command;
|
367
|
-
|
368
|
-
DROP VIEW IF EXISTS event_records;
|
369
|
-
CREATE VIEW event_records (aggregate_id, partition_key, sequence_number, created_at, event_type, event_json, command_record_id, xact_id) AS
|
370
|
-
SELECT aggregate.aggregate_id,
|
371
|
-
event.partition_key,
|
372
|
-
event.sequence_number,
|
373
|
-
event.created_at,
|
374
|
-
type.type,
|
375
|
-
enrich_event_json(event) AS event_json,
|
376
|
-
command_id,
|
377
|
-
event.xact_id
|
378
|
-
FROM events event
|
379
|
-
JOIN aggregates aggregate ON aggregate.aggregate_id = event.aggregate_id AND aggregate.events_partition_key = event.partition_key
|
380
|
-
JOIN event_types type ON event.event_type_id = type.id;
|
381
|
-
|
382
|
-
DROP VIEW IF EXISTS stream_records;
|
383
|
-
CREATE VIEW stream_records (aggregate_id, events_partition_key, aggregate_type, created_at) AS
|
384
|
-
SELECT aggregates.aggregate_id,
|
385
|
-
aggregates.events_partition_key,
|
386
|
-
aggregate_types.type,
|
387
|
-
aggregates.created_at
|
388
|
-
FROM aggregates JOIN aggregate_types ON aggregates.aggregate_type_id = aggregate_types.id;
|
389
|
-
|
390
|
-
CREATE OR REPLACE FUNCTION save_events_on_delete_trigger() RETURNS TRIGGER AS $$
|
391
|
-
BEGIN
|
392
|
-
INSERT INTO saved_event_records (operation, timestamp, "user", aggregate_id, partition_key, sequence_number, created_at, event_type, event_json, command_id, xact_id)
|
393
|
-
SELECT 'D',
|
394
|
-
statement_timestamp(),
|
395
|
-
user,
|
396
|
-
o.aggregate_id,
|
397
|
-
o.partition_key,
|
398
|
-
o.sequence_number,
|
399
|
-
o.created_at,
|
400
|
-
(SELECT type FROM event_types WHERE event_types.id = o.event_type_id),
|
401
|
-
o.event_json,
|
402
|
-
o.command_id,
|
403
|
-
o.xact_id
|
404
|
-
FROM old_table o;
|
405
|
-
RETURN NULL;
|
406
|
-
END;
|
407
|
-
$$ LANGUAGE plpgsql;
|
408
|
-
|
409
|
-
CREATE OR REPLACE FUNCTION save_events_on_update_trigger() RETURNS TRIGGER AS $$
|
410
|
-
BEGIN
|
411
|
-
INSERT INTO saved_event_records (operation, timestamp, "user", aggregate_id, partition_key, sequence_number, created_at, event_type, event_json, command_id, xact_id)
|
412
|
-
SELECT 'U',
|
413
|
-
statement_timestamp(),
|
414
|
-
user,
|
415
|
-
o.aggregate_id,
|
416
|
-
o.partition_key,
|
417
|
-
o.sequence_number,
|
418
|
-
o.created_at,
|
419
|
-
(SELECT type FROM event_types WHERE event_types.id = o.event_type_id),
|
420
|
-
o.event_json,
|
421
|
-
o.command_id,
|
422
|
-
o.xact_id
|
423
|
-
FROM old_table o LEFT JOIN new_table n ON o.aggregate_id = n.aggregate_id AND o.sequence_number = n.sequence_number
|
424
|
-
WHERE n IS NULL
|
425
|
-
-- Only save when event related information changes
|
426
|
-
OR o.created_at <> n.created_at
|
427
|
-
OR o.event_type_id <> n.event_type_id
|
428
|
-
OR o.event_json <> n.event_json;
|
429
|
-
RETURN NULL;
|
430
|
-
END;
|
431
|
-
$$ LANGUAGE plpgsql;
|
432
|
-
|
433
|
-
CREATE OR REPLACE TRIGGER save_events_on_delete_trigger
|
434
|
-
AFTER DELETE ON events
|
435
|
-
REFERENCING OLD TABLE AS old_table
|
436
|
-
FOR EACH STATEMENT EXECUTE FUNCTION save_events_on_delete_trigger();
|
437
|
-
CREATE OR REPLACE TRIGGER save_events_on_update_trigger
|
438
|
-
AFTER UPDATE ON events
|
439
|
-
REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table
|
440
|
-
FOR EACH STATEMENT EXECUTE FUNCTION save_events_on_update_trigger();
|
1
|
+
\ir migrate/sequent/aggregate_event_type_v01.sql
|
2
|
+
\ir migrate/sequent/enrich_command_json_v01.sql
|
3
|
+
\ir migrate/sequent/aggregates_that_need_snapshots_v01.sql
|
4
|
+
\ir migrate/sequent/command_records_v01.sql
|
5
|
+
\ir migrate/sequent/delete_all_snapshots_v01.sql
|
6
|
+
\ir migrate/sequent/delete_snapshots_before_v01.sql
|
7
|
+
\ir migrate/sequent/enrich_event_json_v01.sql
|
8
|
+
\ir migrate/sequent/event_records_v01.sql
|
9
|
+
\ir migrate/sequent/load_event_v01.sql
|
10
|
+
\ir migrate/sequent/load_events_v01.sql
|
11
|
+
\ir migrate/sequent/load_latest_snapshots_v01.sql
|
12
|
+
\ir migrate/sequent/permanently_delete_commands_without_events_v01.sql
|
13
|
+
\ir migrate/sequent/permanently_delete_event_streams_v01.sql
|
14
|
+
\ir migrate/sequent/save_events_trigger_v01.sql
|
15
|
+
\ir migrate/sequent/select_aggregates_for_snapshotting_v01.sql
|
16
|
+
\ir migrate/sequent/store_aggregates_v01.sql
|
17
|
+
\ir migrate/sequent/store_command_v01.sql
|
18
|
+
\ir migrate/sequent/store_events_v01.sql
|
19
|
+
\ir migrate/sequent/store_snapshots_v01.sql
|
20
|
+
\ir migrate/sequent/stream_records_v01.sql
|
21
|
+
\ir migrate/sequent/update_types_v01.sql
|
22
|
+
\ir migrate/sequent/update_unique_keys_v01.sql
|