sequent 7.1.1 → 8.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/bin/sequent +6 -107
  3. data/db/sequent_8_migration.sql +120 -0
  4. data/db/sequent_pgsql.sql +416 -0
  5. data/db/sequent_schema.rb +11 -57
  6. data/db/sequent_schema_indexes.sql +37 -0
  7. data/db/sequent_schema_partitions.sql +34 -0
  8. data/db/sequent_schema_tables.sql +74 -0
  9. data/lib/sequent/cli/app.rb +132 -0
  10. data/lib/sequent/cli/sequent_8_migration.rb +180 -0
  11. data/lib/sequent/configuration.rb +11 -8
  12. data/lib/sequent/core/aggregate_repository.rb +2 -2
  13. data/lib/sequent/core/aggregate_root.rb +32 -9
  14. data/lib/sequent/core/aggregate_snapshotter.rb +8 -6
  15. data/lib/sequent/core/command_record.rb +27 -18
  16. data/lib/sequent/core/command_service.rb +2 -2
  17. data/lib/sequent/core/event_publisher.rb +1 -1
  18. data/lib/sequent/core/event_record.rb +37 -17
  19. data/lib/sequent/core/event_store.rb +101 -119
  20. data/lib/sequent/core/helpers/array_with_type.rb +1 -1
  21. data/lib/sequent/core/helpers/association_validator.rb +2 -2
  22. data/lib/sequent/core/helpers/attribute_support.rb +8 -8
  23. data/lib/sequent/core/helpers/equal_support.rb +3 -3
  24. data/lib/sequent/core/helpers/message_matchers/has_attrs.rb +2 -0
  25. data/lib/sequent/core/helpers/message_router.rb +2 -2
  26. data/lib/sequent/core/helpers/param_support.rb +1 -3
  27. data/lib/sequent/core/helpers/pgsql_helpers.rb +32 -0
  28. data/lib/sequent/core/helpers/string_support.rb +1 -1
  29. data/lib/sequent/core/helpers/string_to_value_parsers.rb +1 -1
  30. data/lib/sequent/core/persistors/active_record_persistor.rb +1 -1
  31. data/lib/sequent/core/persistors/replay_optimized_postgres_persistor.rb +3 -4
  32. data/lib/sequent/core/projector.rb +1 -1
  33. data/lib/sequent/core/snapshot_record.rb +44 -0
  34. data/lib/sequent/core/snapshot_store.rb +105 -0
  35. data/lib/sequent/core/stream_record.rb +10 -15
  36. data/lib/sequent/dry_run/read_only_replay_optimized_postgres_persistor.rb +1 -1
  37. data/lib/sequent/dry_run/view_schema.rb +2 -3
  38. data/lib/sequent/generator/project.rb +5 -7
  39. data/lib/sequent/generator/template_aggregate/template_aggregate/commands.rb +2 -0
  40. data/lib/sequent/generator/template_aggregate/template_aggregate/events.rb +2 -0
  41. data/lib/sequent/generator/template_aggregate/template_aggregate/template_aggregate.rb +2 -0
  42. data/lib/sequent/generator/template_aggregate/template_aggregate/template_aggregate_command_handler.rb +2 -0
  43. data/lib/sequent/generator/template_aggregate/template_aggregate.rb +2 -0
  44. data/lib/sequent/generator/template_project/Gemfile +7 -5
  45. data/lib/sequent/generator/template_project/Rakefile +4 -2
  46. data/lib/sequent/generator/template_project/app/projectors/post_projector.rb +2 -0
  47. data/lib/sequent/generator/template_project/app/records/post_record.rb +2 -0
  48. data/lib/sequent/generator/template_project/config/initializers/sequent.rb +3 -8
  49. data/lib/sequent/generator/template_project/db/migrations.rb +3 -3
  50. data/lib/sequent/generator/template_project/lib/post/commands.rb +2 -0
  51. data/lib/sequent/generator/template_project/lib/post/events.rb +2 -0
  52. data/lib/sequent/generator/template_project/lib/post/post.rb +2 -0
  53. data/lib/sequent/generator/template_project/lib/post/post_command_handler.rb +2 -0
  54. data/lib/sequent/generator/template_project/lib/post.rb +2 -0
  55. data/lib/sequent/generator/template_project/my_app.rb +2 -1
  56. data/lib/sequent/generator/template_project/spec/app/projectors/post_projector_spec.rb +2 -0
  57. data/lib/sequent/generator/template_project/spec/lib/post/post_command_handler_spec.rb +9 -2
  58. data/lib/sequent/generator/template_project/spec/spec_helper.rb +4 -7
  59. data/lib/sequent/generator.rb +1 -1
  60. data/lib/sequent/internal/aggregate_type.rb +12 -0
  61. data/lib/sequent/internal/command_type.rb +12 -0
  62. data/lib/sequent/internal/event_type.rb +12 -0
  63. data/lib/sequent/internal/internal.rb +14 -0
  64. data/lib/sequent/internal/partitioned_aggregate.rb +26 -0
  65. data/lib/sequent/internal/partitioned_command.rb +16 -0
  66. data/lib/sequent/internal/partitioned_event.rb +29 -0
  67. data/lib/sequent/migrations/grouper.rb +90 -0
  68. data/lib/sequent/migrations/sequent_schema.rb +2 -1
  69. data/lib/sequent/migrations/view_schema.rb +76 -77
  70. data/lib/sequent/rake/migration_tasks.rb +49 -24
  71. data/lib/sequent/sequent.rb +1 -0
  72. data/lib/sequent/support/database.rb +20 -16
  73. data/lib/sequent/test/time_comparison.rb +1 -1
  74. data/lib/sequent/util/timer.rb +1 -1
  75. data/lib/version.rb +1 -1
  76. metadata +102 -21
  77. data/lib/sequent/generator/template_project/db/sequent_schema.rb +0 -52
  78. data/lib/sequent/generator/template_project/ruby-version +0 -1
data/db/sequent_schema.rb CHANGED
@@ -1,60 +1,14 @@
1
- ActiveRecord::Schema.define do
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
- execute %Q{
15
- ALTER TABLE event_records ALTER COLUMN xact_id SET DEFAULT pg_current_xact_id()::text::bigint
16
- }
17
- execute %Q{
18
- CREATE UNIQUE INDEX unique_event_per_aggregate ON event_records (
19
- aggregate_id,
20
- sequence_number,
21
- (CASE event_type WHEN 'Sequent::Core::SnapshotEvent' THEN 0 ELSE 1 END)
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 && ActiveRecord.version > Gem::Version.new('6.1.0')
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("[Configuration] Autoregistering CommandHandler #{command_handler_class}")
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 "Duplicate aggregate #{new} with same key as existing #{existing}"
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 "Aggregate with id #{id} not found"
29
+ super("Aggregate with id #{id} not found")
30
30
  end
31
31
  end
32
32