torque-postgresql 0.1.7 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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.