torque-postgresql 3.1.0 → 3.2.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/torque/postgresql/adapter/schema_creation.rb +3 -9
- data/lib/torque/postgresql/auxiliary_statement/recursive.rb +149 -0
- data/lib/torque/postgresql/auxiliary_statement/settings.rb +74 -22
- data/lib/torque/postgresql/auxiliary_statement.rb +39 -40
- data/lib/torque/postgresql/base.rb +11 -1
- data/lib/torque/postgresql/config.rb +4 -0
- data/lib/torque/postgresql/inheritance.rb +3 -1
- data/lib/torque/postgresql/railtie.rb +5 -1
- data/lib/torque/postgresql/relation/auxiliary_statement.rb +28 -15
- data/lib/torque/postgresql/version.rb +1 -1
- data/spec/models/category.rb +2 -0
- data/spec/schema.rb +7 -1
- data/spec/tests/auxiliary_statement_spec.rb +374 -35
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c677b1f2b2cdf150a4e8e9a817cd7a9a6e46680a353c3125aeb7ab65e1b85348
|
4
|
+
data.tar.gz: 72a25a13491d9349a813484be40c43cacc767355060ba16375da7b7f41739249
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1b6bdeeb8ca02a6027a79fc0d896716603792c1d4cee95131995ef944075f13b983a583a22eb4e3b6b590b8430b7b4eb4469522cf76b47daa30b8c2dd35a7318
|
7
|
+
data.tar.gz: eca4f269833a0823442f247cb065f14fe412c85063e5df8e751a20a366fd296f02db3ad2720b02d7a4169c73b38c1bbfe29160f292671a8a5cf0a23b4ffa9261
|
@@ -13,21 +13,15 @@ module Torque
|
|
13
13
|
statements << accept(o.primary_keys) if o.primary_keys
|
14
14
|
|
15
15
|
if supports_indexes_in_create?
|
16
|
-
statements.concat(o.indexes.map
|
17
|
-
index_in_create(o.name, column_name, options)
|
18
|
-
end)
|
16
|
+
statements.concat(o.indexes.map { |c, o| index_in_create(o.name, c, o) })
|
19
17
|
end
|
20
18
|
|
21
19
|
if supports_foreign_keys?
|
22
|
-
statements.concat(o.foreign_keys.map
|
23
|
-
foreign_key_in_create(o.name, to_table, options)
|
24
|
-
end)
|
20
|
+
statements.concat(o.foreign_keys.map { |fk| accept fk })
|
25
21
|
end
|
26
22
|
|
27
23
|
if respond_to?(:supports_check_constraints?) && supports_check_constraints?
|
28
|
-
statements.concat(o.check_constraints.map
|
29
|
-
check_constraint_in_create(o.name, expression, options)
|
30
|
-
end)
|
24
|
+
statements.concat(o.check_constraints.map { |chk| accept chk })
|
31
25
|
end
|
32
26
|
|
33
27
|
create_sql << "(#{statements.join(', ')})" \
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Torque
|
4
|
+
module PostgreSQL
|
5
|
+
class AuxiliaryStatement
|
6
|
+
class Recursive < AuxiliaryStatement
|
7
|
+
# Setup any additional option in the recursive mode
|
8
|
+
def initialize(*, **options)
|
9
|
+
super
|
10
|
+
|
11
|
+
@connect = options[:connect]&.to_a&.first
|
12
|
+
@union_all = options[:union_all]
|
13
|
+
@sub_query = options[:sub_query]
|
14
|
+
|
15
|
+
if options.key?(:with_depth)
|
16
|
+
@depth = options[:with_depth].values_at(:name, :start, :as)
|
17
|
+
@depth[0] ||= 'depth'
|
18
|
+
end
|
19
|
+
|
20
|
+
if options.key?(:with_path)
|
21
|
+
@path = options[:with_path].values_at(:name, :source, :as)
|
22
|
+
@path[0] ||= 'path'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
# Build the string or arel query
|
29
|
+
def build_query(base)
|
30
|
+
# Expose columns and get the list of the ones for select
|
31
|
+
columns = expose_columns(base, @query.try(:arel_table))
|
32
|
+
sub_columns = columns.dup
|
33
|
+
type = @union_all.present? ? 'all' : ''
|
34
|
+
|
35
|
+
# Build any extra columns that are dynamic and from the recursion
|
36
|
+
extra_columns(base, columns, sub_columns)
|
37
|
+
|
38
|
+
# Prepare the query depending on its type
|
39
|
+
if @query.is_a?(String) && @sub_query.is_a?(String)
|
40
|
+
args = @args.each_with_object({}) { |h, (k, v)| h[k] = base.connection.quote(v) }
|
41
|
+
::Arel.sql("(#{@query} UNION #{type.upcase} #{@sub_query})" % args)
|
42
|
+
elsif relation_query?(@query)
|
43
|
+
@query = @query.where(@where) if @where.present?
|
44
|
+
@bound_attributes.concat(@query.send(:bound_attributes))
|
45
|
+
|
46
|
+
if relation_query?(@sub_query)
|
47
|
+
@bound_attributes.concat(@sub_query.send(:bound_attributes))
|
48
|
+
|
49
|
+
sub_query = @sub_query.select(*sub_columns).arel
|
50
|
+
sub_query.from([@sub_query.arel_table, table])
|
51
|
+
else
|
52
|
+
sub_query = ::Arel.sql(@sub_query)
|
53
|
+
end
|
54
|
+
|
55
|
+
@query.select(*columns).arel.union(type, sub_query)
|
56
|
+
else
|
57
|
+
raise ArgumentError, <<-MSG.squish
|
58
|
+
Only String and ActiveRecord::Base objects are accepted as query and sub query
|
59
|
+
objects, #{@query.class.name} given for #{self.class.name}.
|
60
|
+
MSG
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Setup the statement using the class configuration
|
65
|
+
def prepare(base, settings)
|
66
|
+
super
|
67
|
+
|
68
|
+
prepare_sub_query(base, settings)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Make sure that both parts of the union are ready
|
72
|
+
def prepare_sub_query(base, settings)
|
73
|
+
@union_all = settings.union_all if @union_all.nil?
|
74
|
+
@sub_query ||= settings.sub_query
|
75
|
+
@depth ||= settings.depth
|
76
|
+
@path ||= settings.path
|
77
|
+
|
78
|
+
# Collect the connection
|
79
|
+
@connect ||= settings.connect || begin
|
80
|
+
key = base.primary_key
|
81
|
+
[key.to_sym, :"parent_#{key}"] unless key.nil?
|
82
|
+
end
|
83
|
+
|
84
|
+
raise ArgumentError, <<-MSG.squish if @sub_query.nil? && @query.is_a?(String)
|
85
|
+
Unable to generate sub query from a string query. Please provide a `sub_query`
|
86
|
+
property on the "#{table_name}" settings.
|
87
|
+
MSG
|
88
|
+
|
89
|
+
if @sub_query.nil?
|
90
|
+
raise ArgumentError, <<-MSG.squish if @connect.blank?
|
91
|
+
Unable to generate sub query without setting up a proper way to connect it
|
92
|
+
with the main query. Please provide a `connect` property on the "#{table_name}"
|
93
|
+
settings.
|
94
|
+
MSG
|
95
|
+
|
96
|
+
left, right = @connect.map(&:to_s)
|
97
|
+
condition = @query.arel_table[right].eq(table[left])
|
98
|
+
|
99
|
+
if @query.where_values_hash.key?(right)
|
100
|
+
@sub_query = @query.unscope(where: right.to_sym).where(condition)
|
101
|
+
else
|
102
|
+
@sub_query = @query.where(condition)
|
103
|
+
@query = @query.where(right => nil)
|
104
|
+
end
|
105
|
+
elsif @sub_query.respond_to?(:call)
|
106
|
+
# Call a proc to get the real sub query
|
107
|
+
call_args = @sub_query.try(:arity) === 0 ? [] : [OpenStruct.new(@args)]
|
108
|
+
@sub_query = @sub_query.call(*call_args)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Add depth and path if they were defined in settings
|
113
|
+
def extra_columns(base, columns, sub_columns)
|
114
|
+
return if @query.is_a?(String) || @sub_query.is_a?(String)
|
115
|
+
|
116
|
+
# Add the connect attribute to the query
|
117
|
+
if defined?(@connect)
|
118
|
+
columns.unshift(@query.arel_table[@connect[0]])
|
119
|
+
sub_columns.unshift(@sub_query.arel_table[@connect[0]])
|
120
|
+
end
|
121
|
+
|
122
|
+
# Build a column to represent the depth of the recursion
|
123
|
+
if @depth.present?
|
124
|
+
name, start, as = @depth
|
125
|
+
col = table[name]
|
126
|
+
base.select_extra_values += [col.as(as)] unless as.nil?
|
127
|
+
|
128
|
+
columns << ::Arel.sql(start.to_s).as(name)
|
129
|
+
sub_columns << (col + ::Arel.sql('1')).as(name)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Build a column to represent the path of the record access
|
133
|
+
if @path.present?
|
134
|
+
name, source, as = @path
|
135
|
+
source = @query.arel_table[source || @connect[0]]
|
136
|
+
|
137
|
+
col = table[name]
|
138
|
+
base.select_extra_values += [col.as(as)] unless as.nil?
|
139
|
+
parts = [col, source.cast(:varchar)]
|
140
|
+
|
141
|
+
columns << ::Arel.array([source]).cast(:varchar, true).as(name)
|
142
|
+
sub_columns << ::Arel::Nodes::NamedFunction.new('array_append', parts).as(name)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -4,9 +4,9 @@ module Torque
|
|
4
4
|
module PostgreSQL
|
5
5
|
class AuxiliaryStatement
|
6
6
|
class Settings < Collector.new(:attributes, :join, :join_type, :query, :requires,
|
7
|
-
:polymorphic, :through)
|
7
|
+
:polymorphic, :through, :union_all, :connect)
|
8
8
|
|
9
|
-
attr_reader :base, :source
|
9
|
+
attr_reader :base, :source, :depth, :path
|
10
10
|
alias_method :select, :attributes
|
11
11
|
alias_method :cte, :source
|
12
12
|
|
@@ -14,9 +14,10 @@ module Torque
|
|
14
14
|
delegate :table, :table_name, to: :@source
|
15
15
|
delegate :sql, to: ::Arel
|
16
16
|
|
17
|
-
def initialize(base, source)
|
17
|
+
def initialize(base, source, recursive = false)
|
18
18
|
@base = base
|
19
19
|
@source = source
|
20
|
+
@recursive = recursive
|
20
21
|
end
|
21
22
|
|
22
23
|
def base_name
|
@@ -27,6 +28,38 @@ module Torque
|
|
27
28
|
@base.arel_table
|
28
29
|
end
|
29
30
|
|
31
|
+
def recursive?
|
32
|
+
@recursive
|
33
|
+
end
|
34
|
+
|
35
|
+
def depth?
|
36
|
+
defined?(@depth)
|
37
|
+
end
|
38
|
+
|
39
|
+
def path?
|
40
|
+
defined?(@path)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Add an attribute to the result showing the depth of each iteration
|
44
|
+
def with_depth(name = 'depth', start: 0, as: nil)
|
45
|
+
@depth = [name.to_s, start, as&.to_s] if recursive?
|
46
|
+
end
|
47
|
+
|
48
|
+
# Add an attribute to the result showing the path of each record
|
49
|
+
def with_path(name = 'path', source: nil, as: nil)
|
50
|
+
@path = [name.to_s, source&.to_s, as&.to_s] if recursive?
|
51
|
+
end
|
52
|
+
|
53
|
+
# Set recursive operation to use union all
|
54
|
+
def union_all!
|
55
|
+
@union_all = true if recursive?
|
56
|
+
end
|
57
|
+
|
58
|
+
# Add both depth and path to the result
|
59
|
+
def with_depth_and_path
|
60
|
+
with_depth && with_path
|
61
|
+
end
|
62
|
+
|
30
63
|
# Get the arel version of the table set on the query
|
31
64
|
def query_table
|
32
65
|
raise StandardError, 'The query is not defined yet' if query.nil?
|
@@ -41,36 +74,55 @@ module Torque
|
|
41
74
|
|
42
75
|
alias column col
|
43
76
|
|
44
|
-
# There are
|
77
|
+
# There are three ways of setting the query:
|
45
78
|
# - A simple relation based on a Model
|
46
79
|
# - A Arel-based select manager
|
47
|
-
# - A string or a proc
|
80
|
+
# - A string or a proc
|
48
81
|
def query(value = nil, command = nil)
|
49
82
|
return @query if value.nil?
|
50
|
-
return @query = value if relation_query?(value)
|
51
83
|
|
52
|
-
|
53
|
-
|
54
|
-
@query_table = value.source.left.name
|
55
|
-
return
|
56
|
-
end
|
84
|
+
@query = sanitize_query(value, command)
|
85
|
+
end
|
57
86
|
|
58
|
-
|
87
|
+
# Same as query, but for the second part of the union for recursive cte
|
88
|
+
def sub_query(value = nil, command = nil)
|
89
|
+
return unless recursive?
|
90
|
+
return @sub_query if value.nil?
|
59
91
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
92
|
+
@sub_query = sanitize_query(value, command)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Assume `parent_` as the other part if provided a Symbol or String
|
96
|
+
def connect(value = nil)
|
97
|
+
return @connect if value.nil?
|
64
98
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
MSG
|
99
|
+
value = [value.to_sym, :"parent_#{value}"] \
|
100
|
+
if value.is_a?(String) || value.is_a?(Symbol)
|
101
|
+
value = value.to_a.first if value.is_a?(Hash)
|
69
102
|
|
70
|
-
@
|
71
|
-
@query_table = ::Arel::Table.new(value)
|
103
|
+
@connect = value
|
72
104
|
end
|
73
105
|
|
106
|
+
alias connect= connect
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
# Get the query and table from the params
|
111
|
+
def sanitize_query(value, command = nil)
|
112
|
+
return value if relation_query?(value)
|
113
|
+
return value if value.is_a?(::Arel::SelectManager)
|
114
|
+
|
115
|
+
command = value if command.nil? # For compatibility purposes
|
116
|
+
valid_type = command.respond_to?(:call) || command.is_a?(String)
|
117
|
+
|
118
|
+
raise ArgumentError, <<-MSG.squish unless valid_type
|
119
|
+
Only relation, string and proc are valid object types for query,
|
120
|
+
#{command.inspect} given.
|
121
|
+
MSG
|
122
|
+
|
123
|
+
command
|
124
|
+
end
|
125
|
+
|
74
126
|
end
|
75
127
|
end
|
76
128
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative 'auxiliary_statement/settings'
|
4
|
+
require_relative 'auxiliary_statement/recursive'
|
4
5
|
|
5
6
|
module Torque
|
6
7
|
module PostgreSQL
|
@@ -8,17 +9,20 @@ module Torque
|
|
8
9
|
TABLE_COLUMN_AS_STRING = /\A(?:"?(\w+)"?\.)?"?(\w+)"?\z/.freeze
|
9
10
|
|
10
11
|
class << self
|
11
|
-
attr_reader :config
|
12
|
+
attr_reader :config, :table_name
|
12
13
|
|
13
14
|
# Find or create the class that will handle statement
|
14
15
|
def lookup(name, base)
|
15
16
|
const = name.to_s.camelize << '_' << self.name.demodulize
|
16
17
|
return base.const_get(const, false) if base.const_defined?(const, false)
|
17
|
-
|
18
|
+
|
19
|
+
base.const_set(const, Class.new(self)).tap do |klass|
|
20
|
+
klass.instance_variable_set(:@table_name, name.to_s)
|
21
|
+
end
|
18
22
|
end
|
19
23
|
|
20
24
|
# Create a new instance of an auxiliary statement
|
21
|
-
def instantiate(statement, base, options
|
25
|
+
def instantiate(statement, base, **options)
|
22
26
|
klass = while base < ActiveRecord::Base
|
23
27
|
list = base.auxiliary_statements_list
|
24
28
|
break list[statement] if list.present? && list.key?(statement)
|
@@ -26,15 +30,15 @@ module Torque
|
|
26
30
|
base = base.superclass
|
27
31
|
end
|
28
32
|
|
29
|
-
return klass.new(options) unless klass.nil?
|
33
|
+
return klass.new(**options) unless klass.nil?
|
30
34
|
raise ArgumentError, <<-MSG.squish
|
31
35
|
There's no '#{statement}' auxiliary statement defined for #{base.class.name}.
|
32
36
|
MSG
|
33
37
|
end
|
34
38
|
|
35
39
|
# Fast access to statement build
|
36
|
-
def build(statement, base,
|
37
|
-
klass = instantiate(statement, base, options)
|
40
|
+
def build(statement, base, bound_attributes = [], join_sources = [], **options)
|
41
|
+
klass = instantiate(statement, base, **options)
|
38
42
|
result = klass.build(base)
|
39
43
|
|
40
44
|
bound_attributes.concat(klass.bound_attributes)
|
@@ -56,7 +60,7 @@ module Torque
|
|
56
60
|
# A way to create auxiliary statements outside of models configurations,
|
57
61
|
# being able to use on extensions
|
58
62
|
def create(table_or_settings, &block)
|
59
|
-
klass = Class.new(
|
63
|
+
klass = Class.new(self)
|
60
64
|
|
61
65
|
if block_given?
|
62
66
|
klass.instance_variable_set(:@table_name, table_or_settings)
|
@@ -89,7 +93,8 @@ module Torque
|
|
89
93
|
def configure(base, instance)
|
90
94
|
return @config unless @config.respond_to?(:call)
|
91
95
|
|
92
|
-
|
96
|
+
recursive = self < AuxiliaryStatement::Recursive
|
97
|
+
settings = Settings.new(base, instance, recursive)
|
93
98
|
settings.instance_exec(settings, &@config)
|
94
99
|
settings
|
95
100
|
end
|
@@ -98,11 +103,6 @@ module Torque
|
|
98
103
|
def table
|
99
104
|
@table ||= ::Arel::Table.new(table_name)
|
100
105
|
end
|
101
|
-
|
102
|
-
# Get the name of the table of the configurated statement
|
103
|
-
def table_name
|
104
|
-
@table_name ||= self.name.demodulize.split('_').first.underscore
|
105
|
-
end
|
106
106
|
end
|
107
107
|
|
108
108
|
delegate :config, :table, :table_name, :relation, :configure, :relation_query?,
|
@@ -111,15 +111,14 @@ module Torque
|
|
111
111
|
attr_reader :bound_attributes, :join_sources
|
112
112
|
|
113
113
|
# Start a new auxiliary statement giving extra options
|
114
|
-
def initialize(
|
115
|
-
options = args.extract_options!
|
114
|
+
def initialize(*, **options)
|
116
115
|
args_key = Torque::PostgreSQL.config.auxiliary_statement.send_arguments_key
|
117
116
|
|
118
117
|
@join = options.fetch(:join, {})
|
119
118
|
@args = options.fetch(args_key, {})
|
120
119
|
@where = options.fetch(:where, {})
|
121
120
|
@select = options.fetch(:select, {})
|
122
|
-
@join_type = options
|
121
|
+
@join_type = options[:join_type]
|
123
122
|
|
124
123
|
@bound_attributes = []
|
125
124
|
@join_sources = []
|
@@ -131,7 +130,7 @@ module Torque
|
|
131
130
|
@join_sources.clear
|
132
131
|
|
133
132
|
# Prepare all the data for the statement
|
134
|
-
prepare(base)
|
133
|
+
prepare(base, configure(base, self))
|
135
134
|
|
136
135
|
# Add the join condition to the list
|
137
136
|
@join_sources << build_join(base)
|
@@ -141,9 +140,9 @@ module Torque
|
|
141
140
|
end
|
142
141
|
|
143
142
|
private
|
143
|
+
|
144
144
|
# Setup the statement using the class configuration
|
145
|
-
def prepare(base)
|
146
|
-
settings = configure(base, self)
|
145
|
+
def prepare(base, settings)
|
147
146
|
requires = Array.wrap(settings.requires).flatten.compact
|
148
147
|
@dependencies = ensure_dependencies(requires, base).flatten.compact
|
149
148
|
|
@@ -151,14 +150,12 @@ module Torque
|
|
151
150
|
@query = settings.query
|
152
151
|
|
153
152
|
# Call a proc to get the real query
|
154
|
-
if @query.
|
153
|
+
if @query.respond_to?(:call)
|
155
154
|
call_args = @query.try(:arity) === 0 ? [] : [OpenStruct.new(@args)]
|
156
155
|
@query = @query.call(*call_args)
|
157
|
-
@args = []
|
158
156
|
end
|
159
157
|
|
160
|
-
#
|
161
|
-
@query_table = settings.query_table unless relation_query?(@query)
|
158
|
+
# Merge select attributes provided on the instance creation
|
162
159
|
@select = settings.attributes.merge(@select) if settings.attributes.present?
|
163
160
|
|
164
161
|
# Merge join settings
|
@@ -168,7 +165,7 @@ module Torque
|
|
168
165
|
@association = settings.through.to_s
|
169
166
|
elsif relation_query?(@query)
|
170
167
|
@association = base.reflections.find do |name, reflection|
|
171
|
-
break name if @query.klass.eql?
|
168
|
+
break name if @query.klass.eql?(reflection.klass)
|
172
169
|
end
|
173
170
|
end
|
174
171
|
end
|
@@ -234,15 +231,6 @@ module Torque
|
|
234
231
|
as a query object on #{self.class.name}.
|
235
232
|
MSG
|
236
233
|
|
237
|
-
# Expose join columns
|
238
|
-
if relation_query?(@query)
|
239
|
-
query_table = @query.arel_table
|
240
|
-
conditions.children.each do |item|
|
241
|
-
@query.select_values += [query_table[item.left.name]] \
|
242
|
-
if item.left.relation.eql?(table)
|
243
|
-
end
|
244
|
-
end
|
245
|
-
|
246
234
|
# Build the join based on the join type
|
247
235
|
arel_join.new(table, table.create_on(conditions))
|
248
236
|
end
|
@@ -263,21 +251,31 @@ module Torque
|
|
263
251
|
|
264
252
|
# Mount the list of selected attributes
|
265
253
|
def expose_columns(base, query_table = nil)
|
254
|
+
# Add the columns necessary for the join
|
255
|
+
list = @join_sources.each_with_object(@select) do |join, hash|
|
256
|
+
join.right.expr.children.each do |item|
|
257
|
+
hash[item.left.name] = nil if item.left.relation.eql?(table)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
266
261
|
# Add select columns to the query and get exposed columns
|
267
|
-
|
268
|
-
base.select_extra_values += [table[right.to_s]]
|
269
|
-
|
262
|
+
list.filter_map do |left, right|
|
263
|
+
base.select_extra_values += [table[right.to_s]] unless right.nil?
|
264
|
+
next unless query_table
|
265
|
+
|
266
|
+
col = project(left, query_table)
|
267
|
+
right.nil? ? col : col.as(right.to_s)
|
270
268
|
end
|
271
269
|
end
|
272
270
|
|
273
271
|
# Ensure that all the dependencies are loaded in the base relation
|
274
272
|
def ensure_dependencies(list, base)
|
275
273
|
with_options = list.extract_options!.to_a
|
276
|
-
(list + with_options).map do |
|
277
|
-
dependent_klass = base.model.auxiliary_statements_list[
|
274
|
+
(list + with_options).map do |name, options|
|
275
|
+
dependent_klass = base.model.auxiliary_statements_list[name]
|
278
276
|
|
279
277
|
raise ArgumentError, <<-MSG.squish if dependent_klass.nil?
|
280
|
-
The '#{
|
278
|
+
The '#{name}' auxiliary statement dependency can't found on
|
281
279
|
#{self.class.name}.
|
282
280
|
MSG
|
283
281
|
|
@@ -285,7 +283,8 @@ module Torque
|
|
285
283
|
cte.is_a?(dependent_klass)
|
286
284
|
end
|
287
285
|
|
288
|
-
|
286
|
+
options ||= {}
|
287
|
+
AuxiliaryStatement.build(name, base, bound_attributes, join_sources, **options)
|
289
288
|
end
|
290
289
|
end
|
291
290
|
|
@@ -252,7 +252,7 @@ module Torque
|
|
252
252
|
# attributes key:
|
253
253
|
# Provides a map of attributes to be exposed to the main query.
|
254
254
|
#
|
255
|
-
# For
|
255
|
+
# For instance, if the statement query has an 'id' column that you
|
256
256
|
# want it to be accessed on the main query as 'item_id',
|
257
257
|
# you can use:
|
258
258
|
# attributes id: :item_id, 'MAX(id)' => :max_id,
|
@@ -293,6 +293,16 @@ module Torque
|
|
293
293
|
klass.configurator(block)
|
294
294
|
end
|
295
295
|
alias cte auxiliary_statement
|
296
|
+
|
297
|
+
# Creates a new recursive auxiliary statement (CTE) under the base
|
298
|
+
# Very similar to the regular auxiliary statement, but with two-part
|
299
|
+
# query where one is executed first and the second recursively
|
300
|
+
def recursive_auxiliary_statement(table, &block)
|
301
|
+
klass = AuxiliaryStatement::Recursive.lookup(table, self)
|
302
|
+
auxiliary_statements_list[table.to_sym] = klass
|
303
|
+
klass.configurator(block)
|
304
|
+
end
|
305
|
+
alias recursive_cte recursive_auxiliary_statement
|
296
306
|
end
|
297
307
|
end
|
298
308
|
|
@@ -63,6 +63,10 @@ module Torque
|
|
63
63
|
# auxiliary statement in order to perform detached CTEs
|
64
64
|
cte.exposed_class = 'TorqueCTE'
|
65
65
|
|
66
|
+
# Estipulate a class name (which may contain namespace) that expose the
|
67
|
+
# recursive auxiliary statement in order to perform detached CTEs
|
68
|
+
cte.exposed_recursive_class = 'TorqueRecursiveCTE'
|
69
|
+
|
66
70
|
end
|
67
71
|
|
68
72
|
# Configure ENUM features
|
@@ -55,7 +55,9 @@ module Torque
|
|
55
55
|
|
56
56
|
# Check if the model's table depends on any inheritance
|
57
57
|
def physically_inherited?
|
58
|
-
@physically_inherited
|
58
|
+
return @physically_inherited if defined?(@physically_inherited)
|
59
|
+
|
60
|
+
@physically_inherited = connection.schema_cache.dependencies(
|
59
61
|
defined?(@table_name) ? @table_name : decorated_table_name,
|
60
62
|
).present?
|
61
63
|
rescue ActiveRecord::ConnectionNotEstablished
|
@@ -30,11 +30,15 @@ module Torque
|
|
30
30
|
Torque::PostgreSQL::Attributes::Enum.lookup(name).sample
|
31
31
|
end
|
32
32
|
|
33
|
-
# Define the exposed constant for auxiliary statements
|
33
|
+
# Define the exposed constant for both types of auxiliary statements
|
34
34
|
if torque_config.auxiliary_statement.exposed_class.present?
|
35
35
|
*ns, name = torque_config.auxiliary_statement.exposed_class.split('::')
|
36
36
|
base = ns.present? ? Object.const_get(ns.join('::')) : Object
|
37
37
|
base.const_set(name, Torque::PostgreSQL::AuxiliaryStatement)
|
38
|
+
|
39
|
+
*ns, name = torque_config.auxiliary_statement.exposed_recursive_class.split('::')
|
40
|
+
base = ns.present? ? Object.const_get(ns.join('::')) : Object
|
41
|
+
base.const_set(name, Torque::PostgreSQL::AuxiliaryStatement::Recursive)
|
38
42
|
end
|
39
43
|
end
|
40
44
|
end
|
@@ -10,22 +10,14 @@ module Torque
|
|
10
10
|
# :nodoc:
|
11
11
|
def auxiliary_statements_values=(value); set_value(:auxiliary_statements, value); end
|
12
12
|
|
13
|
-
# Set use of an auxiliary statement
|
14
|
-
def with(*args)
|
15
|
-
spawn.with!(*args)
|
13
|
+
# Set use of an auxiliary statement
|
14
|
+
def with(*args, **settings)
|
15
|
+
spawn.with!(*args, **settings)
|
16
16
|
end
|
17
17
|
|
18
18
|
# Like #with, but modifies relation in place.
|
19
|
-
def with!(*args)
|
20
|
-
|
21
|
-
args.each do |table|
|
22
|
-
instance = table.is_a?(Class) && table < PostgreSQL::AuxiliaryStatement \
|
23
|
-
? table.new(options) \
|
24
|
-
: PostgreSQL::AuxiliaryStatement.instantiate(table, self, options)
|
25
|
-
|
26
|
-
self.auxiliary_statements_values += [instance]
|
27
|
-
end
|
28
|
-
|
19
|
+
def with!(*args, **settings)
|
20
|
+
instantiate_auxiliary_statements(*args, **settings)
|
29
21
|
self
|
30
22
|
end
|
31
23
|
|
@@ -47,8 +39,23 @@ module Torque
|
|
47
39
|
# Hook arel build to add the distinct on clause
|
48
40
|
def build_arel(*)
|
49
41
|
arel = super
|
50
|
-
|
51
|
-
|
42
|
+
type = auxiliary_statement_type
|
43
|
+
sub_queries = build_auxiliary_statements(arel)
|
44
|
+
sub_queries.nil? ? arel : arel.with(*type, *sub_queries)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Instantiate one or more auxiliary statements for the given +klass+
|
48
|
+
def instantiate_auxiliary_statements(*args, **options)
|
49
|
+
klass = PostgreSQL::AuxiliaryStatement
|
50
|
+
klass = klass::Recursive if options.delete(:recursive).present?
|
51
|
+
|
52
|
+
self.auxiliary_statements_values += args.map do |table|
|
53
|
+
if table.is_a?(Class) && table < klass
|
54
|
+
table.new(**options)
|
55
|
+
else
|
56
|
+
klass.instantiate(table, self, **options)
|
57
|
+
end
|
58
|
+
end
|
52
59
|
end
|
53
60
|
|
54
61
|
# Build all necessary data for auxiliary statements
|
@@ -59,6 +66,12 @@ module Torque
|
|
59
66
|
end
|
60
67
|
end
|
61
68
|
|
69
|
+
# Return recursive if any auxiliary statement is recursive
|
70
|
+
def auxiliary_statement_type
|
71
|
+
klass = PostgreSQL::AuxiliaryStatement::Recursive
|
72
|
+
:recursive if auxiliary_statements_values.any?(klass)
|
73
|
+
end
|
74
|
+
|
62
75
|
# Throw an error showing that an auxiliary statement of the given
|
63
76
|
# table name isn't defined
|
64
77
|
def auxiliary_statement_error(name)
|