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,41 +4,148 @@ module Torque
|
|
4
4
|
module PostgreSQL
|
5
5
|
# = Torque PostgreSQL Railtie
|
6
6
|
class Railtie < Rails::Railtie # :nodoc:
|
7
|
-
|
8
7
|
# Get information from the running rails app
|
9
8
|
initializer 'torque-postgresql' do |app|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
# initializer
|
15
|
-
Torque::PostgreSQL::Attributes::Enum.include_on(ActiveRecord::Base)
|
16
|
-
Torque::PostgreSQL::Attributes::EnumSet.include_on(ActiveRecord::Base)
|
17
|
-
Torque::PostgreSQL::Attributes::Period.include_on(ActiveRecord::Base)
|
18
|
-
|
19
|
-
# Setup belongs_to_many association
|
20
|
-
ActiveRecord::Base.belongs_to_many_required_by_default = torque_config.associations
|
21
|
-
.belongs_to_many_required_by_default
|
22
|
-
|
23
|
-
# Define a method to find enumaerators based on the namespace
|
24
|
-
torque_config.enum.namespace.define_singleton_method(:const_missing) do |name|
|
25
|
-
Torque::PostgreSQL::Attributes::Enum.lookup(name)
|
26
|
-
end
|
9
|
+
ActiveSupport.on_load(:active_record_postgresqladapter) do
|
10
|
+
ActiveSupport.on_load(:active_record) do
|
11
|
+
torque_config = Torque::PostgreSQL.config
|
12
|
+
torque_config.eager_load = app.config.eager_load
|
27
13
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
14
|
+
# TODO: Only load files that have their features enabled, like CTE
|
15
|
+
|
16
|
+
ar_type = ActiveRecord::Type
|
17
|
+
|
18
|
+
# Setup belongs_to_many association
|
19
|
+
ActiveRecord::Base.belongs_to_many_required_by_default =
|
20
|
+
torque_config.associations.belongs_to_many_required_by_default
|
21
|
+
|
22
|
+
## General features
|
23
|
+
if torque_config.join_series
|
24
|
+
require_relative 'relation/join_series'
|
25
|
+
Relation.include(Relation::JoinSeries)
|
26
|
+
end
|
27
|
+
|
28
|
+
if torque_config.buckets
|
29
|
+
require_relative 'relation/buckets'
|
30
|
+
Relation.include(Relation::Buckets)
|
31
|
+
end
|
32
|
+
|
33
|
+
## Schemas Enabled Setup
|
34
|
+
if (config = torque_config.schemas).enabled
|
35
|
+
require_relative 'adapter/schema_overrides'
|
36
|
+
end
|
37
|
+
|
38
|
+
## CTE Enabled Setup
|
39
|
+
if (config = torque_config.auxiliary_statement).enabled
|
40
|
+
require_relative 'auxiliary_statement'
|
41
|
+
require_relative 'relation/auxiliary_statement'
|
42
|
+
Relation.include(Relation::AuxiliaryStatement)
|
43
|
+
|
44
|
+
# Define the exposed constant for both types of auxiliary statements
|
45
|
+
if config.exposed_class.present?
|
46
|
+
*ns, name = config.exposed_class.split('::')
|
47
|
+
base = ns.present? ? ::Object.const_get(ns.join('::')) : ::Object
|
48
|
+
base.const_set(name, AuxiliaryStatement)
|
49
|
+
|
50
|
+
*ns, name = config.exposed_recursive_class.split('::')
|
51
|
+
base = ns.present? ? ::Object.const_get(ns.join('::')) : ::Object
|
52
|
+
base.const_set(name, AuxiliaryStatement::Recursive)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
## Enum Enabled Setup
|
57
|
+
if (config = torque_config.enum).enabled
|
58
|
+
require_relative 'adapter/oid/enum'
|
59
|
+
require_relative 'adapter/oid/enum_set'
|
60
|
+
|
61
|
+
require_relative 'attributes/enum'
|
62
|
+
require_relative 'attributes/enum_set'
|
63
|
+
|
64
|
+
Attributes::Enum.include_on(ActiveRecord::Base)
|
65
|
+
Attributes::EnumSet.include_on(ActiveRecord::Base)
|
66
|
+
|
67
|
+
ar_type.register(:enum, Adapter::OID::Enum, adapter: :postgresql)
|
68
|
+
ar_type.register(:enum_set, Adapter::OID::EnumSet, adapter: :postgresql)
|
69
|
+
|
70
|
+
if config.namespace == false
|
71
|
+
# TODO: Allow enum classes to exist without a namespace
|
72
|
+
config.namespace = PostgreSQL.const_set('Enum', Module.new)
|
73
|
+
else
|
74
|
+
config.namespace ||= ::Object.const_set('Enum', Module.new)
|
75
|
+
|
76
|
+
# Define a method to find enumerators based on the namespace
|
77
|
+
config.namespace.define_singleton_method(:const_missing) do |name|
|
78
|
+
Attributes::Enum.lookup(name)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Define a helper method to get a sample value
|
82
|
+
config.namespace.define_singleton_method(:sample) do |name|
|
83
|
+
Attributes::Enum.lookup(name).sample
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
## Geometry Enabled Setup
|
89
|
+
if (config = torque_config.geometry).enabled
|
90
|
+
require_relative 'adapter/oid/box'
|
91
|
+
require_relative 'adapter/oid/circle'
|
92
|
+
require_relative 'adapter/oid/line'
|
93
|
+
require_relative 'adapter/oid/segment'
|
94
|
+
|
95
|
+
ar_type.register(:box, Adapter::OID::Box, adapter: :postgresql)
|
96
|
+
ar_type.register(:circle, Adapter::OID::Circle, adapter: :postgresql)
|
97
|
+
ar_type.register(:line, Adapter::OID::Line, adapter: :postgresql)
|
98
|
+
ar_type.register(:segment, Adapter::OID::Segment, adapter: :postgresql)
|
99
|
+
end
|
100
|
+
|
101
|
+
## Period Enabled Setup
|
102
|
+
if (config = torque_config.period).enabled
|
103
|
+
require_relative 'attributes/period'
|
104
|
+
Attributes::Period.include_on(ActiveRecord::Base)
|
105
|
+
end
|
106
|
+
|
107
|
+
## Interval Enabled Setup
|
108
|
+
if (config = torque_config.interval).enabled
|
109
|
+
require_relative 'adapter/oid/interval'
|
110
|
+
ar_type.register(:interval, Adapter::OID::Interval, adapter: :postgresql)
|
111
|
+
end
|
112
|
+
|
113
|
+
## Full Text Search Enabled Setup
|
114
|
+
if (config = torque_config.full_text_search).enabled
|
115
|
+
require_relative 'attributes/full_text_search'
|
116
|
+
Attributes::FullTextSearch.include_on(ActiveRecord::Base)
|
117
|
+
end
|
118
|
+
|
119
|
+
## Arel Setup
|
120
|
+
PostgreSQL::Arel.build_operations(torque_config.arel.infix_operators)
|
121
|
+
if (mod = torque_config.arel.expose_function_helper_on&.to_s)
|
122
|
+
parent, _, name = mod.rpartition('::')
|
123
|
+
parent = parent ? parent.constantize : ::Object
|
124
|
+
|
125
|
+
raise ArgumentError, <<~MSG.squish if parent.const_defined?(name)
|
126
|
+
Unable to expose Arel function helper on #{mod} because the constant
|
127
|
+
#{name} is already defined on #{parent}. Please choose a different name.
|
128
|
+
MSG
|
129
|
+
|
130
|
+
parent.const_set(name, PostgreSQL::FN)
|
131
|
+
end
|
132
|
+
|
133
|
+
## Versioned Commands Setup
|
134
|
+
if (config = torque_config.versioned_commands).enabled
|
135
|
+
require_relative 'versioned_commands'
|
136
|
+
|
137
|
+
ActiveRecord::Schema::Definition.include(Adapter::Definition)
|
138
|
+
end
|
32
139
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
140
|
+
# Make sure to load all the types that are handled by this gem on
|
141
|
+
# each individual PG connection
|
142
|
+
adapter = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
|
143
|
+
ActiveRecord::Base.connection_handler.each_connection_pool do |pool|
|
144
|
+
next unless pool.db_config.adapter_class.is_a?(adapter)
|
38
145
|
|
39
|
-
|
40
|
-
|
41
|
-
|
146
|
+
pool.with_connection { |conn| conn.torque_load_additional_types }
|
147
|
+
end
|
148
|
+
end
|
42
149
|
end
|
43
150
|
end
|
44
151
|
end
|
@@ -5,29 +5,25 @@ module Torque
|
|
5
5
|
module Reflection
|
6
6
|
module AbstractReflection
|
7
7
|
AREL_ATTR = ::Arel::Attributes::Attribute
|
8
|
-
|
9
|
-
ARR_NO_CAST = 'bigint'
|
10
|
-
ARR_CAST = 'bigint[]'
|
8
|
+
AREL_NODE = ::Arel::Nodes::Node
|
11
9
|
|
12
10
|
# Check if the foreign key actually exists
|
13
11
|
def connected_through_array?
|
14
12
|
false
|
15
13
|
end
|
16
14
|
|
17
|
-
#
|
18
|
-
#
|
15
|
+
# Connection through an array-like attribute is more complex then just
|
16
|
+
# a simple eq. This needs to go through the channel that handles larger
|
17
|
+
# situations
|
19
18
|
def join_scope(table, foreign_table, foreign_klass)
|
20
19
|
return super unless connected_through_array?
|
21
20
|
|
22
|
-
|
21
|
+
table_md = ActiveRecord::TableMetadata.new(klass, table)
|
22
|
+
predicate_builder = klass.predicate_builder.with(table_md)
|
23
23
|
scope_chain_items = join_scopes(table, predicate_builder)
|
24
24
|
klass_scope = klass_join_scope(table, predicate_builder)
|
25
25
|
|
26
26
|
klass_scope.where!(build_id_constraint_between(table, foreign_table))
|
27
|
-
klass_scope.where!(type => foreign_klass.polymorphic_name) if type
|
28
|
-
klass_scope.where!(klass.send(:type_condition, table)) \
|
29
|
-
if klass.finder_needs_type_condition?
|
30
|
-
|
31
27
|
scope_chain_items.inject(klass_scope, &:merge!)
|
32
28
|
end
|
33
29
|
|
@@ -40,43 +36,15 @@ module Torque
|
|
40
36
|
result
|
41
37
|
end
|
42
38
|
|
43
|
-
# Build the id constraint checking if both types are perfect matching.
|
44
|
-
# The klass attribute (left side) will always be a column attribute
|
45
|
-
def build_id_constraint(klass_attr, source_attr)
|
46
|
-
return klass_attr.eq(source_attr) unless connected_through_array?
|
47
|
-
|
48
|
-
# Klass and key are associated with the reflection Class
|
49
|
-
klass_type = klass.columns_hash[join_keys.key.to_s]
|
50
|
-
|
51
|
-
# Apply an ANY operation which checks if the single value on the left
|
52
|
-
# side exists in the array on the right side
|
53
|
-
if source_attr.is_a?(AREL_ATTR)
|
54
|
-
any_value = [klass_attr, source_attr]
|
55
|
-
any_value.reverse! if klass_type.try(:array?)
|
56
|
-
return any_value.shift.eq(::Arel::Nodes::NamedFunction.new('ANY', any_value))
|
57
|
-
end
|
58
|
-
|
59
|
-
# If the left side is not an array, just use the IN condition
|
60
|
-
return klass_attr.in(source_attr) unless klass_type.try(:array)
|
61
|
-
|
62
|
-
# Build the overlap condition (array && array) ensuring that the right
|
63
|
-
# side has the same type as the left side
|
64
|
-
source_attr = ::Arel::Nodes.build_quoted(Array.wrap(source_attr))
|
65
|
-
klass_attr.overlaps(source_attr.cast(klass_type.sql_type_metadata.sql_type))
|
66
|
-
end
|
67
|
-
|
68
|
-
# TODO: Deprecate this method
|
69
|
-
def join_keys
|
70
|
-
OpenStruct.new(key: join_primary_key, foreign_key: join_foreign_key)
|
71
|
-
end
|
72
|
-
|
73
39
|
private
|
74
40
|
|
41
|
+
# This one is a lot simpler, now that we have a predicate builder that
|
42
|
+
# knows exactly what to do with 2 array-like attributes
|
75
43
|
def build_id_constraint_between(table, foreign_table)
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
44
|
+
PredicateBuilder::ArelAttributeHandler.call(
|
45
|
+
table[join_primary_key],
|
46
|
+
foreign_table[join_foreign_key],
|
47
|
+
)
|
80
48
|
end
|
81
49
|
end
|
82
50
|
|
@@ -5,7 +5,7 @@ module Torque
|
|
5
5
|
module Reflection
|
6
6
|
module RuntimeReflection
|
7
7
|
delegate :klass, :active_record, :connected_through_array?, :macro, :name,
|
8
|
-
:
|
8
|
+
:array_attribute, to: :@reflection
|
9
9
|
end
|
10
10
|
|
11
11
|
::ActiveRecord::Reflection::RuntimeReflection.include(RuntimeReflection)
|
@@ -6,9 +6,14 @@ module Torque
|
|
6
6
|
module AuxiliaryStatement
|
7
7
|
|
8
8
|
# :nodoc:
|
9
|
-
def auxiliary_statements_values
|
9
|
+
def auxiliary_statements_values
|
10
|
+
@values.fetch(:auxiliary_statements, FROZEN_EMPTY_ARRAY)
|
11
|
+
end
|
10
12
|
# :nodoc:
|
11
|
-
def auxiliary_statements_values=(value)
|
13
|
+
def auxiliary_statements_values=(value)
|
14
|
+
assert_modifiable!
|
15
|
+
@values[:auxiliary_statements] = value
|
16
|
+
end
|
12
17
|
|
13
18
|
# Set use of an auxiliary statement
|
14
19
|
def with(*args, **settings)
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Torque
|
4
|
+
module PostgreSQL
|
5
|
+
module Relation
|
6
|
+
module Buckets
|
7
|
+
|
8
|
+
# :nodoc:
|
9
|
+
def buckets_value
|
10
|
+
@values.fetch(:buckets, nil)
|
11
|
+
end
|
12
|
+
# :nodoc:
|
13
|
+
def buckets_value=(value)
|
14
|
+
assert_modifiable!
|
15
|
+
@values[:buckets] = value
|
16
|
+
end
|
17
|
+
|
18
|
+
# Specifies how to bucket records. It works for both the calculations
|
19
|
+
# or just putting records into groups. For example:
|
20
|
+
#
|
21
|
+
# User.buckets(:created_at, [1.year.ago, 1.month.ago, 1.week.ago])
|
22
|
+
# # Returns all users grouped by created_at in the given time ranges
|
23
|
+
#
|
24
|
+
# User.buckets(:age, 0..100, step: 10).count
|
25
|
+
# # Counts all users grouped by age buckets of 10 years
|
26
|
+
def buckets(*value, **xargs)
|
27
|
+
spawn.buckets!(*value, **xargs)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Like #buckets, but modifies relation in place.
|
31
|
+
def buckets!(attribute, values, count: nil, cast: nil, as: nil)
|
32
|
+
raise ArgumentError, <<~MSG.squish if !values.is_a?(Array) && !values.is_a?(Range)
|
33
|
+
Buckets must be an array or a range.
|
34
|
+
MSG
|
35
|
+
|
36
|
+
count ||= 1 if values.is_a?(Range)
|
37
|
+
attribute = arel_table[attribute] unless ::Arel.arel_node?(attribute)
|
38
|
+
self.buckets_value = [attribute, values, count, cast, as]
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
# When performing calculations with buckets, this method add a grouping
|
43
|
+
# clause to the query by the bucket values, and then adjust the keys
|
44
|
+
# to match provided values
|
45
|
+
def calculate(*)
|
46
|
+
return super if buckets_value.blank?
|
47
|
+
|
48
|
+
raise ArgumentError, <<~MSG.squish if group_values.present?
|
49
|
+
Cannot calculate with buckets when there are already group values.
|
50
|
+
MSG
|
51
|
+
|
52
|
+
keys = buckets_keys
|
53
|
+
self.group_values = [FN.group_by(build_buckets_node, :bucket)]
|
54
|
+
super.transform_keys { |key| keys[key - 1] }
|
55
|
+
end
|
56
|
+
|
57
|
+
module Initializer
|
58
|
+
# Hook into the output of records to make sure we group by the buckets
|
59
|
+
def records
|
60
|
+
return super if buckets_value.blank?
|
61
|
+
|
62
|
+
keys = buckets_keys
|
63
|
+
col = buckets_column
|
64
|
+
super.group_by do |record|
|
65
|
+
val = (record[col] || 0) - 1
|
66
|
+
keys[val] if val >= 0 && val < keys.size
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# Hook arel build to add the column
|
74
|
+
def build_arel(*)
|
75
|
+
return super if buckets_value.blank? || select_values.present?
|
76
|
+
|
77
|
+
self.select_extra_values += [build_buckets_node.as(buckets_column)]
|
78
|
+
super
|
79
|
+
end
|
80
|
+
|
81
|
+
# Build the Arel node for the buckets function
|
82
|
+
def build_buckets_node
|
83
|
+
attribute, values, count, cast, * = buckets_value
|
84
|
+
|
85
|
+
if values.is_a?(Range)
|
86
|
+
FN.width_bucket(
|
87
|
+
attribute,
|
88
|
+
FN.bind_type(values.begin, name: 'bucket_start', cast: 'numeric'),
|
89
|
+
FN.bind_type(values.end, name: 'bucket_end', cast: 'numeric'),
|
90
|
+
FN.bind_type(count, name: 'bucket_count', cast: 'integer'),
|
91
|
+
)
|
92
|
+
else
|
93
|
+
FN.width_bucket(attribute, ::Arel.array(values, cast: cast))
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Returns the column used for buckets, if any
|
98
|
+
def buckets_column
|
99
|
+
buckets_value.last&.to_s || 'bucket'
|
100
|
+
end
|
101
|
+
|
102
|
+
# Transform a range into the proper keys for buckets
|
103
|
+
def buckets_keys
|
104
|
+
keys = buckets_value.second
|
105
|
+
return keys unless keys.is_a?(Range)
|
106
|
+
|
107
|
+
left = nil
|
108
|
+
step = buckets_value.third
|
109
|
+
step = (keys.end - keys.begin).fdiv(step)
|
110
|
+
step = step.to_i if step.to_i == step
|
111
|
+
keys.step(step).each_with_object([]) do |right, result|
|
112
|
+
next left = right if left.nil?
|
113
|
+
|
114
|
+
start, left = left, right
|
115
|
+
result << Range.new(start, left, true)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
Initializer.include(Buckets::Initializer)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -6,9 +6,14 @@ module Torque
|
|
6
6
|
module DistinctOn
|
7
7
|
|
8
8
|
# :nodoc:
|
9
|
-
def distinct_on_values
|
9
|
+
def distinct_on_values
|
10
|
+
@values.fetch(:distinct_on, FROZEN_EMPTY_ARRAY)
|
11
|
+
end
|
10
12
|
# :nodoc:
|
11
|
-
def distinct_on_values=(value)
|
13
|
+
def distinct_on_values=(value)
|
14
|
+
assert_modifiable!
|
15
|
+
@values[:distinct_on] = value
|
16
|
+
end
|
12
17
|
|
13
18
|
# Specifies whether the records should be unique or not by a given set
|
14
19
|
# of fields. For example:
|
@@ -5,17 +5,25 @@ module Torque
|
|
5
5
|
module Relation
|
6
6
|
module Inheritance
|
7
7
|
|
8
|
-
# REGCLASS = ::Arel.sql('tableoid').cast('regclass')
|
9
|
-
|
10
8
|
# :nodoc:
|
11
|
-
def
|
9
|
+
def cast_records_values
|
10
|
+
@values.fetch(:cast_records, FROZEN_EMPTY_ARRAY)
|
11
|
+
end
|
12
12
|
# :nodoc:
|
13
|
-
def
|
13
|
+
def cast_records_values=(value)
|
14
|
+
assert_modifiable!
|
15
|
+
@values[:cast_records] = value
|
16
|
+
end
|
14
17
|
|
15
18
|
# :nodoc:
|
16
|
-
def itself_only_value
|
19
|
+
def itself_only_value
|
20
|
+
@values.fetch(:itself_only, nil)
|
21
|
+
end
|
17
22
|
# :nodoc:
|
18
|
-
def itself_only_value=(value)
|
23
|
+
def itself_only_value=(value)
|
24
|
+
assert_modifiable!
|
25
|
+
@values[:itself_only] = value
|
26
|
+
end
|
19
27
|
|
20
28
|
delegate :quote_table_name, :quote_column_name, to: :connection
|
21
29
|
|
@@ -46,9 +54,9 @@ module Torque
|
|
46
54
|
|
47
55
|
# Like #cast_records, but modifies relation in place
|
48
56
|
def cast_records!(*types, **options)
|
49
|
-
where!(regclass.
|
57
|
+
where!(regclass.pg_cast(:varchar).in(types.map(&:table_name))) if options[:filter]
|
50
58
|
self.select_extra_values += [regclass.as(_record_class_attribute.to_s)]
|
51
|
-
self.
|
59
|
+
self.cast_records_values = (types.present? ? types : model.casted_dependents.values)
|
52
60
|
self
|
53
61
|
end
|
54
62
|
|
@@ -64,23 +72,22 @@ module Torque
|
|
64
72
|
|
65
73
|
# Build all necessary data for inheritances
|
66
74
|
def build_inheritances(arel)
|
67
|
-
return
|
75
|
+
return if self.cast_records_values.empty?
|
68
76
|
|
69
77
|
mergeable = inheritance_mergeable_attributes
|
70
78
|
|
71
|
-
columns = build_inheritances_joins(arel, self.
|
79
|
+
columns = build_inheritances_joins(arel, self.cast_records_values)
|
72
80
|
columns = columns.map do |column, arel_tables|
|
73
81
|
next arel_tables.first[column] if arel_tables.size == 1
|
74
82
|
|
75
83
|
if mergeable.include?(column)
|
76
|
-
|
77
|
-
::Arel::Nodes::NamedFunction.new('COALESCE', list).as(column)
|
84
|
+
FN.coalesce(*arel_tables.each_with_object(column).map(&:[])).as(column)
|
78
85
|
else
|
79
86
|
arel_tables.map { |table| table[column].as("#{table.left.name}__#{column}") }
|
80
87
|
end
|
81
88
|
end
|
82
89
|
|
83
|
-
columns.push(build_auto_caster_marker(arel, self.
|
90
|
+
columns.push(build_auto_caster_marker(arel, self.cast_records_values))
|
84
91
|
self.select_extra_values += columns.flatten if columns.any?
|
85
92
|
end
|
86
93
|
|
@@ -105,12 +112,12 @@ module Torque
|
|
105
112
|
end
|
106
113
|
|
107
114
|
def build_auto_caster_marker(arel, types)
|
108
|
-
attribute = regclass.
|
115
|
+
attribute = regclass.pg_cast(:varchar).in(types.map(&:table_name))
|
109
116
|
attribute.as(self.class._auto_cast_attribute.to_s)
|
110
117
|
end
|
111
118
|
|
112
119
|
def regclass
|
113
|
-
arel_table['tableoid'].
|
120
|
+
arel_table['tableoid'].pg_cast(:regclass)
|
114
121
|
end
|
115
122
|
|
116
123
|
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Torque
|
4
|
+
module PostgreSQL
|
5
|
+
module Relation
|
6
|
+
module JoinSeries
|
7
|
+
|
8
|
+
# Create the proper arel join
|
9
|
+
class << self
|
10
|
+
def build(relation, range, with: nil, as: :series, step: nil, time_zone: nil, mode: :inner, &block)
|
11
|
+
validate_build!(range, step)
|
12
|
+
|
13
|
+
args = [bind_value(range.begin), bind_value(range.end)]
|
14
|
+
args << bind_value(step) if step
|
15
|
+
args << bind_value(time_zone) if time_zone
|
16
|
+
|
17
|
+
result = Arel::Nodes::Ref.new(as.to_s)
|
18
|
+
func = FN.generate_series(*args).as(as.to_s)
|
19
|
+
condition = build_join_on(result, relation, with, &block)
|
20
|
+
arel_join(mode).new(func, func.create_on(condition))
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# Make sure we have a viable range
|
26
|
+
def validate_build!(range, step)
|
27
|
+
raise ArgumentError, <<~MSG.squish unless range.is_a?(Range)
|
28
|
+
Value must be a Range.
|
29
|
+
MSG
|
30
|
+
|
31
|
+
raise ArgumentError, <<~MSG.squish if range.begin.nil?
|
32
|
+
Beginless Ranges are not supported.
|
33
|
+
MSG
|
34
|
+
|
35
|
+
raise ArgumentError, <<~MSG.squish if range.end.nil?
|
36
|
+
Endless Ranges are not supported.
|
37
|
+
MSG
|
38
|
+
|
39
|
+
raise ArgumentError, <<~MSG.squish if !range.begin.is_a?(Numeric) && step.nil?
|
40
|
+
missing keyword: :step
|
41
|
+
MSG
|
42
|
+
end
|
43
|
+
|
44
|
+
# Creates the proper bind value
|
45
|
+
def bind_value(value)
|
46
|
+
case value
|
47
|
+
when Integer
|
48
|
+
FN.bind_type(value, :integer, name: 'series', cast: 'integer')
|
49
|
+
when Float
|
50
|
+
FN.bind_type(value, :float, name: 'series', cast: 'numeric')
|
51
|
+
when String
|
52
|
+
FN.bind_type(value, :string, name: 'series', cast: 'text')
|
53
|
+
when ActiveSupport::TimeWithZone
|
54
|
+
FN.bind_type(value, :time, name: 'series', cast: 'timestamptz')
|
55
|
+
when Time
|
56
|
+
FN.bind_type(value, :time, name: 'series', cast: 'timestamp')
|
57
|
+
when DateTime
|
58
|
+
FN.bind_type(value, :datetime, name: 'series', cast: 'timestamp')
|
59
|
+
when ActiveSupport::Duration
|
60
|
+
type = Adapter::OID::Interval.new
|
61
|
+
FN.bind_type(value, type, name: 'series', cast: 'interval')
|
62
|
+
when Date then bind_value(value.to_time(:utc))
|
63
|
+
when ::Arel::Attributes::Attribute then value
|
64
|
+
else
|
65
|
+
raise ArgumentError, "Unsupported value type: #{value.class}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Get the class of the join on arel
|
70
|
+
def arel_join(mode)
|
71
|
+
case mode.to_sym
|
72
|
+
when :inner then ::Arel::Nodes::InnerJoin
|
73
|
+
when :left then ::Arel::Nodes::OuterJoin
|
74
|
+
when :right then ::Arel::Nodes::RightOuterJoin
|
75
|
+
when :full then ::Arel::Nodes::FullOuterJoin
|
76
|
+
else
|
77
|
+
raise ArgumentError, <<-MSG.squish
|
78
|
+
The '#{mode}' is not implemented as a join type.
|
79
|
+
MSG
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Build the join on clause
|
84
|
+
def build_join_on(result, relation, with)
|
85
|
+
raise ArgumentError, <<~MSG.squish if with.nil? && !block_given?
|
86
|
+
missing keyword: :with
|
87
|
+
MSG
|
88
|
+
|
89
|
+
return yield(result, relation.arel_table) if block_given?
|
90
|
+
|
91
|
+
result.eq(with.is_a?(Symbol) ? relation.arel_table[with.to_s] : with)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Creates a new join based on PG +generate_series()+ function. It is
|
96
|
+
# based on ranges, supports numbers and dates (as per PG documentation),
|
97
|
+
# custom stepping, time zones, and more. This simply coordinates the
|
98
|
+
# initialization of the the proper join
|
99
|
+
def join_series(range, **xargs, &block)
|
100
|
+
spawn.join_series!(range, **xargs, &block)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Like #join_series, but modifies relation in place.
|
104
|
+
def join_series!(range, **xargs, &block)
|
105
|
+
self.joins_values |= [JoinSeries.build(self, range, **xargs, &block)]
|
106
|
+
self
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|