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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/db/sequent_schema.rb +0 -3
  3. data/lib/sequent/configuration.rb +10 -0
  4. data/lib/sequent/core/aggregate_repository.rb +41 -8
  5. data/lib/sequent/core/aggregate_root.rb +0 -24
  6. data/lib/sequent/core/aggregate_snapshotter.rb +2 -2
  7. data/lib/sequent/core/base_command_handler.rb +2 -6
  8. data/lib/sequent/core/base_event_handler.rb +1 -1
  9. data/lib/sequent/core/command.rb +23 -15
  10. data/lib/sequent/core/command_service.rb +1 -1
  11. data/lib/sequent/core/core.rb +1 -1
  12. data/lib/sequent/core/event.rb +0 -21
  13. data/lib/sequent/core/event_record.rb +11 -1
  14. data/lib/sequent/core/event_store.rb +122 -18
  15. data/lib/sequent/core/ext/ext.rb +20 -0
  16. data/lib/sequent/core/helpers/array_with_type.rb +4 -0
  17. data/lib/sequent/core/helpers/association_validator.rb +22 -7
  18. data/lib/sequent/core/helpers/attribute_support.rb +15 -6
  19. data/lib/sequent/core/helpers/param_support.rb +28 -29
  20. data/lib/sequent/core/helpers/self_applier.rb +4 -3
  21. data/lib/sequent/core/helpers/string_to_value_parsers.rb +10 -3
  22. data/lib/sequent/core/helpers/type_conversion_support.rb +5 -0
  23. data/lib/sequent/core/helpers/uuid_helper.rb +2 -2
  24. data/lib/sequent/core/helpers/value_validators.rb +2 -2
  25. data/lib/sequent/core/random_uuid_generator.rb +9 -0
  26. data/lib/sequent/core/record_sessions/active_record_session.rb +11 -5
  27. data/lib/sequent/core/record_sessions/replay_events_session.rb +138 -108
  28. data/lib/sequent/core/sequent_oj.rb +4 -3
  29. data/lib/sequent/core/stream_record.rb +1 -1
  30. data/lib/sequent/core/transactions/active_record_transaction_provider.rb +1 -1
  31. data/lib/sequent/migrations/migrate_events.rb +22 -15
  32. data/lib/sequent/rake/tasks.rb +102 -0
  33. data/lib/sequent/sequent.rb +4 -0
  34. data/lib/sequent/support.rb +3 -0
  35. data/lib/sequent/support/database.rb +55 -0
  36. data/lib/sequent/support/view_projection.rb +58 -0
  37. data/lib/sequent/support/view_schema.rb +22 -0
  38. data/lib/sequent/test/command_handler_helpers.rb +21 -8
  39. data/lib/sequent/test/event_handler_helpers.rb +7 -3
  40. data/lib/version.rb +1 -1
  41. metadata +54 -9
  42. data/lib/sequent/core/tenant_event_store.rb +0 -24
@@ -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
@@ -16,6 +16,10 @@ module Sequent
16
16
  def to_s
17
17
  "Sequent::Core::Helpers::ArrayWithType.new(#{item_type})"
18
18
  end
19
+
20
+ def candidate?(value)
21
+ value.is_a?(Array)
22
+ end
19
23
  end
20
24
  end
21
25
  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[association] = "is not of type #{record.class.types[association]}"
36
- elsif value && value.kind_of?(Array)
37
- item_type = record.class.type_for(association).item_type
38
- record.errors[association] = "is invalid" unless validate_all(value, item_type).all?
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[association] = "is invalid" if value && value.invalid?
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
- !value.kind_of?(Array) && record.class.respond_to?(:types) && !value.kind_of?(record.class.types[association])
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
- .select { |val| val.respond_to?(:validation_errors) }
138
- .each_with_index do |val, index|
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.blank?
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 type.is_a? Sequent::Core::Helpers::ArrayWithType
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.strftime("%d-%m-%Y")
81
+ val.iso8601
73
82
  else
74
83
  val
75
84
  end
76
85
  end
77
86
 
78
- def make_params(root, hash, memo = {})
79
- hash.each do |k, v|
80
- key = "#{root}[#{k}]"
81
- if v.is_a? Hash
82
- make_params(key, v, memo)
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
- end
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
- Integer(value) unless value.blank?
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.strptime(value, "%d-%m-%Y")
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[@klass]
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
- SecureRandom.uuid
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{2}-\d{2}-\d{4}/
31
- !!Date.strptime(value, "%d-%m-%Y") rescue false
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)
@@ -0,0 +1,9 @@
1
+ module Sequent
2
+ module Core
3
+ module RandomUuidGenerator
4
+ def self.uuid
5
+ SecureRandom.uuid
6
+ end
7
+ end
8
+ end
9
+ end
@@ -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.merge(created_at: created_at))
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
- record.sequence_number = event.sequence_number if args[:update_sequence_number]
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
- return true if self.equal?(other)
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
- if indexed?(record_class)
119
- do_with_cache_keys(record_class, record) do |key|
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
- if indexed?(record_class)
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
- values = get_index(record_class, where_clause).map { |field| where_clause[field] }
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
- begin
210
- @record_store.each do |clazz, records|
211
- if records.size > @insert_with_csv_size
212
- csv = CSV.new("")
213
- column_names = clazz.column_names.reject { |name| name == "id" }
214
- records.each do |obj|
215
- begin
216
- csv << column_names.map do |column_name|
217
- obj[column_name]
218
- end
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
- buf = ''
223
- conn = ActiveRecord::Base.connection.raw_connection
224
- copy_data = StringIO.new csv.string
225
- conn.transaction do
226
- conn.exec("COPY #{clazz.table_name} (#{column_names.join(",")}) FROM STDIN WITH csv")
227
- begin
228
- while copy_data.read(1024, buf)
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
- else
251
-
252
- clazz.unscoped do
253
- inserts = []
254
- column_names = clazz.column_names.reject { |name| name == "id" }
255
- prepared_values = (1..column_names.size).map { |i| "$#{i}" }.join(",")
256
- records.each do |r|
257
- values = column_names.map { |name| r[name.to_sym] }
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