sequent 7.1.1 → 8.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/sequent +6 -107
- data/db/sequent_8_migration.sql +120 -0
- data/db/sequent_pgsql.sql +416 -0
- data/db/sequent_schema.rb +11 -57
- data/db/sequent_schema_indexes.sql +37 -0
- data/db/sequent_schema_partitions.sql +34 -0
- data/db/sequent_schema_tables.sql +74 -0
- data/lib/sequent/cli/app.rb +132 -0
- data/lib/sequent/cli/sequent_8_migration.rb +180 -0
- data/lib/sequent/configuration.rb +11 -8
- data/lib/sequent/core/aggregate_repository.rb +2 -2
- data/lib/sequent/core/aggregate_root.rb +32 -9
- data/lib/sequent/core/aggregate_snapshotter.rb +8 -6
- data/lib/sequent/core/command_record.rb +27 -18
- data/lib/sequent/core/command_service.rb +2 -2
- data/lib/sequent/core/event_publisher.rb +1 -1
- data/lib/sequent/core/event_record.rb +37 -17
- data/lib/sequent/core/event_store.rb +101 -119
- data/lib/sequent/core/helpers/array_with_type.rb +1 -1
- data/lib/sequent/core/helpers/association_validator.rb +2 -2
- data/lib/sequent/core/helpers/attribute_support.rb +8 -8
- data/lib/sequent/core/helpers/equal_support.rb +3 -3
- data/lib/sequent/core/helpers/message_matchers/has_attrs.rb +2 -0
- data/lib/sequent/core/helpers/message_router.rb +2 -2
- data/lib/sequent/core/helpers/param_support.rb +1 -3
- data/lib/sequent/core/helpers/pgsql_helpers.rb +32 -0
- data/lib/sequent/core/helpers/string_support.rb +1 -1
- data/lib/sequent/core/helpers/string_to_value_parsers.rb +1 -1
- data/lib/sequent/core/persistors/active_record_persistor.rb +1 -1
- data/lib/sequent/core/persistors/replay_optimized_postgres_persistor.rb +3 -4
- data/lib/sequent/core/projector.rb +1 -1
- data/lib/sequent/core/snapshot_record.rb +44 -0
- data/lib/sequent/core/snapshot_store.rb +105 -0
- data/lib/sequent/core/stream_record.rb +10 -15
- data/lib/sequent/dry_run/read_only_replay_optimized_postgres_persistor.rb +1 -1
- data/lib/sequent/dry_run/view_schema.rb +2 -3
- data/lib/sequent/generator/project.rb +5 -7
- data/lib/sequent/generator/template_aggregate/template_aggregate/commands.rb +2 -0
- data/lib/sequent/generator/template_aggregate/template_aggregate/events.rb +2 -0
- data/lib/sequent/generator/template_aggregate/template_aggregate/template_aggregate.rb +2 -0
- data/lib/sequent/generator/template_aggregate/template_aggregate/template_aggregate_command_handler.rb +2 -0
- data/lib/sequent/generator/template_aggregate/template_aggregate.rb +2 -0
- data/lib/sequent/generator/template_project/Gemfile +7 -5
- data/lib/sequent/generator/template_project/Rakefile +4 -2
- data/lib/sequent/generator/template_project/app/projectors/post_projector.rb +2 -0
- data/lib/sequent/generator/template_project/app/records/post_record.rb +2 -0
- data/lib/sequent/generator/template_project/config/initializers/sequent.rb +3 -8
- data/lib/sequent/generator/template_project/db/migrations.rb +3 -3
- data/lib/sequent/generator/template_project/lib/post/commands.rb +2 -0
- data/lib/sequent/generator/template_project/lib/post/events.rb +2 -0
- data/lib/sequent/generator/template_project/lib/post/post.rb +2 -0
- data/lib/sequent/generator/template_project/lib/post/post_command_handler.rb +2 -0
- data/lib/sequent/generator/template_project/lib/post.rb +2 -0
- data/lib/sequent/generator/template_project/my_app.rb +2 -1
- data/lib/sequent/generator/template_project/spec/app/projectors/post_projector_spec.rb +2 -0
- data/lib/sequent/generator/template_project/spec/lib/post/post_command_handler_spec.rb +9 -2
- data/lib/sequent/generator/template_project/spec/spec_helper.rb +4 -7
- data/lib/sequent/generator.rb +1 -1
- data/lib/sequent/internal/aggregate_type.rb +12 -0
- data/lib/sequent/internal/command_type.rb +12 -0
- data/lib/sequent/internal/event_type.rb +12 -0
- data/lib/sequent/internal/internal.rb +14 -0
- data/lib/sequent/internal/partitioned_aggregate.rb +26 -0
- data/lib/sequent/internal/partitioned_command.rb +16 -0
- data/lib/sequent/internal/partitioned_event.rb +29 -0
- data/lib/sequent/migrations/grouper.rb +90 -0
- data/lib/sequent/migrations/sequent_schema.rb +2 -1
- data/lib/sequent/migrations/view_schema.rb +76 -77
- data/lib/sequent/rake/migration_tasks.rb +49 -24
- data/lib/sequent/sequent.rb +1 -0
- data/lib/sequent/support/database.rb +20 -16
- data/lib/sequent/test/time_comparison.rb +1 -1
- data/lib/sequent/util/timer.rb +1 -1
- data/lib/version.rb +1 -1
- metadata +102 -21
- data/lib/sequent/generator/template_project/db/sequent_schema.rb +0 -52
- data/lib/sequent/generator/template_project/ruby-version +0 -1
data/db/sequent_schema.rb
CHANGED
@@ -1,60 +1,14 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
create_table "event_records", :force => true do |t|
|
4
|
-
t.uuid "aggregate_id", :null => false
|
5
|
-
t.integer "sequence_number", :null => false
|
6
|
-
t.datetime "created_at", :null => false
|
7
|
-
t.string "event_type", :null => false
|
8
|
-
t.text "event_json", :null => false
|
9
|
-
t.integer "command_record_id", :null => false
|
10
|
-
t.integer "stream_record_id", :null => false
|
11
|
-
t.bigint "xact_id"
|
12
|
-
end
|
1
|
+
# frozen_string_literal: true
|
13
2
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
}
|
24
|
-
execute %Q{
|
25
|
-
CREATE INDEX snapshot_events ON event_records (aggregate_id, sequence_number DESC) WHERE event_type = 'Sequent::Core::SnapshotEvent'
|
26
|
-
}
|
27
|
-
|
28
|
-
add_index "event_records", ["command_record_id"], :name => "index_event_records_on_command_record_id"
|
29
|
-
add_index "event_records", ["event_type"], :name => "index_event_records_on_event_type"
|
30
|
-
add_index "event_records", ["created_at"], :name => "index_event_records_on_created_at"
|
31
|
-
add_index "event_records", ["xact_id"], :name => "index_event_records_on_xact_id"
|
32
|
-
|
33
|
-
create_table "command_records", :force => true do |t|
|
34
|
-
t.string "user_id"
|
35
|
-
t.uuid "aggregate_id"
|
36
|
-
t.string "command_type", :null => false
|
37
|
-
t.string "event_aggregate_id"
|
38
|
-
t.integer "event_sequence_number"
|
39
|
-
t.text "command_json", :null => false
|
40
|
-
t.datetime "created_at", :null => false
|
41
|
-
end
|
42
|
-
|
43
|
-
add_index "command_records", ["event_aggregate_id", 'event_sequence_number'], :name => "index_command_records_on_event"
|
44
|
-
|
45
|
-
create_table "stream_records", :force => true do |t|
|
46
|
-
t.datetime "created_at", :null => false
|
47
|
-
t.string "aggregate_type", :null => false
|
48
|
-
t.uuid "aggregate_id", :null => false
|
49
|
-
t.integer "snapshot_threshold"
|
3
|
+
ActiveRecord::Schema.define do
|
4
|
+
say_with_time 'Installing Sequent schema' do
|
5
|
+
say 'Creating tables', true
|
6
|
+
suppress_messages { execute File.read("#{File.dirname(__FILE__)}/sequent_schema_tables.sql") }
|
7
|
+
say 'Creating table partitions', true
|
8
|
+
suppress_messages { execute File.read("#{File.dirname(__FILE__)}/sequent_schema_partitions.sql") }
|
9
|
+
say 'Creating constraints and indexes', true
|
10
|
+
suppress_messages { execute File.read("#{File.dirname(__FILE__)}/sequent_schema_indexes.sql") }
|
11
|
+
say 'Creating stored procedures and views', true
|
12
|
+
suppress_messages { execute File.read("#{File.dirname(__FILE__)}/sequent_pgsql.sql") }
|
50
13
|
end
|
51
|
-
|
52
|
-
add_index "stream_records", ["aggregate_id"], :name => "index_stream_records_on_aggregate_id", :unique => true
|
53
|
-
execute %q{
|
54
|
-
ALTER TABLE event_records ADD CONSTRAINT command_fkey FOREIGN KEY (command_record_id) REFERENCES command_records (id)
|
55
|
-
}
|
56
|
-
execute %q{
|
57
|
-
ALTER TABLE event_records ADD CONSTRAINT stream_fkey FOREIGN KEY (stream_record_id) REFERENCES stream_records (id)
|
58
|
-
}
|
59
|
-
|
60
14
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
ALTER TABLE aggregates ADD PRIMARY KEY (aggregate_id);
|
2
|
+
ALTER TABLE aggregates ADD UNIQUE (events_partition_key, aggregate_id);
|
3
|
+
CREATE INDEX aggregates_aggregate_type_id_idx ON aggregates (aggregate_type_id);
|
4
|
+
|
5
|
+
ALTER TABLE commands ADD PRIMARY KEY (id);
|
6
|
+
CREATE INDEX commands_command_type_id_idx ON commands (command_type_id);
|
7
|
+
CREATE INDEX commands_aggregate_id_idx ON commands (aggregate_id);
|
8
|
+
CREATE INDEX commands_event_idx ON commands (event_aggregate_id, event_sequence_number);
|
9
|
+
|
10
|
+
ALTER TABLE events ADD PRIMARY KEY (partition_key, aggregate_id, sequence_number);
|
11
|
+
CREATE INDEX events_command_id_idx ON events (command_id);
|
12
|
+
CREATE INDEX events_event_type_id_idx ON events (event_type_id);
|
13
|
+
|
14
|
+
ALTER TABLE aggregates
|
15
|
+
ADD FOREIGN KEY (aggregate_type_id) REFERENCES aggregate_types (id) ON UPDATE CASCADE;
|
16
|
+
|
17
|
+
ALTER TABLE events
|
18
|
+
ADD FOREIGN KEY (partition_key, aggregate_id) REFERENCES aggregates (events_partition_key, aggregate_id)
|
19
|
+
ON UPDATE CASCADE ON DELETE RESTRICT;
|
20
|
+
ALTER TABLE events
|
21
|
+
ADD FOREIGN KEY (command_id) REFERENCES commands (id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
22
|
+
ALTER TABLE events
|
23
|
+
ADD FOREIGN KEY (event_type_id) REFERENCES event_types (id) ON UPDATE CASCADE;
|
24
|
+
ALTER TABLE events ALTER COLUMN xact_id SET DEFAULT pg_current_xact_id()::text::bigint;
|
25
|
+
|
26
|
+
ALTER TABLE commands
|
27
|
+
ADD FOREIGN KEY (command_type_id) REFERENCES command_types (id) ON UPDATE CASCADE;
|
28
|
+
|
29
|
+
ALTER TABLE aggregates_that_need_snapshots
|
30
|
+
ADD FOREIGN KEY (aggregate_id) REFERENCES aggregates (aggregate_id) ON UPDATE CASCADE ON DELETE CASCADE;
|
31
|
+
|
32
|
+
CREATE INDEX aggregates_that_need_snapshots_outdated_idx
|
33
|
+
ON aggregates_that_need_snapshots (snapshot_outdated_at ASC, snapshot_sequence_number_high_water_mark DESC, aggregate_id ASC)
|
34
|
+
WHERE snapshot_outdated_at IS NOT NULL;
|
35
|
+
|
36
|
+
ALTER TABLE snapshot_records
|
37
|
+
ADD FOREIGN KEY (aggregate_id) REFERENCES aggregates_that_need_snapshots (aggregate_id) ON UPDATE CASCADE ON DELETE CASCADE;
|
@@ -0,0 +1,34 @@
|
|
1
|
+
-- ### Configure partitions as needed
|
2
|
+
CREATE TABLE commands_default PARTITION OF commands DEFAULT;
|
3
|
+
-- CREATE TABLE commands_0 PARTITION OF commands FOR VALUES FROM (1) TO (100e6);
|
4
|
+
-- CREATE TABLE commands_1 PARTITION OF commands FOR VALUES FROM (100e6) TO (200e6);
|
5
|
+
-- CREATE TABLE commands_2 PARTITION OF commands FOR VALUES FROM (200e6) TO (300e6);
|
6
|
+
-- CREATE TABLE commands_3 PARTITION OF commands FOR VALUES FROM (300e6) TO (400e6);
|
7
|
+
|
8
|
+
-- ### Configure partitions as needed
|
9
|
+
CREATE TABLE aggregates_default PARTITION OF aggregates DEFAULT;
|
10
|
+
-- CREATE TABLE aggregates_0 PARTITION OF aggregates FOR VALUES FROM (MINVALUE) TO ('10000000-0000-0000-0000-000000000000');
|
11
|
+
-- CREATE TABLE aggregates_1 PARTITION OF aggregates FOR VALUES FROM ('10000000-0000-0000-0000-000000000000') TO ('20000000-0000-0000-0000-000000000000');
|
12
|
+
-- CREATE TABLE aggregates_2 PARTITION OF aggregates FOR VALUES FROM ('20000000-0000-0000-0000-000000000000') TO ('30000000-0000-0000-0000-000000000000');
|
13
|
+
-- CREATE TABLE aggregates_3 PARTITION OF aggregates FOR VALUES FROM ('30000000-0000-0000-0000-000000000000') TO ('40000000-0000-0000-0000-000000000000');
|
14
|
+
-- CREATE TABLE aggregates_4 PARTITION OF aggregates FOR VALUES FROM ('40000000-0000-0000-0000-000000000000') TO ('50000000-0000-0000-0000-000000000000');
|
15
|
+
-- CREATE TABLE aggregates_5 PARTITION OF aggregates FOR VALUES FROM ('50000000-0000-0000-0000-000000000000') TO ('60000000-0000-0000-0000-000000000000');
|
16
|
+
-- CREATE TABLE aggregates_6 PARTITION OF aggregates FOR VALUES FROM ('60000000-0000-0000-0000-000000000000') TO ('70000000-0000-0000-0000-000000000000');
|
17
|
+
-- CREATE TABLE aggregates_7 PARTITION OF aggregates FOR VALUES FROM ('70000000-0000-0000-0000-000000000000') TO ('80000000-0000-0000-0000-000000000000');
|
18
|
+
-- CREATE TABLE aggregates_8 PARTITION OF aggregates FOR VALUES FROM ('80000000-0000-0000-0000-000000000000') TO ('90000000-0000-0000-0000-000000000000');
|
19
|
+
-- CREATE TABLE aggregates_9 PARTITION OF aggregates FOR VALUES FROM ('90000000-0000-0000-0000-000000000000') TO ('a0000000-0000-0000-0000-000000000000');
|
20
|
+
-- CREATE TABLE aggregates_a PARTITION OF aggregates FOR VALUES FROM ('a0000000-0000-0000-0000-000000000000') TO ('b0000000-0000-0000-0000-000000000000');
|
21
|
+
-- CREATE TABLE aggregates_b PARTITION OF aggregates FOR VALUES FROM ('b0000000-0000-0000-0000-000000000000') TO ('c0000000-0000-0000-0000-000000000000');
|
22
|
+
-- CREATE TABLE aggregates_c PARTITION OF aggregates FOR VALUES FROM ('c0000000-0000-0000-0000-000000000000') TO ('d0000000-0000-0000-0000-000000000000');
|
23
|
+
-- CREATE TABLE aggregates_d PARTITION OF aggregates FOR VALUES FROM ('d0000000-0000-0000-0000-000000000000') TO ('e0000000-0000-0000-0000-000000000000');
|
24
|
+
-- CREATE TABLE aggregates_e PARTITION OF aggregates FOR VALUES FROM ('e0000000-0000-0000-0000-000000000000') TO ('f0000000-0000-0000-0000-000000000000');
|
25
|
+
-- CREATE TABLE aggregates_f PARTITION OF aggregates FOR VALUES FROM ('f0000000-0000-0000-0000-000000000000') TO (MAXVALUE);
|
26
|
+
|
27
|
+
-- ### Configure partitions as needed
|
28
|
+
CREATE TABLE events_default PARTITION OF events DEFAULT;
|
29
|
+
-- CREATE TABLE events_2023_and_earlier PARTITION OF events FOR VALUES FROM ('Y00') TO ('Y24');
|
30
|
+
-- CREATE TABLE events_2024 PARTITION OF events FOR VALUES FROM ('Y24') TO ('Y25');
|
31
|
+
-- CREATE TABLE events_2025 PARTITION OF events FOR VALUES FROM ('Y25') TO ('Y26');
|
32
|
+
-- CREATE TABLE events_2026 PARTITION OF events FOR VALUES FROM ('Y26') TO ('Y27');
|
33
|
+
-- CREATE TABLE events_2027_and_later PARTITION OF events FOR VALUES FROM ('Y27') TO ('Y99');
|
34
|
+
-- CREATE TABLE events_aggregate PARTITION OF events FOR VALUES FROM ('A') TO ('Ag');
|
@@ -0,0 +1,74 @@
|
|
1
|
+
CREATE TABLE command_types (id SMALLINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, type text UNIQUE NOT NULL);
|
2
|
+
CREATE TABLE aggregate_types (id SMALLINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, type text UNIQUE NOT NULL);
|
3
|
+
CREATE TABLE event_types (id SMALLINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, type text UNIQUE NOT NULL);
|
4
|
+
|
5
|
+
CREATE SEQUENCE IF NOT EXISTS commands_id_seq;
|
6
|
+
|
7
|
+
CREATE TABLE commands (
|
8
|
+
id bigint NOT NULL DEFAULT nextval('commands_id_seq'),
|
9
|
+
created_at timestamp with time zone NOT NULL,
|
10
|
+
user_id uuid,
|
11
|
+
aggregate_id uuid,
|
12
|
+
command_type_id SMALLINT NOT NULL,
|
13
|
+
command_json jsonb NOT NULL,
|
14
|
+
event_aggregate_id uuid,
|
15
|
+
event_sequence_number integer
|
16
|
+
) PARTITION BY RANGE (id);
|
17
|
+
|
18
|
+
ALTER SEQUENCE commands_id_seq OWNED BY commands.id;
|
19
|
+
|
20
|
+
CREATE TABLE aggregates (
|
21
|
+
aggregate_id uuid NOT NULL,
|
22
|
+
events_partition_key text NOT NULL DEFAULT '',
|
23
|
+
aggregate_type_id SMALLINT NOT NULL,
|
24
|
+
snapshot_threshold integer,
|
25
|
+
created_at timestamp with time zone NOT NULL DEFAULT NOW()
|
26
|
+
) PARTITION BY RANGE (aggregate_id);
|
27
|
+
|
28
|
+
CREATE TABLE events (
|
29
|
+
aggregate_id uuid NOT NULL,
|
30
|
+
partition_key text NOT NULL DEFAULT '',
|
31
|
+
sequence_number integer NOT NULL,
|
32
|
+
created_at timestamp with time zone NOT NULL,
|
33
|
+
command_id bigint NOT NULL,
|
34
|
+
event_type_id SMALLINT NOT NULL,
|
35
|
+
event_json jsonb NOT NULL,
|
36
|
+
xact_id bigint
|
37
|
+
) PARTITION BY RANGE (partition_key);
|
38
|
+
|
39
|
+
CREATE TABLE aggregates_that_need_snapshots (
|
40
|
+
aggregate_id uuid NOT NULL PRIMARY KEY,
|
41
|
+
snapshot_sequence_number_high_water_mark integer,
|
42
|
+
snapshot_outdated_at timestamp with time zone,
|
43
|
+
snapshot_scheduled_at timestamp with time zone
|
44
|
+
);
|
45
|
+
|
46
|
+
COMMENT ON TABLE aggregates_that_need_snapshots IS 'Contains a row for every aggregate with more events than its snapshot threshold.';
|
47
|
+
COMMENT ON COLUMN aggregates_that_need_snapshots.snapshot_sequence_number_high_water_mark
|
48
|
+
IS 'The highest sequence number of the stored snapshot. Kept when snapshot are deleted to more easily query aggregates that need snapshotting the most';
|
49
|
+
COMMENT ON COLUMN aggregates_that_need_snapshots.snapshot_outdated_at IS 'Not NULL indicates a snapshot is needed since the stored timestamp';
|
50
|
+
COMMENT ON COLUMN aggregates_that_need_snapshots.snapshot_scheduled_at IS 'Not NULL indicates a snapshot is in the process of being taken';
|
51
|
+
|
52
|
+
CREATE TABLE snapshot_records (
|
53
|
+
aggregate_id uuid NOT NULL,
|
54
|
+
sequence_number integer NOT NULL,
|
55
|
+
created_at timestamptz NOT NULL,
|
56
|
+
snapshot_type text NOT NULL,
|
57
|
+
snapshot_json jsonb NOT NULL,
|
58
|
+
PRIMARY KEY (aggregate_id, sequence_number)
|
59
|
+
);
|
60
|
+
|
61
|
+
CREATE TABLE saved_event_records (
|
62
|
+
operation varchar(1) NOT NULL CHECK (operation IN ('U', 'D')),
|
63
|
+
timestamp timestamptz NOT NULL,
|
64
|
+
"user" text NOT NULL,
|
65
|
+
aggregate_id uuid NOT NULL,
|
66
|
+
partition_key text DEFAULT '',
|
67
|
+
sequence_number integer NOT NULL,
|
68
|
+
created_at timestamp with time zone NOT NULL,
|
69
|
+
command_id bigint NOT NULL,
|
70
|
+
event_type text NOT NULL,
|
71
|
+
event_json jsonb NOT NULL,
|
72
|
+
xact_id bigint,
|
73
|
+
PRIMARY KEY (aggregate_id, sequence_number, timestamp)
|
74
|
+
);
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../generator'
|
4
|
+
require_relative 'sequent8_migration'
|
5
|
+
module Sequent
|
6
|
+
module Cli
|
7
|
+
class App
|
8
|
+
extend GLI::App
|
9
|
+
|
10
|
+
program_desc 'Sequent Command Line Interface (CLI)'
|
11
|
+
|
12
|
+
version Sequent::VERSION
|
13
|
+
on_error do |_error|
|
14
|
+
true
|
15
|
+
end
|
16
|
+
|
17
|
+
desc 'Generate a directory structure for a Sequent project'
|
18
|
+
command :new do |c|
|
19
|
+
prompt = TTY::Prompt.new(interrupt: :exit)
|
20
|
+
|
21
|
+
c.arg_name 'project_name'
|
22
|
+
c.action do |_global, _options, args|
|
23
|
+
help_now!('can only specify one single argument e.g. `sequent new project_name`') if args&.length != 1
|
24
|
+
|
25
|
+
project_name = args[0]
|
26
|
+
Sequent::Generator::Project.new(project_name).execute
|
27
|
+
prompt.say(<<~EOS)
|
28
|
+
Success!
|
29
|
+
|
30
|
+
Your brand spanking new sequent app is waiting for you in:
|
31
|
+
#{File.expand_path(project_name, Dir.pwd)}
|
32
|
+
|
33
|
+
To finish setting up your app:
|
34
|
+
cd #{project_name}
|
35
|
+
bundle install
|
36
|
+
bundle exec rake sequent:db:create
|
37
|
+
bundle exec rake sequent:db:create_view_schema
|
38
|
+
bundle exec rake sequent:migrate:online
|
39
|
+
bundle exec rake sequent:migrate:offline
|
40
|
+
|
41
|
+
Run the example specs:
|
42
|
+
SEQUENT_ENV=test bundle exec rake sequent:db:create
|
43
|
+
bundle exec rspec spec
|
44
|
+
|
45
|
+
To generate new aggregates use:
|
46
|
+
sequent generate <aggregate_name>. e.g. sequent generate address
|
47
|
+
|
48
|
+
For more information see:
|
49
|
+
https://www.sequent.io
|
50
|
+
|
51
|
+
Happy coding!
|
52
|
+
EOS
|
53
|
+
rescue TargetAlreadyExists
|
54
|
+
prompt.error("Target '#{project_name}' already exists, aborting")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
desc 'Generate a new aggregate, command, or event'
|
59
|
+
command [:generate, :g] do |c|
|
60
|
+
prompt = TTY::Prompt.new(interrupt: :exit)
|
61
|
+
|
62
|
+
c.arg_name 'aggregate_name'
|
63
|
+
c.desc 'Generate an aggregate'
|
64
|
+
c.command :aggregate do |a|
|
65
|
+
a.action do |_global, _options, args|
|
66
|
+
if args&.length != 1
|
67
|
+
help_now!('must specify one single argument e.g. `sequent generate aggregate Employee`')
|
68
|
+
end
|
69
|
+
|
70
|
+
aggregate_name = args[0]
|
71
|
+
|
72
|
+
Sequent::Generator::Aggregate.new(aggregate_name).execute
|
73
|
+
|
74
|
+
prompt.say(<<~EOS)
|
75
|
+
#{aggregate_name} aggregate has been generated
|
76
|
+
EOS
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
c.desc 'Generate a command'
|
81
|
+
c.arg_name 'aggregate_name command_name'
|
82
|
+
c.command :command do |command|
|
83
|
+
command.action do |_global, _options, args|
|
84
|
+
if args&.length&.< 2
|
85
|
+
help_now!('must specify at least two arguments e.g. `sequent generate command Employee CreateEmployee`')
|
86
|
+
end
|
87
|
+
|
88
|
+
aggregate_name, command_name, *attributes = args
|
89
|
+
|
90
|
+
Sequent::Generator::Command.new(aggregate_name, command_name, attributes).execute
|
91
|
+
prompt.say(<<~EOS)
|
92
|
+
"#{command_name} command has been added to #{aggregate_name}"
|
93
|
+
EOS
|
94
|
+
rescue NoAggregateFound
|
95
|
+
prompt.error("Aggregate '#{aggregate_name}' not found, aborting")
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
c.desc 'Generate an Event'
|
100
|
+
c.arg_name 'aggregate_name event_name'
|
101
|
+
c.command :event do |command|
|
102
|
+
command.action do |_global, _options, args|
|
103
|
+
if args&.length&.< 2
|
104
|
+
help_now!('must specify at least two arguments e.g. `sequent generate event Employee EmployeeCreated`')
|
105
|
+
end
|
106
|
+
|
107
|
+
aggregate_name, event_name, *attributes = args
|
108
|
+
|
109
|
+
Sequent::Generator::Command.new(aggregate_name, event_name, attributes).execute
|
110
|
+
prompt.say(<<~EOS)
|
111
|
+
"#{event_name} event has been added to #{aggregate_name}"
|
112
|
+
EOS
|
113
|
+
rescue NoAggregateFound
|
114
|
+
prompt.error("Aggregate '#{aggregate_name}' not found, aborting")
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
desc 'Migrates a Sequent 7 project to Sequent 8'
|
120
|
+
command :migrate do |c|
|
121
|
+
prompt = TTY::Prompt.new(interrupt: :exit)
|
122
|
+
c.action do |_global, _options, _args|
|
123
|
+
Sequent8Migration.new(prompt).execute
|
124
|
+
rescue Gem::MissingSpecError
|
125
|
+
prompt.error('Sequent gem not found. Please check your Gemfile.')
|
126
|
+
rescue Sequent8Migration::Stop => e
|
127
|
+
prompt.error(e.message)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sequent
|
4
|
+
module Cli
|
5
|
+
class Sequent8Migration
|
6
|
+
class Stop < StandardError; end
|
7
|
+
|
8
|
+
def initialize(prompt)
|
9
|
+
@prompt = prompt
|
10
|
+
end
|
11
|
+
|
12
|
+
# @raise Gem::MissingSpecError
|
13
|
+
def execute
|
14
|
+
print_introduction
|
15
|
+
abort_if_no('Do you wish to start the migration?')
|
16
|
+
copy_schema_files
|
17
|
+
abort_if_no('Do you which to continue?')
|
18
|
+
stop_application
|
19
|
+
migrate_data
|
20
|
+
prompt.ask('Press <enter> if the migration is done and you checked the results?')
|
21
|
+
migrated = commit_or_rollback
|
22
|
+
|
23
|
+
if migrated
|
24
|
+
prompt.say <<~EOS
|
25
|
+
|
26
|
+
Step 5. Deploy your Sequent 8 based application and start it.
|
27
|
+
|
28
|
+
Congratulations! You are now running your application on Sequent 8!
|
29
|
+
EOS
|
30
|
+
else
|
31
|
+
prompt.say <<~EOS
|
32
|
+
|
33
|
+
We are sorry the migration did not succeed. If you think this is a bug in Sequent don't hesitate to reach
|
34
|
+
out and submit an issue on Github: https://github.com/zilverline/sequent.
|
35
|
+
|
36
|
+
Don't forget to start your application again!
|
37
|
+
EOS
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
attr_reader :prompt
|
44
|
+
|
45
|
+
def print_introduction
|
46
|
+
prompt.say <<~EOS
|
47
|
+
This script will guide you through upgrading your Sequent application to Sequent 8.
|
48
|
+
|
49
|
+
The Sequent 8 database has been further optimized for disk usage and
|
50
|
+
performance. In addition it supports partitioning the tables for aggregates,
|
51
|
+
commands, and events, making it easier to manage the database (VACUUM,
|
52
|
+
CLUSTER) since these can work on the smaller partition tables.
|
53
|
+
|
54
|
+
It is highly recommended to test this upgrade on a copy of your production database first.
|
55
|
+
|
56
|
+
This script consists of the following steps:
|
57
|
+
|
58
|
+
Step 1: Copy the Sequent 8 database schema and
|
59
|
+
migration files to your project's `db/` directory. When this step is completed you
|
60
|
+
can customize these files to your liking and commit the changes.
|
61
|
+
|
62
|
+
One decision you need to make is whether you want to define partitions. This is
|
63
|
+
mainly useful when your database tables are larger than 10 gigabytes or so. By
|
64
|
+
default Sequent 8 uses a single "default" partition.
|
65
|
+
|
66
|
+
The `db/sequent_schema_partitions.sql` file contains the database partitions for
|
67
|
+
the `aggregates`, `commands`, and `events` tables, you can customize your
|
68
|
+
partitions here.
|
69
|
+
|
70
|
+
Step 2: Shutdown your application.
|
71
|
+
|
72
|
+
Step 3: Run the migration script. The script starts a transaction but DOES NOT
|
73
|
+
commit the results.
|
74
|
+
|
75
|
+
Step 4: Check the results and COMMIT or ROLLBACK the result. If you COMMIT,
|
76
|
+
you must perform a VACUUM ANALYZE to ensure PostgreSQL can efficiently query
|
77
|
+
the new tables
|
78
|
+
|
79
|
+
Step 5: Now you can deploy your Sequent 8 based application and start it again.
|
80
|
+
|
81
|
+
EOS
|
82
|
+
end
|
83
|
+
|
84
|
+
def copy_schema_files
|
85
|
+
prompt.say <<~EOS
|
86
|
+
|
87
|
+
Step 1. First a copy of the Sequent 8 database schema and migration scripts are
|
88
|
+
added to your db/ directory.
|
89
|
+
EOS
|
90
|
+
prompt.warn <<~EOS
|
91
|
+
|
92
|
+
WARNING: this may overwrite your existing scripts, please use your version control system to commit or abort any of the changes!
|
93
|
+
EOS
|
94
|
+
|
95
|
+
abort_if_no('Do you which to continue?')
|
96
|
+
|
97
|
+
FileUtils.copy_entry("#{sequent_gem_dir}/db", 'db')
|
98
|
+
|
99
|
+
prompt.warn <<~EOS
|
100
|
+
|
101
|
+
WARNING: The schema files have been copied, please verify and adjust the contents before committing and continuing.
|
102
|
+
EOS
|
103
|
+
end
|
104
|
+
|
105
|
+
def stop_application
|
106
|
+
prompt.say <<~EOS
|
107
|
+
|
108
|
+
Step 2. Please shut down your existing application.
|
109
|
+
EOS
|
110
|
+
|
111
|
+
abort_if_no(<<~EOS)
|
112
|
+
Only proceed once your application is stopped. Is your application stopped and do you want to continue?
|
113
|
+
EOS
|
114
|
+
end
|
115
|
+
|
116
|
+
def migrate_data
|
117
|
+
prompt.say <<~EOS
|
118
|
+
|
119
|
+
Step 3. Open a `psql` connection to the database you wish to migrate.
|
120
|
+
EOS
|
121
|
+
prompt.warn <<~EOS
|
122
|
+
|
123
|
+
It is highly recommended to test this on a copy of your production database first!
|
124
|
+
EOS
|
125
|
+
|
126
|
+
prompt.say <<~EOS
|
127
|
+
|
128
|
+
Depending on the size of your database the migration can take a long time. Open the `psql` connection from a screen session if needed.
|
129
|
+
If you run this from a screen session from another server you will need to copy all needed sql files to that server.
|
130
|
+
|
131
|
+
```
|
132
|
+
psql -U myapp_user myapp_db
|
133
|
+
```
|
134
|
+
EOS
|
135
|
+
|
136
|
+
prompt.ask('Press <enter> to read the next instructions once you connected to the database...')
|
137
|
+
|
138
|
+
prompt.say <<~EOS
|
139
|
+
|
140
|
+
Run the database migration. This doesn't commit anything yet so you can check the results first.
|
141
|
+
|
142
|
+
```
|
143
|
+
psql> \\i db/sequent_8_migration.sql
|
144
|
+
```
|
145
|
+
EOS
|
146
|
+
end
|
147
|
+
|
148
|
+
def commit_or_rollback
|
149
|
+
answer = prompt.yes? 'Did the migration succeed?'
|
150
|
+
if answer
|
151
|
+
prompt.say <<~EOS
|
152
|
+
|
153
|
+
Step 4. After checking everything went OK, COMMIT and optimize the database:
|
154
|
+
|
155
|
+
```
|
156
|
+
psql> COMMIT; VACUUM VERBOSE ANALYZE;
|
157
|
+
```
|
158
|
+
EOS
|
159
|
+
else
|
160
|
+
prompt.say <<~EOS
|
161
|
+
|
162
|
+
Step 4. Rollback the migration:
|
163
|
+
|
164
|
+
```
|
165
|
+
psql> ROLLBACK;
|
166
|
+
```
|
167
|
+
EOS
|
168
|
+
end
|
169
|
+
answer
|
170
|
+
end
|
171
|
+
|
172
|
+
def sequent_gem_dir = Gem::Specification.find_by_name('sequent').gem_dir
|
173
|
+
|
174
|
+
def abort_if_no(message, abort_message: 'Stopped at your request. You can restart this migration at any time.')
|
175
|
+
answer = prompt.yes?(message)
|
176
|
+
fail Stop, abort_message unless answer
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -21,6 +21,7 @@ module Sequent
|
|
21
21
|
MIGRATIONS_CLASS_NAME = 'Sequent::Migrations::Projectors'
|
22
22
|
|
23
23
|
DEFAULT_NUMBER_OF_REPLAY_PROCESSES = 4
|
24
|
+
DEFAULT_REPLAY_GROUP_TARGET_SIZE = 250_000
|
24
25
|
|
25
26
|
DEFAULT_OFFLINE_REPLAY_PERSISTOR_CLASS = Sequent::Core::Persistors::ActiveRecordPersistor
|
26
27
|
DEFAULT_ONLINE_REPLAY_PERSISTOR_CLASS = Sequent::Core::Persistors::ActiveRecordPersistor
|
@@ -35,11 +36,10 @@ module Sequent
|
|
35
36
|
|
36
37
|
attr_accessor :aggregate_repository,
|
37
38
|
:event_store,
|
38
|
-
:event_store_cache_event_types,
|
39
39
|
:command_service,
|
40
40
|
:event_record_class,
|
41
|
+
:snapshot_record_class,
|
41
42
|
:stream_record_class,
|
42
|
-
:snapshot_event_class,
|
43
43
|
:transaction_provider,
|
44
44
|
:event_publisher,
|
45
45
|
:event_record_hooks_class,
|
@@ -56,6 +56,7 @@ module Sequent
|
|
56
56
|
:offline_replay_persistor_class,
|
57
57
|
:online_replay_persistor_class,
|
58
58
|
:number_of_replay_processes,
|
59
|
+
:replay_group_target_size,
|
59
60
|
:database_config_directory,
|
60
61
|
:database_schema_directory,
|
61
62
|
:event_store_schema_name,
|
@@ -91,12 +92,11 @@ module Sequent
|
|
91
92
|
self.command_middleware = Sequent::Core::Middleware::Chain.new
|
92
93
|
|
93
94
|
self.aggregate_repository = Sequent::Core::AggregateRepository.new
|
94
|
-
self.event_store_cache_event_types = true
|
95
95
|
self.event_store = Sequent::Core::EventStore.new
|
96
96
|
self.command_service = Sequent::Core::CommandService.new
|
97
97
|
self.event_record_class = Sequent::Core::EventRecord
|
98
|
+
self.snapshot_record_class = Sequent::Core::SnapshotRecord
|
98
99
|
self.stream_record_class = Sequent::Core::StreamRecord
|
99
|
-
self.snapshot_event_class = Sequent::Core::SnapshotEvent
|
100
100
|
self.transaction_provider = Sequent::Core::Transactions::ActiveRecordTransactionProvider.new
|
101
101
|
self.uuid_generator = Sequent::Core::RandomUuidGenerator
|
102
102
|
self.event_publisher = Sequent::Core::EventPublisher.new
|
@@ -107,6 +107,7 @@ module Sequent
|
|
107
107
|
self.event_store_schema_name = DEFAULT_EVENT_STORE_SCHEMA_NAME
|
108
108
|
self.migrations_class_name = MIGRATIONS_CLASS_NAME
|
109
109
|
self.number_of_replay_processes = DEFAULT_NUMBER_OF_REPLAY_PROCESSES
|
110
|
+
self.replay_group_target_size = DEFAULT_REPLAY_GROUP_TARGET_SIZE
|
110
111
|
|
111
112
|
self.event_record_hooks_class = DEFAULT_EVENT_RECORD_HOOKS_CLASS
|
112
113
|
|
@@ -129,7 +130,7 @@ module Sequent
|
|
129
130
|
end
|
130
131
|
|
131
132
|
def can_use_multiple_databases?
|
132
|
-
enable_multiple_database_support
|
133
|
+
enable_multiple_database_support
|
133
134
|
end
|
134
135
|
|
135
136
|
def versions_table_name=(table_name)
|
@@ -159,18 +160,20 @@ module Sequent
|
|
159
160
|
|
160
161
|
self.class.instance.command_handlers ||= []
|
161
162
|
for_each_autoregisterable_descenant_of(Sequent::CommandHandler) do |command_handler_class|
|
162
|
-
Sequent.logger.debug
|
163
|
+
if Sequent.logger.debug?
|
164
|
+
Sequent.logger.debug("[Configuration] Autoregistering CommandHandler #{command_handler_class}")
|
165
|
+
end
|
163
166
|
self.class.instance.command_handlers << command_handler_class.new
|
164
167
|
end
|
165
168
|
|
166
169
|
self.class.instance.event_handlers ||= []
|
167
170
|
for_each_autoregisterable_descenant_of(Sequent::Projector) do |projector_class|
|
168
|
-
Sequent.logger.debug("[Configuration] Autoregistering Projector #{projector_class}")
|
171
|
+
Sequent.logger.debug("[Configuration] Autoregistering Projector #{projector_class}") if Sequent.logger.debug?
|
169
172
|
self.class.instance.event_handlers << projector_class.new
|
170
173
|
end
|
171
174
|
|
172
175
|
for_each_autoregisterable_descenant_of(Sequent::Workflow) do |workflow_class|
|
173
|
-
Sequent.logger.debug("[Configuration] Autoregistering Workflow #{workflow_class}")
|
176
|
+
Sequent.logger.debug("[Configuration] Autoregistering Workflow #{workflow_class}") if Sequent.logger.debug?
|
174
177
|
self.class.instance.event_handlers << workflow_class.new
|
175
178
|
end
|
176
179
|
|
@@ -20,13 +20,13 @@ module Sequent
|
|
20
20
|
|
21
21
|
class NonUniqueAggregateId < StandardError
|
22
22
|
def initialize(existing, new)
|
23
|
-
super
|
23
|
+
super("Duplicate aggregate #{new} with same key as existing #{existing}")
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
27
|
class AggregateNotFound < StandardError
|
28
28
|
def initialize(id)
|
29
|
-
super
|
29
|
+
super("Aggregate with id #{id} not found")
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|