torque-postgresql 3.1.0 → 3.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 25fb390d67c43461cf2c03096990149d2dbcae04072e96d69896d60f8e368863
4
- data.tar.gz: e2910f192602059a71dbf3cfca2cc412e2d34eb1faacc51620058ce408c1be9f
3
+ metadata.gz: f08898e4218d41dbcc6394500243760cdd29425ad77135a623bea043cfa8d3ae
4
+ data.tar.gz: 93c5d71e1597364b7e434e6c88faf4cfd64dcbc930756398f7539dc525a21f05
5
5
  SHA512:
6
- metadata.gz: c664a0e98b7cc78b5930d4e3273330caa49cb8a18b792892cc42dae96b2392ebc61b71043c758c5e0abc3c54b12f8ee567fc53a48ad9f6765726ed1e000e2770
7
- data.tar.gz: cfdb00e3740a05f2cabc1ccb25070ce05f5a628faa84209a899d5a247c17465dba0cac0a09a305e0c19fb05fb456c8d87e05107c9f0acb66803226a86ae2083b
6
+ metadata.gz: c7f93afd26ff711ba03f5a5b1381a139690acf8d3a5621ddec1d26a94ae345c2a59e06cdd9805a5101b4d81f51c151ea0fd19bba7a66d080d05387b073cfd55a
7
+ data.tar.gz: 5db371506b24fe1c5e28aee69054a83724af93642f44cd3646f6aa9ff9bd58b7a2d2632a186762dca570e2adb0ceea1c38cec5d0119b8f0e4e882b4089913895
@@ -194,23 +194,21 @@ module Torque
194
194
 
195
195
  # Get the list of columns, and their definition, but only from the
196
196
  # actual table, does not include columns that comes from inherited table
197
- def column_definitions(table_name) # :nodoc:
198
- # Only affects inheritance
199
- local_condition = 'AND a.attislocal IS TRUE' if @_dump_mode
197
+ def column_definitions(table_name)
198
+ local = 'AND a.attislocal' if @_dump_mode
200
199
 
201
200
  query(<<-SQL, 'SCHEMA')
202
- SELECT a.attname, format_type(a.atttypid, a.atttypmod),
203
- pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod,
204
- (SELECT c.collname FROM pg_collation c, pg_type t
205
- WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation),
206
- col_description(a.attrelid, a.attnum) AS comment
207
- FROM pg_attribute a
208
- LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum
209
- WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass
210
- AND a.attnum > 0
211
- AND a.attisdropped IS FALSE
212
- #{local_condition}
213
- ORDER BY a.attnum
201
+ SELECT a.attname, format_type(a.atttypid, a.atttypmod),
202
+ pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod,
203
+ c.collname, col_description(a.attrelid, a.attnum) AS comment,
204
+ #{supports_virtual_columns? ? 'attgenerated' : quote('')} as attgenerated
205
+ FROM pg_attribute a
206
+ LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum
207
+ LEFT JOIN pg_type t ON a.atttypid = t.oid
208
+ LEFT JOIN pg_collation c ON a.attcollation = c.oid AND a.attcollation <> t.typcollation
209
+ WHERE a.attrelid = #{quote(quote_table_name(table_name))}::regclass
210
+ AND a.attnum > 0 AND NOT a.attisdropped #{local}
211
+ ORDER BY a.attnum
214
212
  SQL
215
213
  end
216
214
 
@@ -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 do |column_name, options|
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 do |to_table, options|
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 do |expression, options|
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(', ')})" \
@@ -112,6 +112,11 @@ module Torque
112
112
 
113
113
  private
114
114
 
115
+ # Remove the schema from the sequence name
116
+ def sequence_name_from_parts(table_name, column_name, suffix)
117
+ super(table_name.split('.').last, column_name, suffix)
118
+ end
119
+
115
120
  def quote_enum_values(name, values, options)
116
121
  prefix = options[:prefix]
117
122
  prefix = name if prefix === true
@@ -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 two ways of setting the query:
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 that requires the table name as first argument
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
- if value.is_a?(::Arel::SelectManager)
53
- @query = value
54
- @query_table = value.source.left.name
55
- return
56
- end
84
+ @query = sanitize_query(value, command)
85
+ end
57
86
 
58
- valid_type = command.respond_to?(:call) || command.is_a?(String)
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
- raise ArgumentError, <<-MSG.squish if command.nil?
61
- To use proc or string as query, you need to provide the table name
62
- as the first argument
63
- MSG
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
- raise ArgumentError, <<-MSG.squish unless valid_type
66
- Only relation, string and proc are valid object types for query,
67
- #{command.inspect} given.
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
- @query = command
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
- base.const_set(const, Class.new(AuxiliaryStatement))
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 = nil)
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, options = nil, bound_attributes = [], join_sources = [])
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(AuxiliaryStatement)
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
- settings = Settings.new(base, instance)
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(*args)
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.fetch(:join_type, nil)
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.methods.include?(:call)
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
- # Manually set the query table when it's not an relation query
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? reflection.klass
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
- @select.map do |left, right|
268
- base.select_extra_values += [table[right.to_s]]
269
- project(left, query_table).as(right.to_s) if query_table
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 |dependent, options|
277
- dependent_klass = base.model.auxiliary_statements_list[dependent]
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 '#{dependent}' auxiliary statement dependency can't found on
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
- AuxiliaryStatement.build(dependent, base, options, bound_attributes, join_sources)
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 instace, if the statement query has an 'id' column that you
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
 
@@ -59,10 +59,14 @@ module Torque
59
59
  # arguments to format string or send on a proc
60
60
  cte.send_arguments_key = :args
61
61
 
62
- # Estipulate a class name (which may contain namespace) that expose the
62
+ # Estipulate a class name (which may contain namespace) that exposes the
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 exposes 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 ||= connection.schema_cache.dependencies(
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