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.
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