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.
- checksums.yaml +4 -4
- data/lib/generators/torque/function_generator.rb +13 -0
- data/lib/generators/torque/templates/function.sql.erb +4 -0
- data/lib/generators/torque/templates/type.sql.erb +2 -0
- data/lib/generators/torque/templates/view.sql.erb +3 -0
- data/lib/generators/torque/type_generator.rb +13 -0
- data/lib/generators/torque/view_generator.rb +16 -0
- data/lib/torque/postgresql/adapter/database_statements.rb +111 -94
- 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 +58 -0
- data/lib/torque/postgresql/adapter/schema_dumper.rb +136 -34
- data/lib/torque/postgresql/adapter/schema_overrides.rb +45 -0
- data/lib/torque/postgresql/adapter/schema_statements.rb +109 -49
- data/lib/torque/postgresql/arel/infix_operation.rb +15 -28
- data/lib/torque/postgresql/arel/nodes.rb +16 -2
- data/lib/torque/postgresql/arel/operations.rb +7 -1
- data/lib/torque/postgresql/arel/visitors.rb +7 -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 +109 -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 +5 -11
- data/lib/torque/postgresql/collector.rb +1 -1
- data/lib/torque/postgresql/config.rb +129 -5
- data/lib/torque/postgresql/function.rb +94 -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 +137 -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/auxiliary_statement.rb +7 -2
- data/lib/torque/postgresql/relation/buckets.rb +124 -0
- data/lib/torque/postgresql/relation/distinct_on.rb +7 -2
- data/lib/torque/postgresql/relation/inheritance.rb +22 -15
- data/lib/torque/postgresql/relation/join_series.rb +112 -0
- data/lib/torque/postgresql/relation/merger.rb +17 -3
- data/lib/torque/postgresql/relation.rb +24 -38
- data/lib/torque/postgresql/schema_cache.rb +6 -12
- data/lib/torque/postgresql/version.rb +1 -1
- data/lib/torque/postgresql/versioned_commands/command_migration.rb +146 -0
- data/lib/torque/postgresql/versioned_commands/generator.rb +57 -0
- data/lib/torque/postgresql/versioned_commands/migration_context.rb +83 -0
- data/lib/torque/postgresql/versioned_commands/migrator.rb +39 -0
- data/lib/torque/postgresql/versioned_commands/schema_table.rb +101 -0
- data/lib/torque/postgresql/versioned_commands.rb +161 -0
- data/lib/torque/postgresql.rb +2 -1
- data/spec/fixtures/migrations/20250101000001_create_users.rb +0 -0
- data/spec/fixtures/migrations/20250101000002_create_function_count_users_v1.sql +0 -0
- data/spec/fixtures/migrations/20250101000003_create_internal_users.rb +0 -0
- data/spec/fixtures/migrations/20250101000004_update_function_count_users_v2.sql +0 -0
- data/spec/fixtures/migrations/20250101000005_create_view_all_users_v1.sql +0 -0
- data/spec/fixtures/migrations/20250101000006_create_type_user_id_v1.sql +0 -0
- data/spec/fixtures/migrations/20250101000007_remove_function_count_users_v2.sql +0 -0
- data/spec/initialize.rb +67 -0
- data/spec/mocks/cache_query.rb +21 -21
- data/spec/mocks/create_table.rb +6 -26
- data/spec/schema.rb +17 -12
- data/spec/spec_helper.rb +11 -2
- 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 +280 -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/relation_spec.rb +229 -0
- data/spec/tests/schema_spec.rb +6 -9
- data/spec/tests/table_inheritance_spec.rb +25 -26
- data/spec/tests/versioned_commands_spec.rb +513 -0
- 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
|
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,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.
|
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 ||=
|
211
|
+
@arel_default_sql ||= arel_sql_bind(@default.inspect)
|
212
212
|
end
|
213
213
|
|
214
|
-
def
|
215
|
-
"
|
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}\").
|
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}\").
|
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 << '.
|
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 << '.
|
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 = +"
|
282
|
-
result << '
|
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) + '.
|
306
|
-
(real ? arel_real_finish_at : arel_finish_at) + '.
|
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.
|
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?(:
|
321
|
-
" #{value} =
|
322
|
-
(" #{value} = #{value}.
|
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 = #{
|
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 = #{
|
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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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 =
|
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 ||=
|
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
|
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
|
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
|
-
|
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
|
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
|
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 =
|
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 ||=
|
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
|
79
|
+
# Build an active record scope for a given attribute against a value
|
80
80
|
def scope(attribute, value)
|
81
|
-
attribute.contains(
|
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
|
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
|
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
|
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
|
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 ||=
|
12
|
+
method_name ||= PostgreSQL.config.period.base_method
|
13
13
|
Builder.include_on(klass, method_name, Builder::Period)
|
14
14
|
end
|
15
15
|
end
|