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
@@ -4,6 +4,15 @@ module Torque
4
4
  module PostgreSQL
5
5
  module Associations
6
6
  module AssociationScope
7
+ # A customized predicate builder for array attributes that can be used
8
+ # standalone and changes the behavior of the blank state
9
+ class PredicateBuilderArray
10
+ include PredicateBuilder::ArrayHandler
11
+
12
+ def call_with_empty(attribute)
13
+ '1=0' # Does not match records with empty arrays
14
+ end
15
+ end
7
16
 
8
17
  module ClassMethods
9
18
  def get_bind_values(*)
@@ -13,45 +22,34 @@ module Torque
13
22
 
14
23
  private
15
24
 
16
- # When the relation is connected through an array, intercept the
17
- # condition builder and uses an overlap condition building it on
18
- # +build_id_constraint+
25
+ # When loading a join by value (last as in we know which records to
26
+ # load) only has many array need to have a different behavior, so it
27
+ # can properly match array values
19
28
  def last_chain_scope(scope, reflection, owner)
20
29
  return super unless reflection.connected_through_array?
30
+ return super if reflection.macro == :belongs_to_many
21
31
 
22
- keys = reflection.join_keys
23
- value = transform_value(owner[keys.foreign_key])
24
- constraint = build_id_constraint(reflection, keys, value, true)
32
+ constraint = PredicateBuilderArray.new.call_for_array(
33
+ reflection.array_attribute,
34
+ transform_value(owner[reflection.join_foreign_key]),
35
+ )
25
36
 
26
37
  scope.where!(constraint)
27
38
  end
28
39
 
29
- # When the relation is connected through an array, intercept the
30
- # condition builder and uses an overlap condition building it on
31
- # +build_id_constraint+
40
+ # When loading a join by reference (next as in we don't know which
41
+ # records to load), it can take advantage of the new predicate builder
42
+ # to figure out the most optimal way to connect both properties
32
43
  def next_chain_scope(scope, reflection, next_reflection)
33
44
  return super unless reflection.connected_through_array?
34
45
 
35
- keys = reflection.join_keys
36
- foreign_table = next_reflection.aliased_table
37
-
38
- value = foreign_table[keys.foreign_key]
39
- constraint = build_id_constraint(reflection, keys, value)
46
+ primary_key = reflection.aliased_table[reflection.join_primary_key]
47
+ foreign_key = next_reflection.aliased_table[reflection.join_foreign_key]
48
+ constraint = PredicateBuilder::ArelAttributeHandler.call(primary_key, foreign_key)
40
49
 
41
50
  scope.joins!(join(foreign_table, constraint))
42
51
  end
43
52
 
44
- # Trigger the same method on the relation which will build the
45
- # constraint condition using array logics
46
- def build_id_constraint(reflection, keys, value, bind_param = false)
47
- table = reflection.aliased_table
48
- value = Array.wrap(value).map do |value|
49
- build_bind_param_for_constraint(reflection, value, keys.foreign_key)
50
- end if bind_param
51
-
52
- reflection.build_id_constraint(table[keys.key], value)
53
- end
54
-
55
53
  # For array-like values, it needs to call the method as many times as
56
54
  # the array size
57
55
  def transform_value(value)
@@ -61,12 +59,6 @@ module Torque
61
59
  value_transformation.call(value)
62
60
  end
63
61
  end
64
-
65
- def build_bind_param_for_constraint(reflection, value, foreign_key)
66
- ::Arel::Nodes::BindParam.new(::ActiveRecord::Relation::QueryAttribute.new(
67
- foreign_key, value, reflection.klass.attribute_types[foreign_key],
68
- ))
69
- end
70
62
  end
71
63
 
72
64
  ::ActiveRecord::Associations::AssociationScope.singleton_class.prepend(AssociationScope::ClassMethods)
@@ -70,6 +70,27 @@ module Torque
70
70
  @_building_changes = nil
71
71
  end
72
72
 
73
+ def trigger(prefix, before_ids, after_ids)
74
+ removed_ids = before_ids - after_ids
75
+ added_ids = after_ids - before_ids
76
+
77
+ if removed_ids.any?
78
+ callbacks_for(method = :"#{prefix}_remove").each do |callback|
79
+ target_scope.find(removed_ids).each do |record|
80
+ callback.call(method, owner, record)
81
+ end
82
+ end
83
+ end
84
+
85
+ if added_ids.any?
86
+ callbacks_for(method = :"#{prefix}_add").each do |callback|
87
+ target_scope.find(added_ids).each do |record|
88
+ callback.call(method, owner, record)
89
+ end
90
+ end
91
+ end
92
+ end
93
+
73
94
  ## HAS MANY
74
95
  def handle_dependency
75
96
  case options[:dependent]
@@ -193,6 +214,10 @@ module Torque
193
214
  owner.class.columns_hash[source_attr].default
194
215
  end
195
216
 
217
+ def callback(*)
218
+ true # This is handled/trigger when the owner record actually changes
219
+ end
220
+
196
221
  ## HAS MANY
197
222
  def replace_records(*)
198
223
  build_changes(true) { super }
@@ -21,6 +21,7 @@ module Torque
21
21
  super
22
22
  add_touch_callbacks(model, reflection) if reflection.options[:touch]
23
23
  add_default_callbacks(model, reflection) if reflection.options[:default]
24
+ add_change_callbacks(model, reflection)
24
25
  end
25
26
 
26
27
  def self.define_readers(mixin, name)
@@ -94,6 +95,21 @@ module Torque
94
95
  end
95
96
  end
96
97
 
98
+ def self.add_change_callbacks(model, reflection)
99
+ foreign_key = reflection.foreign_key
100
+ name = reflection.name
101
+
102
+ model.before_save ->(record) do
103
+ before, after = record.changes[foreign_key]
104
+ record.association(name).trigger(:before, before, after) if before && after
105
+ end
106
+
107
+ model.after_save ->(record) do
108
+ before, after = record.previous_changes[foreign_key]
109
+ record.association(name).trigger(:after, before, after) if before && after
110
+ end
111
+ end
112
+
97
113
  def self.add_destroy_callbacks(model, reflection)
98
114
  model.after_destroy lambda { |o| o.association(reflection.name).handle_dependency }
99
115
  end
@@ -6,6 +6,7 @@ module Torque
6
6
  module Builder
7
7
  class Enum
8
8
  VALID_TYPES = %i[enum enum_set].freeze
9
+ FN = '::Torque::PostgreSQL::FN'
9
10
 
10
11
  attr_accessor :klass, :attribute, :subtype, :options, :values,
11
12
  :klass_module, :instance_module
@@ -154,15 +155,17 @@ module Torque
154
155
  def set_scopes
155
156
  cast_type = subtype.name.chomp('[]')
156
157
  klass_module.module_eval <<-RUBY, __FILE__, __LINE__ + 1
157
- def has_#{attribute.pluralize}(*values) # def has_roles(*values)
158
- attr = arel_table['#{attribute}'] # attr = arel_table['role']
159
- where(attr.contains(::Arel.array(values, cast: '#{cast_type}'))) # where(attr.contains(::Arel.array(values, cast: 'roles')))
160
- end # end
161
-
162
- def has_any_#{attribute.pluralize}(*values) # def has_roles(*values)
163
- attr = arel_table['#{attribute}'] # attr = arel_table['role']
164
- where(attr.overlaps(::Arel.array(values, cast: '#{cast_type}'))) # where(attr.overlaps(::Arel.array(values, cast: 'roles')))
165
- end # end
158
+ def has_#{attribute.pluralize}(*values) # def has_roles(*values)
159
+ attr = arel_table['#{attribute}'] # attr = arel_table['role']
160
+ value = #{FN}.bind_with(attr, values) # value = ::Torque::PostgreSQL::FN.bind_with(attr, values)
161
+ where(attr.contains(value.pg_cast('#{cast_type}[]'))) # where(attr.contains(value.pg_cast('roles[]')))
162
+ end # end
163
+
164
+ def has_any_#{attribute.pluralize}(*values) # def has_any_roles(*values)
165
+ attr = arel_table['#{attribute}'] # attr = arel_table['role']
166
+ value = #{FN}.bind_with(attr, values) # value = ::Torque::PostgreSQL::FN.bind_with(attr, values)
167
+ where(attr.overlaps(value.pg_cast('#{cast_type}[]'))) # where(attr.overlaps(value.pg_cast('roles[]')))
168
+ end # end
166
169
  RUBY
167
170
  end
168
171
 
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ module Attributes
6
+ module Builder
7
+ class FullTextSearch
8
+ attr_accessor :klass, :attribute, :options, :klass_module,
9
+ :default_rank, :default_mode, :default_order, :default_language
10
+
11
+ def initialize(klass, attribute, options = {})
12
+ @klass = klass
13
+ @attribute = attribute
14
+ @options = options
15
+
16
+ @default_rank = options[:with_rank] == true ? 'rank' : options[:with_rank]&.to_s
17
+ @default_mode = options[:mode] || PostgreSQL.config.full_text_search.default_mode
18
+
19
+ @default_order =
20
+ case options[:order]
21
+ when :asc, true then :asc
22
+ when :desc then :desc
23
+ else false
24
+ end
25
+
26
+ @default_language = options[:language] if options[:language].is_a?(String) ||
27
+ options[:language].is_a?(Symbol)
28
+ @default_language ||= PostgreSQL.config.full_text_search.default_language.to_s
29
+ end
30
+
31
+ # What is the name of the scope to be added to the model
32
+ def scope_name
33
+ @scope_name ||= [
34
+ options[:prefix],
35
+ :full_text_search,
36
+ options[:suffix],
37
+ ].compact.join('_')
38
+ end
39
+
40
+ # Just check if the scope name is already defined
41
+ def conflicting?
42
+ return if options[:force] == true
43
+
44
+ if klass.dangerous_class_method?(scope_name)
45
+ raise Interrupt, scope_name.to_s
46
+ end
47
+ end
48
+
49
+ # Create the proper scope
50
+ def build
51
+ @klass_module = Module.new
52
+ add_scope_to_module
53
+ klass.extend klass_module
54
+ end
55
+
56
+ # Creates a class method as the scope that builds the full text search
57
+ def add_scope_to_module
58
+ klass_module.module_eval <<-RUBY, __FILE__, __LINE__ + 1
59
+ def #{scope_name}(value#{scope_args})
60
+ attr = arel_table['#{attribute}']
61
+ fn = ::Torque::PostgreSQL::FN
62
+
63
+ lang = language.to_s if !language.is_a?(::Symbol)
64
+ lang ||= arel_table[language.to_s] if has_attribute?(language)
65
+ lang ||= public_send(language) if respond_to?(language)
66
+
67
+ function = {
68
+ default: :to_tsquery,
69
+ phrase: :phraseto_tsquery,
70
+ plain: :plainto_tsquery,
71
+ web: :websearch_to_tsquery,
72
+ }[mode.to_sym]
73
+
74
+ raise ::ArgumentError, <<~MSG.squish if lang.blank?
75
+ Unable to determine language from \#{language.inspect}.
76
+ MSG
77
+
78
+ raise ::ArgumentError, <<~MSG.squish if function.nil?
79
+ Invalid mode \#{mode.inspect} for full text search.
80
+ MSG
81
+
82
+ value = fn.bind(:value, value.to_s, attr.type_caster)
83
+ lang = fn.bind(:lang, lang, attr.type_caster) if lang.is_a?(::String)
84
+
85
+ query = fn.public_send(function, lang, value)
86
+ ranker = fn.ts_rank(attr, query) if rank || order
87
+
88
+ result = where(fn.infix(:"@@", attr, query))
89
+ result = result.order(ranker.public_send(order == :desc ? :desc : :asc)) if order
90
+ result.select_extra_values += [ranker.as(rank == true ? 'rank' : rank.to_s)] if rank
91
+ result
92
+ end
93
+ RUBY
94
+ end
95
+
96
+ # Returns the arguments to be used on the scope
97
+ def scope_args
98
+ args = +''
99
+ args << ", order: #{default_order.inspect}"
100
+ args << ", rank: #{default_rank.inspect}"
101
+ args << ", language: #{default_language.inspect}"
102
+ args << ", mode: :#{default_mode}"
103
+ args
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -4,13 +4,11 @@ module Torque
4
4
  module PostgreSQL
5
5
  module Attributes
6
6
  module Builder
7
- # TODO: Allow documenting by building the methods outside and importing
8
- # only the raw string
9
7
  class Period
10
8
  DIRECT_ACCESS_REGEX = /_?%s_?/
11
9
  SUPPORTED_TYPES = %i[daterange tsrange tstzrange].freeze
12
10
  CURRENT_GETTERS = {
13
- daterange: 'Date.today',
11
+ daterange: 'Date.current',
14
12
  tsrange: 'Time.zone.now',
15
13
  tstzrange: 'Time.zone.now',
16
14
  }.freeze
@@ -21,6 +19,8 @@ module Torque
21
19
  tstzrange: :timestamp,
22
20
  }.freeze
23
21
 
22
+ FN = '::Torque::PostgreSQL::FN'
23
+
24
24
  attr_accessor :klass, :attribute, :options, :type, :default, :current_getter,
25
25
  :type_caster, :threshold, :dynamic_threshold, :klass_module, :instance_module
26
26
 
@@ -208,11 +208,11 @@ module Torque
208
208
  end
209
209
 
210
210
  def arel_default_sql
211
- @arel_default_sql ||= arel_sql_quote(@default.inspect)
211
+ @arel_default_sql ||= arel_sql_bind(@default.inspect)
212
212
  end
213
213
 
214
- def arel_sql_quote(value)
215
- "::Arel.sql(connection.quote(#{value}))"
214
+ def arel_sql_bind(value)
215
+ "#{FN}.bind_with(#{arel_attribute}, #{value})"
216
216
  end
217
217
 
218
218
  # Check how to provide the threshold value
@@ -223,12 +223,12 @@ module Torque
223
223
  "arel_table['#{threshold}']"
224
224
  when ActiveSupport::Duration
225
225
  value = "'#{threshold.to_i} seconds'"
226
- "::Arel.sql(\"#{value}\").cast(:interval)"
226
+ "::Arel.sql(\"#{value}\").pg_cast(:interval)"
227
227
  when Numeric
228
228
  value = threshold.to_i.to_s
229
229
  value << type_caster.eql?(:date) ? ' days' : ' seconds'
230
230
  value = "'#{value}'"
231
- "::Arel.sql(\"#{value}\").cast(:interval)"
231
+ "::Arel.sql(\"#{value}\").pg_cast(:interval)"
232
232
  end
233
233
  end
234
234
  end
@@ -248,7 +248,7 @@ module Torque
248
248
  return arel_start_at unless threshold.present?
249
249
  @arel_real_start_at ||= begin
250
250
  result = +"(#{arel_start_at} - #{arel_threshold_value})"
251
- result << '.cast(:date)' if type.eql?(:daterange)
251
+ result << '.pg_cast(:date)' if type.eql?(:daterange)
252
252
  result
253
253
  end
254
254
  end
@@ -258,7 +258,7 @@ module Torque
258
258
  return arel_finish_at unless threshold.present?
259
259
  @arel_real_finish_at ||= begin
260
260
  result = +"(#{arel_finish_at} + #{arel_threshold_value})"
261
- result << '.cast(:date)' if type.eql?(:daterange)
261
+ result << '.pg_cast(:date)' if type.eql?(:daterange)
262
262
  result
263
263
  end
264
264
  end
@@ -278,9 +278,9 @@ module Torque
278
278
 
279
279
  # Create an arel named function
280
280
  def arel_named_function(name, *args)
281
- result = +"::Arel::Nodes::NamedFunction.new(#{name.to_s.inspect}"
282
- result << ', [' << args.join(', ') << ']' if args.present?
283
- result << ')'
281
+ result = +"#{FN}.#{name}"
282
+ result << '(' << args.join(', ') << ')' if args.present?
283
+ result
284
284
  end
285
285
 
286
286
  # Create an arel version of +nullif+ function
@@ -302,24 +302,24 @@ module Torque
302
302
  def arel_daterange(real = false)
303
303
  arel_named_function(
304
304
  'daterange',
305
- (real ? arel_real_start_at : arel_start_at) + '.cast(:date)',
306
- (real ? arel_real_finish_at : arel_finish_at) + '.cast(:date)',
305
+ (real ? arel_real_start_at : arel_start_at) + '.pg_cast(:date)',
306
+ (real ? arel_real_finish_at : arel_finish_at) + '.pg_cast(:date)',
307
307
  '::Arel.sql("\'[]\'")',
308
308
  )
309
309
  end
310
310
 
311
311
  def arel_check_condition(type)
312
312
  checker = arel_nullif(arel_real_attribute, arel_empty_value)
313
- checker << ".#{type}(value.cast(#{type_caster.inspect}))"
313
+ checker << ".#{type}(value.pg_cast(#{type_caster.inspect}))"
314
314
  arel_coalesce(checker, arel_default_sql)
315
315
  end
316
316
 
317
317
  def arel_formatting_value(condition = nil, value = 'value', cast: nil)
318
318
  [
319
319
  "#{value} = arel_table[#{value}] if #{value}.is_a?(Symbol)",
320
- "unless #{value}.respond_to?(:cast)",
321
- " #{value} = ::Arel.sql(connection.quote(#{value}))",
322
- (" #{value} = #{value}.cast(#{cast.inspect})" if cast),
320
+ "unless #{value}.respond_to?(:pg_cast)",
321
+ " #{value} = #{FN}.bind_with(#{arel_attribute}, #{value})",
322
+ (" #{value} = #{value}.pg_cast(#{cast.inspect})" if cast),
323
323
  'end',
324
324
  condition,
325
325
  ].compact.join("\n")
@@ -347,14 +347,14 @@ module Torque
347
347
 
348
348
  def klass_current
349
349
  [
350
- "value = #{arel_sql_quote(current_getter)}",
350
+ "value = #{arel_sql_bind(current_getter)}",
351
351
  "where(#{arel_check_condition(:contains)})",
352
352
  ].join("\n")
353
353
  end
354
354
 
355
355
  def klass_not_current
356
356
  [
357
- "value = #{arel_sql_quote(current_getter)}",
357
+ "value = #{arel_sql_bind(current_getter)}",
358
358
  "where.not(#{arel_check_condition(:contains)})",
359
359
  ].join("\n")
360
360
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'builder/enum'
4
4
  require_relative 'builder/period'
5
+ require_relative 'builder/full_text_search'
5
6
 
6
7
  module Torque
7
8
  module PostgreSQL
@@ -12,20 +13,57 @@ module Torque
12
13
  return unless table_exists?
13
14
 
14
15
  args.each do |attribute|
15
- begin
16
- # Generate methods on self class
17
- builder = builder_klass.new(self, attribute, extra.merge(options))
18
- builder.conflicting?
19
- builder.build
20
-
21
- # Additional settings for the builder
22
- instance_exec(builder, &block) if block.present?
23
- rescue Interrupt
24
- # Not able to build the attribute, maybe pending migrations
25
- end
16
+ # Generate methods on self class
17
+ builder = builder_klass.new(self, attribute, extra.merge(options))
18
+ builder.conflicting?
19
+ builder.build
20
+
21
+ # Additional settings for the builder
22
+ instance_exec(builder, &block) if block.present?
23
+ rescue Interrupt
24
+ # Not able to build the attribute, maybe pending migrations
26
25
  end
27
26
  end
28
27
  end
28
+
29
+ def self.search_vector_options(columns:, language: nil, stored: true, **options)
30
+ weights = to_search_weights(columns)
31
+ operation = to_search_vector_operation(language, weights).to_sql
32
+
33
+ options[:index] = {
34
+ using: PostgreSQL.config.full_text_search.default_index_type,
35
+ } if options[:index] == true
36
+
37
+ options.merge(type: :tsvector, as: operation, stored: stored)
38
+ end
39
+
40
+ def self.to_search_weights(columns)
41
+ if !columns.is_a?(Hash)
42
+ extras = columns.size > 3 ? columns.size - 3 : 0
43
+ weights = %w[A B C] + (['D'] * extras)
44
+ columns = Array.wrap(columns).zip(weights).to_h
45
+ end
46
+
47
+ columns.transform_keys(&:to_s)
48
+ end
49
+
50
+ def self.to_search_vector_operation(language, weights)
51
+ language ||= PostgreSQL.config.full_text_search.default_language
52
+ language = ::Arel.sql(language.is_a?(Symbol) ? language.to_s : "'#{language}'")
53
+ simple = weights.size == 1
54
+
55
+ empty_string = ::Arel.sql("''")
56
+ operations = weights.map do |column, weight|
57
+ column = ::Arel.sql(column.to_s)
58
+ weight = ::Arel.sql("'#{weight}'")
59
+
60
+ op = FN.to_tsvector(language, FN.coalesce(column, empty_string))
61
+ op = FN.setweight(op, weight) unless simple
62
+ op
63
+ end
64
+
65
+ FN.concat(*operations)
66
+ end
29
67
  end
30
68
  end
31
69
  end
@@ -18,7 +18,7 @@ module Torque
18
18
  # Find or create the class that will handle the value
19
19
  def lookup(name)
20
20
  const = name.to_s.camelize
21
- namespace = Torque::PostgreSQL.config.enum.namespace
21
+ namespace = PostgreSQL.config.enum.namespace
22
22
 
23
23
  return namespace.const_get(const) if namespace.const_defined?(const)
24
24
  namespace.const_set(const, Class.new(Enum))
@@ -27,7 +27,7 @@ module Torque
27
27
  # Provide a method on the given class to setup which enums will be
28
28
  # manually initialized
29
29
  def include_on(klass, method_name = nil)
30
- method_name ||= Torque::PostgreSQL.config.enum.base_method
30
+ method_name ||= PostgreSQL.config.enum.base_method
31
31
  Builder.include_on(klass, method_name, Builder::Enum) do |builder|
32
32
  defined_enums[builder.attribute.to_s] = builder.subtype.klass
33
33
  end
@@ -46,7 +46,7 @@ module Torque
46
46
  end
47
47
  end
48
48
 
49
- # List of valus as symbols
49
+ # List of values as symbols
50
50
  def keys
51
51
  values.map(&:to_sym)
52
52
  end
@@ -86,7 +86,7 @@ module Torque
86
86
  self.values.include?(value.to_s)
87
87
  end
88
88
 
89
- # Build an active record scope for a given atribute agains a value
89
+ # Build an active record scope for a given attribute against a value
90
90
  def scope(attribute, value)
91
91
  attribute.eq(value)
92
92
  end
@@ -183,7 +183,7 @@ module Torque
183
183
  list_from = :i18n_scopes
184
184
  end
185
185
 
186
- Torque::PostgreSQL.config.enum.send(list_from).map do |key|
186
+ PostgreSQL.config.enum.send(list_from).map do |key|
187
187
  (key % values).to_sym
188
188
  end
189
189
  end
@@ -209,7 +209,7 @@ module Torque
209
209
  end
210
210
  end
211
211
 
212
- # Throw an exception for invalid valus
212
+ # Throw an exception for invalid values
213
213
  def raise_invalid(value)
214
214
  if value.is_a?(Numeric)
215
215
  raise EnumError, "#{value.inspect} is out of bounds of #{self.class.name}"
@@ -218,7 +218,7 @@ module Torque
218
218
  end
219
219
  end
220
220
 
221
- # Throw an exception for comparasion between different enums
221
+ # Throw an exception for comparison between different enums
222
222
  def raise_comparison(other)
223
223
  raise EnumError, "Comparison of #{self.class.name} with #{self.inspect} failed"
224
224
  end
@@ -18,7 +18,7 @@ module Torque
18
18
  # Find or create the class that will handle the value
19
19
  def lookup(name, enum_klass)
20
20
  const = name.to_s.camelize + 'Set'
21
- namespace = Torque::PostgreSQL.config.enum.namespace
21
+ namespace = PostgreSQL.config.enum.namespace
22
22
 
23
23
  return namespace.const_get(const) if namespace.const_defined?(const)
24
24
 
@@ -30,7 +30,7 @@ module Torque
30
30
  # Provide a method on the given class to setup which enum sets will be
31
31
  # manually initialized
32
32
  def include_on(klass, method_name = nil)
33
- method_name ||= Torque::PostgreSQL.config.enum.set_method
33
+ method_name ||= PostgreSQL.config.enum.set_method
34
34
  Builder.include_on(klass, method_name, Builder::Enum, set_features: true) do |builder|
35
35
  defined_enums[builder.attribute.to_s] = builder.subtype
36
36
  end
@@ -76,14 +76,14 @@ module Torque
76
76
  end.reduce(:+)
77
77
  end
78
78
 
79
- # Build an active record scope for a given atribute agains a value
79
+ # Build an active record scope for a given attribute against a value
80
80
  def scope(attribute, value)
81
- attribute.contains(::Arel.array(value, cast: enum_source.type_name))
81
+ attribute.contains(FN.bind_with(attribute, value).pg_cast(type_name))
82
82
  end
83
83
 
84
84
  private
85
85
 
86
- # Allows checking value existance
86
+ # Allows checking value existence
87
87
  def respond_to_missing?(method_name, include_private = false)
88
88
  valid?(method_name) || super
89
89
  end
@@ -226,7 +226,7 @@ module Torque
226
226
  end
227
227
  end
228
228
 
229
- # Throw an exception for invalid valus
229
+ # Throw an exception for invalid values
230
230
  def raise_invalid(value)
231
231
  if value.is_a?(Numeric)
232
232
  raise EnumSetError, "#{value.inspect} is out of bounds of #{self.class.name}"
@@ -235,7 +235,7 @@ module Torque
235
235
  end
236
236
  end
237
237
 
238
- # Throw an exception for comparasion between different enums
238
+ # Throw an exception for comparison between different enums
239
239
  def raise_comparison(other)
240
240
  raise EnumSetError, "Comparison of #{self.class.name} with #{self.inspect} failed"
241
241
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ module Attributes
6
+ # For now, full text search doesn't have it's own class
7
+ module FullTextSearch
8
+ class << self
9
+ # Provide a method on the given class to setup which full text search
10
+ # columns will be manually initialized
11
+ def include_on(klass, method_name = nil)
12
+ method_name ||= PostgreSQL.config.full_text_search.base_method
13
+ Builder.include_on(klass, method_name, Builder::FullTextSearch)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -3,13 +3,13 @@
3
3
  module Torque
4
4
  module PostgreSQL
5
5
  module Attributes
6
- # For naw, period doesn't have it's own class
6
+ # For now, period doesn't have it's own class
7
7
  module Period
8
8
  class << self
9
9
  # Provide a method on the given class to setup which period columns
10
10
  # will be manually initialized
11
11
  def include_on(klass, method_name = nil)
12
- method_name ||= Torque::PostgreSQL.config.period.base_method
12
+ method_name ||= PostgreSQL.config.period.base_method
13
13
  Builder.include_on(klass, method_name, Builder::Period)
14
14
  end
15
15
  end