torque-postgresql 0.1.7 → 0.2.1

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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/README.rdoc +74 -0
  3. data/lib/torque/postgresql/adapter/database_statements.rb +44 -2
  4. data/lib/torque/postgresql/adapter/schema_creation.rb +55 -0
  5. data/lib/torque/postgresql/adapter/schema_definitions.rb +26 -1
  6. data/lib/torque/postgresql/adapter/schema_statements.rb +20 -0
  7. data/lib/torque/postgresql/adapter.rb +1 -1
  8. data/lib/torque/postgresql/arel/join_source.rb +15 -0
  9. data/lib/torque/postgresql/arel/select_manager.rb +21 -0
  10. data/lib/torque/postgresql/arel/using.rb +10 -0
  11. data/lib/torque/postgresql/arel/visitors.rb +18 -1
  12. data/lib/torque/postgresql/arel.rb +4 -0
  13. data/lib/torque/postgresql/attributes/enum.rb +7 -1
  14. data/lib/torque/postgresql/attributes.rb +9 -1
  15. data/lib/torque/postgresql/auxiliary_statement/settings.rb +7 -0
  16. data/lib/torque/postgresql/auxiliary_statement.rb +19 -10
  17. data/lib/torque/postgresql/base.rb +76 -7
  18. data/lib/torque/postgresql/coder.rb +132 -0
  19. data/lib/torque/postgresql/collector.rb +2 -0
  20. data/lib/torque/postgresql/config.rb +35 -1
  21. data/lib/torque/postgresql/inheritance.rb +133 -0
  22. data/lib/torque/postgresql/railtie.rb +16 -0
  23. data/lib/torque/postgresql/relation/auxiliary_statement.rb +26 -19
  24. data/lib/torque/postgresql/relation/distinct_on.rb +9 -7
  25. data/lib/torque/postgresql/relation/inheritance.rb +112 -0
  26. data/lib/torque/postgresql/relation/merger.rb +48 -0
  27. data/lib/torque/postgresql/relation.rb +67 -2
  28. data/lib/torque/postgresql/schema_cache.rb +192 -0
  29. data/lib/torque/postgresql/schema_dumper.rb +49 -1
  30. data/lib/torque/postgresql/version.rb +1 -1
  31. data/lib/torque/postgresql.rb +6 -4
  32. metadata +14 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e2b7ce3a0529219858f1d5f92553a4326bf22344
4
- data.tar.gz: 8b012bc439db2dd8110d19a6c387e5046a4af67a
3
+ metadata.gz: 979764ebdcc3d060ce4aab1c28de3dc4f0aff8b1
4
+ data.tar.gz: e522c438e3d8d04f61d761d5738217b471724075
5
5
  SHA512:
6
- metadata.gz: c95aa9ac2d48a6328d77cbe888c970e5994b675203f86dceb0ab7a068604850e42b0e0f1b6349807dd09fdc1977c2cfe21a3db5c55d0af36b331cfc7913863dd
7
- data.tar.gz: 8466225dd5f79f98c6837f00f854244ed15251da90f2441238426a486d9d4ca018b7a4f4ef53a7e3d6e3bbeb4f266394d63cdc3acfb9670a92b278999bb2ef4e
6
+ metadata.gz: ccbc5797fdac9fecf2208acda837db28990235576848a22d389664237f45d6cc9d2cbfe6101aa1992615a2daad01f4fc6e1f792c59c18ed51d32a943f93fbfbd
7
+ data.tar.gz: 20e7a8e8dbe6b4c4fc0eed20d84a003fc908a855da63ff7df00df2b6b9d706db21e28ecb36c5785156f663c6dc4ddb1168383c4d8e18dba1916f7b888f12ee02
data/README.rdoc ADDED
@@ -0,0 +1,74 @@
1
+ = Torque PostgreSQL -- Add support to complex resources of PostgreSQL, like data
2
+ types, user-defined types and auxiliary statements (CTE)
3
+
4
+ This is a plugin that enhance Ruby on Rails enabling easy access to existing
5
+ PostgreSQL advanced resources, such as data types and queries statements. Its
6
+ features are design to be as similar as Rails architecture and they work as
7
+ smooth as possible.
8
+
9
+ 100% plug-and-play, with optional configurations so that can be adapted to
10
+ your's project design pattern.
11
+
12
+ A short rundown of some of the major features:
13
+
14
+ * Enum type manager
15
+
16
+ It creates a separated class to hold each enum set that can be used by multiple
17
+ models, it also keeps the database consistent. The enum type is known to have
18
+ better performance against string- and integer-like enums.
19
+ {PostgreSQL Docs}[https://www.postgresql.org/docs/9.2/static/datatype-enum.html]
20
+
21
+ create_enum :roles, %i(visitor manager admin)
22
+
23
+ add_column :users, :role, :roles
24
+
25
+ Enum::Roles.admin
26
+
27
+ Users.roles
28
+
29
+ {Learn more}[link:classes/Torque/PostgreSQL/Attributes/Enum.html]
30
+
31
+ * Distinct On
32
+
33
+ MySQL-like group by statement on queries. It keeps only the first row of each
34
+ set of rows where the given expressions evaluate to equal.
35
+ {PostgreSQL Docs}[https://www.postgresql.org/docs/9.5/static/sql-select.html#SQL-DISTINCT]
36
+
37
+ User.distinct_on(:name).all
38
+
39
+ {Learn more}[link:classes/Torque/PostgreSQL/Relation/DistinctOn.html]
40
+
41
+ * Auxiliary Statements
42
+
43
+ Provides a way to write auxiliary statements for use in a larger query. It's
44
+ reconfigured on the model, and then can be used during querying process.
45
+ {PostgreSQL Docs}[https://www.postgresql.org/docs/9.1/static/queries-with.html]
46
+
47
+ class User < ActiveRecord::Base
48
+ auxiliary_statement :last_comment do |cte|
49
+ cte.query Comment.distinct_on(:user_id).order(:user_id, id: :desc)
50
+ cte.attributes content: :last_comment_content
51
+ end
52
+ end
53
+
54
+ user = User.with(:last_comment).first
55
+
56
+ {Learn more}[link:classes/Torque/PostgreSQL/AuxiliaryStatement.html]
57
+
58
+
59
+ == Download and installation
60
+
61
+ The latest version of Torque PostgreSQL can be installed with RubyGems:
62
+
63
+ $ gem install torque-postgresql
64
+
65
+ Source code can be downloaded direct from the GitHub repository:
66
+
67
+ * https://github.com/crashtech/torque-postgresql
68
+
69
+
70
+ == License
71
+
72
+ Torque PostgreSQL is released under the MIT license:
73
+
74
+ * http://www.opensource.org/licenses/MIT
@@ -5,6 +5,11 @@ module Torque
5
5
 
6
6
  EXTENDED_DATABASE_TYPES = %i(enum interval)
7
7
 
8
+ # Switch between dump mode or not
9
+ def dump_mode!
10
+ @_dump_mode = !!!@_dump_mode
11
+ end
12
+
8
13
  # Check if a given type is valid.
9
14
  def valid_type?(type)
10
15
  super || extended_types.include?(type)
@@ -63,7 +68,7 @@ module Torque
63
68
  typtype = row['typtype']
64
69
  type = begin
65
70
  case
66
- when typtype == 'e'.freeze then OID::Enum.create(row)
71
+ when typtype == 'e' then OID::Enum.create(row)
67
72
  end
68
73
  end
69
74
 
@@ -73,7 +78,7 @@ module Torque
73
78
  end
74
79
 
75
80
  # Gets a list of user defined types.
76
- # You can even choose the +typcategory+ filter
81
+ # You can even choose the +category+ filter
77
82
  def user_defined_types(category = nil)
78
83
  category_condition = "AND typtype = '#{category}'" unless category.nil?
79
84
  select_all(<<-SQL).rows.to_h
@@ -97,6 +102,43 @@ module Torque
97
102
  SQL
98
103
  end
99
104
 
105
+ # Get the list of inherited tables associated with their parent tables
106
+ def inherited_tables
107
+ tables = select_all(<<-SQL).rows
108
+ SELECT child.relname AS table_name,
109
+ array_agg(parent.relname) AS inheritances
110
+ FROM pg_inherits
111
+ JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
112
+ JOIN pg_class child ON pg_inherits.inhrelid = child.oid
113
+ GROUP BY child.relname, pg_inherits.inhrelid
114
+ ORDER BY pg_inherits.inhrelid
115
+ SQL
116
+
117
+ tables.map do |(table, refs)|
118
+ [table, Coder.decode(refs)]
119
+ end.to_h
120
+ end
121
+
122
+ # Get the list of columns, and their definition, but only from the
123
+ # actual table, does not include columns that comes from inherited table
124
+ def column_definitions(table_name) # :nodoc:
125
+ local_condition = 'AND a.attislocal IS TRUE' if @_dump_mode
126
+ query(<<-SQL, 'SCHEMA')
127
+ SELECT a.attname, format_type(a.atttypid, a.atttypmod),
128
+ pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod,
129
+ (SELECT c.collname FROM pg_collation c, pg_type t
130
+ WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation),
131
+ col_description(a.attrelid, a.attnum) AS comment
132
+ FROM pg_attribute a
133
+ LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum
134
+ WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass
135
+ AND a.attnum > 0
136
+ AND a.attisdropped IS FALSE
137
+ #{local_condition}
138
+ ORDER BY a.attnum
139
+ SQL
140
+ end
141
+
100
142
  end
101
143
  end
102
144
  end
@@ -0,0 +1,55 @@
1
+ module Torque
2
+ module PostgreSQL
3
+ module Adapter
4
+ module SchemaCreation
5
+
6
+ # Redefine original table creation command to ensure PostgreSQL standard
7
+ def visit_TableDefinition(o)
8
+ create_sql = "CREATE#{' TEMPORARY' if o.temporary}"
9
+ create_sql << " TABLE #{quote_table_name(o.name)}"
10
+
11
+ statements = o.columns.map{ |c| accept c }
12
+ statements << accept(o.primary_keys) if o.primary_keys
13
+
14
+ if supports_indexes_in_create?
15
+ statements.concat(o.indexes.map do |column_name, options|
16
+ index_in_create(o.name, column_name, options)
17
+ end)
18
+ end
19
+
20
+ if supports_foreign_keys_in_create?
21
+ statements.concat(o.foreign_keys.map do |to_table, options|
22
+ foreign_key_in_create(o.name, to_table, options)
23
+ end)
24
+ end
25
+
26
+ if o.as
27
+ create_sql << " AS #{@conn.to_sql(o.as)}"
28
+ else
29
+ create_sql << " (#{statements.join(', ')})"
30
+ add_table_options!(create_sql, table_options(o))
31
+
32
+ if o.inherits.present?
33
+ tables = o.inherits.map(&method(:quote_table_name))
34
+ create_sql << " INHERITS ( #{tables.join(' , ')} )"
35
+ end
36
+ end
37
+
38
+ create_sql
39
+ end
40
+
41
+ # Keep rails 5.0 and 5.1 compatibility
42
+ def supports_foreign_keys_in_create?
43
+ if defined?(super)
44
+ super
45
+ else
46
+ supports_foreign_keys?
47
+ end
48
+ end
49
+
50
+ end
51
+
52
+ ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaCreation.prepend SchemaCreation
53
+ end
54
+ end
55
+ end
@@ -16,8 +16,33 @@ module Torque
16
16
 
17
17
  end
18
18
 
19
+ module TableDefinition
20
+ include ColumnMethods
21
+
22
+ attr_reader :inherits
23
+
24
+ def initialize(name, *args, **options)
25
+ old_args = []
26
+ old_args << options.delete(:temporary) || false
27
+ old_args << options.delete(:options)
28
+ old_args << options.delete(:as)
29
+ comment = options.delete(:comment)
30
+
31
+ super(name, *old_args, comment: comment)
32
+
33
+ if options.key?(:inherits)
34
+ @inherits = Array[options.delete(:inherits)].flatten.compact
35
+ @inherited_id = !(options.key?(:primary_key) || options.key?(:id))
36
+ end
37
+ end
38
+
39
+ def inherited_id?
40
+ @inherited_id
41
+ end
42
+ end
43
+
19
44
  ActiveRecord::ConnectionAdapters::PostgreSQL::Table.include ColumnMethods
20
- ActiveRecord::ConnectionAdapters::PostgreSQL::TableDefinition.include ColumnMethods
45
+ ActiveRecord::ConnectionAdapters::PostgreSQL::TableDefinition.include TableDefinition
21
46
 
22
47
  if ActiveRecord::ConnectionAdapters::PostgreSQL.const_defined?('ColumnDefinition')
23
48
  module ColumnDefinition
@@ -3,6 +3,8 @@ module Torque
3
3
  module Adapter
4
4
  module SchemaStatements
5
5
 
6
+ TableDefinition = ActiveRecord::ConnectionAdapters::PostgreSQL::TableDefinition
7
+
6
8
  # Drops a type.
7
9
  def drop_type(name, options = {})
8
10
  force = options.fetch(:force, '').upcase
@@ -69,8 +71,26 @@ module Torque
69
71
  select_values("SELECT unnest(enum_range(NULL::#{name}))")
70
72
  end
71
73
 
74
+ # Rewrite the method that creates tables to easily accept extra options
75
+ def create_table(table_name, **options, &block)
76
+ td = create_table_definition(table_name, **options)
77
+ options[:id] = false if td.inherited_id?
78
+ options[:temporary] = td
79
+
80
+ super table_name, **options, &block
81
+ end
82
+
72
83
  private
73
84
 
85
+ # This waits for the second call to really return the table definition
86
+ def create_table_definition(*args, **options) # :nodoc:
87
+ if !args.second.kind_of?(TableDefinition)
88
+ TableDefinition.new(*args, **options)
89
+ else
90
+ args.second
91
+ end
92
+ end
93
+
74
94
  def quote_enum_values(name, values, options)
75
95
  prefix = options[:prefix]
76
96
  prefix = name if prefix === true
@@ -1,7 +1,7 @@
1
-
2
1
  require_relative 'adapter/database_statements'
3
2
  require_relative 'adapter/oid'
4
3
  require_relative 'adapter/quoting'
4
+ require_relative 'adapter/schema_creation'
5
5
  require_relative 'adapter/schema_definitions'
6
6
  require_relative 'adapter/schema_dumper'
7
7
  require_relative 'adapter/schema_statements'
@@ -0,0 +1,15 @@
1
+ module Torque
2
+ module PostgreSQL
3
+ module Arel
4
+ module JoinSource
5
+ attr_accessor :only
6
+
7
+ def only?
8
+ only === true
9
+ end
10
+ end
11
+
12
+ ::Arel::Nodes::JoinSource.include JoinSource
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,21 @@
1
+ module Torque
2
+ module PostgreSQL
3
+ module Arel
4
+ module SelectManager
5
+
6
+ def using column
7
+ column = ::Arel::Nodes::SqlLiteral.new(column.to_s)
8
+ @ctx.source.right.last.right = Using.new(column)
9
+ self
10
+ end
11
+
12
+ def only
13
+ @ctx.source.only = true
14
+ end
15
+
16
+ end
17
+
18
+ ::Arel::SelectManager.include SelectManager
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,10 @@
1
+ module Torque
2
+ module PostgreSQL
3
+ module Arel
4
+ class Using < ::Arel::Nodes::Unary
5
+ end
6
+
7
+ ::Arel::Nodes::Using = Using
8
+ end
9
+ end
10
+ end
@@ -2,13 +2,30 @@ module Torque
2
2
  module PostgreSQL
3
3
  module Arel
4
4
  module Visitors
5
+
6
+ # Enclose select manager with parenthesis
7
+ # :TODO: Remove when checking the new version of Arel
5
8
  def visit_Arel_SelectManager o, collector
6
9
  collector << '('
7
10
  visit(o.ast, collector) << ')'
8
11
  end
12
+
13
+ # Add ONLY modifier to query
14
+ def visit_Arel_Nodes_JoinSource(o, collector)
15
+ collector << 'ONLY ' if o.only?
16
+ super
17
+ end
18
+
19
+ # Add USING modifier to query
20
+ def visit_Torque_PostgreSQL_Arel_Using(o, collector)
21
+ collector << 'USING ( '
22
+ collector << o.expr
23
+ collector << ' )'
24
+ end
25
+
9
26
  end
10
27
 
11
- ::Arel::Visitors::ToSql.prepend Visitors
28
+ ::Arel::Visitors::PostgreSQL.include Visitors
12
29
  end
13
30
  end
14
31
  end
@@ -0,0 +1,4 @@
1
+ require_relative 'arel/join_source'
2
+ require_relative 'arel/select_manager'
3
+ require_relative 'arel/visitors'
4
+ require_relative 'arel/using'
@@ -36,7 +36,7 @@ module Torque
36
36
  @values ||= self == Enum ? nil : begin
37
37
  conn_name = connection_specification_name
38
38
  conn = connection(conn_name)
39
- conn.enum_values(type_name)
39
+ conn.enum_values(type_name).freeze
40
40
  end
41
41
  end
42
42
 
@@ -227,9 +227,15 @@ module Torque
227
227
  defined_enums[attribute] = subtype.klass
228
228
  end
229
229
 
230
+ # Define a method to find yet to define constants
230
231
  Torque::PostgreSQL.config.enum.namespace.define_singleton_method(:const_missing) do |name|
231
232
  Enum.lookup(name)
232
233
  end
234
+
235
+ # Define a helper method to get a sample value
236
+ Torque::PostgreSQL.config.enum.namespace.define_singleton_method(:sample) do |name|
237
+ Enum.lookup(name).sample
238
+ end
233
239
  end
234
240
  end
235
241
  end
@@ -1,4 +1,3 @@
1
-
2
1
  require_relative 'attributes/type_map'
3
2
  require_relative 'attributes/lazy'
4
3
 
@@ -11,14 +10,23 @@ module Torque
11
10
  module Attributes
12
11
  extend ActiveSupport::Concern
13
12
 
13
+ # Configure enum_save_on_bang behavior
14
14
  included do
15
15
  class_attribute :enum_save_on_bang, instance_accessor: true
16
16
  self.enum_save_on_bang = Torque::PostgreSQL.config.enum.save_on_bang
17
17
  end
18
18
 
19
19
  module ClassMethods
20
+
20
21
  private
21
22
 
23
+ # If the attributes are not loaded,
24
+ def method_missing(method_name, *args, &block)
25
+ return super unless define_attribute_methods
26
+ self.send(method_name, *args, &block)
27
+ end
28
+
29
+ # Use local type map to identify attribute decorator
22
30
  def define_attribute_method(attribute)
23
31
  type = attribute_types[attribute]
24
32
  super unless TypeMap.lookup(type, self, attribute, true)
@@ -35,11 +35,18 @@ module Torque
35
35
 
36
36
  # There are two ways of setting the query:
37
37
  # - A simple relation based on a Model
38
+ # - A Arel-based select manager
38
39
  # - A string or a proc that requires the table name as first argument
39
40
  def query(value = nil, command = nil)
40
41
  return @query if value.nil?
41
42
  return @query = value if relation_query?(value)
42
43
 
44
+ if value.is_a?(::Arel::SelectManager)
45
+ @query = value
46
+ @query_table = value.source.left.name
47
+ return
48
+ end
49
+
43
50
  valid_type = command.respond_to?(:call) || command.is_a?(String)
44
51
  raise ArgumentError, <<-MSG.strip.gsub(/\n +/, ' ') if command.nil?
45
52
  To use proc or string as query, you need to provide the table name
@@ -42,6 +42,11 @@ module Torque
42
42
  obj.ancestors.include?(ActiveRecord::Base)
43
43
  end
44
44
 
45
+ # Identify if the query set may be used as arel
46
+ def arel_query?(obj)
47
+ !obj.nil? && obj.is_a?(::Arel::SelectManager)
48
+ end
49
+
45
50
  # Set a configuration block, if the class is already set up, just clean
46
51
  # the query and wait it to be setup again
47
52
  def configurator(block)
@@ -183,10 +188,10 @@ module Torque
183
188
  # Start a new auxiliary statement giving extra options
184
189
  def initialize(*args)
185
190
  options = args.extract_options!
186
- uses_key = Torque::PostgreSQL.config.auxiliary_statement.send_arguments_key
191
+ args_key = Torque::PostgreSQL.config.auxiliary_statement.send_arguments_key
187
192
 
188
193
  @join = options.fetch(:join, {})
189
- @uses = options.fetch(uses_key, [])
194
+ @args = options.fetch(args_key, {})
190
195
  @select = options.fetch(:select, {})
191
196
  @join_type = options.fetch(:join_type, join_type)
192
197
  end
@@ -214,11 +219,14 @@ module Torque
214
219
  # Ensure that all the dependencies are loaded in the base relation
215
220
  def ensure_dependencies!(base)
216
221
  requires.each do |dependent|
217
- next if base.auxiliary_statements.key?(dependent)
222
+ dependent_klass = base.model.auxiliary_statements_list[dependent]
223
+ next if base.auxiliary_statements_values.any? do |cte|
224
+ cte.is_a?(dependent_klass)
225
+ end
218
226
 
219
227
  instance = AuxiliaryStatement.instantiate(dependent, base)
220
228
  instance.ensure_dependencies!(base)
221
- base.auxiliary_statements[dependent] = instance
229
+ base.auxiliary_statements_values += [instance]
222
230
  end
223
231
  end
224
232
 
@@ -242,18 +250,19 @@ module Torque
242
250
  def mount_query
243
251
  klass = self.class
244
252
  query = klass.query
245
- uses = @uses
253
+ args = @args
246
254
 
247
255
  # Call a proc to get the query
248
- if query.respond_to?(:call)
249
- query = query.call(*uses)
250
- uses = []
256
+ if query.methods.include?(:call)
257
+ call_args = query.try(:arity) === 0 ? [] : [OpenStruct.new(args)]
258
+ query = query.call(*call_args)
259
+ args = []
251
260
  end
252
261
 
253
262
  # Prepare the query depending on its type
254
263
  if query.is_a?(String)
255
- uses.map!(&klass.parent.connection.method(:quote))
256
- ::Arel::Nodes::SqlLiteral.new("(#{query})" % uses)
264
+ args = args.map{ |k, v| [k, klass.parent.connection.quote(v)] }.to_h
265
+ ::Arel::Nodes::SqlLiteral.new("(#{query})" % args)
257
266
  elsif relation_query?(query)
258
267
  query.select(*select_columns).arel
259
268
  else
@@ -4,19 +4,88 @@ module Torque
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  module ClassMethods
7
- delegate :distinct_on, :with, to: :all
7
+ delegate :distinct_on, :with, :itself_only, :cast_records, to: :all
8
8
 
9
- private
9
+ # Wenever it's inherited, add a new list of auxiliary statements
10
+ # It also adds an auxiliary statement to load inherited records' relname
11
+ def inherited(subclass)
12
+ super
10
13
 
11
- # Wenever it's inherited, add a new list of auxiliary statements
12
- def inherited(subclass)
13
- subclass.class_attribute(:auxiliary_statements_list)
14
- subclass.auxiliary_statements_list = Hash.new
15
- super
14
+ subclass.class_attribute(:auxiliary_statements_list)
15
+ subclass.auxiliary_statements_list = Hash.new
16
+ record_class = ActiveRecord::Relation._record_class_attribute
17
+
18
+ # Define helper methods to return the class of the given records
19
+ subclass.auxiliary_statement record_class do |cte|
20
+ pg_class = ::Arel::Table.new('pg_class')
21
+ arel_query = ::Arel::SelectManager.new(pg_class)
22
+ arel_query.project(pg_class['oid'], pg_class['relname'].as(record_class.to_s))
23
+
24
+ cte.query 'pg_class', arel_query.to_sql
25
+ cte.attributes col(record_class) => record_class
26
+ cte.join tableoid: :oid
27
+ end
28
+
29
+ # Define the dynamic attribute that returns the same information as
30
+ # the one provided by the auxiliary statement
31
+ subclass.dynamic_attribute(record_class) do
32
+ next self.class.table_name unless self.class.physically_inheritances?
33
+
34
+ pg_class = ::Arel::Table.new('pg_class')
35
+ source = ::Arel::Table.new(subclass.table_name, as: 'source')
36
+ quoted_id = ::Arel::Nodes::Quoted.new(self.class.connection.quote(id))
37
+
38
+ query = ::Arel::SelectManager.new(pg_class)
39
+ query.join(source).on(pg_class['oid'].eq(source['tableoid']))
40
+ query.where(source[subclass.primary_key].eq(quoted_id))
41
+ query.project(pg_class['relname'])
42
+
43
+ self.class.connection.select_value(query)
16
44
  end
45
+ end
17
46
 
18
47
  protected
19
48
 
49
+ # Allow optional select attributes to be loaded manually when they are
50
+ # not present. This is associated with auxiliary statement, which
51
+ # permits columns that can be loaded through CTEs, be loaded
52
+ # individually for a single record
53
+ #
54
+ # For instance, if you have a statement that can load an user's last
55
+ # comment content, by querying the comments using an auxiliary
56
+ # statement.
57
+ # subclass.auxiliary_statement :last_comment do |cte|
58
+ # cte.query Comment.order(:user_id, id: :desc)
59
+ # .distinct_on(:user_id)
60
+ # cte.attributes col(:content) => :last_comment
61
+ # cte.join_type :left
62
+ # end
63
+ #
64
+ # In case you don't use 'with(:last_comment)', you can do the
65
+ # following.
66
+ # dynamic_attribute(:last_comment) do
67
+ # comments.order(id: :desc).first.content
68
+ # end
69
+ #
70
+ # This means that any auxiliary statements can have their columns
71
+ # granted even when they are not used
72
+ def dynamic_attribute(name, &block)
73
+ define_method(name) do
74
+ return read_attribute(name) if has_attribute?(name)
75
+ result = self.instance_exec(&block)
76
+
77
+ type_klass = ActiveRecord::Type.respond_to?(:default_value) \
78
+ ? ActiveRecord::Type.default_value \
79
+ : self.class.connection.type_map.send(:default_value)
80
+
81
+ @attributes[name.to_s] = ActiveRecord::Relation::QueryAttribute.new(
82
+ name.to_s, result, type_klass,
83
+ )
84
+
85
+ read_attribute(name)
86
+ end
87
+ end
88
+
20
89
  # Creates a new auxiliary statement (CTE) under the base class
21
90
  # attributes key:
22
91
  # Provides a map of attributes to be exposed to the main query.