sequent 3.3.1 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/bin/sequent +31 -25
  3. data/lib/notices.rb +6 -0
  4. data/lib/sequent/application_record.rb +2 -0
  5. data/lib/sequent/configuration.rb +29 -29
  6. data/lib/sequent/core/aggregate_repository.rb +24 -14
  7. data/lib/sequent/core/aggregate_root.rb +16 -7
  8. data/lib/sequent/core/aggregate_roots.rb +24 -0
  9. data/lib/sequent/core/aggregate_snapshotter.rb +8 -5
  10. data/lib/sequent/core/base_command_handler.rb +4 -2
  11. data/lib/sequent/core/command.rb +30 -11
  12. data/lib/sequent/core/command_record.rb +12 -4
  13. data/lib/sequent/core/command_service.rb +41 -25
  14. data/lib/sequent/core/core.rb +2 -0
  15. data/lib/sequent/core/current_event.rb +2 -0
  16. data/lib/sequent/core/event.rb +16 -11
  17. data/lib/sequent/core/event_publisher.rb +20 -15
  18. data/lib/sequent/core/event_record.rb +7 -7
  19. data/lib/sequent/core/event_store.rb +75 -49
  20. data/lib/sequent/core/ext/ext.rb +9 -1
  21. data/lib/sequent/core/helpers/array_with_type.rb +4 -1
  22. data/lib/sequent/core/helpers/association_validator.rb +9 -7
  23. data/lib/sequent/core/helpers/attribute_support.rb +64 -33
  24. data/lib/sequent/core/helpers/autoset_attributes.rb +4 -4
  25. data/lib/sequent/core/helpers/boolean_validator.rb +6 -1
  26. data/lib/sequent/core/helpers/copyable.rb +2 -2
  27. data/lib/sequent/core/helpers/date_time_validator.rb +4 -1
  28. data/lib/sequent/core/helpers/date_validator.rb +6 -1
  29. data/lib/sequent/core/helpers/default_validators.rb +12 -10
  30. data/lib/sequent/core/helpers/equal_support.rb +8 -6
  31. data/lib/sequent/core/helpers/helpers.rb +2 -0
  32. data/lib/sequent/core/helpers/mergable.rb +6 -4
  33. data/lib/sequent/core/helpers/message_handler.rb +3 -1
  34. data/lib/sequent/core/helpers/param_support.rb +19 -15
  35. data/lib/sequent/core/helpers/secret.rb +14 -12
  36. data/lib/sequent/core/helpers/string_support.rb +5 -4
  37. data/lib/sequent/core/helpers/string_to_value_parsers.rb +7 -2
  38. data/lib/sequent/core/helpers/string_validator.rb +6 -1
  39. data/lib/sequent/core/helpers/type_conversion_support.rb +5 -3
  40. data/lib/sequent/core/helpers/uuid_helper.rb +5 -2
  41. data/lib/sequent/core/helpers/value_validators.rb +23 -9
  42. data/lib/sequent/core/persistors/active_record_persistor.rb +19 -9
  43. data/lib/sequent/core/persistors/persistor.rb +16 -14
  44. data/lib/sequent/core/persistors/persistors.rb +2 -0
  45. data/lib/sequent/core/persistors/replay_optimized_postgres_persistor.rb +70 -47
  46. data/lib/sequent/core/projector.rb +25 -22
  47. data/lib/sequent/core/random_uuid_generator.rb +2 -0
  48. data/lib/sequent/core/sequent_oj.rb +2 -0
  49. data/lib/sequent/core/stream_record.rb +9 -3
  50. data/lib/sequent/core/transactions/active_record_transaction_provider.rb +7 -9
  51. data/lib/sequent/core/transactions/no_transactions.rb +2 -1
  52. data/lib/sequent/core/transactions/transactions.rb +2 -0
  53. data/lib/sequent/core/value_object.rb +8 -10
  54. data/lib/sequent/core/workflow.rb +7 -5
  55. data/lib/sequent/generator/aggregate.rb +16 -10
  56. data/lib/sequent/generator/command.rb +26 -19
  57. data/lib/sequent/generator/event.rb +19 -17
  58. data/lib/sequent/generator/generator.rb +6 -0
  59. data/lib/sequent/generator/project.rb +3 -1
  60. data/lib/sequent/generator/template_project/Gemfile +1 -1
  61. data/lib/sequent/generator/template_project/spec/app/projectors/post_projector_spec.rb +1 -1
  62. data/lib/sequent/generator/template_project/spec/lib/post/post_command_handler_spec.rb +1 -1
  63. data/lib/sequent/generator.rb +3 -4
  64. data/lib/sequent/migrations/executor.rb +30 -9
  65. data/lib/sequent/migrations/functions.rb +5 -6
  66. data/lib/sequent/migrations/migrate_events.rb +12 -9
  67. data/lib/sequent/migrations/migrations.rb +2 -1
  68. data/lib/sequent/migrations/planner.rb +33 -23
  69. data/lib/sequent/migrations/projectors.rb +4 -3
  70. data/lib/sequent/migrations/sql.rb +2 -0
  71. data/lib/sequent/migrations/view_schema.rb +93 -44
  72. data/lib/sequent/rake/migration_tasks.rb +59 -23
  73. data/lib/sequent/rake/tasks.rb +5 -2
  74. data/lib/sequent/sequent.rb +6 -1
  75. data/lib/sequent/support/database.rb +39 -17
  76. data/lib/sequent/support/view_projection.rb +6 -3
  77. data/lib/sequent/support/view_schema.rb +2 -0
  78. data/lib/sequent/support.rb +2 -0
  79. data/lib/sequent/test/command_handler_helpers.rb +39 -17
  80. data/lib/sequent/test/event_handler_helpers.rb +10 -4
  81. data/lib/sequent/test/event_stream_helpers.rb +7 -3
  82. data/lib/sequent/test/time_comparison.rb +12 -5
  83. data/lib/sequent/test.rb +2 -0
  84. data/lib/sequent/util/dry_run.rb +194 -0
  85. data/lib/sequent/util/printer.rb +6 -5
  86. data/lib/sequent/util/skip_if_already_processing.rb +21 -5
  87. data/lib/sequent/util/timer.rb +2 -0
  88. data/lib/sequent/util/util.rb +3 -0
  89. data/lib/sequent.rb +4 -0
  90. data/lib/version.rb +3 -1
  91. metadata +110 -59
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_model'
2
4
 
3
5
  module Sequent
@@ -5,7 +7,6 @@ module Sequent
5
7
  TypeConversionError = Class.new(RuntimeError)
6
8
 
7
9
  module Helpers
8
-
9
10
  # Will parse all values to the correct types.
10
11
  # The raw values are typically posted from the web and are therefor mostly strings.
11
12
  # To parse a raw value your class must have a parse_from_string method that returns the parsed values.
@@ -14,8 +15,9 @@ module Sequent
14
15
  def parse_attrs_to_correct_types
15
16
  the_copy = dup
16
17
  the_copy.class.types.each do |name, type|
17
- raw_value = the_copy.send("#{name}")
18
+ raw_value = the_copy.send(name.to_s)
18
19
  next if raw_value.nil?
20
+
19
21
  if raw_value.respond_to?(:parse_attrs_to_correct_types)
20
22
  the_copy.send("#{name}=", raw_value.parse_attrs_to_correct_types)
21
23
  else
@@ -24,7 +26,7 @@ module Sequent
24
26
  end
25
27
  end
26
28
  the_copy
27
- rescue => e
29
+ rescue StandardError => e
28
30
  raise TypeConversionError, e.message
29
31
  end
30
32
  end
@@ -1,12 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'securerandom'
2
4
 
3
5
  module Sequent
4
6
  module Core
5
7
  module Helpers
6
8
  module UuidHelper
7
-
8
9
  def new_uuid
9
- warn "DEPRECATION WARNING: Sequent::Core::Helpers::UuidHelper.new_uuid is deprecated. Use Sequent.new_uuid instead"
10
+ warn <<~EOS
11
+ DEPRECATION WARNING: Sequent::Core::Helpers::UuidHelper.new_uuid is deprecated. Use Sequent.new_uuid instead
12
+ EOS
10
13
  Sequent.new_uuid
11
14
  end
12
15
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../ext/ext'
2
4
 
3
5
  module Sequent
@@ -6,7 +8,7 @@ module Sequent
6
8
  class ValueValidators
7
9
  INVALID_STRING_CHARS = [
8
10
  "\u0000",
9
- ]
11
+ ].freeze
10
12
 
11
13
  VALIDATORS = {
12
14
  ::Symbol => ->(_) { true },
@@ -14,36 +16,48 @@ module Sequent
14
16
  ::Integer => ->(value) { valid_integer?(value) },
15
17
  ::Boolean => ->(value) { valid_bool?(value) },
16
18
  ::Date => ->(value) { valid_date?(value) },
17
- ::DateTime => ->(value) { valid_date_time?(value) }
18
- }
19
+ ::DateTime => ->(value) { valid_date_time?(value) },
20
+ }.freeze
19
21
 
20
22
  def self.valid_integer?(value)
21
23
  value.blank? || Integer(value)
22
- rescue
24
+ rescue StandardError
23
25
  false
24
26
  end
25
27
 
26
28
  def self.valid_bool?(value)
27
29
  return true if value.blank?
28
- value.is_a?(TrueClass) || value.is_a?(FalseClass) || value == "true" || value == "false"
30
+
31
+ value.is_a?(TrueClass) || value.is_a?(FalseClass) || value == 'true' || value == 'false'
29
32
  end
30
33
 
31
34
  def self.valid_date?(value)
32
35
  return true if value.blank?
33
36
  return true if value.is_a?(Date)
34
37
  return false unless value =~ /\d{4}-\d{2}-\d{2}/
35
- !!Date.iso8601(value) rescue false
38
+
39
+ begin
40
+ !!Date.iso8601(value)
41
+ rescue StandardError
42
+ false
43
+ end
36
44
  end
37
45
 
38
46
  def self.valid_date_time?(value)
39
47
  return true if value.blank?
40
- value.is_a?(DateTime) || !!DateTime.iso8601(value.dup) rescue false
48
+
49
+ begin
50
+ value.is_a?(DateTime) || !!DateTime.iso8601(value.dup)
51
+ rescue StandardError
52
+ false
53
+ end
41
54
  end
42
55
 
43
56
  def self.valid_string?(value)
44
57
  return true if value.nil?
45
- value.to_s && !INVALID_STRING_CHARS.any? { |invalid_char| value.to_s.include?(invalid_char) }
46
- rescue
58
+
59
+ value.to_s && INVALID_STRING_CHARS.none? { |invalid_char| value.to_s.include?(invalid_char) }
60
+ rescue StandardError
47
61
  false
48
62
  end
49
63
 
@@ -1,10 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_record'
2
4
  require_relative './persistor'
3
5
 
4
6
  module Sequent
5
7
  module Core
6
8
  module Persistors
7
-
8
9
  #
9
10
  # The ActiveRecordPersistor uses ActiveRecord to update the projection
10
11
  #
@@ -17,14 +18,21 @@ module Sequent
17
18
  class ActiveRecordPersistor
18
19
  include Persistor
19
20
 
20
- def update_record(record_class, event, where_clause = {aggregate_id: event.aggregate_id}, options = {}, &block)
21
+ def update_record(record_class, event, where_clause = {aggregate_id: event.aggregate_id}, options = {})
21
22
  record = record_class.unscoped.where(where_clause).first
22
- raise("Record of class #{record_class} with where clause #{where_clause} not found while handling event #{event}") unless record
23
+ unless record
24
+ fail(<<~EOS)
25
+ Record of class #{record_class} with where clause #{where_clause} not found while handling event #{event}
26
+ EOS
27
+ end
28
+
23
29
  record.updated_at = event.created_at if record.respond_to?(:updated_at)
24
30
  yield record if block_given?
25
- update_sequence_number = options.key?(:update_sequence_number) ?
26
- options[:update_sequence_number] :
31
+ update_sequence_number = if options.key?(:update_sequence_number)
32
+ options[:update_sequence_number]
33
+ else
27
34
  record.respond_to?(:sequence_number=)
35
+ end
28
36
  record.sequence_number = event.sequence_number if update_sequence_number
29
37
  record.save!
30
38
  end
@@ -42,11 +50,13 @@ module Sequent
42
50
  query = array_of_value_hashes.map do |values|
43
51
  insert_manager = new_insert_manager
44
52
  insert_manager.into(table)
45
- insert_manager.insert(values.map do |key, value|
46
- convert_to_values(key, table, value)
47
- end)
53
+ insert_manager.insert(
54
+ values.map do |key, value|
55
+ convert_to_values(key, table, value)
56
+ end,
57
+ )
48
58
  insert_manager.to_sql
49
- end.join(";")
59
+ end.join(';')
50
60
 
51
61
  execute_sql(query)
52
62
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sequent
2
4
  module Core
3
5
  module Persistors
@@ -9,74 +11,74 @@ module Sequent
9
11
  module Persistor
10
12
  # Updates the view state
11
13
  def update_record
12
- fail "Method not supported in this persistor"
14
+ fail 'Method not supported in this persistor'
13
15
  end
14
16
 
15
17
  # Create a single record in the view state
16
18
  def create_record
17
- fail "Method not supported in this persistor"
19
+ fail 'Method not supported in this persistor'
18
20
  end
19
21
 
20
22
  # Creates multiple records at once in the view state
21
23
  def create_records
22
- fail "Method not supported in this persistor"
24
+ fail 'Method not supported in this persistor'
23
25
  end
24
26
 
25
27
  # Creates or updates a record in the view state.
26
28
  def create_or_update_record
27
- fail "Method not supported in this persistor"
29
+ fail 'Method not supported in this persistor'
28
30
  end
29
31
  # Gets a record from the view state, fails if it not exists
30
32
  def get_record!
31
- fail "Method not supported in this persistor"
33
+ fail 'Method not supported in this persistor'
32
34
  end
33
35
 
34
36
  # Gets a record from the view state, returns +nil+ if it not exists
35
37
  def get_record
36
- fail "Method not supported in this persistor"
38
+ fail 'Method not supported in this persistor'
37
39
  end
38
40
 
39
41
  # Deletes all records given a where
40
42
  def delete_all_records
41
- fail "Method not supported in this persistor"
43
+ fail 'Method not supported in this persistor'
42
44
  end
43
45
 
44
46
  # Updates all record given a where and an update clause
45
47
  def update_all_records
46
- fail "Method not supported in this persistor"
48
+ fail 'Method not supported in this persistor'
47
49
  end
48
50
 
49
51
  # Decide for yourself what to do with the records
50
52
  # @deprecated
51
53
  def do_with_records
52
- fail "Method not supported in this persistor"
54
+ fail 'Method not supported in this persistor'
53
55
  end
54
56
 
55
57
  # Decide for yourself what to do with a single record
56
58
  # @deprecated
57
59
  def do_with_record
58
- fail "Method not supported in this persistor"
60
+ fail 'Method not supported in this persistor'
59
61
  end
60
62
 
61
63
  # Delete a single record
62
64
  # @deprecated
63
65
  def delete_record
64
- fail "Method not supported in this persistor"
66
+ fail 'Method not supported in this persistor'
65
67
  end
66
68
 
67
69
  # Find records given a where
68
70
  def find_records
69
- fail "Method not supported in this persistor"
71
+ fail 'Method not supported in this persistor'
70
72
  end
71
73
 
72
74
  # Returns the last record given a where
73
75
  def last_record
74
- fail "Method not supported in this persistor"
76
+ fail 'Method not supported in this persistor'
75
77
  end
76
78
 
77
79
  # Hook to implement for instance the persistor batches statements
78
80
  def commit
79
- fail "Method not supported in this persistor"
81
+ fail 'Method not supported in this persistor'
80
82
  end
81
83
  end
82
84
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'persistor'
2
4
  require_relative 'active_record_persistor'
3
5
  require_relative 'replay_optimized_postgres_persistor'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'set'
2
4
  require 'active_record'
3
5
  require 'csv'
@@ -13,9 +15,10 @@ module Sequent
13
15
  # using normal sql.
14
16
  #
15
17
  # Rebuilding the view state (or projection) of an aggregate typically consists
16
- # of an initial insert and then many updates and maybe a delete. With a normal Persistor (like ActiveRecordPersistor)
17
- # each action is executed to the database. This persitor creates an inmemory store first and finally flushes
18
- # the in memory store to the database. This can significantly reduces the amount of queries to the database.
18
+ # of an initial insert and then many updates and maybe a delete.
19
+ # With a normal Persistor (like ActiveRecordPersistor) each action is executed to the database.
20
+ # This persistor creates an in-memory store first and finally flushes
21
+ # the in-memory store to the database. This can significantly reduce the amount of queries to the database.
19
22
  # E.g. 1 insert, 6 updates is only a single insert using this Persistor.
20
23
  #
21
24
  # After lot of experimenting this turned out to be the fastest way to to bulk inserts in the database.
@@ -29,22 +32,26 @@ module Sequent
29
32
  #
30
33
  # class InvoiceProjector < Sequent::Core::Projector
31
34
  # on RecipientMovedEvent do |event|
32
- # update_all_records InvoiceRecord, recipient_id: event.recipient.aggregate_id do |record|
33
- # record.recipient_street = record.recipient.street
35
+ # update_all_records(
36
+ # InvoiceRecord,
37
+ # { aggregate_id: event.aggregate_id, recipient_id: event.recipient.aggregate_id },
38
+ # { recipient_street: event.recipient.street },
34
39
  # end
35
40
  # end
36
41
  # end
37
42
  #
38
- # In this case it is wise to create an index on InvoiceRecord on the recipient_id like you would in the database.
43
+ # In this case it is wise to create an index on InvoiceRecord on the aggregate_id and recipient_id
44
+ # like you would in the database.
39
45
  #
40
46
  # Example:
41
47
  #
42
48
  # ReplayOptimizedPostgresPersistor.new(
43
49
  # 50,
44
- # {InvoiceRecord => [[:recipient_id]]}
50
+ # {InvoiceRecord => [[:aggregate_id, :recipient_id]]}
45
51
  # )
46
52
  class ReplayOptimizedPostgresPersistor
47
53
  include Persistor
54
+ CHUNK_SIZE = 1024
48
55
 
49
56
  attr_reader :record_store
50
57
  attr_accessor :insert_with_csv_size
@@ -65,14 +72,18 @@ module Sequent
65
72
  class Index
66
73
  def initialize(indexed_columns)
67
74
  @indexed_columns = Hash.new do |hash, record_class|
68
- if record_class.column_names.include? 'aggregate_id'
69
- hash[record_class] = [:aggregate_id]
70
- else
71
- hash[record_class] = []
72
- end
75
+ hash[record_class] = if record_class.column_names.include? 'aggregate_id'
76
+ ['aggregate_id']
77
+ else
78
+ []
79
+ end
73
80
  end
74
81
 
75
- @indexed_columns.merge!(indexed_columns)
82
+ @indexed_columns = @indexed_columns.merge(
83
+ indexed_columns.reduce({}) do |memo, (key, ics)|
84
+ memo.merge({key => ics.map { |c| c.map(&:to_s) }})
85
+ end,
86
+ )
76
87
 
77
88
  @index = {}
78
89
  @reverse_index = {}
@@ -82,10 +93,10 @@ module Sequent
82
93
  return unless indexed?(record_class)
83
94
 
84
95
  get_keys(record_class, record).each do |key|
85
- @index[key.hash] = [] unless @index.has_key? key.hash
96
+ @index[key.hash] = [] unless @index.key? key.hash
86
97
  @index[key.hash] << record
87
98
 
88
- @reverse_index[record.object_id.hash] = [] unless @reverse_index.has_key? record.object_id.hash
99
+ @reverse_index[record.object_id.hash] = [] unless @reverse_index.key? record.object_id.hash
89
100
  @reverse_index[record.object_id.hash] << key.hash
90
101
  end
91
102
  end
@@ -112,7 +123,7 @@ module Sequent
112
123
  key = [record_class.name]
113
124
  get_index(record_class, where_clause).each do |field|
114
125
  key << field
115
- key << where_clause[field]
126
+ key << where_clause.stringify_keys[field]
116
127
  end
117
128
  @index[key.hash] || []
118
129
  end
@@ -123,13 +134,13 @@ module Sequent
123
134
  end
124
135
 
125
136
  def use_index?(record_class, where_clause)
126
- @indexed_columns.has_key?(record_class) && @indexed_columns[record_class].any? { |indexed_where| where_clause.keys.size == indexed_where.size && (where_clause.keys - indexed_where).empty? }
137
+ @indexed_columns.key?(record_class) && get_index(record_class, where_clause).present?
127
138
  end
128
139
 
129
140
  private
130
141
 
131
142
  def indexed?(record_class)
132
- @indexed_columns.has_key?(record_class)
143
+ @indexed_columns.key?(record_class)
133
144
  end
134
145
 
135
146
  def get_keys(record_class, record)
@@ -144,7 +155,9 @@ module Sequent
144
155
  end
145
156
 
146
157
  def get_index(record_class, where_clause)
147
- @indexed_columns[record_class].find { |indexed_where| where_clause.keys.size == indexed_where.size && (where_clause.keys - indexed_where).empty? }
158
+ @indexed_columns[record_class].find do |indexed_where|
159
+ where_clause.keys.size == indexed_where.size && (where_clause.keys.map(&:to_s) - indexed_where).empty?
160
+ end
148
161
  end
149
162
  end
150
163
 
@@ -152,37 +165,40 @@ module Sequent
152
165
  #
153
166
  # +indices+ Hash of indices to create in memory. Greatly speeds up the replaying.
154
167
  # Key corresponds to the name of the 'Record'
155
- # Values contains list of lists on which columns to index. E.g. [[:first_index_column], [:another_index, :with_to_columns]]
168
+ # Values contains list of lists on which columns to index.
169
+ # E.g. [[:first_index_column], [:another_index, :with_to_columns]]
156
170
  def initialize(insert_with_csv_size = 50, indices = {})
157
171
  @insert_with_csv_size = insert_with_csv_size
158
172
  @record_store = Hash.new { |h, k| h[k] = Set.new }
159
173
  @record_index = Index.new(indices)
160
174
  end
161
175
 
162
- def update_record(record_class, event, where_clause = {aggregate_id: event.aggregate_id}, options = {}, &block)
176
+ def update_record(record_class, event, where_clause = {aggregate_id: event.aggregate_id}, options = {})
163
177
  record = get_record!(record_class, where_clause)
164
178
  record.updated_at = event.created_at if record.respond_to?(:updated_at)
165
179
  yield record if block_given?
166
180
  @record_index.update(record_class, record)
167
- update_sequence_number = options.key?(:update_sequence_number) ?
168
- options[:update_sequence_number] :
181
+ update_sequence_number = if options.key?(:update_sequence_number)
182
+ options[:update_sequence_number]
183
+ else
169
184
  record.respond_to?(:sequence_number=)
185
+ end
170
186
  record.sequence_number = event.sequence_number if update_sequence_number
171
187
  end
172
188
 
173
189
  def create_record(record_class, values)
174
190
  column_names = record_class.column_names
175
191
  values = record_class.column_defaults.with_indifferent_access.merge(values)
176
- values.merge!(updated_at: values[:created_at]) if column_names.include?("updated_at")
177
- struct_class_name = "#{record_class.to_s}Struct"
178
- if self.class.struct_cache.has_key?(struct_class_name)
192
+ values.merge!(updated_at: values[:created_at]) if column_names.include?('updated_at')
193
+ struct_class_name = "#{record_class}Struct"
194
+ if self.class.struct_cache.key?(struct_class_name)
179
195
  struct_class = self.class.struct_cache[struct_class_name]
180
196
  else
181
197
  # We create a struct on the fly.
182
198
  # Since the replay happens in memory we implement the ==, eql? and hash methods
183
199
  # to point to the same object. A record is the same if and only if they point to
184
200
  # the same object. These methods are necessary since we use Set instead of [].
185
- class_def=<<-EOD
201
+ class_def = <<-EOD
186
202
  #{struct_class_name} = Struct.new(*#{column_names.map(&:to_sym)})
187
203
  class #{struct_class_name}
188
204
  include InitStruct
@@ -194,7 +210,9 @@ module Sequent
194
210
  end
195
211
  end
196
212
  EOD
197
- eval("#{class_def}")
213
+ # rubocop:disable Security/Eval
214
+ eval(class_def.to_s)
215
+ # rubocop:enable Security/Eval
198
216
  struct_class = ReplayOptimizedPostgresPersistor.const_get(struct_class_name)
199
217
  self.class.struct_cache[struct_class_name] = struct_class
200
218
  end
@@ -214,9 +232,7 @@ module Sequent
214
232
 
215
233
  def create_or_update_record(record_class, values, created_at = Time.now)
216
234
  record = get_record(record_class, values)
217
- unless record
218
- record = create_record(record_class, values.merge(created_at: created_at))
219
- end
235
+ record ||= create_record(record_class, values.merge(created_at: created_at))
220
236
  yield record if block_given?
221
237
  @record_index.update(record_class, record)
222
238
  record
@@ -224,7 +240,10 @@ module Sequent
224
240
 
225
241
  def get_record!(record_class, where_clause)
226
242
  record = get_record(record_class, where_clause)
227
- raise("record #{record_class} not found for #{where_clause}, store: #{@record_store[record_class]}") unless record
243
+ unless record
244
+ fail("record #{record_class} not found for #{where_clause}, store: #{@record_store[record_class]}")
245
+ end
246
+
228
247
  record
229
248
  end
230
249
 
@@ -273,10 +292,10 @@ module Sequent
273
292
  else
274
293
  @record_store[record_class].select do |record|
275
294
  where_clause.all? do |k, v|
276
- expected_value = v.kind_of?(Symbol) ? v.to_s : v
295
+ expected_value = v.is_a?(Symbol) ? v.to_s : v
277
296
  actual_value = record[k.to_sym]
278
- actual_value = actual_value.to_s if actual_value.kind_of? Symbol
279
- if expected_value.kind_of?(Array)
297
+ actual_value = actual_value.to_s if actual_value.is_a? Symbol
298
+ if expected_value.is_a?(Array)
280
299
  expected_value.include?(actual_value)
281
300
  else
282
301
  actual_value == expected_value
@@ -295,39 +314,38 @@ module Sequent
295
314
  @record_store.each do |clazz, records|
296
315
  @column_cache ||= {}
297
316
  @column_cache[clazz.name] ||= clazz.columns.reduce({}) do |hash, column|
298
- hash.merge({ column.name => column })
317
+ hash.merge({column.name => column})
299
318
  end
300
319
  if records.size > @insert_with_csv_size
301
- csv = CSV.new("")
302
- column_names = clazz.column_names.reject { |name| name == "id" }
320
+ csv = CSV.new(StringIO.new)
321
+ column_names = clazz.column_names.reject { |name| name == 'id' }
303
322
  records.each do |record|
304
323
  csv << column_names.map do |column_name|
305
324
  cast_value_to_column_type(clazz, column_name, record)
306
325
  end
307
326
  end
308
327
 
309
- buf = ''
310
328
  conn = Sequent::ApplicationRecord.connection.raw_connection
311
- copy_data = StringIO.new csv.string
329
+ copy_data = StringIO.new(csv.string)
312
330
  conn.transaction do
313
- conn.copy_data("COPY #{clazz.table_name} (#{column_names.join(",")}) FROM STDIN WITH csv") do
314
- while copy_data.read(1024, buf)
315
- conn.put_copy_data(buf)
331
+ conn.copy_data("COPY #{clazz.table_name} (#{column_names.join(',')}) FROM STDIN WITH csv") do
332
+ while (out = copy_data.read(CHUNK_SIZE))
333
+ conn.put_copy_data(out)
316
334
  end
317
335
  end
318
336
  end
319
337
  else
320
338
  clazz.unscoped do
321
339
  inserts = []
322
- column_names = clazz.column_names.reject { |name| name == "id" }
323
- prepared_values = (1..column_names.size).map { |i| "$#{i}" }.join(",")
340
+ column_names = clazz.column_names.reject { |name| name == 'id' }
341
+ prepared_values = (1..column_names.size).map { |i| "$#{i}" }.join(',')
324
342
  records.each do |record|
325
343
  values = column_names.map do |column_name|
326
344
  cast_value_to_column_type(clazz, column_name, record)
327
345
  end
328
346
  inserts << values
329
347
  end
330
- sql = %Q{insert into #{clazz.table_name} (#{column_names.join(",")}) values (#{prepared_values})}
348
+ sql = %{insert into #{clazz.table_name} (#{column_names.join(',')}) values (#{prepared_values})}
331
349
  inserts.each do |insert|
332
350
  clazz.connection.raw_connection.async_exec(sql, insert)
333
351
  end
@@ -346,7 +364,12 @@ module Sequent
346
364
  private
347
365
 
348
366
  def cast_value_to_column_type(clazz, column_name, record)
349
- Sequent::ApplicationRecord.connection.type_cast(record[column_name.to_sym], @column_cache[clazz.name][column_name])
367
+ uncasted_value = ActiveModel::Attribute.from_database(
368
+ column_name,
369
+ record[column_name.to_sym],
370
+ Sequent::ApplicationRecord.connection.lookup_cast_type_from_column(@column_cache[clazz.name][column_name]),
371
+ ).value_for_database
372
+ Sequent::ApplicationRecord.connection.type_cast(uncasted_value)
350
373
  end
351
374
  end
352
375
  end
@@ -1,9 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'helpers/message_handler'
2
4
  require_relative './persistors/active_record_persistor'
3
5
 
4
6
  module Sequent
5
7
  module Core
6
-
7
8
  module Migratable
8
9
  module ClassMethods
9
10
  def manages_tables(*tables)
@@ -16,7 +17,7 @@ module Sequent
16
17
 
17
18
  def manages_no_tables
18
19
  @manages_no_tables = true
19
- manages_tables *[]
20
+ manages_tables
20
21
  end
21
22
 
22
23
  def manages_no_tables?
@@ -26,11 +27,11 @@ module Sequent
26
27
  private
27
28
 
28
29
  def managed_tables_from_superclass
29
- self.superclass.managed_tables if self.superclass.respond_to?(:managed_tables)
30
+ superclass.managed_tables if superclass.respond_to?(:managed_tables)
30
31
  end
31
32
 
32
33
  def manages_no_tables_from_superclass?
33
- self.superclass.manages_no_tables? if self.superclass.respond_to?(:manages_no_tables?)
34
+ superclass.manages_no_tables? if superclass.respond_to?(:manages_no_tables?)
34
35
  end
35
36
  end
36
37
 
@@ -53,7 +54,6 @@ module Sequent
53
54
  def managed_tables
54
55
  self.class.managed_tables
55
56
  end
56
-
57
57
  end
58
58
 
59
59
  # Projectors listen to events and update the view state as they see fit.
@@ -87,7 +87,6 @@ module Sequent
87
87
  include Helpers::MessageHandler
88
88
  include Migratable
89
89
 
90
-
91
90
  def initialize(persistor = Sequent::Core::Persistors::ActiveRecordPersistor.new)
92
91
  ensure_valid!
93
92
  @persistor = persistor
@@ -98,28 +97,32 @@ module Sequent
98
97
  end
99
98
 
100
99
  def_delegators :@persistor,
101
- :update_record,
102
- :create_record,
103
- :create_records,
104
- :create_or_update_record,
105
- :get_record!,
106
- :get_record,
107
- :delete_all_records,
108
- :update_all_records,
109
- :do_with_records,
110
- :do_with_record,
111
- :delete_record,
112
- :find_records,
113
- :last_record,
114
- :execute_sql,
115
- :commit
100
+ :update_record,
101
+ :create_record,
102
+ :create_records,
103
+ :create_or_update_record,
104
+ :get_record!,
105
+ :get_record,
106
+ :delete_all_records,
107
+ :update_all_records,
108
+ :do_with_records,
109
+ :do_with_record,
110
+ :delete_record,
111
+ :find_records,
112
+ :last_record,
113
+ :execute_sql,
114
+ :commit
116
115
 
117
116
  private
118
117
 
119
118
  def ensure_valid!
120
119
  return if self.class.manages_no_tables?
121
120
 
122
- fail "A Projector must manage at least one table. Did you forget to add `managed_tables` to #{self.class.name}?" if self.class.managed_tables.nil? || self.class.managed_tables.empty?
121
+ if self.class.managed_tables.nil? || self.class.managed_tables.empty?
122
+ fail <<~EOS.chomp
123
+ A Projector must manage at least one table. Did you forget to add `managed_tables` to #{self.class.name}?
124
+ EOS
125
+ end
123
126
  end
124
127
  end
125
128
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sequent
2
4
  module Core
3
5
  module RandomUuidGenerator