torque-postgresql 3.4.1 → 4.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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/torque/function_generator.rb +13 -0
  3. data/lib/generators/torque/templates/function.sql.erb +4 -0
  4. data/lib/generators/torque/templates/type.sql.erb +2 -0
  5. data/lib/generators/torque/templates/view.sql.erb +3 -0
  6. data/lib/generators/torque/type_generator.rb +13 -0
  7. data/lib/generators/torque/view_generator.rb +16 -0
  8. data/lib/torque/postgresql/adapter/database_statements.rb +111 -94
  9. data/lib/torque/postgresql/adapter/oid/array.rb +17 -0
  10. data/lib/torque/postgresql/adapter/oid/line.rb +2 -6
  11. data/lib/torque/postgresql/adapter/oid/range.rb +4 -4
  12. data/lib/torque/postgresql/adapter/oid.rb +1 -23
  13. data/lib/torque/postgresql/adapter/quoting.rb +13 -7
  14. data/lib/torque/postgresql/adapter/schema_creation.rb +7 -28
  15. data/lib/torque/postgresql/adapter/schema_definitions.rb +58 -0
  16. data/lib/torque/postgresql/adapter/schema_dumper.rb +136 -34
  17. data/lib/torque/postgresql/adapter/schema_overrides.rb +45 -0
  18. data/lib/torque/postgresql/adapter/schema_statements.rb +109 -49
  19. data/lib/torque/postgresql/arel/infix_operation.rb +15 -28
  20. data/lib/torque/postgresql/arel/nodes.rb +16 -2
  21. data/lib/torque/postgresql/arel/operations.rb +7 -1
  22. data/lib/torque/postgresql/arel/visitors.rb +7 -9
  23. data/lib/torque/postgresql/associations/association_scope.rb +23 -31
  24. data/lib/torque/postgresql/associations/belongs_to_many_association.rb +25 -0
  25. data/lib/torque/postgresql/associations/builder/belongs_to_many.rb +16 -0
  26. data/lib/torque/postgresql/attributes/builder/enum.rb +12 -9
  27. data/lib/torque/postgresql/attributes/builder/full_text_search.rb +109 -0
  28. data/lib/torque/postgresql/attributes/builder/period.rb +21 -21
  29. data/lib/torque/postgresql/attributes/builder.rb +49 -11
  30. data/lib/torque/postgresql/attributes/enum.rb +7 -7
  31. data/lib/torque/postgresql/attributes/enum_set.rb +7 -7
  32. data/lib/torque/postgresql/attributes/full_text_search.rb +19 -0
  33. data/lib/torque/postgresql/attributes/period.rb +2 -2
  34. data/lib/torque/postgresql/attributes.rb +0 -4
  35. data/lib/torque/postgresql/auxiliary_statement/recursive.rb +3 -3
  36. data/lib/torque/postgresql/base.rb +5 -11
  37. data/lib/torque/postgresql/collector.rb +1 -1
  38. data/lib/torque/postgresql/config.rb +129 -5
  39. data/lib/torque/postgresql/function.rb +94 -0
  40. data/lib/torque/postgresql/inheritance.rb +52 -36
  41. data/lib/torque/postgresql/predicate_builder/arel_attribute_handler.rb +33 -0
  42. data/lib/torque/postgresql/predicate_builder/array_handler.rb +47 -0
  43. data/lib/torque/postgresql/predicate_builder/enumerator_lazy_handler.rb +37 -0
  44. data/lib/torque/postgresql/predicate_builder/regexp_handler.rb +21 -0
  45. data/lib/torque/postgresql/predicate_builder.rb +35 -0
  46. data/lib/torque/postgresql/railtie.rb +137 -30
  47. data/lib/torque/postgresql/reflection/abstract_reflection.rb +12 -44
  48. data/lib/torque/postgresql/reflection/belongs_to_many_reflection.rb +4 -0
  49. data/lib/torque/postgresql/reflection/has_many_reflection.rb +4 -0
  50. data/lib/torque/postgresql/reflection/runtime_reflection.rb +1 -1
  51. data/lib/torque/postgresql/relation/auxiliary_statement.rb +7 -2
  52. data/lib/torque/postgresql/relation/buckets.rb +124 -0
  53. data/lib/torque/postgresql/relation/distinct_on.rb +7 -2
  54. data/lib/torque/postgresql/relation/inheritance.rb +22 -15
  55. data/lib/torque/postgresql/relation/join_series.rb +112 -0
  56. data/lib/torque/postgresql/relation/merger.rb +17 -3
  57. data/lib/torque/postgresql/relation.rb +24 -38
  58. data/lib/torque/postgresql/schema_cache.rb +6 -12
  59. data/lib/torque/postgresql/version.rb +1 -1
  60. data/lib/torque/postgresql/versioned_commands/command_migration.rb +146 -0
  61. data/lib/torque/postgresql/versioned_commands/generator.rb +57 -0
  62. data/lib/torque/postgresql/versioned_commands/migration_context.rb +83 -0
  63. data/lib/torque/postgresql/versioned_commands/migrator.rb +39 -0
  64. data/lib/torque/postgresql/versioned_commands/schema_table.rb +101 -0
  65. data/lib/torque/postgresql/versioned_commands.rb +161 -0
  66. data/lib/torque/postgresql.rb +2 -1
  67. data/spec/fixtures/migrations/20250101000001_create_users.rb +0 -0
  68. data/spec/fixtures/migrations/20250101000002_create_function_count_users_v1.sql +0 -0
  69. data/spec/fixtures/migrations/20250101000003_create_internal_users.rb +0 -0
  70. data/spec/fixtures/migrations/20250101000004_update_function_count_users_v2.sql +0 -0
  71. data/spec/fixtures/migrations/20250101000005_create_view_all_users_v1.sql +0 -0
  72. data/spec/fixtures/migrations/20250101000006_create_type_user_id_v1.sql +0 -0
  73. data/spec/fixtures/migrations/20250101000007_remove_function_count_users_v2.sql +0 -0
  74. data/spec/initialize.rb +67 -0
  75. data/spec/mocks/cache_query.rb +21 -21
  76. data/spec/mocks/create_table.rb +6 -26
  77. data/spec/schema.rb +17 -12
  78. data/spec/spec_helper.rb +11 -2
  79. data/spec/tests/arel_spec.rb +32 -7
  80. data/spec/tests/auxiliary_statement_spec.rb +3 -3
  81. data/spec/tests/belongs_to_many_spec.rb +72 -5
  82. data/spec/tests/enum_set_spec.rb +12 -11
  83. data/spec/tests/enum_spec.rb +4 -2
  84. data/spec/tests/full_text_seach_test.rb +280 -0
  85. data/spec/tests/function_spec.rb +42 -0
  86. data/spec/tests/has_many_spec.rb +21 -8
  87. data/spec/tests/interval_spec.rb +1 -7
  88. data/spec/tests/period_spec.rb +61 -61
  89. data/spec/tests/predicate_builder_spec.rb +132 -0
  90. data/spec/tests/relation_spec.rb +229 -0
  91. data/spec/tests/schema_spec.rb +6 -9
  92. data/spec/tests/table_inheritance_spec.rb +25 -26
  93. data/spec/tests/versioned_commands_spec.rb +513 -0
  94. metadata +64 -39
@@ -1,6 +1,2 @@
1
1
  require_relative 'attributes/lazy'
2
2
  require_relative 'attributes/builder'
3
-
4
- require_relative 'attributes/enum'
5
- require_relative 'attributes/enum_set'
6
- require_relative 'attributes/period'
@@ -136,10 +136,10 @@ module Torque
136
136
 
137
137
  col = table[name]
138
138
  base.select_extra_values += [col.as(as)] unless as.nil?
139
- parts = [col, source.cast(:varchar)]
139
+ parts = [col, source.pg_cast(:varchar)]
140
140
 
141
- columns << ::Arel.array([source]).cast(:varchar, true).as(name)
142
- sub_columns << ::Arel::Nodes::NamedFunction.new('array_append', parts).as(name)
141
+ columns << ::Arel.array([source]).pg_cast(:varchar, true).as(name)
142
+ sub_columns << FN.array_append(*parts).as(name)
143
143
  end
144
144
  end
145
145
 
@@ -16,11 +16,13 @@ module Torque
16
16
  class_attribute :schema, instance_writer: false
17
17
  end
18
18
 
19
- module ClassMethods
20
- delegate :distinct_on, :with, :itself_only, :cast_records, to: :all
19
+ class_methods do
20
+ delegate :distinct_on, :with, :itself_only, :cast_records, :join_series,
21
+ :buckets, to: :all
21
22
 
22
23
  # Make sure that table name is an instance of TableName class
23
24
  def reset_table_name
25
+ return super unless PostgreSQL.config.schemas.enabled
24
26
  self.table_name = TableName.new(self, super)
25
27
  end
26
28
 
@@ -41,7 +43,7 @@ module Torque
41
43
  next klass.table_name unless klass.physically_inheritances?
42
44
 
43
45
  query = klass.unscoped.where(subclass.primary_key => id)
44
- query.pluck(klass.arel_table['tableoid'].cast('regclass')).first
46
+ query.pluck(klass.arel_table['tableoid'].pg_cast('regclass')).first
45
47
  end
46
48
  end
47
49
 
@@ -200,14 +202,6 @@ module Torque
200
202
  ::ActiveRecord::Reflection.add_reflection(self, name, reflection)
201
203
  end
202
204
 
203
- # Allow extra keyword arguments to be sent to +InsertAll+
204
- unless Torque::PostgreSQL::AR720
205
- def upsert_all(attributes, **xargs)
206
- xargs = xargs.reverse_merge(on_duplicate: :update)
207
- ::ActiveRecord::InsertAll.new(self, attributes, **xargs).execute
208
- end
209
- end
210
-
211
205
  protected
212
206
 
213
207
  # Allow optional select attributes to be loaded manually when they are
@@ -4,7 +4,7 @@ module Torque
4
4
  module PostgreSQL
5
5
  module Collector
6
6
 
7
- # This classe helps to collect data in different ways. Used to configure
7
+ # This class helps to collect data in different ways. Used to configure
8
8
  # auxiliary statements
9
9
  def self.new(*args)
10
10
  klass = Class.new
@@ -4,10 +4,6 @@ module Torque
4
4
  module PostgreSQL
5
5
  include ActiveSupport::Configurable
6
6
 
7
- # Stores a version check for compatibility purposes
8
- AR710 = (ActiveRecord.gem_version >= Gem::Version.new('7.1.0'))
9
- AR720 = (ActiveRecord.gem_version >= Gem::Version.new('7.2.0'))
10
-
11
7
  # Use the same logger as the Active Record one
12
8
  def self.logger
13
9
  ActiveRecord::Base.logger
@@ -26,6 +22,12 @@ module Torque
26
22
  # same configuration is set to true
27
23
  config.eager_load = false
28
24
 
25
+ # Add support for joining any query/association with a generated series
26
+ config.join_series = true
27
+
28
+ # Add support for querying and calculating histogram buckets
29
+ config.buckets = true
30
+
29
31
  # Set a list of irregular model name when associated with table names
30
32
  config.irregular_models = {}
31
33
  def config.irregular_models=(hash)
@@ -41,11 +43,19 @@ module Torque
41
43
  # default. False means that no validation will be performed
42
44
  assoc.belongs_to_many_required_by_default = false
43
45
 
46
+ # Although +belongs_to_many+ does not need a custom handler when joining
47
+ # the last chain scope, this can allow devs to pick which way they prefer:
48
+ # Rails default, or ANY with a single bind to improve prepared statements
49
+ # assoc.optimize_for_binds = false TODO: Add support
50
+
44
51
  end
45
52
 
46
53
  # Configure multiple schemas
47
54
  config.nested(:schemas) do |schemas|
48
55
 
56
+ # Enables schemas handler by this gem, not Rails's own implementation
57
+ schemas.enabled = true
58
+
49
59
  # Defines a list of LIKE-based schemas to not consider for a multiple
50
60
  # schema database
51
61
  schemas.blacklist = %w[information_schema pg_%]
@@ -59,6 +69,10 @@ module Torque
59
69
  # Configure auxiliary statement features
60
70
  config.nested(:auxiliary_statement) do |cte|
61
71
 
72
+ # Enables auxiliary statements handler by this gem, not Rails's own
73
+ # implementation
74
+ cte.enabled = true
75
+
62
76
  # Define the key that is used on auxiliary statements to send extra
63
77
  # arguments to format string or send on a proc
64
78
  cte.send_arguments_key = :args
@@ -76,6 +90,9 @@ module Torque
76
90
  # Configure ENUM features
77
91
  config.nested(:enum) do |enum|
78
92
 
93
+ # Enables enum handler by this gem, not Rails's own implementation
94
+ enum.enabled = true
95
+
79
96
  # The name of the method to be used on any ActiveRecord::Base to
80
97
  # initialize model-based enum features
81
98
  enum.base_method = :torque_enum
@@ -93,7 +110,7 @@ module Torque
93
110
  enum.raise_conflicting = false
94
111
 
95
112
  # Specify the namespace of each enum type of value
96
- enum.namespace = ::Object.const_set('Enum', Module.new)
113
+ enum.namespace = nil
97
114
 
98
115
  # Specify the scopes for I18n translations
99
116
  enum.i18n_scopes = [
@@ -117,6 +134,9 @@ module Torque
117
134
  # Configure geometry data types
118
135
  config.nested(:geometry) do |geometry|
119
136
 
137
+ # Enables geometry handler by this gem, not Rails's own implementation
138
+ geometry.enabled = true
139
+
120
140
  # Define the class that will be handling Point data types after decoding
121
141
  # it. Any class provided here must respond to 'x', and 'y'
122
142
  geometry.point_class = ActiveRecord::Point
@@ -162,6 +182,9 @@ module Torque
162
182
  # Configure period features
163
183
  config.nested(:period) do |period|
164
184
 
185
+ # Enables period handler by this gem
186
+ period.enabled = true
187
+
165
188
  # The name of the method to be used on any ActiveRecord::Base to
166
189
  # initialize model-based period features
167
190
  period.base_method = :period_for
@@ -228,5 +251,106 @@ module Torque
228
251
  }
229
252
 
230
253
  end
254
+
255
+ # Configure period features
256
+ config.nested(:interval) do |interval|
257
+
258
+ # Enables interval handler by this gem, not Rails's own implementation
259
+ interval.enabled = true
260
+
261
+ end
262
+
263
+ # Configure arel additional features
264
+ config.nested(:arel) do |arel|
265
+
266
+ # When provided, the initializer will expose the Arel function helper on
267
+ # the given module
268
+ config.expose_function_helper_on = nil
269
+
270
+ # List of Arel INFIX operators that will be made available for using as
271
+ # methods on Arel::Nodes::Node and Arel::Attribute
272
+ arel.infix_operators = {
273
+ 'contained_by' => '<@',
274
+ 'has_key' => '?',
275
+ 'has_all_keys' => '?&',
276
+ 'has_any_keys' => '?|',
277
+ 'strictly_left' => '<<',
278
+ 'strictly_right' => '>>',
279
+ 'doesnt_right_extend' => '&<',
280
+ 'doesnt_left_extend' => '&>',
281
+ 'adjacent_to' => '-|-',
282
+ }
283
+
284
+ end
285
+
286
+ # Configure full text search features
287
+ config.nested(:full_text_search) do |fts|
288
+
289
+ # Enables full text search handler by this gem
290
+ fts.enabled = true
291
+
292
+ # The name of the method to be used on any ActiveRecord::Base to
293
+ # initialize model-based full text search features
294
+ fts.base_method = :torque_search_for
295
+
296
+ # Defines the default language when generating search vector columns
297
+ fts.default_language = 'english'
298
+
299
+ # Defines the default mode to be used when generating full text search
300
+ # queries. It can be one of the following:
301
+ # - :default (to_tsquery)
302
+ # - :phrase (phraseto_tsquery)
303
+ # - :plain (plainto_tsquery)
304
+ # - :web (websearch_to_tsquery)
305
+ fts.default_mode = :phrase
306
+
307
+ # Defines the default index type to be used when creating search vector.
308
+ # It still requires that the column requests an index
309
+ fts.default_index_type = :gin
310
+
311
+ end
312
+
313
+ # Configure predicate builder additional features
314
+ config.nested(:predicate_builder) do |builder|
315
+
316
+ # List which handlers are enabled by default
317
+ builder.enabled = %i[regexp arel_attribute enumerator_lazy]
318
+
319
+ # When active, values provided to array attributes will be handled more
320
+ # friendly. It will use the +ANY+ operator on a equality check and
321
+ # overlaps when the given value is an array
322
+ builder.handle_array_attributes = false
323
+
324
+ # Make sure that the predicate builder will not spend more than 20ms
325
+ # trying to produce the underlying array
326
+ builder.lazy_timeout = 0.02
327
+
328
+ # Since lazy array is uncommon, it is better to limit the number of
329
+ # entries we try to pull so we don't cause a timeout or a long wait
330
+ # iteration
331
+ builder.lazy_limit = 2_000
332
+
333
+ end
334
+
335
+ # Configure versioned commands features
336
+ config.nested(:versioned_commands) do |vs|
337
+
338
+ # This is a feature that developers must explicitly opt-in. It is designed
339
+ # in a way that prevents a large impact on Rails' original migrations
340
+ # behavior. But, it is still a feature that everyone may not need, and
341
+ # some may complain about the additional schema table, which also uses
342
+ # inheritance
343
+ vs.enabled = false
344
+
345
+ # Define the list of commands that are going to be versioned by this
346
+ # method
347
+ vs.types = %i[function type view]
348
+
349
+ # The name of the table that will inherit from +schema_migrations+ and
350
+ # store the list of versioned commands that have been executed
351
+ vs.table_name = 'schema_versioned_commands'
352
+
353
+ end
354
+
231
355
  end
232
356
  end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ # Simplified module for creating arel functions. This is used internally
6
+ # but can also be made available to other devs on their own projects
7
+ module Function
8
+ class << self
9
+
10
+ # A facilitator to create a bind param that is fully compatible with
11
+ # Arel and ActiveRecord
12
+ def bind(*args)
13
+ attr = ::ActiveRecord::Relation::QueryAttribute.new(*args)
14
+ ::Arel::Nodes::BindParam.new(attr)
15
+ end
16
+
17
+ # Just a shortcut to create a bind param for a model attribute and a
18
+ # value for it
19
+ def bind_for(model, attribute, value)
20
+ bind(attribute, value, model.attribute_types[attribute])
21
+ end
22
+
23
+ # Another shortcut, when we already have the arel attribute at hand
24
+ def bind_with(arel_attribute, value)
25
+ bind(arel_attribute.name, value, arel_attribute.type_caster)
26
+ end
27
+
28
+ # A facilitator to create a bind param with a specific type
29
+ def bind_type(value, type = nil, name: 'value', cast: nil)
30
+ type ||= ruby_type_to_model_type(value)
31
+ type = ActiveModel::Type.lookup(type) if type.is_a?(Symbol)
32
+ result = bind(name, value, type)
33
+ cast ? result.pg_cast(cast) : result
34
+ end
35
+
36
+ # A facilitator to create an infix operation
37
+ def infix(op, left, right)
38
+ ::Arel::Nodes::InfixOperation.new(op, left, right)
39
+ end
40
+
41
+ # A facilitator to use several Infix operators to concatenate all the
42
+ # provided arguments. Arguments won't be sanitized, as other methods
43
+ # under this module
44
+ def concat(*args)
45
+ return args.first if args.one?
46
+ args.reduce { |left, right| infix(:"||", left, right) }
47
+ end
48
+
49
+ # A simple helper to trick Rails into producing the right SQL for
50
+ # grouping operations
51
+ def group_by(arel, name)
52
+ Arel::Nodes::Ref.new(name.to_s, arel)
53
+ end
54
+
55
+ # As of now, this indicates that it supports any direct calls, since
56
+ # the idea is to simply map to an Arel function with the same name,
57
+ # without checking if it actually exists
58
+ def respond_to_missing?(*)
59
+ true
60
+ end
61
+
62
+ # This method is used to catch any method calls that are not defined
63
+ # in this module. It will simply return an Arel function with the same
64
+ # name as the method called, passing all arguments to it, without
65
+ # any sanitization
66
+ def method_missing(name, *args, &block)
67
+ ::Arel::Nodes::NamedFunction.new(name.to_s.upcase, args)
68
+ end
69
+
70
+ private
71
+
72
+ def ruby_type_to_model_type(value)
73
+ case value
74
+ when Integer then :integer
75
+ when Float then :float
76
+ when String then :string
77
+ when Time, ActiveSupport::TimeWithZone then :time
78
+ when TrueClass, FalseClass then :boolean
79
+ when DateTime then :datetime
80
+ when Date then :date
81
+ when BigDecimal then :decimal
82
+ when ActiveSupport::Duration
83
+ Adapter::OID::Interval.new
84
+ else
85
+ raise ArgumentError, "Cannot infer type from value: #{value.inspect}."
86
+ end
87
+ end
88
+
89
+ end
90
+ end
91
+
92
+ FN = Function
93
+ end
94
+ end
@@ -20,15 +20,14 @@ module Torque
20
20
  klass.find(self.id)
21
21
  end
22
22
 
23
- module ClassMethods
23
+ class_methods do
24
24
  delegate :_auto_cast_attribute, :_record_class_attribute, to: ActiveRecord::Relation
25
25
 
26
26
  # Get a full list of all attributes from a model and all its dependents
27
27
  def inheritance_merged_attributes
28
28
  @inheritance_merged_attributes ||= begin
29
- list = attribute_names
30
- list += casted_dependents.values.map(&:attribute_names)
31
- list.flatten.uniq.freeze
29
+ children = casted_dependents.values.flat_map(&:attribute_names)
30
+ attribute_names.to_set.merge(children).to_a.freeze
32
31
  end
33
32
  end
34
33
 
@@ -45,11 +44,11 @@ module Torque
45
44
  end
46
45
  end
47
46
 
48
- result = types.select do
49
- |_, types| types.each_with_object(types.shift).all?(&:==)
50
- end.keys + attribute_names
47
+ result = types.filter_map do |attribute, types|
48
+ attribute if types.each_with_object(types.shift).all?(&:==)
49
+ end
51
50
 
52
- result.freeze
51
+ (attribute_names + result).freeze
53
52
  end
54
53
  end
55
54
 
@@ -111,22 +110,19 @@ module Torque
111
110
  # For all main purposes, physical inherited classes should have
112
111
  # base_class as their own
113
112
  def base_class
114
- return super unless physically_inherited?
115
- self
113
+ physically_inherited? ? self : super
116
114
  end
117
115
 
118
116
  # Primary key is one exception when getting information about the class,
119
117
  # it must returns the superclass PK
120
118
  def primary_key
121
- return super unless physically_inherited?
122
- superclass.primary_key
119
+ physically_inherited? ? superclass.primary_key : super
123
120
  end
124
121
 
125
122
  # Add an additional check to return the name of the table even when the
126
123
  # class is inherited, but only if it is a physical inheritance
127
124
  def compute_table_name
128
- return super unless physically_inherited?
129
- decorated_table_name
125
+ physically_inherited? ? decorated_table_name : super
130
126
  end
131
127
 
132
128
  # Raises an error message saying that the giver record class was not
@@ -142,37 +138,57 @@ module Torque
142
138
 
143
139
  private
144
140
 
145
- def instantiate_instance_of(klass, attributes, column_types = {}, &block)
141
+ # If the class is physically inherited, the klass needs to be properly
142
+ # changed before moving forward
143
+ def instantiate_instance_of(klass, attributes, types = {}, &block)
146
144
  return super unless klass.physically_inheritances?
147
145
 
148
- auto_cast = _auto_cast_attribute.to_s
149
- record_class = _record_class_attribute.to_s
150
- return super unless attributes.key?(record_class) &&
151
- attributes.delete(auto_cast) && attributes[record_class] != table_name
152
-
153
- klass = casted_dependents[attributes[record_class]]
154
- raise_unable_to_cast(attributes[record_class]) if klass.nil?
155
- filter_attributes_for_cast(attributes, klass)
146
+ real_class = torque_discriminate_class_for_record(klass, attributes)
147
+ return super if real_class.nil?
156
148
 
157
- super(klass, attributes, column_types, &block)
149
+ attributes, types = sanitize_attributes(real_class, attributes, types)
150
+ super(real_class, attributes, types, &block)
158
151
  end
159
152
 
160
- # Filter the record attributes to be loaded to not included those from
161
- # another inherited dependent
162
- def filter_attributes_for_cast(record, klass)
163
- new_record = record.slice(*klass.attribute_names)
164
- table = new_record[_record_class_attribute.to_s] = klass.table_name
165
-
166
- # Recover aliased attributes
167
- (klass.attribute_names - inheritance_mergeable_attributes).each do |attribute|
168
- new_record[attribute] = record["#{table}__#{attribute}"]
153
+ # Unwrap the attributes and column types from the given class when
154
+ # there are unmergeable attributes
155
+ def sanitize_attributes(real_class, attributes, types)
156
+ skip = (inheritance_merged_attributes - real_class.attribute_names).to_set
157
+ skip.merge(real_class.attribute_names - inheritance_mergeable_attributes)
158
+ return [attributes, types] if skip.empty?
159
+
160
+ dropped = 0
161
+ new_types = {}
162
+
163
+ row = attributes.instance_variable_get(:@row).dup
164
+ indexes = attributes.instance_variable_get(:@column_indexes).dup
165
+ indexes = indexes.each_with_object({}) do |(column, index), new_indexes|
166
+ attribute, prefix = column.split('__', 2).reverse
167
+ current_index = index - dropped
168
+
169
+ if prefix != table_name && skip.include?(attribute)
170
+ row.delete_at(current_index)
171
+ dropped += 1
172
+ else
173
+ new_types.merge!(types.slice(attribute))
174
+ new_types[current_index] = types[index]
175
+ new_indexes[attribute] = current_index
176
+ end
169
177
  end
170
178
 
171
- # Add any additional columns and replace the record with the new record data
172
- new_record.merge!(record.slice(*(record.keys - inheritance_merged_attributes)))
173
- record.replace(new_record)
179
+ [ActiveRecord::Result::IndexedRow.new(indexes, row), new_types]
174
180
  end
175
181
 
182
+ # Get the real class when handling physical inheritances and casting
183
+ # the record when existing properly is present
184
+ def torque_discriminate_class_for_record(klass, record)
185
+ return if record[_auto_cast_attribute.to_s] == false
186
+
187
+ embedded_type = record[_record_class_attribute.to_s]
188
+ return if embedded_type.blank? || embedded_type == table_name
189
+
190
+ casted_dependents[embedded_type] || raise_unable_to_cast(embedded_type)
191
+ end
176
192
  end
177
193
  end
178
194
 
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ module PredicateBuilder
6
+ class ArelAttributeHandler
7
+ # Shortcut
8
+ def self.call(*args)
9
+ new.call(*args)
10
+ end
11
+
12
+ def initialize(*)
13
+ # There is no need to use or save the predicate builder here
14
+ end
15
+
16
+ def call(attribute, value)
17
+ case
18
+ when array_typed?(attribute) && array_typed?(value) then attribute.overlaps(value)
19
+ when array_typed?(attribute) then value.eq(FN.any(attribute))
20
+ when array_typed?(value) then attribute.eq(FN.any(value))
21
+ else attribute.eq(value)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def array_typed?(attribute)
28
+ attribute.able_to_type_cast? && attribute.type_caster.is_a?(ARRAY_OID)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ module PredicateBuilder
6
+ module ArrayHandler
7
+ def call(attribute, value)
8
+ return super unless array_attribute?(attribute) &&
9
+ PostgreSQL.config.predicate_builder.handle_array_attributes
10
+
11
+ call_for_array(attribute, value)
12
+ end
13
+
14
+ def call_for_array(attribute, value)
15
+ if !value.is_a?(::Array)
16
+ call_with_value(attribute, value)
17
+ elsif value.any?
18
+ call_with_array(attribute, value)
19
+ else
20
+ call_with_empty(attribute)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def call_with_value(attribute, value)
27
+ FN.infix(:"=", FN.bind_with(attribute, value), FN.any(attribute))
28
+ end
29
+
30
+ def call_with_array(attribute, value)
31
+ attribute.overlaps(FN.bind_with(attribute, value))
32
+ end
33
+
34
+ def call_with_empty(attribute)
35
+ FN.cardinality(attribute).eq(0)
36
+ end
37
+
38
+ def array_attribute?(attribute)
39
+ attribute.type_caster.is_a?(ARRAY_OID)
40
+ end
41
+ end
42
+
43
+ ::ActiveRecord::PredicateBuilder::ArrayHandler.prepend(ArrayHandler)
44
+ ::ActiveRecord::PredicateBuilder::BasicObjectHandler.prepend(ArrayHandler)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ module PredicateBuilder
6
+ class EnumeratorLazyHandler < ::ActiveRecord::PredicateBuilder::ArrayHandler
7
+ Timeout = Class.new(::Timeout::Error)
8
+
9
+ def call(attribute, value)
10
+ with_timeout do
11
+ super(attribute, limit.nil? ? value.force : value.first(limit))
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def with_timeout
18
+ return yield if timeout.nil?
19
+
20
+ begin
21
+ ::Timeout.timeout(timeout) { yield }
22
+ rescue ::Timeout::Error
23
+ raise Timeout, "Lazy predicate builder timed out after #{timeout} seconds"
24
+ end
25
+ end
26
+
27
+ def timeout
28
+ PostgreSQL.config.predicate_builder.lazy_timeout
29
+ end
30
+
31
+ def limit
32
+ PostgreSQL.config.predicate_builder.lazy_limit
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ module PredicateBuilder
6
+ class RegexpHandler
7
+ def initialize(predicate_builder)
8
+ @predicate_builder = predicate_builder
9
+ end
10
+
11
+ def call(attribute, value)
12
+ operator = value.casefold? ? :"~*" : :"~"
13
+ FN.infix(operator, attribute, FN.bind_with(attribute, value.source))
14
+ end
15
+
16
+ private
17
+ attr_reader :predicate_builder
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'predicate_builder/array_handler'
4
+
5
+ require_relative 'predicate_builder/regexp_handler'
6
+ require_relative 'predicate_builder/arel_attribute_handler'
7
+ require_relative 'predicate_builder/enumerator_lazy_handler'
8
+
9
+ module Torque
10
+ module PostgreSQL
11
+ module PredicateBuilder
12
+ ARRAY_OID = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array
13
+
14
+ def initialize(*)
15
+ super
16
+
17
+ handlers = Array.wrap(PostgreSQL.config.predicate_builder.enabled).inquiry
18
+
19
+ if handlers.regexp?
20
+ register_handler(Regexp, RegexpHandler.new(self))
21
+ end
22
+
23
+ if handlers.enumerator_lazy?
24
+ register_handler(Enumerator::Lazy, EnumeratorLazyHandler.new(self))
25
+ end
26
+
27
+ if handlers.arel_attribute?
28
+ register_handler(::Arel::Attributes::Attribute, ArelAttributeHandler.new(self))
29
+ end
30
+ end
31
+ end
32
+
33
+ ::ActiveRecord::PredicateBuilder.prepend(PredicateBuilder)
34
+ end
35
+ end