sequent 3.3.1 → 4.1.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 +31 -25
- data/lib/notices.rb +6 -0
- data/lib/sequent/application_record.rb +2 -0
- data/lib/sequent/configuration.rb +29 -29
- data/lib/sequent/core/aggregate_repository.rb +24 -14
- data/lib/sequent/core/aggregate_root.rb +16 -7
- data/lib/sequent/core/aggregate_roots.rb +24 -0
- data/lib/sequent/core/aggregate_snapshotter.rb +8 -5
- data/lib/sequent/core/base_command_handler.rb +4 -2
- data/lib/sequent/core/command.rb +30 -11
- data/lib/sequent/core/command_record.rb +12 -4
- data/lib/sequent/core/command_service.rb +41 -25
- data/lib/sequent/core/core.rb +2 -0
- data/lib/sequent/core/current_event.rb +2 -0
- data/lib/sequent/core/event.rb +16 -11
- data/lib/sequent/core/event_publisher.rb +20 -15
- data/lib/sequent/core/event_record.rb +7 -7
- data/lib/sequent/core/event_store.rb +75 -49
- data/lib/sequent/core/ext/ext.rb +9 -1
- data/lib/sequent/core/helpers/array_with_type.rb +4 -1
- data/lib/sequent/core/helpers/association_validator.rb +9 -7
- data/lib/sequent/core/helpers/attribute_support.rb +64 -33
- data/lib/sequent/core/helpers/autoset_attributes.rb +4 -4
- data/lib/sequent/core/helpers/boolean_validator.rb +6 -1
- data/lib/sequent/core/helpers/copyable.rb +2 -2
- data/lib/sequent/core/helpers/date_time_validator.rb +4 -1
- data/lib/sequent/core/helpers/date_validator.rb +6 -1
- data/lib/sequent/core/helpers/default_validators.rb +12 -10
- data/lib/sequent/core/helpers/equal_support.rb +8 -6
- data/lib/sequent/core/helpers/helpers.rb +2 -0
- data/lib/sequent/core/helpers/mergable.rb +6 -4
- data/lib/sequent/core/helpers/message_handler.rb +3 -1
- data/lib/sequent/core/helpers/param_support.rb +19 -15
- data/lib/sequent/core/helpers/secret.rb +14 -12
- data/lib/sequent/core/helpers/string_support.rb +5 -4
- data/lib/sequent/core/helpers/string_to_value_parsers.rb +7 -2
- data/lib/sequent/core/helpers/string_validator.rb +6 -1
- data/lib/sequent/core/helpers/type_conversion_support.rb +5 -3
- data/lib/sequent/core/helpers/uuid_helper.rb +5 -2
- data/lib/sequent/core/helpers/value_validators.rb +23 -9
- data/lib/sequent/core/persistors/active_record_persistor.rb +19 -9
- data/lib/sequent/core/persistors/persistor.rb +16 -14
- data/lib/sequent/core/persistors/persistors.rb +2 -0
- data/lib/sequent/core/persistors/replay_optimized_postgres_persistor.rb +70 -47
- data/lib/sequent/core/projector.rb +25 -22
- data/lib/sequent/core/random_uuid_generator.rb +2 -0
- data/lib/sequent/core/sequent_oj.rb +2 -0
- data/lib/sequent/core/stream_record.rb +9 -3
- data/lib/sequent/core/transactions/active_record_transaction_provider.rb +7 -9
- data/lib/sequent/core/transactions/no_transactions.rb +2 -1
- data/lib/sequent/core/transactions/transactions.rb +2 -0
- data/lib/sequent/core/value_object.rb +8 -10
- data/lib/sequent/core/workflow.rb +7 -5
- data/lib/sequent/generator/aggregate.rb +16 -10
- data/lib/sequent/generator/command.rb +26 -19
- data/lib/sequent/generator/event.rb +19 -17
- data/lib/sequent/generator/generator.rb +6 -0
- data/lib/sequent/generator/project.rb +3 -1
- data/lib/sequent/generator/template_project/Gemfile +1 -1
- data/lib/sequent/generator/template_project/spec/app/projectors/post_projector_spec.rb +1 -1
- data/lib/sequent/generator/template_project/spec/lib/post/post_command_handler_spec.rb +1 -1
- data/lib/sequent/generator.rb +3 -4
- data/lib/sequent/migrations/executor.rb +30 -9
- data/lib/sequent/migrations/functions.rb +5 -6
- data/lib/sequent/migrations/migrate_events.rb +12 -9
- data/lib/sequent/migrations/migrations.rb +2 -1
- data/lib/sequent/migrations/planner.rb +33 -23
- data/lib/sequent/migrations/projectors.rb +4 -3
- data/lib/sequent/migrations/sql.rb +2 -0
- data/lib/sequent/migrations/view_schema.rb +93 -44
- data/lib/sequent/rake/migration_tasks.rb +59 -23
- data/lib/sequent/rake/tasks.rb +5 -2
- data/lib/sequent/sequent.rb +6 -1
- data/lib/sequent/support/database.rb +39 -17
- data/lib/sequent/support/view_projection.rb +6 -3
- data/lib/sequent/support/view_schema.rb +2 -0
- data/lib/sequent/support.rb +2 -0
- data/lib/sequent/test/command_handler_helpers.rb +39 -17
- data/lib/sequent/test/event_handler_helpers.rb +10 -4
- data/lib/sequent/test/event_stream_helpers.rb +7 -3
- data/lib/sequent/test/time_comparison.rb +12 -5
- data/lib/sequent/test.rb +2 -0
- data/lib/sequent/util/dry_run.rb +194 -0
- data/lib/sequent/util/printer.rb +6 -5
- data/lib/sequent/util/skip_if_already_processing.rb +21 -5
- data/lib/sequent/util/timer.rb +2 -0
- data/lib/sequent/util/util.rb +3 -0
- data/lib/sequent.rb +4 -0
- data/lib/version.rb +3 -1
- metadata +110 -59
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_model'
|
2
4
|
|
3
5
|
module Sequent
|
@@ -5,7 +7,6 @@ module Sequent
|
|
5
7
|
TypeConversionError = Class.new(RuntimeError)
|
6
8
|
|
7
9
|
module Helpers
|
8
|
-
|
9
10
|
# Will parse all values to the correct types.
|
10
11
|
# The raw values are typically posted from the web and are therefor mostly strings.
|
11
12
|
# To parse a raw value your class must have a parse_from_string method that returns the parsed values.
|
@@ -14,8 +15,9 @@ module Sequent
|
|
14
15
|
def parse_attrs_to_correct_types
|
15
16
|
the_copy = dup
|
16
17
|
the_copy.class.types.each do |name, type|
|
17
|
-
raw_value = the_copy.send(
|
18
|
+
raw_value = the_copy.send(name.to_s)
|
18
19
|
next if raw_value.nil?
|
20
|
+
|
19
21
|
if raw_value.respond_to?(:parse_attrs_to_correct_types)
|
20
22
|
the_copy.send("#{name}=", raw_value.parse_attrs_to_correct_types)
|
21
23
|
else
|
@@ -24,7 +26,7 @@ module Sequent
|
|
24
26
|
end
|
25
27
|
end
|
26
28
|
the_copy
|
27
|
-
rescue => e
|
29
|
+
rescue StandardError => e
|
28
30
|
raise TypeConversionError, e.message
|
29
31
|
end
|
30
32
|
end
|
@@ -1,12 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'securerandom'
|
2
4
|
|
3
5
|
module Sequent
|
4
6
|
module Core
|
5
7
|
module Helpers
|
6
8
|
module UuidHelper
|
7
|
-
|
8
9
|
def new_uuid
|
9
|
-
warn
|
10
|
+
warn <<~EOS
|
11
|
+
DEPRECATION WARNING: Sequent::Core::Helpers::UuidHelper.new_uuid is deprecated. Use Sequent.new_uuid instead
|
12
|
+
EOS
|
10
13
|
Sequent.new_uuid
|
11
14
|
end
|
12
15
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative '../ext/ext'
|
2
4
|
|
3
5
|
module Sequent
|
@@ -6,7 +8,7 @@ module Sequent
|
|
6
8
|
class ValueValidators
|
7
9
|
INVALID_STRING_CHARS = [
|
8
10
|
"\u0000",
|
9
|
-
]
|
11
|
+
].freeze
|
10
12
|
|
11
13
|
VALIDATORS = {
|
12
14
|
::Symbol => ->(_) { true },
|
@@ -14,36 +16,48 @@ module Sequent
|
|
14
16
|
::Integer => ->(value) { valid_integer?(value) },
|
15
17
|
::Boolean => ->(value) { valid_bool?(value) },
|
16
18
|
::Date => ->(value) { valid_date?(value) },
|
17
|
-
::DateTime => ->(value) { valid_date_time?(value) }
|
18
|
-
}
|
19
|
+
::DateTime => ->(value) { valid_date_time?(value) },
|
20
|
+
}.freeze
|
19
21
|
|
20
22
|
def self.valid_integer?(value)
|
21
23
|
value.blank? || Integer(value)
|
22
|
-
rescue
|
24
|
+
rescue StandardError
|
23
25
|
false
|
24
26
|
end
|
25
27
|
|
26
28
|
def self.valid_bool?(value)
|
27
29
|
return true if value.blank?
|
28
|
-
|
30
|
+
|
31
|
+
value.is_a?(TrueClass) || value.is_a?(FalseClass) || value == 'true' || value == 'false'
|
29
32
|
end
|
30
33
|
|
31
34
|
def self.valid_date?(value)
|
32
35
|
return true if value.blank?
|
33
36
|
return true if value.is_a?(Date)
|
34
37
|
return false unless value =~ /\d{4}-\d{2}-\d{2}/
|
35
|
-
|
38
|
+
|
39
|
+
begin
|
40
|
+
!!Date.iso8601(value)
|
41
|
+
rescue StandardError
|
42
|
+
false
|
43
|
+
end
|
36
44
|
end
|
37
45
|
|
38
46
|
def self.valid_date_time?(value)
|
39
47
|
return true if value.blank?
|
40
|
-
|
48
|
+
|
49
|
+
begin
|
50
|
+
value.is_a?(DateTime) || !!DateTime.iso8601(value.dup)
|
51
|
+
rescue StandardError
|
52
|
+
false
|
53
|
+
end
|
41
54
|
end
|
42
55
|
|
43
56
|
def self.valid_string?(value)
|
44
57
|
return true if value.nil?
|
45
|
-
|
46
|
-
|
58
|
+
|
59
|
+
value.to_s && INVALID_STRING_CHARS.none? { |invalid_char| value.to_s.include?(invalid_char) }
|
60
|
+
rescue StandardError
|
47
61
|
false
|
48
62
|
end
|
49
63
|
|
@@ -1,10 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_record'
|
2
4
|
require_relative './persistor'
|
3
5
|
|
4
6
|
module Sequent
|
5
7
|
module Core
|
6
8
|
module Persistors
|
7
|
-
|
8
9
|
#
|
9
10
|
# The ActiveRecordPersistor uses ActiveRecord to update the projection
|
10
11
|
#
|
@@ -17,14 +18,21 @@ module Sequent
|
|
17
18
|
class ActiveRecordPersistor
|
18
19
|
include Persistor
|
19
20
|
|
20
|
-
def update_record(record_class, event, where_clause = {aggregate_id: event.aggregate_id}, options = {}
|
21
|
+
def update_record(record_class, event, where_clause = {aggregate_id: event.aggregate_id}, options = {})
|
21
22
|
record = record_class.unscoped.where(where_clause).first
|
22
|
-
|
23
|
+
unless record
|
24
|
+
fail(<<~EOS)
|
25
|
+
Record of class #{record_class} with where clause #{where_clause} not found while handling event #{event}
|
26
|
+
EOS
|
27
|
+
end
|
28
|
+
|
23
29
|
record.updated_at = event.created_at if record.respond_to?(:updated_at)
|
24
30
|
yield record if block_given?
|
25
|
-
update_sequence_number = options.key?(:update_sequence_number)
|
26
|
-
options[:update_sequence_number]
|
31
|
+
update_sequence_number = if options.key?(:update_sequence_number)
|
32
|
+
options[:update_sequence_number]
|
33
|
+
else
|
27
34
|
record.respond_to?(:sequence_number=)
|
35
|
+
end
|
28
36
|
record.sequence_number = event.sequence_number if update_sequence_number
|
29
37
|
record.save!
|
30
38
|
end
|
@@ -42,11 +50,13 @@ module Sequent
|
|
42
50
|
query = array_of_value_hashes.map do |values|
|
43
51
|
insert_manager = new_insert_manager
|
44
52
|
insert_manager.into(table)
|
45
|
-
insert_manager.insert(
|
46
|
-
|
47
|
-
|
53
|
+
insert_manager.insert(
|
54
|
+
values.map do |key, value|
|
55
|
+
convert_to_values(key, table, value)
|
56
|
+
end,
|
57
|
+
)
|
48
58
|
insert_manager.to_sql
|
49
|
-
end.join(
|
59
|
+
end.join(';')
|
50
60
|
|
51
61
|
execute_sql(query)
|
52
62
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Sequent
|
2
4
|
module Core
|
3
5
|
module Persistors
|
@@ -9,74 +11,74 @@ module Sequent
|
|
9
11
|
module Persistor
|
10
12
|
# Updates the view state
|
11
13
|
def update_record
|
12
|
-
fail
|
14
|
+
fail 'Method not supported in this persistor'
|
13
15
|
end
|
14
16
|
|
15
17
|
# Create a single record in the view state
|
16
18
|
def create_record
|
17
|
-
fail
|
19
|
+
fail 'Method not supported in this persistor'
|
18
20
|
end
|
19
21
|
|
20
22
|
# Creates multiple records at once in the view state
|
21
23
|
def create_records
|
22
|
-
fail
|
24
|
+
fail 'Method not supported in this persistor'
|
23
25
|
end
|
24
26
|
|
25
27
|
# Creates or updates a record in the view state.
|
26
28
|
def create_or_update_record
|
27
|
-
fail
|
29
|
+
fail 'Method not supported in this persistor'
|
28
30
|
end
|
29
31
|
# Gets a record from the view state, fails if it not exists
|
30
32
|
def get_record!
|
31
|
-
fail
|
33
|
+
fail 'Method not supported in this persistor'
|
32
34
|
end
|
33
35
|
|
34
36
|
# Gets a record from the view state, returns +nil+ if it not exists
|
35
37
|
def get_record
|
36
|
-
fail
|
38
|
+
fail 'Method not supported in this persistor'
|
37
39
|
end
|
38
40
|
|
39
41
|
# Deletes all records given a where
|
40
42
|
def delete_all_records
|
41
|
-
fail
|
43
|
+
fail 'Method not supported in this persistor'
|
42
44
|
end
|
43
45
|
|
44
46
|
# Updates all record given a where and an update clause
|
45
47
|
def update_all_records
|
46
|
-
fail
|
48
|
+
fail 'Method not supported in this persistor'
|
47
49
|
end
|
48
50
|
|
49
51
|
# Decide for yourself what to do with the records
|
50
52
|
# @deprecated
|
51
53
|
def do_with_records
|
52
|
-
fail
|
54
|
+
fail 'Method not supported in this persistor'
|
53
55
|
end
|
54
56
|
|
55
57
|
# Decide for yourself what to do with a single record
|
56
58
|
# @deprecated
|
57
59
|
def do_with_record
|
58
|
-
fail
|
60
|
+
fail 'Method not supported in this persistor'
|
59
61
|
end
|
60
62
|
|
61
63
|
# Delete a single record
|
62
64
|
# @deprecated
|
63
65
|
def delete_record
|
64
|
-
fail
|
66
|
+
fail 'Method not supported in this persistor'
|
65
67
|
end
|
66
68
|
|
67
69
|
# Find records given a where
|
68
70
|
def find_records
|
69
|
-
fail
|
71
|
+
fail 'Method not supported in this persistor'
|
70
72
|
end
|
71
73
|
|
72
74
|
# Returns the last record given a where
|
73
75
|
def last_record
|
74
|
-
fail
|
76
|
+
fail 'Method not supported in this persistor'
|
75
77
|
end
|
76
78
|
|
77
79
|
# Hook to implement for instance the persistor batches statements
|
78
80
|
def commit
|
79
|
-
fail
|
81
|
+
fail 'Method not supported in this persistor'
|
80
82
|
end
|
81
83
|
end
|
82
84
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'set'
|
2
4
|
require 'active_record'
|
3
5
|
require 'csv'
|
@@ -13,9 +15,10 @@ module Sequent
|
|
13
15
|
# using normal sql.
|
14
16
|
#
|
15
17
|
# Rebuilding the view state (or projection) of an aggregate typically consists
|
16
|
-
# of an initial insert and then many updates and maybe a delete.
|
17
|
-
# each action is executed to the database.
|
18
|
-
#
|
18
|
+
# of an initial insert and then many updates and maybe a delete.
|
19
|
+
# With a normal Persistor (like ActiveRecordPersistor) each action is executed to the database.
|
20
|
+
# This persistor creates an in-memory store first and finally flushes
|
21
|
+
# the in-memory store to the database. This can significantly reduce the amount of queries to the database.
|
19
22
|
# E.g. 1 insert, 6 updates is only a single insert using this Persistor.
|
20
23
|
#
|
21
24
|
# After lot of experimenting this turned out to be the fastest way to to bulk inserts in the database.
|
@@ -29,22 +32,26 @@ module Sequent
|
|
29
32
|
#
|
30
33
|
# class InvoiceProjector < Sequent::Core::Projector
|
31
34
|
# on RecipientMovedEvent do |event|
|
32
|
-
# update_all_records
|
33
|
-
#
|
35
|
+
# update_all_records(
|
36
|
+
# InvoiceRecord,
|
37
|
+
# { aggregate_id: event.aggregate_id, recipient_id: event.recipient.aggregate_id },
|
38
|
+
# { recipient_street: event.recipient.street },
|
34
39
|
# end
|
35
40
|
# end
|
36
41
|
# end
|
37
42
|
#
|
38
|
-
# In this case it is wise to create an index on InvoiceRecord on the
|
43
|
+
# In this case it is wise to create an index on InvoiceRecord on the aggregate_id and recipient_id
|
44
|
+
# like you would in the database.
|
39
45
|
#
|
40
46
|
# Example:
|
41
47
|
#
|
42
48
|
# ReplayOptimizedPostgresPersistor.new(
|
43
49
|
# 50,
|
44
|
-
# {InvoiceRecord => [[:recipient_id]]}
|
50
|
+
# {InvoiceRecord => [[:aggregate_id, :recipient_id]]}
|
45
51
|
# )
|
46
52
|
class ReplayOptimizedPostgresPersistor
|
47
53
|
include Persistor
|
54
|
+
CHUNK_SIZE = 1024
|
48
55
|
|
49
56
|
attr_reader :record_store
|
50
57
|
attr_accessor :insert_with_csv_size
|
@@ -65,14 +72,18 @@ module Sequent
|
|
65
72
|
class Index
|
66
73
|
def initialize(indexed_columns)
|
67
74
|
@indexed_columns = Hash.new do |hash, record_class|
|
68
|
-
if record_class.column_names.include? 'aggregate_id'
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
75
|
+
hash[record_class] = if record_class.column_names.include? 'aggregate_id'
|
76
|
+
['aggregate_id']
|
77
|
+
else
|
78
|
+
[]
|
79
|
+
end
|
73
80
|
end
|
74
81
|
|
75
|
-
@indexed_columns.merge
|
82
|
+
@indexed_columns = @indexed_columns.merge(
|
83
|
+
indexed_columns.reduce({}) do |memo, (key, ics)|
|
84
|
+
memo.merge({key => ics.map { |c| c.map(&:to_s) }})
|
85
|
+
end,
|
86
|
+
)
|
76
87
|
|
77
88
|
@index = {}
|
78
89
|
@reverse_index = {}
|
@@ -82,10 +93,10 @@ module Sequent
|
|
82
93
|
return unless indexed?(record_class)
|
83
94
|
|
84
95
|
get_keys(record_class, record).each do |key|
|
85
|
-
@index[key.hash] = [] unless @index.
|
96
|
+
@index[key.hash] = [] unless @index.key? key.hash
|
86
97
|
@index[key.hash] << record
|
87
98
|
|
88
|
-
@reverse_index[record.object_id.hash] = [] unless @reverse_index.
|
99
|
+
@reverse_index[record.object_id.hash] = [] unless @reverse_index.key? record.object_id.hash
|
89
100
|
@reverse_index[record.object_id.hash] << key.hash
|
90
101
|
end
|
91
102
|
end
|
@@ -112,7 +123,7 @@ module Sequent
|
|
112
123
|
key = [record_class.name]
|
113
124
|
get_index(record_class, where_clause).each do |field|
|
114
125
|
key << field
|
115
|
-
key << where_clause[field]
|
126
|
+
key << where_clause.stringify_keys[field]
|
116
127
|
end
|
117
128
|
@index[key.hash] || []
|
118
129
|
end
|
@@ -123,13 +134,13 @@ module Sequent
|
|
123
134
|
end
|
124
135
|
|
125
136
|
def use_index?(record_class, where_clause)
|
126
|
-
@indexed_columns.
|
137
|
+
@indexed_columns.key?(record_class) && get_index(record_class, where_clause).present?
|
127
138
|
end
|
128
139
|
|
129
140
|
private
|
130
141
|
|
131
142
|
def indexed?(record_class)
|
132
|
-
@indexed_columns.
|
143
|
+
@indexed_columns.key?(record_class)
|
133
144
|
end
|
134
145
|
|
135
146
|
def get_keys(record_class, record)
|
@@ -144,7 +155,9 @@ module Sequent
|
|
144
155
|
end
|
145
156
|
|
146
157
|
def get_index(record_class, where_clause)
|
147
|
-
@indexed_columns[record_class].find
|
158
|
+
@indexed_columns[record_class].find do |indexed_where|
|
159
|
+
where_clause.keys.size == indexed_where.size && (where_clause.keys.map(&:to_s) - indexed_where).empty?
|
160
|
+
end
|
148
161
|
end
|
149
162
|
end
|
150
163
|
|
@@ -152,37 +165,40 @@ module Sequent
|
|
152
165
|
#
|
153
166
|
# +indices+ Hash of indices to create in memory. Greatly speeds up the replaying.
|
154
167
|
# Key corresponds to the name of the 'Record'
|
155
|
-
# Values contains list of lists on which columns to index.
|
168
|
+
# Values contains list of lists on which columns to index.
|
169
|
+
# E.g. [[:first_index_column], [:another_index, :with_to_columns]]
|
156
170
|
def initialize(insert_with_csv_size = 50, indices = {})
|
157
171
|
@insert_with_csv_size = insert_with_csv_size
|
158
172
|
@record_store = Hash.new { |h, k| h[k] = Set.new }
|
159
173
|
@record_index = Index.new(indices)
|
160
174
|
end
|
161
175
|
|
162
|
-
def update_record(record_class, event, where_clause = {aggregate_id: event.aggregate_id}, options = {}
|
176
|
+
def update_record(record_class, event, where_clause = {aggregate_id: event.aggregate_id}, options = {})
|
163
177
|
record = get_record!(record_class, where_clause)
|
164
178
|
record.updated_at = event.created_at if record.respond_to?(:updated_at)
|
165
179
|
yield record if block_given?
|
166
180
|
@record_index.update(record_class, record)
|
167
|
-
update_sequence_number = options.key?(:update_sequence_number)
|
168
|
-
options[:update_sequence_number]
|
181
|
+
update_sequence_number = if options.key?(:update_sequence_number)
|
182
|
+
options[:update_sequence_number]
|
183
|
+
else
|
169
184
|
record.respond_to?(:sequence_number=)
|
185
|
+
end
|
170
186
|
record.sequence_number = event.sequence_number if update_sequence_number
|
171
187
|
end
|
172
188
|
|
173
189
|
def create_record(record_class, values)
|
174
190
|
column_names = record_class.column_names
|
175
191
|
values = record_class.column_defaults.with_indifferent_access.merge(values)
|
176
|
-
values.merge!(updated_at: values[:created_at]) if column_names.include?(
|
177
|
-
struct_class_name = "#{record_class
|
178
|
-
if self.class.struct_cache.
|
192
|
+
values.merge!(updated_at: values[:created_at]) if column_names.include?('updated_at')
|
193
|
+
struct_class_name = "#{record_class}Struct"
|
194
|
+
if self.class.struct_cache.key?(struct_class_name)
|
179
195
|
struct_class = self.class.struct_cache[struct_class_name]
|
180
196
|
else
|
181
197
|
# We create a struct on the fly.
|
182
198
|
# Since the replay happens in memory we implement the ==, eql? and hash methods
|
183
199
|
# to point to the same object. A record is the same if and only if they point to
|
184
200
|
# the same object. These methods are necessary since we use Set instead of [].
|
185
|
-
class_def
|
201
|
+
class_def = <<-EOD
|
186
202
|
#{struct_class_name} = Struct.new(*#{column_names.map(&:to_sym)})
|
187
203
|
class #{struct_class_name}
|
188
204
|
include InitStruct
|
@@ -194,7 +210,9 @@ module Sequent
|
|
194
210
|
end
|
195
211
|
end
|
196
212
|
EOD
|
197
|
-
|
213
|
+
# rubocop:disable Security/Eval
|
214
|
+
eval(class_def.to_s)
|
215
|
+
# rubocop:enable Security/Eval
|
198
216
|
struct_class = ReplayOptimizedPostgresPersistor.const_get(struct_class_name)
|
199
217
|
self.class.struct_cache[struct_class_name] = struct_class
|
200
218
|
end
|
@@ -214,9 +232,7 @@ module Sequent
|
|
214
232
|
|
215
233
|
def create_or_update_record(record_class, values, created_at = Time.now)
|
216
234
|
record = get_record(record_class, values)
|
217
|
-
|
218
|
-
record = create_record(record_class, values.merge(created_at: created_at))
|
219
|
-
end
|
235
|
+
record ||= create_record(record_class, values.merge(created_at: created_at))
|
220
236
|
yield record if block_given?
|
221
237
|
@record_index.update(record_class, record)
|
222
238
|
record
|
@@ -224,7 +240,10 @@ module Sequent
|
|
224
240
|
|
225
241
|
def get_record!(record_class, where_clause)
|
226
242
|
record = get_record(record_class, where_clause)
|
227
|
-
|
243
|
+
unless record
|
244
|
+
fail("record #{record_class} not found for #{where_clause}, store: #{@record_store[record_class]}")
|
245
|
+
end
|
246
|
+
|
228
247
|
record
|
229
248
|
end
|
230
249
|
|
@@ -273,10 +292,10 @@ module Sequent
|
|
273
292
|
else
|
274
293
|
@record_store[record_class].select do |record|
|
275
294
|
where_clause.all? do |k, v|
|
276
|
-
expected_value = v.
|
295
|
+
expected_value = v.is_a?(Symbol) ? v.to_s : v
|
277
296
|
actual_value = record[k.to_sym]
|
278
|
-
actual_value = actual_value.to_s if actual_value.
|
279
|
-
if expected_value.
|
297
|
+
actual_value = actual_value.to_s if actual_value.is_a? Symbol
|
298
|
+
if expected_value.is_a?(Array)
|
280
299
|
expected_value.include?(actual_value)
|
281
300
|
else
|
282
301
|
actual_value == expected_value
|
@@ -295,39 +314,38 @@ module Sequent
|
|
295
314
|
@record_store.each do |clazz, records|
|
296
315
|
@column_cache ||= {}
|
297
316
|
@column_cache[clazz.name] ||= clazz.columns.reduce({}) do |hash, column|
|
298
|
-
hash.merge({
|
317
|
+
hash.merge({column.name => column})
|
299
318
|
end
|
300
319
|
if records.size > @insert_with_csv_size
|
301
|
-
csv = CSV.new(
|
302
|
-
column_names = clazz.column_names.reject { |name| name ==
|
320
|
+
csv = CSV.new(StringIO.new)
|
321
|
+
column_names = clazz.column_names.reject { |name| name == 'id' }
|
303
322
|
records.each do |record|
|
304
323
|
csv << column_names.map do |column_name|
|
305
324
|
cast_value_to_column_type(clazz, column_name, record)
|
306
325
|
end
|
307
326
|
end
|
308
327
|
|
309
|
-
buf = ''
|
310
328
|
conn = Sequent::ApplicationRecord.connection.raw_connection
|
311
|
-
copy_data = StringIO.new
|
329
|
+
copy_data = StringIO.new(csv.string)
|
312
330
|
conn.transaction do
|
313
|
-
conn.copy_data("COPY #{clazz.table_name} (#{column_names.join(
|
314
|
-
while copy_data.read(
|
315
|
-
conn.put_copy_data(
|
331
|
+
conn.copy_data("COPY #{clazz.table_name} (#{column_names.join(',')}) FROM STDIN WITH csv") do
|
332
|
+
while (out = copy_data.read(CHUNK_SIZE))
|
333
|
+
conn.put_copy_data(out)
|
316
334
|
end
|
317
335
|
end
|
318
336
|
end
|
319
337
|
else
|
320
338
|
clazz.unscoped do
|
321
339
|
inserts = []
|
322
|
-
column_names = clazz.column_names.reject { |name| name ==
|
323
|
-
prepared_values = (1..column_names.size).map { |i| "$#{i}" }.join(
|
340
|
+
column_names = clazz.column_names.reject { |name| name == 'id' }
|
341
|
+
prepared_values = (1..column_names.size).map { |i| "$#{i}" }.join(',')
|
324
342
|
records.each do |record|
|
325
343
|
values = column_names.map do |column_name|
|
326
344
|
cast_value_to_column_type(clazz, column_name, record)
|
327
345
|
end
|
328
346
|
inserts << values
|
329
347
|
end
|
330
|
-
sql = %
|
348
|
+
sql = %{insert into #{clazz.table_name} (#{column_names.join(',')}) values (#{prepared_values})}
|
331
349
|
inserts.each do |insert|
|
332
350
|
clazz.connection.raw_connection.async_exec(sql, insert)
|
333
351
|
end
|
@@ -346,7 +364,12 @@ module Sequent
|
|
346
364
|
private
|
347
365
|
|
348
366
|
def cast_value_to_column_type(clazz, column_name, record)
|
349
|
-
|
367
|
+
uncasted_value = ActiveModel::Attribute.from_database(
|
368
|
+
column_name,
|
369
|
+
record[column_name.to_sym],
|
370
|
+
Sequent::ApplicationRecord.connection.lookup_cast_type_from_column(@column_cache[clazz.name][column_name]),
|
371
|
+
).value_for_database
|
372
|
+
Sequent::ApplicationRecord.connection.type_cast(uncasted_value)
|
350
373
|
end
|
351
374
|
end
|
352
375
|
end
|
@@ -1,9 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'helpers/message_handler'
|
2
4
|
require_relative './persistors/active_record_persistor'
|
3
5
|
|
4
6
|
module Sequent
|
5
7
|
module Core
|
6
|
-
|
7
8
|
module Migratable
|
8
9
|
module ClassMethods
|
9
10
|
def manages_tables(*tables)
|
@@ -16,7 +17,7 @@ module Sequent
|
|
16
17
|
|
17
18
|
def manages_no_tables
|
18
19
|
@manages_no_tables = true
|
19
|
-
manages_tables
|
20
|
+
manages_tables
|
20
21
|
end
|
21
22
|
|
22
23
|
def manages_no_tables?
|
@@ -26,11 +27,11 @@ module Sequent
|
|
26
27
|
private
|
27
28
|
|
28
29
|
def managed_tables_from_superclass
|
29
|
-
|
30
|
+
superclass.managed_tables if superclass.respond_to?(:managed_tables)
|
30
31
|
end
|
31
32
|
|
32
33
|
def manages_no_tables_from_superclass?
|
33
|
-
|
34
|
+
superclass.manages_no_tables? if superclass.respond_to?(:manages_no_tables?)
|
34
35
|
end
|
35
36
|
end
|
36
37
|
|
@@ -53,7 +54,6 @@ module Sequent
|
|
53
54
|
def managed_tables
|
54
55
|
self.class.managed_tables
|
55
56
|
end
|
56
|
-
|
57
57
|
end
|
58
58
|
|
59
59
|
# Projectors listen to events and update the view state as they see fit.
|
@@ -87,7 +87,6 @@ module Sequent
|
|
87
87
|
include Helpers::MessageHandler
|
88
88
|
include Migratable
|
89
89
|
|
90
|
-
|
91
90
|
def initialize(persistor = Sequent::Core::Persistors::ActiveRecordPersistor.new)
|
92
91
|
ensure_valid!
|
93
92
|
@persistor = persistor
|
@@ -98,28 +97,32 @@ module Sequent
|
|
98
97
|
end
|
99
98
|
|
100
99
|
def_delegators :@persistor,
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
100
|
+
:update_record,
|
101
|
+
:create_record,
|
102
|
+
:create_records,
|
103
|
+
:create_or_update_record,
|
104
|
+
:get_record!,
|
105
|
+
:get_record,
|
106
|
+
:delete_all_records,
|
107
|
+
:update_all_records,
|
108
|
+
:do_with_records,
|
109
|
+
:do_with_record,
|
110
|
+
:delete_record,
|
111
|
+
:find_records,
|
112
|
+
:last_record,
|
113
|
+
:execute_sql,
|
114
|
+
:commit
|
116
115
|
|
117
116
|
private
|
118
117
|
|
119
118
|
def ensure_valid!
|
120
119
|
return if self.class.manages_no_tables?
|
121
120
|
|
122
|
-
|
121
|
+
if self.class.managed_tables.nil? || self.class.managed_tables.empty?
|
122
|
+
fail <<~EOS.chomp
|
123
|
+
A Projector must manage at least one table. Did you forget to add `managed_tables` to #{self.class.name}?
|
124
|
+
EOS
|
125
|
+
end
|
123
126
|
end
|
124
127
|
end
|
125
128
|
end
|