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.
- checksums.yaml +4 -4
- data/lib/torque/postgresql/adapter/database_statements.rb +63 -84
- data/lib/torque/postgresql/adapter/oid/array.rb +17 -0
- data/lib/torque/postgresql/adapter/oid/line.rb +2 -6
- data/lib/torque/postgresql/adapter/oid/range.rb +4 -4
- data/lib/torque/postgresql/adapter/oid.rb +1 -23
- data/lib/torque/postgresql/adapter/quoting.rb +13 -7
- data/lib/torque/postgresql/adapter/schema_creation.rb +7 -28
- data/lib/torque/postgresql/adapter/schema_definitions.rb +36 -0
- data/lib/torque/postgresql/adapter/schema_dumper.rb +90 -34
- data/lib/torque/postgresql/adapter/schema_overrides.rb +45 -0
- data/lib/torque/postgresql/adapter/schema_statements.rb +64 -49
- data/lib/torque/postgresql/arel/infix_operation.rb +15 -28
- data/lib/torque/postgresql/arel/nodes.rb +2 -2
- data/lib/torque/postgresql/arel/operations.rb +7 -1
- data/lib/torque/postgresql/arel/visitors.rb +3 -9
- data/lib/torque/postgresql/associations/association_scope.rb +23 -31
- data/lib/torque/postgresql/associations/belongs_to_many_association.rb +25 -0
- data/lib/torque/postgresql/associations/builder/belongs_to_many.rb +16 -0
- data/lib/torque/postgresql/attributes/builder/enum.rb +12 -9
- data/lib/torque/postgresql/attributes/builder/full_text_search.rb +121 -0
- data/lib/torque/postgresql/attributes/builder/period.rb +21 -21
- data/lib/torque/postgresql/attributes/builder.rb +49 -11
- data/lib/torque/postgresql/attributes/enum.rb +7 -7
- data/lib/torque/postgresql/attributes/enum_set.rb +7 -7
- data/lib/torque/postgresql/attributes/full_text_search.rb +19 -0
- data/lib/torque/postgresql/attributes/period.rb +2 -2
- data/lib/torque/postgresql/attributes.rb +0 -4
- data/lib/torque/postgresql/auxiliary_statement/recursive.rb +3 -3
- data/lib/torque/postgresql/base.rb +3 -10
- data/lib/torque/postgresql/collector.rb +1 -1
- data/lib/torque/postgresql/config.rb +95 -5
- data/lib/torque/postgresql/function.rb +61 -0
- data/lib/torque/postgresql/inheritance.rb +52 -36
- data/lib/torque/postgresql/predicate_builder/arel_attribute_handler.rb +33 -0
- data/lib/torque/postgresql/predicate_builder/array_handler.rb +47 -0
- data/lib/torque/postgresql/predicate_builder/enumerator_lazy_handler.rb +37 -0
- data/lib/torque/postgresql/predicate_builder/regexp_handler.rb +21 -0
- data/lib/torque/postgresql/predicate_builder.rb +35 -0
- data/lib/torque/postgresql/railtie.rb +112 -30
- data/lib/torque/postgresql/reflection/abstract_reflection.rb +12 -44
- data/lib/torque/postgresql/reflection/belongs_to_many_reflection.rb +4 -0
- data/lib/torque/postgresql/reflection/has_many_reflection.rb +4 -0
- data/lib/torque/postgresql/reflection/runtime_reflection.rb +1 -1
- data/lib/torque/postgresql/relation/inheritance.rb +4 -7
- data/lib/torque/postgresql/relation.rb +6 -10
- data/lib/torque/postgresql/schema_cache.rb +6 -12
- data/lib/torque/postgresql/version.rb +1 -1
- data/lib/torque/postgresql.rb +2 -1
- data/spec/initialize.rb +58 -0
- data/spec/mocks/cache_query.rb +21 -21
- data/spec/mocks/create_table.rb +6 -26
- data/spec/schema.rb +19 -12
- data/spec/spec_helper.rb +5 -1
- data/spec/tests/arel_spec.rb +32 -7
- data/spec/tests/auxiliary_statement_spec.rb +3 -3
- data/spec/tests/belongs_to_many_spec.rb +72 -5
- data/spec/tests/enum_set_spec.rb +12 -11
- data/spec/tests/enum_spec.rb +4 -2
- data/spec/tests/full_text_seach_test.rb +252 -0
- data/spec/tests/function_spec.rb +42 -0
- data/spec/tests/has_many_spec.rb +21 -8
- data/spec/tests/interval_spec.rb +1 -7
- data/spec/tests/period_spec.rb +61 -61
- data/spec/tests/predicate_builder_spec.rb +132 -0
- data/spec/tests/schema_spec.rb +2 -8
- data/spec/tests/table_inheritance_spec.rb +25 -26
- 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
|
-
|
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
|
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
|
-
|
38
|
-
|
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
|
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
|
-
|
88
|
-
|
95
|
+
# Add the schema option when extracting table options
|
96
|
+
def table_options(table_name)
|
97
|
+
options = super
|
89
98
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
-
|
97
|
-
|
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
|
-
|
103
|
-
|
104
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
27
|
-
|
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
|
-
|
30
|
-
|
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
|
-
|
33
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
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.
|
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
|
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
|
-
|
30
|
-
|
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
|
17
|
-
#
|
18
|
-
#
|
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
|
-
|
23
|
-
|
24
|
-
|
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
|
30
|
-
#
|
31
|
-
#
|
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
|
-
|
36
|
-
|
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)
|
158
|
-
attr = arel_table['#{attribute}']
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
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
|