torque-postgresql 3.4.1 → 4.0.0.rc1

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/lib/torque/postgresql/adapter/database_statements.rb +63 -84
  3. data/lib/torque/postgresql/adapter/oid/array.rb +17 -0
  4. data/lib/torque/postgresql/adapter/oid/line.rb +2 -6
  5. data/lib/torque/postgresql/adapter/oid/range.rb +4 -4
  6. data/lib/torque/postgresql/adapter/oid.rb +1 -23
  7. data/lib/torque/postgresql/adapter/quoting.rb +13 -7
  8. data/lib/torque/postgresql/adapter/schema_creation.rb +7 -28
  9. data/lib/torque/postgresql/adapter/schema_definitions.rb +36 -0
  10. data/lib/torque/postgresql/adapter/schema_dumper.rb +90 -34
  11. data/lib/torque/postgresql/adapter/schema_overrides.rb +45 -0
  12. data/lib/torque/postgresql/adapter/schema_statements.rb +64 -49
  13. data/lib/torque/postgresql/arel/infix_operation.rb +15 -28
  14. data/lib/torque/postgresql/arel/nodes.rb +2 -2
  15. data/lib/torque/postgresql/arel/operations.rb +7 -1
  16. data/lib/torque/postgresql/arel/visitors.rb +3 -9
  17. data/lib/torque/postgresql/associations/association_scope.rb +23 -31
  18. data/lib/torque/postgresql/associations/belongs_to_many_association.rb +25 -0
  19. data/lib/torque/postgresql/associations/builder/belongs_to_many.rb +16 -0
  20. data/lib/torque/postgresql/attributes/builder/enum.rb +12 -9
  21. data/lib/torque/postgresql/attributes/builder/full_text_search.rb +121 -0
  22. data/lib/torque/postgresql/attributes/builder/period.rb +21 -21
  23. data/lib/torque/postgresql/attributes/builder.rb +49 -11
  24. data/lib/torque/postgresql/attributes/enum.rb +7 -7
  25. data/lib/torque/postgresql/attributes/enum_set.rb +7 -7
  26. data/lib/torque/postgresql/attributes/full_text_search.rb +19 -0
  27. data/lib/torque/postgresql/attributes/period.rb +2 -2
  28. data/lib/torque/postgresql/attributes.rb +0 -4
  29. data/lib/torque/postgresql/auxiliary_statement/recursive.rb +3 -3
  30. data/lib/torque/postgresql/base.rb +3 -10
  31. data/lib/torque/postgresql/collector.rb +1 -1
  32. data/lib/torque/postgresql/config.rb +95 -5
  33. data/lib/torque/postgresql/function.rb +61 -0
  34. data/lib/torque/postgresql/inheritance.rb +52 -36
  35. data/lib/torque/postgresql/predicate_builder/arel_attribute_handler.rb +33 -0
  36. data/lib/torque/postgresql/predicate_builder/array_handler.rb +47 -0
  37. data/lib/torque/postgresql/predicate_builder/enumerator_lazy_handler.rb +37 -0
  38. data/lib/torque/postgresql/predicate_builder/regexp_handler.rb +21 -0
  39. data/lib/torque/postgresql/predicate_builder.rb +35 -0
  40. data/lib/torque/postgresql/railtie.rb +112 -30
  41. data/lib/torque/postgresql/reflection/abstract_reflection.rb +12 -44
  42. data/lib/torque/postgresql/reflection/belongs_to_many_reflection.rb +4 -0
  43. data/lib/torque/postgresql/reflection/has_many_reflection.rb +4 -0
  44. data/lib/torque/postgresql/reflection/runtime_reflection.rb +1 -1
  45. data/lib/torque/postgresql/relation/inheritance.rb +4 -7
  46. data/lib/torque/postgresql/relation.rb +6 -10
  47. data/lib/torque/postgresql/schema_cache.rb +6 -12
  48. data/lib/torque/postgresql/version.rb +1 -1
  49. data/lib/torque/postgresql.rb +2 -1
  50. data/spec/initialize.rb +58 -0
  51. data/spec/mocks/cache_query.rb +21 -21
  52. data/spec/mocks/create_table.rb +6 -26
  53. data/spec/schema.rb +19 -12
  54. data/spec/spec_helper.rb +5 -1
  55. data/spec/tests/arel_spec.rb +32 -7
  56. data/spec/tests/auxiliary_statement_spec.rb +3 -3
  57. data/spec/tests/belongs_to_many_spec.rb +72 -5
  58. data/spec/tests/enum_set_spec.rb +12 -11
  59. data/spec/tests/enum_spec.rb +4 -2
  60. data/spec/tests/full_text_seach_test.rb +252 -0
  61. data/spec/tests/function_spec.rb +42 -0
  62. data/spec/tests/has_many_spec.rb +21 -8
  63. data/spec/tests/interval_spec.rb +1 -7
  64. data/spec/tests/period_spec.rb +61 -61
  65. data/spec/tests/predicate_builder_spec.rb +132 -0
  66. data/spec/tests/schema_spec.rb +2 -8
  67. data/spec/tests/table_inheritance_spec.rb +25 -26
  68. metadata +34 -39
@@ -4,42 +4,55 @@ module Torque
4
4
  module PostgreSQL
5
5
  module Adapter
6
6
  module SchemaStatements
7
-
8
- TableDefinition = ActiveRecord::ConnectionAdapters::PostgreSQL::TableDefinition
9
-
10
- # Create a new schema
11
- def create_schema(name, options = {})
12
- drop_schema(name, options) if options[:force]
13
-
14
- check = 'IF NOT EXISTS' if options.fetch(:check, true)
15
- execute("CREATE SCHEMA #{check} #{quote_schema_name(name.to_s)}")
16
- end
17
-
18
- # Drop an existing schema
19
- def drop_schema(name, options = {})
20
- force = options.fetch(:force, '').upcase
21
- check = 'IF EXISTS' if options.fetch(:check, true)
22
- execute("DROP SCHEMA #{check} #{quote_schema_name(name.to_s)} #{force}")
23
- end
24
-
25
- # Drops a type.
7
+ # Drops a type
26
8
  def drop_type(name, options = {})
27
9
  force = options.fetch(:force, '').upcase
28
10
  check = 'IF EXISTS' if options.fetch(:check, true)
29
- execute <<-SQL.squish
11
+ name = sanitize_name_with_schema(name, options)
12
+
13
+ internal_exec_query(<<-SQL.squish).tap { reload_type_map }
30
14
  DROP TYPE #{check}
31
- #{quote_type_name(name, options[:schema])} #{force}
15
+ #{quote_type_name(name)} #{force}
32
16
  SQL
33
17
  end
34
18
 
35
- # Renames a type.
19
+ # Renames a type
36
20
  def rename_type(type_name, new_name, options = {})
37
- execute <<-SQL.squish
38
- ALTER TYPE #{quote_type_name(type_name, options[:schema])}
21
+ type_name = sanitize_name_with_schema(type_name, options)
22
+ internal_exec_query(<<-SQL.squish).tap { reload_type_map }
23
+ ALTER TYPE #{quote_type_name(type_name)}
39
24
  RENAME TO #{Quoting::Name.new(nil, new_name.to_s).quoted}
40
25
  SQL
41
26
  end
42
27
 
28
+ # Creates a column that stores the underlying language of the record so
29
+ # that a search vector can be created dynamically based on it. It uses
30
+ # a `regconfig` type, so string conversions are mandatory
31
+ def add_search_language(table, name, options = {})
32
+ add_column(table, name, :regconfig, options)
33
+ end
34
+
35
+ # Creates a column and setup a search vector as a virtual column. The
36
+ # options are dev-friendly and controls how the vector function will be
37
+ # defined
38
+ #
39
+ # === Options
40
+ # [:columns]
41
+ # The list of columns that will be used to create the search vector.
42
+ # It can be a single column, an array of columns, or a hash as a
43
+ # combination of column name and weight (A, B, C, or D).
44
+ # [:language]
45
+ # Specify the language config to be used for the search vector. If a
46
+ # string is provided, then the value will be statically embedded. If a
47
+ # symbol is provided, then it will reference another column.
48
+ # [:stored]
49
+ # Specify if the value should be stored in the database. As of now,
50
+ # PostgreSQL only supports `true`, which will create a stored column.
51
+ def add_search_vector(table, name, columns, options = {})
52
+ options = Builder.search_vector_options(columns: columns, **options)
53
+ add_column(table, name, options.delete(:type), options)
54
+ end
55
+
43
56
  # Changes the enumerator by adding new values
44
57
  #
45
58
  # Example:
@@ -48,6 +61,7 @@ module Torque
48
61
  # add_enum_values 'status', ['baz'], after: 'foo'
49
62
  # add_enum_values 'status', ['baz'], prepend: true
50
63
  def add_enum_values(name, values, options = {})
64
+ name = sanitize_name_with_schema(name, options)
51
65
  before = options.fetch(:before, false)
52
66
  after = options.fetch(:after, false)
53
67
 
@@ -59,7 +73,7 @@ module Torque
59
73
  reference = "BEFORE #{before}" unless before == false
60
74
  reference = "AFTER #{after}" unless after == false
61
75
  execute <<-SQL.squish
62
- ALTER TYPE #{quote_type_name(name, options[:schema])}
76
+ ALTER TYPE #{quote_type_name(name)}
63
77
  ADD VALUE #{value} #{reference}
64
78
  SQL
65
79
 
@@ -77,34 +91,26 @@ module Torque
77
91
  SQL
78
92
  end
79
93
 
80
- # Rewrite the method that creates tables to easily accept extra options
81
- def create_table(table_name, **options, &block)
82
- table_name = "#{options[:schema]}.#{table_name}" if options[:schema].present?
83
-
84
- options[:id] = false if options[:inherits].present? &&
85
- options[:primary_key].blank? && options[:id].blank?
86
94
 
87
- super table_name, **options, &block
88
- end
95
+ # Add the schema option when extracting table options
96
+ def table_options(table_name)
97
+ options = super
89
98
 
90
- # Simply add the schema to the table name when changing a table
91
- def change_table(table_name, **options)
92
- table_name = "#{options[:schema]}.#{table_name}" if options[:schema].present?
93
- super table_name, **options
94
- end
99
+ if PostgreSQL.config.schemas.enabled
100
+ table, schema = table_name.split('.').reverse
101
+ if table.present? && schema.present? && schema != current_schema
102
+ options[:schema] = schema
103
+ end
104
+ end
95
105
 
96
- # Simply add the schema to the table name when dropping a table
97
- def drop_table(table_name, **options)
98
- table_name = "#{options[:schema]}.#{table_name}" if options[:schema].present?
99
- super table_name, **options
100
- end
106
+ if options[:options]&.start_with?('INHERITS (')
107
+ options.delete(:options)
101
108
 
102
- # Add the schema option when extracting table options
103
- def table_options(table_name)
104
- parts = table_name.split('.').reverse
105
- return super unless parts.size == 2 && parts[1] != 'public'
109
+ tables = inherited_table_names(table_name)
110
+ options[:inherits] = tables.one? ? tables.first : tables
111
+ end
106
112
 
107
- (super || {}).merge(schema: parts[1])
113
+ options
108
114
  end
109
115
 
110
116
  # When dumping the schema we need to add all schemas, not only those
@@ -112,7 +118,10 @@ module Torque
112
118
  def quoted_scope(name = nil, type: nil)
113
119
  return super unless name.nil?
114
120
 
115
- super.merge(schema: "ANY ('{#{user_defined_schemas.join(',')}}')")
121
+ scope = super
122
+ global = scope[:schema].start_with?('ANY (')
123
+ scope[:schema] = "ANY ('{#{user_defined_schemas.join(',')}}')"
124
+ scope
116
125
  end
117
126
 
118
127
  # Fix the query to include the schema on tables names when dumping
@@ -135,6 +144,12 @@ module Torque
135
144
  super(table_name.split('.').last, column_name, suffix)
136
145
  end
137
146
 
147
+ # Helper for supporting schema name in several methods
148
+ def sanitize_name_with_schema(name, options)
149
+ return name if (schema = options&.delete(:schema)).blank?
150
+ Quoting::Name.new(schema.to_s, name.to_s)
151
+ end
152
+
138
153
  def quote_enum_values(name, values, options)
139
154
  prefix = options[:prefix]
140
155
  prefix = name if prefix === true
@@ -3,39 +3,26 @@
3
3
  module Torque
4
4
  module PostgreSQL
5
5
  module Arel
6
- nodes = ::Arel::Nodes
7
- inflix = nodes::InfixOperation
8
- visitors = ::Arel::Visitors::PostgreSQL
9
- default_alias = :visit_Arel_Nodes_InfixOperation
10
-
11
6
  Math = Module.new
12
- INFLIX_OPERATION = {
13
- 'Overlaps' => :'&&',
14
- 'Contains' => :'@>',
15
- 'ContainedBy' => :'<@',
16
- 'HasKey' => :'?',
17
- 'HasAllKeys' => :'?&',
18
- 'HasAnyKeys' => :'?|',
19
- 'StrictlyLeft' => :'<<',
20
- 'StrictlyRight' => :'>>',
21
- 'DoesntRightExtend' => :'&<',
22
- 'DoesntLeftExtend' => :'&>',
23
- 'AdjacentTo' => :'-|-',
24
- }.freeze
25
7
 
26
- INFLIX_OPERATION.each do |operator_name, operator|
27
- next if nodes.const_defined?(operator_name)
8
+ def self.build_operations(operations)
9
+ default_alias = :visit_Arel_Nodes_InfixOperation
10
+
11
+ operations&.each do |name, operator|
12
+ klass_name = name.to_s.camelize
13
+ next if ::Arel::Nodes.const_defined?(klass_name)
28
14
 
29
- klass = Class.new(inflix)
30
- klass.send(:define_method, :initialize) { |*args| super(operator, *args) }
15
+ klass = Class.new(::Arel::Nodes::InfixOperation)
16
+ operator = (-operator).to_sym
17
+ klass.send(:define_method, :initialize) { |*args| super(operator, *args) }
31
18
 
32
- nodes.const_set(operator_name, klass)
33
- visitors.send(:alias_method, :"visit_Arel_Nodes_#{operator_name}", default_alias)
19
+ ::Arel::Nodes.const_set(klass_name, klass)
20
+ visitor = :"visit_Arel_Nodes_#{klass_name}"
21
+ ::Arel::Visitors::PostgreSQL.send(:alias_method, visitor, default_alias)
34
22
 
35
- # Don't worry about quoting here, if the right side is something that
36
- # doesn't need quoting, it will leave it as it is
37
- Math.send(:define_method, operator_name.underscore) do |other|
38
- klass.new(self, other)
23
+ # Don't worry about quoting here, if the right side is something that
24
+ # doesn't need quoting, it will leave it as it is
25
+ Math.send(:define_method, klass_name.underscore) { |other| klass.new(self, other) }
39
26
  end
40
27
  end
41
28
 
@@ -13,7 +13,7 @@ module Torque
13
13
  include ::Arel::Math
14
14
 
15
15
  def initialize(left, right, array = false)
16
- right = right.to_s
16
+ right = +right.to_s
17
17
  right << '[]' if array
18
18
  super left, right
19
19
  end
@@ -24,7 +24,7 @@ module Torque
24
24
  ::Arel.define_singleton_method(:array) do |*values, cast: nil|
25
25
  values = values.first if values.size.eql?(1) && values.first.is_a?(::Enumerable)
26
26
  result = ::Arel::Nodes.build_quoted(values)
27
- result = result.cast(cast, true) if cast.present?
27
+ result = result.pg_cast(cast, true) if cast.present?
28
28
  result
29
29
  end
30
30
 
@@ -6,10 +6,16 @@ module Torque
6
6
  module Operations
7
7
 
8
8
  # Create a cast operation
9
- def cast(type, array = false)
9
+ def pg_cast(type, array = false)
10
10
  Nodes::Cast.new(self, type, array)
11
11
  end
12
12
 
13
+ # Make sure to add proper support over AR's own +cast+ method while
14
+ # still allow attributes to be casted
15
+ def cast(type, array = false)
16
+ defined?(super) && !array ? super(type) : pg_cast(type, array)
17
+ end
18
+
13
19
  end
14
20
 
15
21
  ::Arel::Attributes::Attribute.include(Operations)
@@ -4,13 +4,6 @@ module Torque
4
4
  module PostgreSQL
5
5
  module Arel
6
6
  module Visitors
7
- # Enclose select manager with parenthesis
8
- # :TODO: Remove when checking the new version of Arel
9
- def visit_Arel_SelectManager(o, collector)
10
- collector << '('
11
- visit(o.ast, collector) << ')'
12
- end
13
-
14
7
  # Add ONLY modifier to query
15
8
  def visit_Arel_Nodes_JoinSource(o, collector)
16
9
  collector << 'ONLY ' if o.only?
@@ -26,8 +19,9 @@ module Torque
26
19
  # Allow quoted arrays to get here
27
20
  def visit_Arel_Nodes_Casted(o, collector)
28
21
  value = o.value_for_database
29
- return super unless value.is_a?(::Enumerable)
30
- quote_array(value, collector)
22
+ klass = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array::Data
23
+ return super unless value.is_a?(klass)
24
+ quote_array(value.values, collector)
31
25
  end
32
26
 
33
27
  ## TORQUE VISITORS
@@ -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,121 @@
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_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
+
18
+ @default_order =
19
+ case options[:order]
20
+ when :asc, true then :asc
21
+ when :desc then :desc
22
+ else false
23
+ end
24
+
25
+ @default_language = options[:language] if options[:language].is_a?(String) ||
26
+ options[:language].is_a?(Symbol)
27
+ @default_language ||= PostgreSQL.config.full_text_search.default_language.to_s
28
+ end
29
+
30
+ # What is the name of the scope to be added to the model
31
+ def scope_name
32
+ @scope_name ||= [
33
+ options[:prefix],
34
+ :full_text_search,
35
+ options[:suffix],
36
+ ].compact.join('_')
37
+ end
38
+
39
+ # Just check if the scope name is already defined
40
+ def conflicting?
41
+ return if options[:force] == true
42
+
43
+ if klass.dangerous_class_method?(scope_name)
44
+ raise Interrupt, scope_name.to_s
45
+ end
46
+ end
47
+
48
+ # Create the proper scope
49
+ def build
50
+ @klass_module = Module.new
51
+ add_scope_to_module
52
+ klass.extend klass_module
53
+ end
54
+
55
+ # Creates a class method as the scope that builds the full text search
56
+ #
57
+ # def full_text_search(value, order: :asc, rank: :rank, language: 'english', phrase: true)
58
+ # attr = arel_table["search_vector"]
59
+ # fn = ::Torque::PostgreSQL::FN
60
+ #
61
+ # lang = language.to_s if !language.is_a?(::Symbol)
62
+ # lang ||= arel_table[language.to_s].pg_cast(:regconfig) if has_attribute?(language)
63
+ # lang ||= public_send(language) if respond_to?(language)
64
+ #
65
+ # raise ArgumentError, <<~MSG.squish if lang.nil?
66
+ # Unable to determine language from #{language.inspect}.
67
+ # MSG
68
+ #
69
+ # value = fn.bind(:value, value.to_s, attr.type_caster)
70
+ # lang = fn.bind(:lang, lang, attr.type_caster) if lang.is_a?(::String)
71
+ #
72
+ # query = fn.public_send(phrase ? :phraseto_tsquery : :to_tsquery, lang, value)
73
+ # ranker = fn.ts_rank(attr, query) if rank || order
74
+ #
75
+ # result = where(fn.infix(:"@@", attr, query))
76
+ # result = result.order(ranker.public_send(order == :desc ? :desc : :asc)) if order
77
+ # result.select_extra_values += [ranker.as(rank == true ? 'rank' : rank.to_s)] if rank
78
+ # result
79
+ # end
80
+ def add_scope_to_module
81
+ klass_module.module_eval <<-RUBY, __FILE__, __LINE__ + 1
82
+ def #{scope_name}(value#{scope_args})
83
+ attr = arel_table['#{attribute}']
84
+ fn = ::Torque::PostgreSQL::FN
85
+
86
+ lang = language.to_s if !language.is_a?(::Symbol)
87
+ lang ||= arel_table[language.to_s] if has_attribute?(language)
88
+ lang ||= public_send(language) if respond_to?(language)
89
+
90
+ raise ::ArgumentError, <<~MSG.squish if lang.nil?
91
+ Unable to determine language from \#{language.inspect}.
92
+ MSG
93
+
94
+ value = fn.bind(:value, value.to_s, attr.type_caster)
95
+ lang = fn.bind(:lang, lang, attr.type_caster) if lang.is_a?(::String)
96
+
97
+ query = fn.public_send(phrase ? :phraseto_tsquery : :to_tsquery, lang, value)
98
+ ranker = fn.ts_rank(attr, query) if rank || order
99
+
100
+ result = where(fn.infix(:"@@", attr, query))
101
+ result = result.order(ranker.public_send(order == :desc ? :desc : :asc)) if order
102
+ result.select_extra_values += [ranker.as(rank == true ? 'rank' : rank.to_s)] if rank
103
+ result
104
+ end
105
+ RUBY
106
+ end
107
+
108
+ # Returns the arguments to be used on the scope
109
+ def scope_args
110
+ args = +''
111
+ args << ", order: #{default_order.inspect}"
112
+ args << ", rank: #{default_rank.inspect}"
113
+ args << ", language: #{default_language.inspect}"
114
+ args << ", phrase: true"
115
+ args
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end