sequent 0.1.10 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/db/sequent_schema.rb +0 -3
- data/lib/sequent/configuration.rb +10 -0
- data/lib/sequent/core/aggregate_repository.rb +41 -8
- data/lib/sequent/core/aggregate_root.rb +0 -24
- data/lib/sequent/core/aggregate_snapshotter.rb +2 -2
- data/lib/sequent/core/base_command_handler.rb +2 -6
- data/lib/sequent/core/base_event_handler.rb +1 -1
- data/lib/sequent/core/command.rb +23 -15
- data/lib/sequent/core/command_service.rb +1 -1
- data/lib/sequent/core/core.rb +1 -1
- data/lib/sequent/core/event.rb +0 -21
- data/lib/sequent/core/event_record.rb +11 -1
- data/lib/sequent/core/event_store.rb +122 -18
- data/lib/sequent/core/ext/ext.rb +20 -0
- data/lib/sequent/core/helpers/array_with_type.rb +4 -0
- data/lib/sequent/core/helpers/association_validator.rb +22 -7
- data/lib/sequent/core/helpers/attribute_support.rb +15 -6
- data/lib/sequent/core/helpers/param_support.rb +28 -29
- data/lib/sequent/core/helpers/self_applier.rb +4 -3
- data/lib/sequent/core/helpers/string_to_value_parsers.rb +10 -3
- data/lib/sequent/core/helpers/type_conversion_support.rb +5 -0
- data/lib/sequent/core/helpers/uuid_helper.rb +2 -2
- data/lib/sequent/core/helpers/value_validators.rb +2 -2
- data/lib/sequent/core/random_uuid_generator.rb +9 -0
- data/lib/sequent/core/record_sessions/active_record_session.rb +11 -5
- data/lib/sequent/core/record_sessions/replay_events_session.rb +138 -108
- data/lib/sequent/core/sequent_oj.rb +4 -3
- data/lib/sequent/core/stream_record.rb +1 -1
- data/lib/sequent/core/transactions/active_record_transaction_provider.rb +1 -1
- data/lib/sequent/migrations/migrate_events.rb +22 -15
- data/lib/sequent/rake/tasks.rb +102 -0
- data/lib/sequent/sequent.rb +4 -0
- data/lib/sequent/support.rb +3 -0
- data/lib/sequent/support/database.rb +55 -0
- data/lib/sequent/support/view_projection.rb +58 -0
- data/lib/sequent/support/view_schema.rb +22 -0
- data/lib/sequent/test/command_handler_helpers.rb +21 -8
- data/lib/sequent/test/event_handler_helpers.rb +7 -3
- data/lib/version.rb +1 -1
- metadata +54 -9
- data/lib/sequent/core/tenant_event_store.rb +0 -24
data/lib/sequent/core/ext/ext.rb
CHANGED
@@ -22,6 +22,13 @@ class Float
|
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
|
+
class BigDecimal
|
26
|
+
def self.deserialize_from_json(value)
|
27
|
+
return nil if value.nil?
|
28
|
+
BigDecimal.new(value)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
25
32
|
class Boolean
|
26
33
|
def self.deserialize_from_json(value)
|
27
34
|
value.nil? ? nil : (value.present? ? value : false)
|
@@ -29,12 +36,25 @@ class Boolean
|
|
29
36
|
end
|
30
37
|
|
31
38
|
class Date
|
39
|
+
def self.from_params(value)
|
40
|
+
return value if value.is_a?(Date)
|
41
|
+
value.blank? ? nil : Date.iso8601(value.dup)
|
42
|
+
rescue ArgumentError
|
43
|
+
value
|
44
|
+
end
|
45
|
+
|
32
46
|
def self.deserialize_from_json(value)
|
33
47
|
value.blank? ? nil : Date.iso8601(value.dup)
|
34
48
|
end
|
35
49
|
end
|
36
50
|
|
37
51
|
class DateTime
|
52
|
+
def self.from_params(value)
|
53
|
+
value.blank? ? nil : DateTime.iso8601(value.dup)
|
54
|
+
rescue ArgumentError
|
55
|
+
value
|
56
|
+
end
|
57
|
+
|
38
58
|
def self.deserialize_from_json(value)
|
39
59
|
value.blank? ? nil : DateTime.iso8601(value.dup)
|
40
60
|
end
|
@@ -1,4 +1,5 @@
|
|
1
|
-
require 'active_model'
|
1
|
+
require 'active_model/validator'
|
2
|
+
|
2
3
|
module Sequent
|
3
4
|
module Core
|
4
5
|
module Helpers
|
@@ -32,12 +33,12 @@ module Sequent
|
|
32
33
|
associations.each do |association|
|
33
34
|
value = record.instance_variable_get("@#{association.to_s}")
|
34
35
|
if value && incorrect_type?(value, record, association)
|
35
|
-
record.errors
|
36
|
-
elsif value && value.
|
37
|
-
item_type = record.class.
|
38
|
-
record.errors
|
36
|
+
record.errors.add(association, "is not of type #{describe_type(record.class.types[association])}")
|
37
|
+
elsif value && value.is_a?(Array)
|
38
|
+
item_type = record.class.types.fetch(association).item_type
|
39
|
+
record.errors.add(association, "is invalid") unless validate_all(value, item_type).all?
|
39
40
|
else
|
40
|
-
record.errors
|
41
|
+
record.errors.add(association, "is invalid") if value && value.invalid?
|
41
42
|
end
|
42
43
|
end
|
43
44
|
end
|
@@ -45,7 +46,13 @@ module Sequent
|
|
45
46
|
private
|
46
47
|
|
47
48
|
def incorrect_type?(value, record, association)
|
48
|
-
|
49
|
+
return unless record.class.respond_to?(:types)
|
50
|
+
type = record.class.types[association]
|
51
|
+
if type.respond_to?(:candidate?)
|
52
|
+
!type.candidate?(value)
|
53
|
+
else
|
54
|
+
!value.is_a?(type)
|
55
|
+
end
|
49
56
|
end
|
50
57
|
|
51
58
|
def validate_all(values, item_type)
|
@@ -59,6 +66,14 @@ module Sequent
|
|
59
66
|
end
|
60
67
|
end
|
61
68
|
end
|
69
|
+
|
70
|
+
def describe_type(type)
|
71
|
+
if type.is_a?(ArrayWithType)
|
72
|
+
'array'
|
73
|
+
else
|
74
|
+
type.to_s
|
75
|
+
end
|
76
|
+
end
|
62
77
|
end
|
63
78
|
end
|
64
79
|
end
|
@@ -33,10 +33,6 @@ module Sequent
|
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
|
-
def type_for(name)
|
37
|
-
@types.find { |k, _| k == name }.last
|
38
|
-
end
|
39
|
-
|
40
36
|
def attrs(args)
|
41
37
|
@types ||= {}
|
42
38
|
@types.merge!(args)
|
@@ -122,6 +118,19 @@ EOS
|
|
122
118
|
hash
|
123
119
|
end
|
124
120
|
|
121
|
+
def as_json(opts = {})
|
122
|
+
hash = HashWithIndifferentAccess.new
|
123
|
+
self.class.types.each do |name, _|
|
124
|
+
value = self.instance_variable_get("@#{name}")
|
125
|
+
hash[name] = if value.respond_to?(:as_json)
|
126
|
+
value.as_json(opts)
|
127
|
+
else
|
128
|
+
value
|
129
|
+
end
|
130
|
+
end
|
131
|
+
hash
|
132
|
+
end
|
133
|
+
|
125
134
|
def update(changes)
|
126
135
|
self.class.new(attributes.merge(changes))
|
127
136
|
end
|
@@ -134,8 +143,8 @@ EOS
|
|
134
143
|
value.validation_errors.each { |k, v| result["#{field[0].to_s}_#{k.to_s}".to_sym] = v }
|
135
144
|
elsif field[1].class == ArrayWithType and value.present?
|
136
145
|
value
|
137
|
-
|
138
|
-
|
146
|
+
.select { |val| val.respond_to?(:validation_errors) }
|
147
|
+
.each_with_index do |val, index|
|
139
148
|
val.validation_errors.each do |k, v|
|
140
149
|
result["#{field[0].to_s}_#{index}_#{k.to_s}".to_sym] = v
|
141
150
|
end
|
@@ -11,9 +11,17 @@ module Sequent
|
|
11
11
|
#
|
12
12
|
module ParamSupport
|
13
13
|
module ClassMethods
|
14
|
-
def from_params(params = {})
|
15
|
-
allocate.tap { |x| x.from_params(params) }
|
14
|
+
def from_params(params = {}, strict_nil_check = true)
|
15
|
+
allocate.tap { |x| x.from_params(params, strict_nil_check) }
|
16
16
|
end
|
17
|
+
|
18
|
+
# Create an object based on HTTP form data
|
19
|
+
# This differs from form_params that an empty string
|
20
|
+
# is the same as nil since HTTP form post will send empty text fields
|
21
|
+
def from_form_data(params = {})
|
22
|
+
from_params(params, false)
|
23
|
+
end
|
24
|
+
|
17
25
|
end
|
18
26
|
|
19
27
|
# extend host class with class methods when we're included
|
@@ -21,18 +29,19 @@ module Sequent
|
|
21
29
|
host_class.extend(ClassMethods)
|
22
30
|
end
|
23
31
|
|
24
|
-
def from_params(params)
|
32
|
+
def from_params(params, strict_nil_check = true)
|
25
33
|
params = HashWithIndifferentAccess.new(params)
|
26
34
|
self.class.types.each do |attribute, type|
|
27
35
|
value = params[attribute]
|
28
36
|
|
29
|
-
next if value.
|
37
|
+
next if strict_nil_check && value.nil?
|
38
|
+
next if (!strict_nil_check && value.blank?)
|
30
39
|
if type.respond_to? :from_params
|
31
40
|
value = type.from_params(value)
|
32
|
-
elsif
|
41
|
+
elsif value.is_a?(Array)
|
33
42
|
value = value.map do |v|
|
34
43
|
if type.item_type.respond_to?(:from_params)
|
35
|
-
type.item_type.from_params(v)
|
44
|
+
type.item_type.from_params(v, strict_nil_check)
|
36
45
|
else
|
37
46
|
v
|
38
47
|
end
|
@@ -53,7 +62,7 @@ module Sequent
|
|
53
62
|
next if field[0] == "errors"
|
54
63
|
hash[field[0]] = if value.kind_of?(Array)
|
55
64
|
next if value.blank?
|
56
|
-
value.map{|v|value_to_string(v)}
|
65
|
+
value.map { |v| value_to_string(v) }
|
57
66
|
else
|
58
67
|
value_to_string(value)
|
59
68
|
end
|
@@ -69,34 +78,24 @@ module Sequent
|
|
69
78
|
elsif val.is_a? DateTime
|
70
79
|
val.iso8601
|
71
80
|
elsif val.is_a? Date
|
72
|
-
val.
|
81
|
+
val.iso8601
|
73
82
|
else
|
74
83
|
val
|
75
84
|
end
|
76
85
|
end
|
77
86
|
|
78
|
-
def make_params(
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
elsif v.is_a?(Array) && v.first.is_a?(Hash)
|
84
|
-
key = "#{key}[]"
|
85
|
-
v.each { |value| make_params(key, value, memo) }
|
86
|
-
elsif v.is_a?(Array)
|
87
|
-
memo["#{key}[]"] = v
|
88
|
-
else
|
89
|
-
string_value = v.nil? ? "" : v.to_s
|
90
|
-
if memo.has_key?(key)
|
91
|
-
if memo[:key].is_a? Array
|
92
|
-
memo[key] << string_value
|
93
|
-
else
|
94
|
-
memo[key] = [memo[key], string_value]
|
95
|
-
end
|
96
|
-
else
|
97
|
-
memo[key] = string_value
|
87
|
+
def make_params(key, enumerable, memo = {})
|
88
|
+
case enumerable
|
89
|
+
when Array
|
90
|
+
enumerable.each_with_index do |object, index|
|
91
|
+
make_params("#{key}[#{index}]", object, memo)
|
98
92
|
end
|
99
|
-
|
93
|
+
when Hash
|
94
|
+
enumerable.each do |hash_key, object|
|
95
|
+
make_params("#{key}[#{hash_key}]", object, memo)
|
96
|
+
end
|
97
|
+
else
|
98
|
+
memo[key] = enumerable
|
100
99
|
end
|
101
100
|
memo
|
102
101
|
end
|
@@ -17,7 +17,6 @@ module Sequent
|
|
17
17
|
module SelfApplier
|
18
18
|
|
19
19
|
module ClassMethods
|
20
|
-
|
21
20
|
def on(*message_classes, &block)
|
22
21
|
message_classes.each { |message_class| message_mapping[message_class] = block }
|
23
22
|
end
|
@@ -25,6 +24,10 @@ module Sequent
|
|
25
24
|
def message_mapping
|
26
25
|
@message_mapping ||= {}
|
27
26
|
end
|
27
|
+
|
28
|
+
def handles_message?(message)
|
29
|
+
message_mapping.keys.include? message.class
|
30
|
+
end
|
28
31
|
end
|
29
32
|
|
30
33
|
def self.included(host_class)
|
@@ -35,9 +38,7 @@ module Sequent
|
|
35
38
|
handler = self.class.message_mapping[message.class]
|
36
39
|
self.instance_exec(message, &handler) if handler
|
37
40
|
end
|
38
|
-
|
39
41
|
end
|
40
|
-
|
41
42
|
end
|
42
43
|
end
|
43
44
|
end
|
@@ -9,6 +9,7 @@ module Sequent
|
|
9
9
|
::Symbol => ->(value) { Symbol.deserialize_from_json(value) },
|
10
10
|
::String => ->(value) { value },
|
11
11
|
::Integer => ->(value) { parse_to_integer(value) },
|
12
|
+
::BigDecimal => ->(value) { parse_to_bigdecimal(value) },
|
12
13
|
::Float => ->(value) { parse_to_float(value) },
|
13
14
|
::Boolean => ->(value) { parse_to_bool(value) },
|
14
15
|
::Date => ->(value) { parse_to_date(value) },
|
@@ -17,7 +18,12 @@ module Sequent
|
|
17
18
|
}
|
18
19
|
|
19
20
|
def self.parse_to_integer(value)
|
20
|
-
|
21
|
+
return value if value.is_a?(Integer)
|
22
|
+
Integer(value, 10) unless value.blank?
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.parse_to_bigdecimal(value)
|
26
|
+
BigDecimal.new(value) unless value.blank?
|
21
27
|
end
|
22
28
|
|
23
29
|
def self.parse_to_float(value)
|
@@ -34,7 +40,7 @@ module Sequent
|
|
34
40
|
|
35
41
|
def self.parse_to_date(value)
|
36
42
|
return if value.blank?
|
37
|
-
value.is_a?(Date) ? value : Date.
|
43
|
+
value.is_a?(Date) ? value : Date.iso8601(value.dup)
|
38
44
|
end
|
39
45
|
|
40
46
|
def self.parse_to_date_time(value)
|
@@ -42,6 +48,7 @@ module Sequent
|
|
42
48
|
end
|
43
49
|
|
44
50
|
def self.parse_array(values, type_in_array)
|
51
|
+
fail "invalid value for array(): \"#{values}\"" unless values.is_a?(Array)
|
45
52
|
values.map do |item|
|
46
53
|
if item.respond_to?(:parse_attrs_to_correct_types)
|
47
54
|
item.parse_attrs_to_correct_types
|
@@ -65,7 +72,7 @@ module Sequent
|
|
65
72
|
end
|
66
73
|
|
67
74
|
def parse_from_string(value)
|
68
|
-
parser = PARSERS
|
75
|
+
parser = PARSERS.fetch(@klass) { |key| fail "Unsupported value type: #{key}" }
|
69
76
|
if @array_with_type
|
70
77
|
parser.call(value, @array_with_type.item_type)
|
71
78
|
else
|
@@ -2,7 +2,10 @@ require 'active_model'
|
|
2
2
|
|
3
3
|
module Sequent
|
4
4
|
module Core
|
5
|
+
TypeConversionError = Class.new(RuntimeError)
|
6
|
+
|
5
7
|
module Helpers
|
8
|
+
|
6
9
|
# Will parse all values to the correct types.
|
7
10
|
# The raw values are typically posted from the web and are therefor mostly strings.
|
8
11
|
# To parse a raw value your class must have a parse_from_string method that returns the parsed values.
|
@@ -21,6 +24,8 @@ module Sequent
|
|
21
24
|
end
|
22
25
|
end
|
23
26
|
the_copy
|
27
|
+
rescue => e
|
28
|
+
raise TypeConversionError, e.message
|
24
29
|
end
|
25
30
|
end
|
26
31
|
end
|
@@ -6,11 +6,11 @@ module Sequent
|
|
6
6
|
module UuidHelper
|
7
7
|
|
8
8
|
def new_uuid
|
9
|
-
|
9
|
+
warn "DEPRECATION WARNING: Sequent::Core::Helpers::UuidHelper.new_uuid is deprecated. Use Sequent.new_uuid instead"
|
10
|
+
Sequent.new_uuid
|
10
11
|
end
|
11
12
|
|
12
13
|
module_function :new_uuid
|
13
|
-
|
14
14
|
end
|
15
15
|
end
|
16
16
|
end
|
@@ -27,8 +27,8 @@ module Sequent
|
|
27
27
|
def self.valid_date?(value)
|
28
28
|
return true if value.blank?
|
29
29
|
return true if value.is_a?(Date)
|
30
|
-
return false unless value =~ /\d{
|
31
|
-
!!Date.
|
30
|
+
return false unless value =~ /\d{4}-\d{2}-\d{2}/
|
31
|
+
!!Date.iso8601(value) rescue false
|
32
32
|
end
|
33
33
|
|
34
34
|
def self.valid_date_time?(value)
|
@@ -11,16 +11,21 @@ module Sequent
|
|
11
11
|
class ActiveRecordSession
|
12
12
|
|
13
13
|
def update_record(record_class, event, where_clause = {aggregate_id: event.aggregate_id}, options = {}, &block)
|
14
|
-
defaults = {update_sequence_number: true}
|
15
|
-
args = defaults.merge(options)
|
16
14
|
record = record_class.unscoped.where(where_clause).first
|
17
15
|
raise("Record of class #{record_class} with where clause #{where_clause} not found while handling event #{event}") unless record
|
18
|
-
yield record if block_given?
|
19
|
-
record.sequence_number = event.sequence_number if args[:update_sequence_number]
|
20
16
|
record.updated_at = event.created_at if record.respond_to?(:updated_at)
|
17
|
+
yield record if block_given?
|
18
|
+
update_sequence_number = options.key?(:update_sequence_number) ?
|
19
|
+
options[:update_sequence_number] :
|
20
|
+
record.respond_to?(:sequence_number=)
|
21
|
+
record.sequence_number = event.sequence_number if update_sequence_number
|
21
22
|
record.save!
|
22
23
|
end
|
23
24
|
|
25
|
+
def execute(statement)
|
26
|
+
ActiveRecord::Base.connection.execute(statement)
|
27
|
+
end
|
28
|
+
|
24
29
|
def create_record(record_class, values)
|
25
30
|
record = new_record(record_class, values)
|
26
31
|
yield record if block_given?
|
@@ -31,7 +36,8 @@ module Sequent
|
|
31
36
|
def create_or_update_record(record_class, values, created_at = Time.now)
|
32
37
|
record = get_record(record_class, values)
|
33
38
|
unless record
|
34
|
-
record = new_record(record_class, values
|
39
|
+
record = new_record(record_class, values)
|
40
|
+
record.created_at = created_at if record.respond_to?(:created_at)
|
35
41
|
end
|
36
42
|
yield record
|
37
43
|
record.save!
|
@@ -52,6 +52,92 @@ module Sequent
|
|
52
52
|
end
|
53
53
|
end
|
54
54
|
|
55
|
+
class Index
|
56
|
+
def initialize(indexed_columns)
|
57
|
+
@indexed_columns = Hash.new do |hash, record_class|
|
58
|
+
if record_class.column_names.include? 'aggregate_id'
|
59
|
+
hash[record_class] = [:aggregate_id]
|
60
|
+
else
|
61
|
+
hash[record_class] = []
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
@indexed_columns.merge!(indexed_columns)
|
66
|
+
|
67
|
+
@index = {}
|
68
|
+
@reverse_index = {}
|
69
|
+
end
|
70
|
+
|
71
|
+
def add(record_class, record)
|
72
|
+
return unless indexed?(record_class)
|
73
|
+
|
74
|
+
get_keys(record_class, record).each do |key|
|
75
|
+
@index[key.hash] = [] unless @index.has_key? key.hash
|
76
|
+
@index[key.hash] << record
|
77
|
+
|
78
|
+
@reverse_index[record.object_id.hash] = [] unless @reverse_index.has_key? record.object_id.hash
|
79
|
+
@reverse_index[record.object_id.hash] << key.hash
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def remove(record_class, record)
|
84
|
+
return unless indexed?(record_class)
|
85
|
+
|
86
|
+
keys = @reverse_index.delete(record.object_id.hash) { [] }
|
87
|
+
|
88
|
+
return unless keys.any?
|
89
|
+
|
90
|
+
keys.each do |key|
|
91
|
+
@index[key].delete(record)
|
92
|
+
@index.delete(key) if @index[key].count == 0
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def update(record_class, record)
|
97
|
+
remove(record_class, record)
|
98
|
+
add(record_class, record)
|
99
|
+
end
|
100
|
+
|
101
|
+
def find(record_class, where_clause)
|
102
|
+
key = [record_class.name]
|
103
|
+
get_index(record_class, where_clause).each do |field|
|
104
|
+
key << field
|
105
|
+
key << where_clause[field]
|
106
|
+
end
|
107
|
+
@index[key.hash] || []
|
108
|
+
end
|
109
|
+
|
110
|
+
def clear
|
111
|
+
@index = {}
|
112
|
+
@reverse_index = {}
|
113
|
+
end
|
114
|
+
|
115
|
+
def use_index?(record_class, where_clause)
|
116
|
+
@indexed_columns.has_key?(record_class) && @indexed_columns[record_class].any? { |indexed_where| where_clause.keys.size == indexed_where.size && (where_clause.keys - indexed_where).empty? }
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def indexed?(record_class)
|
122
|
+
@indexed_columns.has_key?(record_class)
|
123
|
+
end
|
124
|
+
|
125
|
+
def get_keys(record_class, record)
|
126
|
+
@indexed_columns[record_class].map do |index|
|
127
|
+
arr = [record_class.name]
|
128
|
+
index.each do |key|
|
129
|
+
arr << key
|
130
|
+
arr << record[key]
|
131
|
+
end
|
132
|
+
arr
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def get_index(record_class, where_clause)
|
137
|
+
@indexed_columns[record_class].find { |indexed_where| where_clause.keys.size == indexed_where.size && (where_clause.keys - indexed_where).empty? }
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
55
141
|
# +insert_with_csv_size+ number of records to insert in a single batch
|
56
142
|
#
|
57
143
|
# +indices+ Hash of indices to create in memory. Greatly speeds up the replaying.
|
@@ -60,17 +146,18 @@ module Sequent
|
|
60
146
|
def initialize(insert_with_csv_size = 50, indices = {})
|
61
147
|
@insert_with_csv_size = insert_with_csv_size
|
62
148
|
@record_store = Hash.new { |h, k| h[k] = Set.new }
|
63
|
-
@record_index =
|
64
|
-
@indices = indices
|
149
|
+
@record_index = Index.new(indices)
|
65
150
|
end
|
66
151
|
|
67
152
|
def update_record(record_class, event, where_clause = {aggregate_id: event.aggregate_id}, options = {}, &block)
|
68
|
-
defaults = {update_sequence_number: true}
|
69
|
-
args = defaults.merge(options)
|
70
153
|
record = get_record!(record_class, where_clause)
|
71
154
|
record.updated_at = event.created_at if record.respond_to?(:updated_at)
|
72
155
|
yield record if block_given?
|
73
|
-
|
156
|
+
@record_index.update(record_class, record)
|
157
|
+
update_sequence_number = options.key?(:update_sequence_number) ?
|
158
|
+
options[:update_sequence_number] :
|
159
|
+
record.respond_to?(:sequence_number=)
|
160
|
+
record.sequence_number = event.sequence_number if update_sequence_number
|
74
161
|
end
|
75
162
|
|
76
163
|
def create_record(record_class, values)
|
@@ -81,11 +168,6 @@ module Sequent
|
|
81
168
|
if self.class.struct_cache.has_key?(struct_class_name)
|
82
169
|
struct_class = self.class.struct_cache[struct_class_name]
|
83
170
|
else
|
84
|
-
if column_names.include? :aggregate_id
|
85
|
-
@indices[record_class] ||= []
|
86
|
-
@indices[record_class] << [:aggregate_id]
|
87
|
-
end
|
88
|
-
|
89
171
|
# We create a struct on the fly.
|
90
172
|
# Since the replay happens in memory we implement the ==, eql? and hash methods
|
91
173
|
# to point to the same object. A record is the same if and only if they point to
|
@@ -95,11 +177,7 @@ module Sequent
|
|
95
177
|
class #{struct_class_name}
|
96
178
|
include InitStruct
|
97
179
|
def ==(other)
|
98
|
-
|
99
|
-
super
|
100
|
-
end
|
101
|
-
def eql?(other)
|
102
|
-
self == other
|
180
|
+
self.equal?(other)
|
103
181
|
end
|
104
182
|
def hash
|
105
183
|
self.object_id.hash
|
@@ -115,12 +193,8 @@ module Sequent
|
|
115
193
|
yield record if block_given?
|
116
194
|
@record_store[record_class] << record
|
117
195
|
|
118
|
-
|
119
|
-
|
120
|
-
@record_index[key] = [] unless @record_index.has_key?(key)
|
121
|
-
@record_index[key] << record
|
122
|
-
end
|
123
|
-
end
|
196
|
+
@record_index.add(record_class, record)
|
197
|
+
|
124
198
|
record
|
125
199
|
end
|
126
200
|
|
@@ -130,6 +204,7 @@ module Sequent
|
|
130
204
|
record = create_record(record_class, values.merge(created_at: created_at))
|
131
205
|
end
|
132
206
|
yield record if block_given?
|
207
|
+
@record_index.update(record_class, record)
|
133
208
|
record
|
134
209
|
end
|
135
210
|
|
@@ -152,11 +227,7 @@ module Sequent
|
|
152
227
|
|
153
228
|
def delete_record(record_class, record)
|
154
229
|
@record_store[record_class].delete(record)
|
155
|
-
|
156
|
-
do_with_cache_keys(record_class, record) do |key|
|
157
|
-
@record_index[key].delete(record) if @record_index.has_key?(key)
|
158
|
-
end
|
159
|
-
end
|
230
|
+
@record_index.remove(record_class, record)
|
160
231
|
end
|
161
232
|
|
162
233
|
def update_all_records(record_class, where_clause, updates)
|
@@ -164,6 +235,7 @@ module Sequent
|
|
164
235
|
updates.each_pair do |k, v|
|
165
236
|
record[k.to_sym] = v
|
166
237
|
end
|
238
|
+
@record_index.update(record_class, record)
|
167
239
|
end
|
168
240
|
end
|
169
241
|
|
@@ -171,18 +243,19 @@ module Sequent
|
|
171
243
|
records = find_records(record_class, where_clause)
|
172
244
|
records.each do |record|
|
173
245
|
yield record
|
246
|
+
@record_index.update(record_class, record)
|
174
247
|
end
|
175
248
|
end
|
176
249
|
|
177
250
|
def do_with_record(record_class, where_clause)
|
178
251
|
record = get_record!(record_class, where_clause)
|
179
252
|
yield record
|
253
|
+
@record_index.update(record_class, record)
|
180
254
|
end
|
181
255
|
|
182
256
|
def find_records(record_class, where_clause)
|
183
|
-
if use_index?(record_class, where_clause)
|
184
|
-
|
185
|
-
@record_index[[record_class, *values]] || []
|
257
|
+
if @record_index.use_index?(record_class, where_clause)
|
258
|
+
@record_index.find(record_class, where_clause)
|
186
259
|
else
|
187
260
|
@record_store[record_class].select do |record|
|
188
261
|
where_clause.all? do |k, v|
|
@@ -195,7 +268,6 @@ module Sequent
|
|
195
268
|
actual_value == expected_value
|
196
269
|
end
|
197
270
|
end
|
198
|
-
|
199
271
|
end
|
200
272
|
end.dup
|
201
273
|
end
|
@@ -206,101 +278,59 @@ module Sequent
|
|
206
278
|
end
|
207
279
|
|
208
280
|
def commit
|
209
|
-
|
210
|
-
@
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
281
|
+
@record_store.each do |clazz, records|
|
282
|
+
@column_cache ||= {}
|
283
|
+
@column_cache[clazz.name] ||= clazz.columns.reduce({}) do |hash, column|
|
284
|
+
hash.merge({ column.name => column })
|
285
|
+
end
|
286
|
+
if records.size > @insert_with_csv_size
|
287
|
+
csv = CSV.new("")
|
288
|
+
column_names = clazz.column_names.reject { |name| name == "id" }
|
289
|
+
records.each do |obj|
|
290
|
+
begin
|
291
|
+
csv << column_names.map do |column_name|
|
292
|
+
@column_cache[clazz.name][column_name].type_cast_for_database(obj[column_name])
|
219
293
|
end
|
220
294
|
end
|
295
|
+
end
|
221
296
|
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
### Uncomment this to test error-handling for exceptions from the reader side:
|
230
|
-
# raise Errno::ECONNRESET, "socket closed while reading"
|
231
|
-
until conn.put_copy_data(buf)
|
232
|
-
sleep 0.1
|
233
|
-
end
|
234
|
-
end
|
235
|
-
rescue Errno => err
|
236
|
-
errmsg = "%s while reading copy data: %s" % [err.class.name, err.message]
|
237
|
-
conn.put_copy_end(errmsg)
|
238
|
-
ensure
|
239
|
-
conn.put_copy_end
|
240
|
-
copy_data.close
|
241
|
-
while res = conn.get_result
|
242
|
-
status = res.res_status(res.result_status)
|
243
|
-
if status != "PGRES_COMMAND_OK"
|
244
|
-
raise "Postgres copy command failed: #{status}, #{res.error_message}"
|
245
|
-
end
|
246
|
-
end
|
297
|
+
buf = ''
|
298
|
+
conn = ActiveRecord::Base.connection.raw_connection
|
299
|
+
copy_data = StringIO.new csv.string
|
300
|
+
conn.transaction do
|
301
|
+
conn.copy_data("COPY #{clazz.table_name} (#{column_names.join(",")}) FROM STDIN WITH csv") do
|
302
|
+
while copy_data.read(1024, buf)
|
303
|
+
conn.put_copy_data(buf)
|
247
304
|
end
|
248
305
|
end
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
inserts << values
|
259
|
-
end
|
260
|
-
sql = %Q{insert into #{clazz.table_name} (#{column_names.join(",")}) values (#{prepared_values})}
|
261
|
-
inserts.each do |insert|
|
262
|
-
clazz.connection.raw_connection.async_exec(sql, insert)
|
306
|
+
end
|
307
|
+
else
|
308
|
+
clazz.unscoped do
|
309
|
+
inserts = []
|
310
|
+
column_names = clazz.column_names.reject { |name| name == "id" }
|
311
|
+
prepared_values = (1..column_names.size).map { |i| "$#{i}" }.join(",")
|
312
|
+
records.each do |r|
|
313
|
+
values = column_names.map do |column_name|
|
314
|
+
@column_cache[clazz.name][column_name].type_cast_for_database(r[column_name.to_sym])
|
263
315
|
end
|
316
|
+
inserts << values
|
317
|
+
end
|
318
|
+
sql = %Q{insert into #{clazz.table_name} (#{column_names.join(",")}) values (#{prepared_values})}
|
319
|
+
inserts.each do |insert|
|
320
|
+
clazz.connection.raw_connection.async_exec(sql, insert)
|
264
321
|
end
|
265
322
|
end
|
266
323
|
end
|
267
|
-
|
268
|
-
|
269
|
-
ensure
|
270
|
-
clear
|
271
324
|
end
|
325
|
+
ensure
|
326
|
+
clear
|
272
327
|
end
|
273
328
|
|
274
329
|
def clear
|
275
330
|
@record_store.clear
|
276
331
|
@record_index.clear
|
277
332
|
end
|
278
|
-
|
279
|
-
private
|
280
|
-
def indexed?(record_class)
|
281
|
-
@indices.has_key?(record_class)
|
282
|
-
end
|
283
|
-
|
284
|
-
def do_with_cache_keys(record_class, record)
|
285
|
-
@indices[record_class].each do |index|
|
286
|
-
cache_key = [record_class]
|
287
|
-
index.each do |key|
|
288
|
-
cache_key << record[key]
|
289
|
-
end
|
290
|
-
yield cache_key
|
291
|
-
end
|
292
|
-
end
|
293
|
-
|
294
|
-
def use_index?(record_class, where_clause)
|
295
|
-
@indices.has_key?(record_class) and @indices[record_class].any? { |indexed_where| where_clause.keys.size == indexed_where.size and (where_clause.keys - indexed_where).empty? }
|
296
|
-
end
|
297
|
-
|
298
|
-
def get_index(record_class, where_clause)
|
299
|
-
@indices[record_class].find { |indexed_where| where_clause.keys.size == indexed_where.size and (where_clause.keys - indexed_where).empty? }
|
300
|
-
end
|
301
|
-
|
302
333
|
end
|
303
|
-
|
304
334
|
end
|
305
335
|
end
|
306
336
|
end
|