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.
- 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.
|