sequent 7.1.1 → 8.0.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/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
|
|