sequent 0.1.10 → 1.0.0

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