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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/db/migrate/20250101000000_sequent_initial_schema.rb +166 -0
  3. data/db/migrate/20250101000001_sequent_stored_procedures.rb +48 -0
  4. data/db/migrate/20250312105100_sequent_store_events_v02.rb +29 -0
  5. data/db/migrate/sequent/aggregate_event_type_v01.sql +7 -0
  6. data/db/migrate/sequent/aggregates_that_need_snapshots_v01.sql +12 -0
  7. data/db/migrate/sequent/command_records_v01.sql +11 -0
  8. data/db/migrate/sequent/delete_all_snapshots_v01.sql +9 -0
  9. data/db/migrate/sequent/delete_snapshots_before_v01.sql +14 -0
  10. data/db/migrate/sequent/enrich_command_json_v01.sql +14 -0
  11. data/db/migrate/sequent/enrich_event_json_v01.sql +11 -0
  12. data/db/migrate/sequent/event_records_v01.sql +13 -0
  13. data/db/migrate/sequent/load_event_v01.sql +19 -0
  14. data/db/migrate/sequent/load_events_v01.sql +40 -0
  15. data/db/migrate/sequent/load_latest_snapshots_v01.sql +12 -0
  16. data/db/migrate/sequent/permanently_delete_commands_without_events_v01.sql +13 -0
  17. data/db/migrate/sequent/permanently_delete_event_streams_v01.sql +13 -0
  18. data/db/migrate/sequent/save_events_trigger_v01.sql +53 -0
  19. data/db/migrate/sequent/select_aggregates_for_snapshotting_v01.sql +19 -0
  20. data/db/migrate/sequent/store_aggregates_v01.sql +39 -0
  21. data/db/migrate/sequent/store_command_v01.sql +21 -0
  22. data/db/migrate/sequent/store_events_v01.sql +37 -0
  23. data/db/migrate/sequent/store_events_v02.sql +37 -0
  24. data/db/migrate/sequent/store_snapshots_v01.sql +30 -0
  25. data/db/migrate/sequent/stream_records_v01.sql +7 -0
  26. data/db/migrate/sequent/update_types_v01.sql +32 -0
  27. data/db/migrate/sequent/update_unique_keys_v01.sql +33 -0
  28. data/db/sequent_pgsql.sql +22 -440
  29. data/db/structure.sql +1358 -0
  30. data/lib/sequent/core/command_record.rb +0 -1
  31. data/lib/sequent/core/event_record.rb +0 -1
  32. data/lib/sequent/core/event_store.rb +64 -6
  33. data/lib/sequent/core/helpers/message_router.rb +9 -6
  34. data/lib/sequent/core/persistors/active_record_persistor.rb +1 -22
  35. data/lib/sequent/generator/template_project/db/database.yml +1 -3
  36. data/lib/sequent/generator/template_project/spec/spec_helper.rb +0 -1
  37. data/lib/sequent/migrations/sequent_schema.rb +2 -11
  38. data/lib/sequent/migrations/view_schema.rb +1 -1
  39. data/lib/sequent/rake/migration_files.rb +69 -0
  40. data/lib/sequent/rake/migration_tasks.rb +71 -9
  41. data/lib/sequent/support/database.rb +12 -31
  42. data/lib/sequent/test/command_handler_helpers.rb +14 -1
  43. data/lib/sequent/util/dry_run.rb +8 -0
  44. data/lib/version.rb +1 -1
  45. metadata +29 -2
  46. 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
- DROP TYPE IF EXISTS aggregate_event_type CASCADE;
2
- CREATE TYPE aggregate_event_type AS (
3
- aggregate_type text,
4
- aggregate_id uuid,
5
- events_partition_key text,
6
- event_type text,
7
- event_json jsonb
8
- );
9
-
10
- CREATE OR REPLACE FUNCTION enrich_command_json(command commands) RETURNS jsonb
11
- LANGUAGE plpgsql AS $$
12
- BEGIN
13
- RETURN jsonb_build_object(
14
- 'command_type', (SELECT type FROM command_types WHERE command_types.id = command.command_type_id),
15
- 'created_at', command.created_at,
16
- 'user_id', command.user_id,
17
- 'aggregate_id', command.aggregate_id,
18
- 'event_aggregate_id', command.event_aggregate_id,
19
- 'event_sequence_number', command.event_sequence_number
20
- )
21
- || command.command_json;
22
- END
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