sequent 3.3.1 → 4.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/sequent +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
|