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.
- checksums.yaml +4 -4
- data/README.rdoc +74 -0
- data/lib/torque/postgresql/adapter/database_statements.rb +44 -2
- data/lib/torque/postgresql/adapter/schema_creation.rb +55 -0
- data/lib/torque/postgresql/adapter/schema_definitions.rb +26 -1
- data/lib/torque/postgresql/adapter/schema_statements.rb +20 -0
- data/lib/torque/postgresql/adapter.rb +1 -1
- data/lib/torque/postgresql/arel/join_source.rb +15 -0
- data/lib/torque/postgresql/arel/select_manager.rb +21 -0
- data/lib/torque/postgresql/arel/using.rb +10 -0
- data/lib/torque/postgresql/arel/visitors.rb +18 -1
- data/lib/torque/postgresql/arel.rb +4 -0
- data/lib/torque/postgresql/attributes/enum.rb +7 -1
- data/lib/torque/postgresql/attributes.rb +9 -1
- data/lib/torque/postgresql/auxiliary_statement/settings.rb +7 -0
- data/lib/torque/postgresql/auxiliary_statement.rb +19 -10
- data/lib/torque/postgresql/base.rb +76 -7
- data/lib/torque/postgresql/coder.rb +132 -0
- data/lib/torque/postgresql/collector.rb +2 -0
- data/lib/torque/postgresql/config.rb +35 -1
- data/lib/torque/postgresql/inheritance.rb +133 -0
- data/lib/torque/postgresql/railtie.rb +16 -0
- data/lib/torque/postgresql/relation/auxiliary_statement.rb +26 -19
- data/lib/torque/postgresql/relation/distinct_on.rb +9 -7
- data/lib/torque/postgresql/relation/inheritance.rb +112 -0
- data/lib/torque/postgresql/relation/merger.rb +48 -0
- data/lib/torque/postgresql/relation.rb +67 -2
- data/lib/torque/postgresql/schema_cache.rb +192 -0
- data/lib/torque/postgresql/schema_dumper.rb +49 -1
- data/lib/torque/postgresql/version.rb +1 -1
- data/lib/torque/postgresql.rb +6 -4
- metadata +14 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 979764ebdcc3d060ce4aab1c28de3dc4f0aff8b1
|
4
|
+
data.tar.gz: e522c438e3d8d04f61d761d5738217b471724075
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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'
|
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 +
|
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
|
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,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
|
@@ -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::
|
28
|
+
::Arel::Visitors::PostgreSQL.include Visitors
|
12
29
|
end
|
13
30
|
end
|
14
31
|
end
|
@@ -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
|
-
|
191
|
+
args_key = Torque::PostgreSQL.config.auxiliary_statement.send_arguments_key
|
187
192
|
|
188
193
|
@join = options.fetch(:join, {})
|
189
|
-
@
|
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
|
-
|
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.
|
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
|
-
|
253
|
+
args = @args
|
246
254
|
|
247
255
|
# Call a proc to get the query
|
248
|
-
if query.
|
249
|
-
|
250
|
-
|
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
|
-
|
256
|
-
::Arel::Nodes::SqlLiteral.new("(#{query})" %
|
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
|
-
|
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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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.
|