sequent 3.3.1 → 4.1.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 (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