query-composer 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +408 -0
- data/Rakefile +17 -0
- data/examples/library.rb +200 -0
- data/lib/query/base.rb +55 -0
- data/lib/query/composer.rb +315 -0
- data/lib/query/composer/version.rb +11 -0
- data/lib/query/wrapper.rb +7 -0
- data/query-composer.gemspec +28 -0
- data/test/query/base_test.rb +62 -0
- data/test/query/composer_test.rb +102 -0
- data/test/query/wrapper_test.rb +10 -0
- data/test/test_helper.rb +46 -0
- metadata +133 -0
data/examples/library.rb
ADDED
@@ -0,0 +1,200 @@
|
|
1
|
+
# A simple example that demonstrates the use of Query::Composer in a
|
2
|
+
# library reporting system. Given a data model that includes sets of
|
3
|
+
# libraries, topics, books, and patrons, and permis books to be lended
|
4
|
+
# from a library to a patron on a given date, this script builds and
|
5
|
+
# executes a query that shows how many books from a given set of topics
|
6
|
+
# and libraries each patron borrowed during a given period of time, and
|
7
|
+
# compares it to the corresponding period of the previous month.
|
8
|
+
|
9
|
+
require 'active_record'
|
10
|
+
require 'query/composer'
|
11
|
+
require 'query/base'
|
12
|
+
|
13
|
+
# connect to the DB
|
14
|
+
ActiveRecord::Base.establish_connection(
|
15
|
+
adapter: "sqlite3",
|
16
|
+
database: ":memory:"
|
17
|
+
)
|
18
|
+
|
19
|
+
# generate the schema
|
20
|
+
ActiveRecord::Schema.verbose = false
|
21
|
+
ActiveRecord::Schema.define do
|
22
|
+
create_table :libraries do |t|
|
23
|
+
t.string :name
|
24
|
+
end
|
25
|
+
|
26
|
+
create_table :topics do |t|
|
27
|
+
t.string :name
|
28
|
+
end
|
29
|
+
|
30
|
+
create_table :patrons do |t|
|
31
|
+
t.string :name
|
32
|
+
end
|
33
|
+
|
34
|
+
create_table :books do |t|
|
35
|
+
t.string :name
|
36
|
+
t.integer :library_id
|
37
|
+
t.integer :topic_id
|
38
|
+
end
|
39
|
+
|
40
|
+
create_table :lendings do |t|
|
41
|
+
t.integer :book_id
|
42
|
+
t.integer :patron_id
|
43
|
+
t.date :created_at
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# populate the database
|
48
|
+
|
49
|
+
ActiveRecord::Base.connection.execute <<-SQL
|
50
|
+
INSERT INTO libraries (id, name)
|
51
|
+
VALUES (1, 'Gotham'),
|
52
|
+
(2, 'Hogwarts')
|
53
|
+
SQL
|
54
|
+
|
55
|
+
ActiveRecord::Base.connection.execute <<-SQL
|
56
|
+
INSERT INTO topics (id, name)
|
57
|
+
VALUES (1, 'Warts'),
|
58
|
+
(2, 'Seaweed'),
|
59
|
+
(3, 'Dryer Lint'),
|
60
|
+
(4, 'Sitcoms')
|
61
|
+
SQL
|
62
|
+
|
63
|
+
ActiveRecord::Base.connection.execute <<-SQL
|
64
|
+
INSERT INTO patrons (id, name)
|
65
|
+
VALUES (1, 'Harry'),
|
66
|
+
(2, 'Mary'),
|
67
|
+
(3, 'Larry'),
|
68
|
+
(4, 'Carry'),
|
69
|
+
(5, 'Terry'),
|
70
|
+
(6, 'Cheri')
|
71
|
+
SQL
|
72
|
+
|
73
|
+
ActiveRecord::Base.connection.execute <<-SQL
|
74
|
+
INSERT INTO books (id, name, library_id, topic_id)
|
75
|
+
VALUES (1, 'Odd Growths', 1, 1),
|
76
|
+
(2, 'Nose Accessories', 2, 1),
|
77
|
+
(3, 'Health Foods', 1, 2),
|
78
|
+
(4, 'Green', 1, 2),
|
79
|
+
(5, 'Slimy Things', 1, 2),
|
80
|
+
(6, 'Household Chores', 2, 3),
|
81
|
+
(7, 'Starting Fires', 2, 3),
|
82
|
+
(8, 'Laughter', 1, 4),
|
83
|
+
(9, 'Funny People', 1, 4),
|
84
|
+
(10, 'Silliness', 2, 4)
|
85
|
+
SQL
|
86
|
+
|
87
|
+
ActiveRecord::Base.connection.execute <<-SQL
|
88
|
+
INSERT INTO lendings (book_id, patron_id, created_at)
|
89
|
+
VALUES (1, 1, '2016-01-01'),
|
90
|
+
(2, 2, '2016-01-04'),
|
91
|
+
(3, 3, '2016-01-05'),
|
92
|
+
(4, 4, '2016-01-06'),
|
93
|
+
(5, 4, '2016-01-08'),
|
94
|
+
(6, 4, '2016-01-08'),
|
95
|
+
(7, 5, '2016-01-11'),
|
96
|
+
(8, 6, '2016-01-12'),
|
97
|
+
(9, 6, '2016-01-14'),
|
98
|
+
(10, 6, '2016-01-15'),
|
99
|
+
(1, 6, '2016-02-01'),
|
100
|
+
(2, 6, '2016-02-04'),
|
101
|
+
(3, 5, '2016-02-05'),
|
102
|
+
(4, 5, '2016-02-06'),
|
103
|
+
(5, 4, '2016-02-08'),
|
104
|
+
(6, 3, '2016-02-08'),
|
105
|
+
(7, 3, '2016-02-11'),
|
106
|
+
(8, 2, '2016-02-12'),
|
107
|
+
(9, 2, '2016-02-14'),
|
108
|
+
(10, 1, '2016-02-15')
|
109
|
+
SQL
|
110
|
+
|
111
|
+
# define the models
|
112
|
+
|
113
|
+
class Library < ActiveRecord::Base
|
114
|
+
has_many :books
|
115
|
+
end
|
116
|
+
|
117
|
+
class Topic < ActiveRecord::Base
|
118
|
+
has_many :books
|
119
|
+
end
|
120
|
+
|
121
|
+
class Patron < ActiveRecord::Base
|
122
|
+
has_many :lendings
|
123
|
+
has_many :books, through: :lendings
|
124
|
+
end
|
125
|
+
|
126
|
+
class Book < ActiveRecord::Base
|
127
|
+
belongs_to :library
|
128
|
+
belongs_to :topic
|
129
|
+
has_many :lendings
|
130
|
+
has_many :patrons, through: :lendings
|
131
|
+
end
|
132
|
+
|
133
|
+
class Lending < ActiveRecord::Base
|
134
|
+
belongs_to :patron
|
135
|
+
belongs_to :book
|
136
|
+
end
|
137
|
+
|
138
|
+
# Construct the reporting query
|
139
|
+
|
140
|
+
composer = Query::Composer.new
|
141
|
+
|
142
|
+
composer.use(:libraries_set) { Library.where(id: [ 1, 2 ]) }
|
143
|
+
composer.use(:topics_set) { Topic.where(id: [ 1, 2, 3, 4 ]) }
|
144
|
+
composer.use(:patrons_set) { Patron.all }
|
145
|
+
|
146
|
+
composer.use(:books_set) do |libraries_set, topics_set|
|
147
|
+
books = Book.arel_table
|
148
|
+
|
149
|
+
Query::Base.new(books).
|
150
|
+
project(books[:id]).
|
151
|
+
join(libraries_set).
|
152
|
+
on(books[:library_id].eq(libraries_set[:id])).
|
153
|
+
join(topics_set).
|
154
|
+
on(books[:topic_id].eq(topics_set[:id]))
|
155
|
+
end
|
156
|
+
|
157
|
+
composer.use(:current_set) do |books_set|
|
158
|
+
lendings_set(books_set, '2016-02-01', '2016-02-15')
|
159
|
+
end
|
160
|
+
|
161
|
+
composer.use(:prior_set) do |books_set|
|
162
|
+
lendings_set(books_set, '2016-01-01', '2016-01-15')
|
163
|
+
end
|
164
|
+
|
165
|
+
composer.use(:combined_set) do |patrons_set, current_set, prior_set|
|
166
|
+
Query::Base.new(patrons_set).
|
167
|
+
project(patrons_set[Arel.star],
|
168
|
+
current_set[:total].as("current_total"),
|
169
|
+
prior_set[:total].as("prior_total")).
|
170
|
+
join(current_set).
|
171
|
+
on(current_set[:patron_id].eq(patrons_set[:id])).
|
172
|
+
join(prior_set, Arel::Nodes::OuterJoin).
|
173
|
+
on(prior_set[:patron_id].eq(patrons_set[:id]))
|
174
|
+
end
|
175
|
+
|
176
|
+
def lendings_set(books_set, from_date, to_date)
|
177
|
+
lendings = Lending.arel_table
|
178
|
+
|
179
|
+
patron_id = lendings[:patron_id]
|
180
|
+
count = patron_id.count.as("total")
|
181
|
+
|
182
|
+
Query::Base.new(lendings).
|
183
|
+
project(patron_id, count).
|
184
|
+
join(books_set).
|
185
|
+
on(lendings[:book_id].eq(books_set[:id])).
|
186
|
+
where(lendings[:created_at].between(from_date..to_date)).
|
187
|
+
group(patron_id)
|
188
|
+
end
|
189
|
+
|
190
|
+
sql = composer.build(:combined_set).to_sql
|
191
|
+
puts "---- SQL ----"
|
192
|
+
puts sql
|
193
|
+
|
194
|
+
puts
|
195
|
+
puts "%-6s | %3s | %5s" % %w(Patron Now Prior)
|
196
|
+
puts "%-6s-+-%3s-+-%5s-" % ["-"*6, "-"*3, "-"*5]
|
197
|
+
|
198
|
+
Patron.find_by_sql(sql).each do |patron|
|
199
|
+
puts "%-6s | %3d | %5d" % [ patron.name, patron.current_total, patron.prior_total||0]
|
200
|
+
end
|
data/lib/query/base.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'arel'
|
2
|
+
|
3
|
+
module Query
|
4
|
+
class Base
|
5
|
+
attr_reader :primary_table, :arel
|
6
|
+
|
7
|
+
def initialize(primary, *args)
|
8
|
+
@primary_table = _make_table(primary)
|
9
|
+
|
10
|
+
@arel = Arel::SelectManager.new(ActiveRecord::Base).
|
11
|
+
from(@primary_table)
|
12
|
+
|
13
|
+
_configure(*args)
|
14
|
+
end
|
15
|
+
|
16
|
+
def reproject(*projections)
|
17
|
+
arel.projections = projections
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def all
|
22
|
+
arel.project(primary_table[Arel.star])
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
# ensures #as returns an Arel node, and not the Query object
|
27
|
+
# (so that it plays nice with our custom #with method, above).
|
28
|
+
def as(name)
|
29
|
+
arel.as(name)
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_sql
|
33
|
+
@arel.to_sql
|
34
|
+
end
|
35
|
+
|
36
|
+
alias to_s to_sql
|
37
|
+
|
38
|
+
def method_missing(sym, *args, &block)
|
39
|
+
arel.send(sym, *args, &block)
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
def _make_table(value)
|
44
|
+
case value
|
45
|
+
when Arel::Table then value
|
46
|
+
when Arel::Nodes::TableAlias then value
|
47
|
+
else Arel::Table.new(value)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def _configure(*args)
|
52
|
+
# overridden by subclasses for per-query configuration
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,315 @@
|
|
1
|
+
require 'arel'
|
2
|
+
|
3
|
+
module Query
|
4
|
+
|
5
|
+
# A class for composing queries into large, complicated reporting
|
6
|
+
# monstrosities that return data for trends, histograms, and all
|
7
|
+
# kinds of other things.
|
8
|
+
#
|
9
|
+
# The idea is that you first create a composer object:
|
10
|
+
#
|
11
|
+
# q = Query::Composer.new
|
12
|
+
#
|
13
|
+
# Then, you tell the composer about a few queries:
|
14
|
+
#
|
15
|
+
# q.use(:entities) { User.all }
|
16
|
+
# q.use(:companies) { Company.all }
|
17
|
+
#
|
18
|
+
# These queries are *independent*, in that they have no dependencies.
|
19
|
+
# But we can add some queries now that depend on those. We declare
|
20
|
+
# another query, giving it one or more parameters. Those parameter
|
21
|
+
# names must match the identifiers of queries given to the composer.
|
22
|
+
# Here, we have a query that is dependent on the "entities" and
|
23
|
+
# "companies" queries, above.
|
24
|
+
#
|
25
|
+
# q.use(:entities_with_extra) do |entities, companies|
|
26
|
+
# team_table = Arel::Table.new(:teams)
|
27
|
+
#
|
28
|
+
# Arel::SelectManager.new(ActiveRecord::Base).
|
29
|
+
# from(entities).
|
30
|
+
# project(
|
31
|
+
# entities[Arel.star],
|
32
|
+
# team_table[:name].as('team_name'),
|
33
|
+
# companies[:name].as('company_name')).
|
34
|
+
# join(team_table).
|
35
|
+
# on(team_table[:id].eq(entities[:team_id])).
|
36
|
+
# join(companies).
|
37
|
+
# on(companies[:id].eq(entities[:company_id]))
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# After you've defined a bunch of these queries, you should have
|
41
|
+
# one of them (and ONLY one of them) that nothing else depends on.
|
42
|
+
# This is the "root" query--the one that returns the data set you're
|
43
|
+
# looking for. The composer can now do its job and accumulate and
|
44
|
+
# aggregate all those queries together, by calling the #build method
|
45
|
+
# with the identifier for the root query you want to build.
|
46
|
+
#
|
47
|
+
# query = q.build(:some_query_identifier)
|
48
|
+
#
|
49
|
+
# By default, this will create a query with each component represented
|
50
|
+
# as derived tables (nested subqueries):
|
51
|
+
#
|
52
|
+
# SELECT "a".*,
|
53
|
+
# "b"."name" AS "company_name",
|
54
|
+
# "c"."name" AS "team_name"
|
55
|
+
# FROM (
|
56
|
+
# SELECT "users".* FROM "users"
|
57
|
+
# ) a
|
58
|
+
# INNER JOIN (
|
59
|
+
# SELECT "companies".* FROM "companies"
|
60
|
+
# ) b
|
61
|
+
# ON "b"."id" = "a"."company_id"
|
62
|
+
# INNER JOIN (
|
63
|
+
# SELECT "teams".* FROM "teams"
|
64
|
+
# ) c
|
65
|
+
# ON "c"."id" = "a"."team_id"
|
66
|
+
# WHERE ...
|
67
|
+
#
|
68
|
+
# If you would rather use CTEs (Common Table Expressions, or "with"
|
69
|
+
# queries), you can pass ":use_cte => true" to generate the following:
|
70
|
+
#
|
71
|
+
# WITH
|
72
|
+
# "a" AS (SELECT "users".* FROM "users"),
|
73
|
+
# "b" AS (SELECT "companies".* FROM "companies"),
|
74
|
+
# "c" AS (
|
75
|
+
# SELECT "a".*,
|
76
|
+
# "teams"."name" as "team_name",
|
77
|
+
# "b"."name" as "company_name"
|
78
|
+
# FROM "a"
|
79
|
+
# INNER JOIN "teams"
|
80
|
+
# ON "teams"."id" = "a"."team_id"
|
81
|
+
# INNER JOIN "b"
|
82
|
+
# ON "b".id = "a"."company_id")
|
83
|
+
# ...
|
84
|
+
# SELECT ...
|
85
|
+
# FROM ...
|
86
|
+
#
|
87
|
+
# Be aware, though, that some DBMS's (like Postgres) do not optimize
|
88
|
+
# CTE's, and so the resulting queries may be very inefficient.
|
89
|
+
#
|
90
|
+
# If you don't want the short, opaque identifiers to be used as
|
91
|
+
# aliases, you can pass ":use_aliases => false" to #build:
|
92
|
+
#
|
93
|
+
# query = q.build(:entities_with_extra, :use_aliases => false)
|
94
|
+
#
|
95
|
+
# That way, the query identifiers themselves will be used as the
|
96
|
+
# query aliases.
|
97
|
+
|
98
|
+
class Composer
|
99
|
+
class Error < RuntimeError; end
|
100
|
+
class UnknownQuery < Error; end
|
101
|
+
class CircularDependency < Error; end
|
102
|
+
class InvalidQuery < Error; end
|
103
|
+
|
104
|
+
@@prefer_cte = false
|
105
|
+
@@prefer_aliases = true
|
106
|
+
|
107
|
+
class <<self
|
108
|
+
def prefer_cte?
|
109
|
+
@@prefer_cte
|
110
|
+
end
|
111
|
+
|
112
|
+
# By default, the composer generates queries that use derived
|
113
|
+
# tables. If you'd rather default to CTE's,
|
114
|
+
# set Query::Composer.prefer_cte to true.
|
115
|
+
def prefer_cte=(preference)
|
116
|
+
@@prefer_cte = preference
|
117
|
+
end
|
118
|
+
|
119
|
+
def prefer_aliases?
|
120
|
+
@@prefer_aliases
|
121
|
+
end
|
122
|
+
|
123
|
+
# By default, the composer generates queries that use shortened
|
124
|
+
# names as aliases for the full names of the components. If you'd
|
125
|
+
# rather use the full names instead of aliases,
|
126
|
+
def prefer_aliases=(preference)
|
127
|
+
@@prefer_aliases = preference
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Create an empty query object. If a block is given, the query
|
132
|
+
# object will be yielded to it.
|
133
|
+
def initialize
|
134
|
+
@parts = {}
|
135
|
+
yield self if block_given?
|
136
|
+
end
|
137
|
+
|
138
|
+
# Indicate that the named identifier should be defined by the given
|
139
|
+
# block. The names used for the parameters of the block are significant,
|
140
|
+
# and must exactly match the identifiers of other elements in the
|
141
|
+
# query.
|
142
|
+
#
|
143
|
+
# The block should return an Arel object, for use in composing the
|
144
|
+
# larger reporting query. If the return value of the block responds
|
145
|
+
# to :arel, the result of that method will be returned instead.
|
146
|
+
def use(name, &definition)
|
147
|
+
@parts[name] = definition
|
148
|
+
self
|
149
|
+
end
|
150
|
+
|
151
|
+
# Aliases the given query component with the new name. This can be
|
152
|
+
# useful for redefining an existing component, where you still
|
153
|
+
# want to retain the old definition.
|
154
|
+
#
|
155
|
+
# composer.use(:source) { Something.all }
|
156
|
+
# composer.alias(:old_source, :source)
|
157
|
+
# composer.use(:source) { |old_source| ... }
|
158
|
+
def alias(new_name, name)
|
159
|
+
@parts[new_name] = @parts[name]
|
160
|
+
self
|
161
|
+
end
|
162
|
+
|
163
|
+
# Removes the named component from the composer.
|
164
|
+
def delete(name)
|
165
|
+
@parts.delete(name)
|
166
|
+
self
|
167
|
+
end
|
168
|
+
|
169
|
+
# Return an Arel object representing the query starting at the
|
170
|
+
# component named `root`. Supported options are:
|
171
|
+
#
|
172
|
+
# * :use_cte (false) - the query should use common table expressions.
|
173
|
+
# If false, the query will use derived tables, instead.
|
174
|
+
# * :use_aliases (true) - the query will use short, opaque identifiers
|
175
|
+
# for aliases. If false, the query will use the full dependency
|
176
|
+
# names to identify the elements.
|
177
|
+
def build(root, options={})
|
178
|
+
deps = _resolve(root)
|
179
|
+
aliases = _alias_queries(deps, options)
|
180
|
+
|
181
|
+
if _use_cte?(options)
|
182
|
+
_query_with_cte(root, deps, aliases)
|
183
|
+
else
|
184
|
+
_query_with_derived_table(root, deps, aliases)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def _use_cte?(options)
|
189
|
+
options.fetch(:use_cte, self.class.prefer_cte?)
|
190
|
+
end
|
191
|
+
|
192
|
+
def _use_aliases?(options)
|
193
|
+
options.fetch(:use_aliases, self.class.prefer_aliases?)
|
194
|
+
end
|
195
|
+
|
196
|
+
# Builds an Arel object using derived tables.
|
197
|
+
def _query_with_derived_table(root, deps, aliases)
|
198
|
+
queries = {}
|
199
|
+
|
200
|
+
deps.each do |name|
|
201
|
+
queries[name] = _invoke(name, queries).as(aliases[name].name)
|
202
|
+
end
|
203
|
+
|
204
|
+
_invoke(root, queries)
|
205
|
+
end
|
206
|
+
|
207
|
+
# Builds an Arel object using common table expressions.
|
208
|
+
def _query_with_cte(root, deps, aliases)
|
209
|
+
query = _invoke(root, aliases)
|
210
|
+
components = []
|
211
|
+
|
212
|
+
deps.each do |name|
|
213
|
+
component = _invoke(name, aliases)
|
214
|
+
aliased = Arel::Nodes::As.new(aliases[name], component)
|
215
|
+
components << aliased
|
216
|
+
end
|
217
|
+
|
218
|
+
query.with(*components) if components.any?
|
219
|
+
query
|
220
|
+
end
|
221
|
+
|
222
|
+
# Invokes the named dependency, using the given aliases mapping.
|
223
|
+
def _invoke(name, aliases)
|
224
|
+
block = @parts[name]
|
225
|
+
params = block.parameters.map { |(_, name)| aliases[name] }
|
226
|
+
result = block.call(*params)
|
227
|
+
|
228
|
+
if result.respond_to?(:arel)
|
229
|
+
result.arel
|
230
|
+
elsif result.respond_to?(:to_sql)
|
231
|
+
result
|
232
|
+
else
|
233
|
+
raise InvalidQuery, "query elements must quack like #arel or #to_sql (`#{name}` returned #{result.class})"
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# Ensure that all referenced dependencies exist in the graph.
|
238
|
+
# Otherwise, raise Query::Composer::UnknownQuery.
|
239
|
+
def _validate_dependencies!(name)
|
240
|
+
raise UnknownQuery, "`#{name}`" unless @parts.key?(name)
|
241
|
+
dependencies = []
|
242
|
+
|
243
|
+
@parts[name].parameters.each do |(_, pname)|
|
244
|
+
unless @parts.key?(pname)
|
245
|
+
raise UnknownQuery, "`#{pname}` referenced by `#{name}`"
|
246
|
+
end
|
247
|
+
|
248
|
+
dependencies << pname
|
249
|
+
end
|
250
|
+
|
251
|
+
dependencies
|
252
|
+
end
|
253
|
+
|
254
|
+
# Resolves the tree of dependent components by traversing the graph
|
255
|
+
# starting at `root`. Returns an array of identifiers where elements
|
256
|
+
# later in the list depend on zero or more elements earlier in the
|
257
|
+
# list. The resulting list includes only the dependencies of the
|
258
|
+
# `root` element, but not the `root` element itself.
|
259
|
+
def _resolve(root)
|
260
|
+
_resolve2(root).flatten.uniq - [root]
|
261
|
+
end
|
262
|
+
|
263
|
+
# This is a utility function, used only by #_resolve. It recursively
|
264
|
+
# tranverses the tree, depth-first, and returns a "tree" (array of
|
265
|
+
# recursively nested arrays) representing the graph at root. The
|
266
|
+
# root of each subtree is at the end of the corresponding array.
|
267
|
+
#
|
268
|
+
# [ [ [:a], [:b], :c ], [ [:d], [:e], :f ], :root ]
|
269
|
+
def _resolve2(root, dependents=[])
|
270
|
+
deps = _validate_dependencies!(root)
|
271
|
+
return [ root ] if deps.empty?
|
272
|
+
|
273
|
+
# Circular dependency exists if anything in the dependents
|
274
|
+
# (that which depends on root) exists in root's own dependency
|
275
|
+
# list
|
276
|
+
|
277
|
+
dependents = [ root, *dependents ]
|
278
|
+
overlap = deps & dependents
|
279
|
+
if overlap.any?
|
280
|
+
raise CircularDependency, "#{root} -> #{overlap.join(', ')}"
|
281
|
+
end
|
282
|
+
|
283
|
+
all = []
|
284
|
+
|
285
|
+
deps.each do |dep|
|
286
|
+
all << _resolve2(dep, dependents)
|
287
|
+
end
|
288
|
+
|
289
|
+
|
290
|
+
all << root
|
291
|
+
end
|
292
|
+
|
293
|
+
# Build a mapping of dependency names, to Arel::Table objects. The
|
294
|
+
# Arel::Table names will use opaque, short identifiers ("a", "b", etc.),
|
295
|
+
# unless the :use_aliases option is false, when the dependency names
|
296
|
+
# themselves will be used.
|
297
|
+
def _alias_queries(deps, options={})
|
298
|
+
use_aliases = _use_aliases?(options)
|
299
|
+
|
300
|
+
aliases = {}
|
301
|
+
current_alias = "a"
|
302
|
+
|
303
|
+
deps.each do |key|
|
304
|
+
if use_aliases
|
305
|
+
aliases[key] = Arel::Table.new(current_alias)
|
306
|
+
current_alias = current_alias.succ
|
307
|
+
else
|
308
|
+
aliases[key] = Arel::Table.new(key)
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
aliases
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|