sequent 0.1.10 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|