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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eac2900ab837f6b6f12aa09f4cc2bf0f40bfc4ed06292ff501141cba82739e14
|
4
|
+
data.tar.gz: 719335f0d2640357255509a690de4bfe14f423d538aa4c402e0513f14305dfcf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 66929197b669aa7aa466eea3cacc6ee3eb0514a6cb2bb80f4b240e68d7877cded40e5e4d2b47ed3f92a83c14426d989a4b74290e154bbdcc621ade9c787192fc
|
7
|
+
data.tar.gz: d0b950e021daded940fd6c519a4824d656787ad32eede7f09cb900fbd4d18c1bf31aaef6bd63df7d581a51bdf093e13b942f33e2c99714167f74e4bafc136c7a
|
@@ -0,0 +1,166 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class SequentInitialSchema < ActiveRecord::Migration[7.2]
|
4
|
+
def up
|
5
|
+
create_schema Sequent.configuration.event_store_schema_name, if_not_exists: true
|
6
|
+
create_schema Sequent.configuration.view_schema_name, if_not_exists: true
|
7
|
+
|
8
|
+
Sequent::Support::Database.with_search_path(Sequent.configuration.event_store_schema_name) do
|
9
|
+
say 'Creating Sequent tables', true
|
10
|
+
suppress_messages do
|
11
|
+
execute <<~SQL
|
12
|
+
CREATE TABLE command_types (id SMALLINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, type text UNIQUE NOT NULL);
|
13
|
+
CREATE TABLE aggregate_types (id SMALLINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, type text UNIQUE NOT NULL);
|
14
|
+
CREATE TABLE event_types (id SMALLINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, type text UNIQUE NOT NULL);
|
15
|
+
|
16
|
+
CREATE TABLE commands (
|
17
|
+
id bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
|
18
|
+
created_at timestamp with time zone NOT NULL,
|
19
|
+
user_id uuid,
|
20
|
+
aggregate_id uuid,
|
21
|
+
command_type_id SMALLINT NOT NULL,
|
22
|
+
command_json jsonb NOT NULL,
|
23
|
+
event_aggregate_id uuid,
|
24
|
+
event_sequence_number integer,
|
25
|
+
FOREIGN KEY (command_type_id) REFERENCES command_types (id) ON UPDATE CASCADE
|
26
|
+
) PARTITION BY RANGE (id);
|
27
|
+
|
28
|
+
CREATE TABLE aggregates (
|
29
|
+
aggregate_id uuid NOT NULL PRIMARY KEY,
|
30
|
+
events_partition_key text NOT NULL DEFAULT '',
|
31
|
+
aggregate_type_id SMALLINT NOT NULL,
|
32
|
+
created_at timestamp with time zone NOT NULL DEFAULT NOW(),
|
33
|
+
UNIQUE (events_partition_key, aggregate_id),
|
34
|
+
FOREIGN KEY (aggregate_type_id) REFERENCES aggregate_types (id) ON UPDATE CASCADE
|
35
|
+
) PARTITION BY RANGE (aggregate_id);
|
36
|
+
|
37
|
+
CREATE TABLE aggregate_unique_keys (
|
38
|
+
aggregate_id uuid NOT NULL,
|
39
|
+
scope text NOT NULL,
|
40
|
+
key jsonb NOT NULL,
|
41
|
+
PRIMARY KEY (aggregate_id, scope),
|
42
|
+
UNIQUE (scope, key),
|
43
|
+
FOREIGN KEY (aggregate_id) REFERENCES aggregates (aggregate_id) ON UPDATE CASCADE ON DELETE CASCADE
|
44
|
+
);
|
45
|
+
|
46
|
+
CREATE TABLE events (
|
47
|
+
aggregate_id uuid NOT NULL,
|
48
|
+
partition_key text NOT NULL DEFAULT '',
|
49
|
+
sequence_number integer NOT NULL,
|
50
|
+
created_at timestamp with time zone NOT NULL,
|
51
|
+
command_id bigint NOT NULL,
|
52
|
+
event_type_id SMALLINT NOT NULL,
|
53
|
+
event_json jsonb NOT NULL,
|
54
|
+
xact_id bigint DEFAULT pg_current_xact_id()::text::bigint,
|
55
|
+
PRIMARY KEY (partition_key, aggregate_id, sequence_number),
|
56
|
+
FOREIGN KEY (partition_key, aggregate_id) REFERENCES aggregates (events_partition_key, aggregate_id)
|
57
|
+
ON UPDATE CASCADE ON DELETE RESTRICT,
|
58
|
+
FOREIGN KEY (command_id) REFERENCES commands (id) ON UPDATE RESTRICT ON DELETE RESTRICT,
|
59
|
+
FOREIGN KEY (event_type_id) REFERENCES event_types (id) ON UPDATE CASCADE
|
60
|
+
) PARTITION BY RANGE (partition_key);
|
61
|
+
|
62
|
+
CREATE TABLE aggregates_that_need_snapshots (
|
63
|
+
aggregate_id uuid NOT NULL PRIMARY KEY,
|
64
|
+
snapshot_sequence_number_high_water_mark integer,
|
65
|
+
snapshot_outdated_at timestamp with time zone,
|
66
|
+
snapshot_scheduled_at timestamp with time zone,
|
67
|
+
FOREIGN KEY (aggregate_id) REFERENCES aggregates (aggregate_id) ON UPDATE CASCADE ON DELETE CASCADE
|
68
|
+
);
|
69
|
+
|
70
|
+
COMMENT ON TABLE aggregates_that_need_snapshots IS 'Contains a row for every aggregate with more events than its snapshot threshold.';
|
71
|
+
COMMENT ON COLUMN aggregates_that_need_snapshots.snapshot_sequence_number_high_water_mark
|
72
|
+
IS 'The highest sequence number of the stored snapshot. Kept when snapshot are deleted to more easily query aggregates that need snapshotting the most';
|
73
|
+
COMMENT ON COLUMN aggregates_that_need_snapshots.snapshot_outdated_at IS 'Not NULL indicates a snapshot is needed since the stored timestamp';
|
74
|
+
COMMENT ON COLUMN aggregates_that_need_snapshots.snapshot_scheduled_at IS 'Not NULL indicates a snapshot is in the process of being taken';
|
75
|
+
|
76
|
+
CREATE TABLE snapshot_records (
|
77
|
+
aggregate_id uuid NOT NULL,
|
78
|
+
sequence_number integer NOT NULL,
|
79
|
+
created_at timestamptz NOT NULL,
|
80
|
+
snapshot_type text NOT NULL,
|
81
|
+
snapshot_json jsonb NOT NULL,
|
82
|
+
PRIMARY KEY (aggregate_id, sequence_number),
|
83
|
+
FOREIGN KEY (aggregate_id) REFERENCES aggregates_that_need_snapshots (aggregate_id)
|
84
|
+
ON UPDATE CASCADE ON DELETE CASCADE
|
85
|
+
);
|
86
|
+
|
87
|
+
CREATE TABLE saved_event_records (
|
88
|
+
operation varchar(1) NOT NULL CHECK (operation IN ('U', 'D')),
|
89
|
+
timestamp timestamptz NOT NULL,
|
90
|
+
"user" text NOT NULL,
|
91
|
+
aggregate_id uuid NOT NULL,
|
92
|
+
partition_key text DEFAULT '',
|
93
|
+
sequence_number integer NOT NULL,
|
94
|
+
created_at timestamp with time zone NOT NULL,
|
95
|
+
command_id bigint NOT NULL,
|
96
|
+
event_type text NOT NULL,
|
97
|
+
event_json jsonb NOT NULL,
|
98
|
+
xact_id bigint,
|
99
|
+
PRIMARY KEY (aggregate_id, sequence_number, timestamp)
|
100
|
+
);
|
101
|
+
SQL
|
102
|
+
end
|
103
|
+
|
104
|
+
say 'Creating table partitions', true
|
105
|
+
suppress_messages do
|
106
|
+
execute <<~SQL
|
107
|
+
-- ### Configure partitions as needed
|
108
|
+
CREATE TABLE commands_default PARTITION OF commands DEFAULT;
|
109
|
+
-- CREATE TABLE commands_0 PARTITION OF commands FOR VALUES FROM (1) TO (100e6);
|
110
|
+
-- CREATE TABLE commands_1 PARTITION OF commands FOR VALUES FROM (100e6) TO (200e6);
|
111
|
+
-- CREATE TABLE commands_2 PARTITION OF commands FOR VALUES FROM (200e6) TO (300e6);
|
112
|
+
-- CREATE TABLE commands_3 PARTITION OF commands FOR VALUES FROM (300e6) TO (400e6);
|
113
|
+
|
114
|
+
-- ### Configure partitions as needed
|
115
|
+
CREATE TABLE aggregates_default PARTITION OF aggregates DEFAULT;
|
116
|
+
-- CREATE TABLE aggregates_0 PARTITION OF aggregates FOR VALUES FROM (MINVALUE) TO ('10000000-0000-0000-0000-000000000000');
|
117
|
+
-- CREATE TABLE aggregates_1 PARTITION OF aggregates FOR VALUES FROM ('10000000-0000-0000-0000-000000000000') TO ('20000000-0000-0000-0000-000000000000');
|
118
|
+
-- CREATE TABLE aggregates_2 PARTITION OF aggregates FOR VALUES FROM ('20000000-0000-0000-0000-000000000000') TO ('30000000-0000-0000-0000-000000000000');
|
119
|
+
-- CREATE TABLE aggregates_3 PARTITION OF aggregates FOR VALUES FROM ('30000000-0000-0000-0000-000000000000') TO ('40000000-0000-0000-0000-000000000000');
|
120
|
+
-- CREATE TABLE aggregates_4 PARTITION OF aggregates FOR VALUES FROM ('40000000-0000-0000-0000-000000000000') TO ('50000000-0000-0000-0000-000000000000');
|
121
|
+
-- CREATE TABLE aggregates_5 PARTITION OF aggregates FOR VALUES FROM ('50000000-0000-0000-0000-000000000000') TO ('60000000-0000-0000-0000-000000000000');
|
122
|
+
-- CREATE TABLE aggregates_6 PARTITION OF aggregates FOR VALUES FROM ('60000000-0000-0000-0000-000000000000') TO ('70000000-0000-0000-0000-000000000000');
|
123
|
+
-- CREATE TABLE aggregates_7 PARTITION OF aggregates FOR VALUES FROM ('70000000-0000-0000-0000-000000000000') TO ('80000000-0000-0000-0000-000000000000');
|
124
|
+
-- CREATE TABLE aggregates_8 PARTITION OF aggregates FOR VALUES FROM ('80000000-0000-0000-0000-000000000000') TO ('90000000-0000-0000-0000-000000000000');
|
125
|
+
-- CREATE TABLE aggregates_9 PARTITION OF aggregates FOR VALUES FROM ('90000000-0000-0000-0000-000000000000') TO ('a0000000-0000-0000-0000-000000000000');
|
126
|
+
-- CREATE TABLE aggregates_a PARTITION OF aggregates FOR VALUES FROM ('a0000000-0000-0000-0000-000000000000') TO ('b0000000-0000-0000-0000-000000000000');
|
127
|
+
-- CREATE TABLE aggregates_b PARTITION OF aggregates FOR VALUES FROM ('b0000000-0000-0000-0000-000000000000') TO ('c0000000-0000-0000-0000-000000000000');
|
128
|
+
-- CREATE TABLE aggregates_c PARTITION OF aggregates FOR VALUES FROM ('c0000000-0000-0000-0000-000000000000') TO ('d0000000-0000-0000-0000-000000000000');
|
129
|
+
-- CREATE TABLE aggregates_d PARTITION OF aggregates FOR VALUES FROM ('d0000000-0000-0000-0000-000000000000') TO ('e0000000-0000-0000-0000-000000000000');
|
130
|
+
-- CREATE TABLE aggregates_e PARTITION OF aggregates FOR VALUES FROM ('e0000000-0000-0000-0000-000000000000') TO ('f0000000-0000-0000-0000-000000000000');
|
131
|
+
-- CREATE TABLE aggregates_f PARTITION OF aggregates FOR VALUES FROM ('f0000000-0000-0000-0000-000000000000') TO (MAXVALUE);
|
132
|
+
|
133
|
+
-- ### Configure partitions as needed
|
134
|
+
CREATE TABLE events_default PARTITION OF events DEFAULT;
|
135
|
+
-- CREATE TABLE events_2023_and_earlier PARTITION OF events FOR VALUES FROM ('Y00') TO ('Y24');
|
136
|
+
-- CREATE TABLE events_2024 PARTITION OF events FOR VALUES FROM ('Y24') TO ('Y25');
|
137
|
+
-- CREATE TABLE events_2025 PARTITION OF events FOR VALUES FROM ('Y25') TO ('Y26');
|
138
|
+
-- CREATE TABLE events_2026 PARTITION OF events FOR VALUES FROM ('Y26') TO ('Y27');
|
139
|
+
-- CREATE TABLE events_2027_and_later PARTITION OF events FOR VALUES FROM ('Y27') TO ('Y99');
|
140
|
+
-- CREATE TABLE events_aggregate PARTITION OF events FOR VALUES FROM ('A') TO ('Ag');
|
141
|
+
SQL
|
142
|
+
end
|
143
|
+
|
144
|
+
say 'Creating indexes', true
|
145
|
+
suppress_messages do
|
146
|
+
execute <<~SQL
|
147
|
+
CREATE INDEX aggregates_aggregate_type_id_idx ON aggregates (aggregate_type_id);
|
148
|
+
CREATE INDEX commands_command_type_id_idx ON commands (command_type_id);
|
149
|
+
CREATE INDEX commands_aggregate_id_idx ON commands (aggregate_id);
|
150
|
+
CREATE INDEX commands_event_idx ON commands (event_aggregate_id, event_sequence_number);
|
151
|
+
CREATE INDEX events_command_id_idx ON events (command_id);
|
152
|
+
CREATE INDEX events_event_type_id_idx ON events (event_type_id);
|
153
|
+
CREATE INDEX aggregates_that_need_snapshots_outdated_idx
|
154
|
+
ON aggregates_that_need_snapshots (snapshot_outdated_at ASC, snapshot_sequence_number_high_water_mark DESC, aggregate_id ASC)
|
155
|
+
WHERE snapshot_outdated_at IS NOT NULL;
|
156
|
+
|
157
|
+
SQL
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def down
|
163
|
+
execute "DROP SCHEMA #{Sequent.configuration.view_schema_name} CASCADE"
|
164
|
+
execute "DROP SCHEMA #{Sequent.configuration.event_store_schema_name} CASCADE"
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class SequentStoredProcedures < ActiveRecord::Migration[7.2]
|
4
|
+
def up
|
5
|
+
Sequent::Support::Database.with_search_path(Sequent.configuration.event_store_schema_name) do
|
6
|
+
execute_sql_file 'aggregate_event_type', version: 1
|
7
|
+
execute_sql_file 'enrich_command_json', version: 1
|
8
|
+
execute_sql_file 'aggregates_that_need_snapshots', version: 1
|
9
|
+
execute_sql_file 'command_records', version: 1
|
10
|
+
execute_sql_file 'delete_all_snapshots', version: 1
|
11
|
+
execute_sql_file 'delete_snapshots_before', version: 1
|
12
|
+
execute_sql_file 'enrich_event_json', version: 1
|
13
|
+
execute_sql_file 'event_records', version: 1
|
14
|
+
execute_sql_file 'load_event', version: 1
|
15
|
+
execute_sql_file 'load_events', version: 1
|
16
|
+
execute_sql_file 'load_latest_snapshots', version: 1
|
17
|
+
execute_sql_file 'permanently_delete_commands_without_events', version: 1
|
18
|
+
execute_sql_file 'permanently_delete_event_streams', version: 1
|
19
|
+
execute_sql_file 'save_events_trigger', version: 1
|
20
|
+
execute_sql_file 'select_aggregates_for_snapshotting', version: 1
|
21
|
+
execute_sql_file 'store_aggregates', version: 1
|
22
|
+
execute_sql_file 'store_command', version: 1
|
23
|
+
execute_sql_file 'store_events', version: 1
|
24
|
+
execute_sql_file 'store_snapshots', version: 1
|
25
|
+
execute_sql_file 'stream_records', version: 1
|
26
|
+
execute_sql_file 'update_types', version: 1
|
27
|
+
execute_sql_file 'update_unique_keys', version: 1
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def down
|
32
|
+
fail ActiveRecord::IrreversibleMigration
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def execute_sql_file(filename, version:)
|
38
|
+
say "Applying '#{filename}' version #{version}", true
|
39
|
+
suppress_messages do
|
40
|
+
execute File.read(
|
41
|
+
File.join(
|
42
|
+
File.dirname(__FILE__),
|
43
|
+
format('sequent/%s_v%02d.sql', filename, version),
|
44
|
+
),
|
45
|
+
)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class SequentStoreEventsV02 < ActiveRecord::Migration[7.2]
|
4
|
+
def up
|
5
|
+
Sequent::Support::Database.with_search_path(Sequent.configuration.event_store_schema_name) do
|
6
|
+
execute_sql_file 'store_events', version: 2
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def down
|
11
|
+
Sequent::Support::Database.with_search_path(Sequent.configuration.event_store_schema_name) do
|
12
|
+
execute_sql_file 'store_events', version: 1
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def execute_sql_file(filename, version:)
|
19
|
+
say "Applying '#{filename}' version #{version}", true
|
20
|
+
suppress_messages do
|
21
|
+
execute File.read(
|
22
|
+
File.join(
|
23
|
+
File.dirname(__FILE__),
|
24
|
+
format('sequent/%s_v%02d.sql', filename, version),
|
25
|
+
),
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
CREATE OR REPLACE FUNCTION aggregates_that_need_snapshots(_last_aggregate_id uuid, _limit integer)
|
2
|
+
RETURNS TABLE (aggregate_id uuid)
|
3
|
+
LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
|
4
|
+
BEGIN
|
5
|
+
RETURN QUERY SELECT a.aggregate_id
|
6
|
+
FROM aggregates_that_need_snapshots a
|
7
|
+
WHERE a.snapshot_outdated_at IS NOT NULL
|
8
|
+
AND (_last_aggregate_id IS NULL OR a.aggregate_id > _last_aggregate_id)
|
9
|
+
ORDER BY 1
|
10
|
+
LIMIT _limit;
|
11
|
+
END;
|
12
|
+
$$;
|
@@ -0,0 +1,11 @@
|
|
1
|
+
DROP VIEW IF EXISTS command_records;
|
2
|
+
CREATE VIEW command_records (id, user_id, aggregate_id, command_type, command_json, created_at, event_aggregate_id, event_sequence_number) AS
|
3
|
+
SELECT id,
|
4
|
+
user_id,
|
5
|
+
aggregate_id,
|
6
|
+
(SELECT type FROM command_types WHERE command_types.id = command.command_type_id),
|
7
|
+
enrich_command_json(command),
|
8
|
+
created_at,
|
9
|
+
event_aggregate_id,
|
10
|
+
event_sequence_number
|
11
|
+
FROM commands command;
|
@@ -0,0 +1,9 @@
|
|
1
|
+
CREATE OR REPLACE PROCEDURE delete_all_snapshots(_now timestamp with time zone DEFAULT NOW())
|
2
|
+
LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
|
3
|
+
BEGIN
|
4
|
+
UPDATE aggregates_that_need_snapshots
|
5
|
+
SET snapshot_outdated_at = _now
|
6
|
+
WHERE snapshot_outdated_at IS NULL;
|
7
|
+
DELETE FROM snapshot_records;
|
8
|
+
END;
|
9
|
+
$$;
|
@@ -0,0 +1,14 @@
|
|
1
|
+
CREATE OR REPLACE PROCEDURE delete_snapshots_before(_aggregate_id uuid, _sequence_number integer, _now timestamp with time zone DEFAULT NOW())
|
2
|
+
LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
|
3
|
+
BEGIN
|
4
|
+
DELETE FROM snapshot_records
|
5
|
+
WHERE aggregate_id = _aggregate_id
|
6
|
+
AND sequence_number < _sequence_number;
|
7
|
+
|
8
|
+
UPDATE aggregates_that_need_snapshots
|
9
|
+
SET snapshot_outdated_at = _now
|
10
|
+
WHERE aggregate_id = _aggregate_id
|
11
|
+
AND snapshot_outdated_at IS NULL
|
12
|
+
AND NOT EXISTS (SELECT 1 FROM snapshot_records WHERE aggregate_id = _aggregate_id);
|
13
|
+
END;
|
14
|
+
$$;
|
@@ -0,0 +1,14 @@
|
|
1
|
+
CREATE OR REPLACE FUNCTION enrich_command_json(command commands) RETURNS jsonb RETURNS NULL ON NULL INPUT
|
2
|
+
LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
|
3
|
+
BEGIN
|
4
|
+
RETURN jsonb_build_object(
|
5
|
+
'command_type', (SELECT type FROM command_types WHERE command_types.id = command.command_type_id),
|
6
|
+
'created_at', command.created_at,
|
7
|
+
'user_id', command.user_id,
|
8
|
+
'aggregate_id', command.aggregate_id,
|
9
|
+
'event_aggregate_id', command.event_aggregate_id,
|
10
|
+
'event_sequence_number', command.event_sequence_number
|
11
|
+
)
|
12
|
+
|| command.command_json;
|
13
|
+
END
|
14
|
+
$$;
|
@@ -0,0 +1,11 @@
|
|
1
|
+
CREATE OR REPLACE FUNCTION enrich_event_json(event events) RETURNS jsonb RETURNS NULL ON NULL INPUT
|
2
|
+
LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
|
3
|
+
BEGIN
|
4
|
+
RETURN jsonb_build_object(
|
5
|
+
'aggregate_id', event.aggregate_id,
|
6
|
+
'sequence_number', event.sequence_number,
|
7
|
+
'created_at', event.created_at
|
8
|
+
)
|
9
|
+
|| event.event_json;
|
10
|
+
END
|
11
|
+
$$;
|
@@ -0,0 +1,13 @@
|
|
1
|
+
DROP VIEW IF EXISTS event_records;
|
2
|
+
CREATE VIEW event_records (aggregate_id, partition_key, sequence_number, created_at, event_type, event_json, command_record_id, xact_id) AS
|
3
|
+
SELECT aggregate.aggregate_id,
|
4
|
+
event.partition_key,
|
5
|
+
event.sequence_number,
|
6
|
+
event.created_at,
|
7
|
+
type.type,
|
8
|
+
enrich_event_json(event) AS event_json,
|
9
|
+
command_id,
|
10
|
+
event.xact_id
|
11
|
+
FROM events event
|
12
|
+
JOIN aggregates aggregate ON aggregate.aggregate_id = event.aggregate_id AND aggregate.events_partition_key = event.partition_key
|
13
|
+
JOIN event_types type ON event.event_type_id = type.id;
|
@@ -0,0 +1,19 @@
|
|
1
|
+
CREATE OR REPLACE FUNCTION load_event(
|
2
|
+
_aggregate_id uuid,
|
3
|
+
_sequence_number integer
|
4
|
+
) RETURNS SETOF aggregate_event_type RETURNS NULL ON NULL INPUT
|
5
|
+
LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
|
6
|
+
BEGIN
|
7
|
+
RETURN QUERY SELECT aggregate_types.type,
|
8
|
+
a.aggregate_id,
|
9
|
+
a.events_partition_key,
|
10
|
+
event_types.type,
|
11
|
+
enrich_event_json(e)
|
12
|
+
FROM aggregates a
|
13
|
+
INNER JOIN events e ON (a.events_partition_key, a.aggregate_id) = (e.partition_key, e.aggregate_id)
|
14
|
+
INNER JOIN aggregate_types ON a.aggregate_type_id = aggregate_types.id
|
15
|
+
INNER JOIN event_types ON e.event_type_id = event_types.id
|
16
|
+
WHERE a.aggregate_id = _aggregate_id
|
17
|
+
AND e.sequence_number = _sequence_number;
|
18
|
+
END;
|
19
|
+
$$;
|
@@ -0,0 +1,40 @@
|
|
1
|
+
CREATE OR REPLACE FUNCTION load_events(
|
2
|
+
_aggregate_ids jsonb,
|
3
|
+
_use_snapshots boolean DEFAULT TRUE,
|
4
|
+
_until timestamptz DEFAULT NULL
|
5
|
+
) RETURNS SETOF aggregate_event_type
|
6
|
+
LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
|
7
|
+
DECLARE
|
8
|
+
_aggregate_id aggregates.aggregate_id%TYPE;
|
9
|
+
BEGIN
|
10
|
+
FOR _aggregate_id IN SELECT * FROM jsonb_array_elements_text(_aggregate_ids) LOOP
|
11
|
+
-- Use a single query to avoid race condition with UPDATEs to the events partition key
|
12
|
+
-- in case transaction isolation level is lower than repeatable read (the default of
|
13
|
+
-- PostgreSQL is read committed).
|
14
|
+
RETURN QUERY WITH
|
15
|
+
aggregate AS (
|
16
|
+
SELECT aggregate_types.type, aggregate_id, events_partition_key
|
17
|
+
FROM aggregates
|
18
|
+
JOIN aggregate_types ON aggregate_type_id = aggregate_types.id
|
19
|
+
WHERE aggregate_id = _aggregate_id
|
20
|
+
),
|
21
|
+
snapshot AS (
|
22
|
+
SELECT *
|
23
|
+
FROM snapshot_records
|
24
|
+
WHERE _use_snapshots
|
25
|
+
AND aggregate_id = _aggregate_id
|
26
|
+
AND (_until IS NULL OR created_at < _until)
|
27
|
+
ORDER BY sequence_number DESC LIMIT 1
|
28
|
+
)
|
29
|
+
(SELECT a.*, s.snapshot_type, s.snapshot_json FROM aggregate a, snapshot s)
|
30
|
+
UNION ALL
|
31
|
+
(SELECT a.*, event_types.type, enrich_event_json(e)
|
32
|
+
FROM aggregate a
|
33
|
+
JOIN events e ON (a.events_partition_key, a.aggregate_id) = (e.partition_key, e.aggregate_id)
|
34
|
+
JOIN event_types ON e.event_type_id = event_types.id
|
35
|
+
WHERE e.sequence_number >= COALESCE((SELECT sequence_number FROM snapshot), 0)
|
36
|
+
AND (_until IS NULL OR e.created_at < _until)
|
37
|
+
ORDER BY e.sequence_number ASC);
|
38
|
+
END LOOP;
|
39
|
+
END;
|
40
|
+
$$;
|
@@ -0,0 +1,12 @@
|
|
1
|
+
CREATE OR REPLACE FUNCTION load_latest_snapshot(_aggregate_id uuid) RETURNS aggregate_event_type
|
2
|
+
LANGUAGE SQL SET search_path FROM CURRENT AS $$
|
3
|
+
SELECT (SELECT type FROM aggregate_types WHERE id = a.aggregate_type_id),
|
4
|
+
a.aggregate_id,
|
5
|
+
a.events_partition_key,
|
6
|
+
s.snapshot_type,
|
7
|
+
s.snapshot_json
|
8
|
+
FROM aggregates a JOIN snapshot_records s ON a.aggregate_id = s.aggregate_id
|
9
|
+
WHERE a.aggregate_id = _aggregate_id
|
10
|
+
ORDER BY s.sequence_number DESC
|
11
|
+
LIMIT 1;
|
12
|
+
$$;
|
@@ -0,0 +1,13 @@
|
|
1
|
+
DROP PROCEDURE IF EXISTS permanently_delete_commands_without_events(uuid, uuid);
|
2
|
+
CREATE OR REPLACE PROCEDURE permanently_delete_commands_without_events(_aggregate_id uuid)
|
3
|
+
LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
|
4
|
+
BEGIN
|
5
|
+
IF _aggregate_id IS NULL THEN
|
6
|
+
RAISE EXCEPTION 'aggregate_id must be specified to delete commands';
|
7
|
+
END IF;
|
8
|
+
|
9
|
+
DELETE FROM commands
|
10
|
+
WHERE aggregate_id = _aggregate_id
|
11
|
+
AND NOT EXISTS (SELECT 1 FROM events WHERE command_id = commands.id);
|
12
|
+
END;
|
13
|
+
$$;
|
@@ -0,0 +1,13 @@
|
|
1
|
+
CREATE OR REPLACE PROCEDURE permanently_delete_event_streams(_aggregate_ids jsonb)
|
2
|
+
LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
|
3
|
+
BEGIN
|
4
|
+
DELETE FROM events
|
5
|
+
USING jsonb_array_elements_text(_aggregate_ids) AS ids (id)
|
6
|
+
JOIN aggregates ON ids.id::uuid = aggregates.aggregate_id
|
7
|
+
WHERE events.partition_key = aggregates.events_partition_key
|
8
|
+
AND events.aggregate_id = aggregates.aggregate_id;
|
9
|
+
DELETE FROM aggregates
|
10
|
+
USING jsonb_array_elements_text(_aggregate_ids) AS ids (id)
|
11
|
+
WHERE aggregates.aggregate_id = ids.id::uuid;
|
12
|
+
END;
|
13
|
+
$$;
|
@@ -0,0 +1,53 @@
|
|
1
|
+
CREATE OR REPLACE FUNCTION save_events_on_delete_trigger() RETURNS TRIGGER
|
2
|
+
LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
|
3
|
+
BEGIN
|
4
|
+
INSERT INTO saved_event_records (operation, timestamp, "user", aggregate_id, partition_key, sequence_number, created_at, event_type, event_json, command_id, xact_id)
|
5
|
+
SELECT 'D',
|
6
|
+
statement_timestamp(),
|
7
|
+
user,
|
8
|
+
o.aggregate_id,
|
9
|
+
o.partition_key,
|
10
|
+
o.sequence_number,
|
11
|
+
o.created_at,
|
12
|
+
(SELECT type FROM event_types WHERE event_types.id = o.event_type_id),
|
13
|
+
o.event_json,
|
14
|
+
o.command_id,
|
15
|
+
o.xact_id
|
16
|
+
FROM old_table o;
|
17
|
+
RETURN NULL;
|
18
|
+
END;
|
19
|
+
$$;
|
20
|
+
|
21
|
+
CREATE OR REPLACE FUNCTION save_events_on_update_trigger() RETURNS TRIGGER
|
22
|
+
LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
|
23
|
+
BEGIN
|
24
|
+
INSERT INTO saved_event_records (operation, timestamp, "user", aggregate_id, partition_key, sequence_number, created_at, event_type, event_json, command_id, xact_id)
|
25
|
+
SELECT 'U',
|
26
|
+
statement_timestamp(),
|
27
|
+
user,
|
28
|
+
o.aggregate_id,
|
29
|
+
o.partition_key,
|
30
|
+
o.sequence_number,
|
31
|
+
o.created_at,
|
32
|
+
(SELECT type FROM event_types WHERE event_types.id = o.event_type_id),
|
33
|
+
o.event_json,
|
34
|
+
o.command_id,
|
35
|
+
o.xact_id
|
36
|
+
FROM old_table o LEFT JOIN new_table n ON o.aggregate_id = n.aggregate_id AND o.sequence_number = n.sequence_number
|
37
|
+
WHERE n IS NULL
|
38
|
+
-- Only save when event related information changes
|
39
|
+
OR o.created_at <> n.created_at
|
40
|
+
OR o.event_type_id <> n.event_type_id
|
41
|
+
OR o.event_json <> n.event_json;
|
42
|
+
RETURN NULL;
|
43
|
+
END;
|
44
|
+
$$;
|
45
|
+
|
46
|
+
CREATE OR REPLACE TRIGGER save_events_on_delete_trigger
|
47
|
+
AFTER DELETE ON events
|
48
|
+
REFERENCING OLD TABLE AS old_table
|
49
|
+
FOR EACH STATEMENT EXECUTE FUNCTION save_events_on_delete_trigger();
|
50
|
+
CREATE OR REPLACE TRIGGER save_events_on_update_trigger
|
51
|
+
AFTER UPDATE ON events
|
52
|
+
REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table
|
53
|
+
FOR EACH STATEMENT EXECUTE FUNCTION save_events_on_update_trigger();
|
@@ -0,0 +1,19 @@
|
|
1
|
+
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())
|
2
|
+
RETURNS TABLE (aggregate_id uuid)
|
3
|
+
LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
|
4
|
+
BEGIN
|
5
|
+
RETURN QUERY WITH scheduled AS MATERIALIZED (
|
6
|
+
SELECT a.aggregate_id
|
7
|
+
FROM aggregates_that_need_snapshots AS a
|
8
|
+
WHERE snapshot_outdated_at IS NOT NULL
|
9
|
+
ORDER BY snapshot_outdated_at ASC, snapshot_sequence_number_high_water_mark DESC, aggregate_id ASC
|
10
|
+
LIMIT _limit
|
11
|
+
FOR UPDATE
|
12
|
+
) UPDATE aggregates_that_need_snapshots AS row
|
13
|
+
SET snapshot_scheduled_at = _now
|
14
|
+
FROM scheduled
|
15
|
+
WHERE row.aggregate_id = scheduled.aggregate_id
|
16
|
+
AND (row.snapshot_scheduled_at IS NULL OR row.snapshot_scheduled_at < _reschedule_snapshot_scheduled_before)
|
17
|
+
RETURNING row.aggregate_id;
|
18
|
+
END;
|
19
|
+
$$;
|
@@ -0,0 +1,39 @@
|
|
1
|
+
CREATE OR REPLACE PROCEDURE store_aggregates(_aggregates_with_events jsonb)
|
2
|
+
LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
|
3
|
+
DECLARE
|
4
|
+
_aggregate jsonb;
|
5
|
+
_events jsonb;
|
6
|
+
_aggregate_id aggregates.aggregate_id%TYPE;
|
7
|
+
_events_partition_key aggregates.events_partition_key%TYPE;
|
8
|
+
_snapshot_outdated_at aggregates_that_need_snapshots.snapshot_outdated_at%TYPE;
|
9
|
+
BEGIN
|
10
|
+
FOR _aggregate, _events IN SELECT row->0, row->1 FROM jsonb_array_elements(_aggregates_with_events) AS row LOOP
|
11
|
+
_aggregate_id = _aggregate->>'aggregate_id';
|
12
|
+
|
13
|
+
_events_partition_key = COALESCE(
|
14
|
+
_aggregate->>'events_partition_key',
|
15
|
+
(SELECT events_partition_key FROM aggregates WHERE aggregate_id = _aggregate_id),
|
16
|
+
''
|
17
|
+
);
|
18
|
+
|
19
|
+
INSERT INTO aggregates (aggregate_id, created_at, aggregate_type_id, events_partition_key)
|
20
|
+
VALUES (
|
21
|
+
_aggregate_id,
|
22
|
+
(_events->0->>'created_at')::timestamptz,
|
23
|
+
(SELECT id FROM aggregate_types WHERE type = _aggregate->>'aggregate_type'),
|
24
|
+
_events_partition_key
|
25
|
+
) ON CONFLICT (aggregate_id)
|
26
|
+
DO UPDATE SET events_partition_key = EXCLUDED.events_partition_key
|
27
|
+
WHERE aggregates.events_partition_key IS DISTINCT FROM EXCLUDED.events_partition_key;
|
28
|
+
|
29
|
+
_snapshot_outdated_at = _aggregate->>'snapshot_outdated_at';
|
30
|
+
IF _snapshot_outdated_at IS NOT NULL THEN
|
31
|
+
INSERT INTO aggregates_that_need_snapshots AS row (aggregate_id, snapshot_outdated_at)
|
32
|
+
VALUES (_aggregate_id, _snapshot_outdated_at)
|
33
|
+
ON CONFLICT (aggregate_id) DO UPDATE
|
34
|
+
SET snapshot_outdated_at = LEAST(row.snapshot_outdated_at, EXCLUDED.snapshot_outdated_at)
|
35
|
+
WHERE row.snapshot_outdated_at IS DISTINCT FROM EXCLUDED.snapshot_outdated_at;
|
36
|
+
END IF;
|
37
|
+
END LOOP;
|
38
|
+
END;
|
39
|
+
$$;
|
@@ -0,0 +1,21 @@
|
|
1
|
+
CREATE OR REPLACE FUNCTION store_command(_command jsonb) RETURNS bigint RETURNS NULL ON NULL INPUT
|
2
|
+
LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
|
3
|
+
DECLARE
|
4
|
+
_id commands.id%TYPE;
|
5
|
+
_command_json jsonb = _command->'command_json';
|
6
|
+
BEGIN
|
7
|
+
INSERT INTO commands (
|
8
|
+
created_at, user_id, aggregate_id, command_type_id, command_json,
|
9
|
+
event_aggregate_id, event_sequence_number
|
10
|
+
) VALUES (
|
11
|
+
(_command->>'created_at')::timestamptz,
|
12
|
+
(_command_json->>'user_id')::uuid,
|
13
|
+
(_command_json->>'aggregate_id')::uuid,
|
14
|
+
(SELECT id FROM command_types WHERE type = _command->>'command_type'),
|
15
|
+
(_command->'command_json') - '{command_type,created_at,user_id,aggregate_id,event_aggregate_id,event_sequence_number}'::text[],
|
16
|
+
(_command_json->>'event_aggregate_id')::uuid,
|
17
|
+
NULLIF(_command_json->'event_sequence_number', 'null'::jsonb)::integer
|
18
|
+
) RETURNING id INTO STRICT _id;
|
19
|
+
RETURN _id;
|
20
|
+
END;
|
21
|
+
$$;
|
@@ -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
|
+
_aggregates = (SELECT jsonb_agg(row->0) FROM jsonb_array_elements(_aggregates_with_events) AS row);
|
18
|
+
CALL update_unique_keys(_aggregates);
|
19
|
+
|
20
|
+
FOR _aggregate, _events IN SELECT row->0, row->1 FROM jsonb_array_elements(_aggregates_with_events) AS row
|
21
|
+
ORDER BY row->0->'aggregate_id', row->1->0->'event_json'->'sequence_number'
|
22
|
+
LOOP
|
23
|
+
_aggregate_id = _aggregate->>'aggregate_id';
|
24
|
+
SELECT events_partition_key INTO STRICT _events_partition_key FROM aggregates WHERE aggregate_id = _aggregate_id;
|
25
|
+
|
26
|
+
INSERT INTO events (partition_key, aggregate_id, sequence_number, created_at, command_id, event_type_id, event_json)
|
27
|
+
SELECT _events_partition_key,
|
28
|
+
_aggregate_id,
|
29
|
+
(event->'event_json'->'sequence_number')::integer,
|
30
|
+
(event->>'created_at')::timestamptz,
|
31
|
+
_command_id,
|
32
|
+
(SELECT id FROM event_types WHERE type = event->>'event_type'),
|
33
|
+
(event->'event_json') - '{aggregate_id,created_at,event_type,sequence_number}'::text[]
|
34
|
+
FROM jsonb_array_elements(_events) AS event;
|
35
|
+
END LOOP;
|
36
|
+
END;
|
37
|
+
$$;
|