Class: Query::Composer
- Inherits:
-
Object
- Object
- Query::Composer
- Defined in:
- lib/query/composer.rb,
lib/query/composer/version.rb
Overview
A class for composing queries into large, complicated reporting monstrosities that return data for trends, histograms, and all kinds of other things.
The idea is that you first create a composer object:
q = Query::Composer.new
Then, you tell the composer about a few queries:
q.use(:entities) { User.all }
q.use(:companies) { Company.all }
These queries are independent, in that they have no dependencies. But we can add some queries now that depend on those. We declare another query, giving it one or more parameters. Those parameter names must match the identifiers of queries given to the composer. Here, we have a query that is dependent on the “entities” and “companies” queries, above.
q.use(:entities_with_extra) do |entities, companies|
team_table = Arel::Table.new(:teams)
Arel::SelectManager.new(ActiveRecord::Base).
from(entities).
project(
entities[Arel.star],
team_table[:name].as('team_name'),
companies[:name].as('company_name')).
join(team_table).
on(team_table[:id].eq(entities[:team_id])).
join(companies).
on(companies[:id].eq(entities[:company_id]))
end
After you’ve defined a bunch of these queries, you should have one of them (and ONLY one of them) that nothing else depends on. This is the “root” query–the one that returns the data set you’re looking for. The composer can now do its job and accumulate and aggregate all those queries together, by calling the #build method with the identifier for the root query you want to build.
query = q.build(:some_query_identifier)
By default, this will create a query with each component represented as derived tables (nested subqueries):
SELECT "a".*,
"b"."name" AS "company_name",
"c"."name" AS "team_name"
FROM (
SELECT "users".* FROM "users"
) a
INNER JOIN (
SELECT "companies".* FROM "companies"
) b
ON "b"."id" = "a"."company_id"
INNER JOIN (
SELECT "teams".* FROM "teams"
) c
ON "c"."id" = "a"."team_id"
WHERE ...
If you would rather use CTEs (Common Table Expressions, or “with” queries), you can pass “:use_cte => true” to generate the following:
WITH
"a" AS (SELECT "users".* FROM "users"),
"b" AS (SELECT "companies".* FROM "companies"),
"c" AS (
SELECT "a".*,
"teams"."name" as "team_name",
"b"."name" as "company_name"
FROM "a"
INNER JOIN "teams"
ON "teams"."id" = "a"."team_id"
INNER JOIN "b"
ON "b".id = "a"."company_id")
...
SELECT ...
FROM ...
Be aware, though, that some DBMS’s (like Postgres) do not optimize CTE’s, and so the resulting queries may be very inefficient.
If you don’t want the short, opaque identifiers to be used as aliases, you can pass “:use_aliases => false” to #build:
query = q.build(:entities_with_extra, :use_aliases => false)
That way, the query identifiers themselves will be used as the query aliases.
Defined Under Namespace
Modules: Version Classes: CircularDependency, Error, InvalidQuery, UnknownQuery
Constant Summary collapse
- @@prefer_cte =
false- @@prefer_aliases =
true
Class Method Summary collapse
-
.prefer_aliases=(preference) ⇒ Object
By default, the composer generates queries that use shortened names as aliases for the full names of the components.
- .prefer_aliases? ⇒ Boolean
-
.prefer_cte=(preference) ⇒ Object
By default, the composer generates queries that use derived tables.
- .prefer_cte? ⇒ Boolean
Instance Method Summary collapse
-
#_alias_queries(deps, options = {}) ⇒ Object
Build a mapping of dependency names, to Arel::Table objects.
-
#_invoke(name, aliases) ⇒ Object
Invokes the named dependency, using the given aliases mapping.
-
#_query_with_cte(root, deps, aliases) ⇒ Object
Builds an Arel object using common table expressions.
-
#_query_with_derived_table(root, deps, aliases) ⇒ Object
Builds an Arel object using derived tables.
-
#_resolve(root) ⇒ Object
Resolves the tree of dependent components by traversing the graph starting at ‘root`.
-
#_resolve2(root, dependents = []) ⇒ Object
This is a utility function, used only by #_resolve.
- #_use_aliases?(options) ⇒ Boolean
- #_use_cte?(options) ⇒ Boolean
-
#_validate_dependencies!(name) ⇒ Object
Ensure that all referenced dependencies exist in the graph.
-
#alias(new_name, name) ⇒ Object
Aliases the given query component with the new name.
-
#build(root, options = {}) ⇒ Object
Return an Arel object representing the query starting at the component named ‘root`.
-
#delete(name) ⇒ Object
Removes the named component from the composer.
-
#initialize {|_self| ... } ⇒ Composer
constructor
Create an empty query object.
-
#use(name, &definition) ⇒ Object
Indicate that the named identifier should be defined by the given block.
Constructor Details
#initialize {|_self| ... } ⇒ Composer
Create an empty query object. If a block is given, the query object will be yielded to it.
133 134 135 136 |
# File 'lib/query/composer.rb', line 133 def initialize @parts = {} yield self if block_given? end |
Class Method Details
.prefer_aliases=(preference) ⇒ Object
By default, the composer generates queries that use shortened names as aliases for the full names of the components. If you’d rather use the full names instead of aliases,
126 127 128 |
# File 'lib/query/composer.rb', line 126 def prefer_aliases=(preference) @@prefer_aliases = preference end |
.prefer_aliases? ⇒ Boolean
119 120 121 |
# File 'lib/query/composer.rb', line 119 def prefer_aliases? @@prefer_aliases end |
.prefer_cte=(preference) ⇒ Object
By default, the composer generates queries that use derived tables. If you’d rather default to CTE’s, set Query::Composer.prefer_cte to true.
115 116 117 |
# File 'lib/query/composer.rb', line 115 def prefer_cte=(preference) @@prefer_cte = preference end |
.prefer_cte? ⇒ Boolean
108 109 110 |
# File 'lib/query/composer.rb', line 108 def prefer_cte? @@prefer_cte end |
Instance Method Details
#_alias_queries(deps, options = {}) ⇒ Object
Build a mapping of dependency names, to Arel::Table objects. The Arel::Table names will use opaque, short identifiers (“a”, “b”, etc.), unless the :use_aliases option is false, when the dependency names themselves will be used.
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 |
# File 'lib/query/composer.rb', line 297 def _alias_queries(deps, ={}) use_aliases = _use_aliases?() aliases = {} current_alias = "a" deps.each do |key| if use_aliases aliases[key] = Arel::Table.new(current_alias) current_alias = current_alias.succ else aliases[key] = Arel::Table.new(key) end end aliases end |
#_invoke(name, aliases) ⇒ Object
Invokes the named dependency, using the given aliases mapping.
223 224 225 226 227 228 229 230 231 232 233 234 235 |
# File 'lib/query/composer.rb', line 223 def _invoke(name, aliases) block = @parts[name] params = block.parameters.map { |(_, name)| aliases[name] } result = block.call(*params) if result.respond_to?(:arel) result.arel elsif result.respond_to?(:to_sql) result else raise InvalidQuery, "query elements must quack like #arel or #to_sql (`#{name}` returned #{result.class})" end end |
#_query_with_cte(root, deps, aliases) ⇒ Object
Builds an Arel object using common table expressions.
208 209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/query/composer.rb', line 208 def _query_with_cte(root, deps, aliases) query = _invoke(root, aliases) components = [] deps.each do |name| component = _invoke(name, aliases) aliased = Arel::Nodes::As.new(aliases[name], component) components << aliased end query.with(*components) if components.any? query end |
#_query_with_derived_table(root, deps, aliases) ⇒ Object
Builds an Arel object using derived tables.
197 198 199 200 201 202 203 204 205 |
# File 'lib/query/composer.rb', line 197 def _query_with_derived_table(root, deps, aliases) queries = {} deps.each do |name| queries[name] = _invoke(name, queries).as(aliases[name].name) end _invoke(root, queries) end |
#_resolve(root) ⇒ Object
Resolves the tree of dependent components by traversing the graph starting at ‘root`. Returns an array of identifiers where elements later in the list depend on zero or more elements earlier in the list. The resulting list includes only the dependencies of the `root` element, but not the `root` element itself.
259 260 261 |
# File 'lib/query/composer.rb', line 259 def _resolve(root) _resolve2(root).flatten.uniq - [root] end |
#_resolve2(root, dependents = []) ⇒ Object
This is a utility function, used only by #_resolve. It recursively tranverses the tree, depth-first, and returns a “tree” (array of recursively nested arrays) representing the graph at root. The root of each subtree is at the end of the corresponding array.
[ [ [:a], [:b], :c ], [ [:d], [:e], :f ], :root ]
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 |
# File 'lib/query/composer.rb', line 269 def _resolve2(root, dependents=[]) deps = _validate_dependencies!(root) return [ root ] if deps.empty? # Circular dependency exists if anything in the dependents # (that which depends on root) exists in root's own dependency # list dependents = [ root, *dependents ] overlap = deps & dependents if overlap.any? raise CircularDependency, "#{root} -> #{overlap.join(', ')}" end all = [] deps.each do |dep| all << _resolve2(dep, dependents) end all << root end |
#_use_aliases?(options) ⇒ Boolean
192 193 194 |
# File 'lib/query/composer.rb', line 192 def _use_aliases?() .fetch(:use_aliases, self.class.prefer_aliases?) end |
#_use_cte?(options) ⇒ Boolean
188 189 190 |
# File 'lib/query/composer.rb', line 188 def _use_cte?() .fetch(:use_cte, self.class.prefer_cte?) end |
#_validate_dependencies!(name) ⇒ Object
Ensure that all referenced dependencies exist in the graph. Otherwise, raise Query::Composer::UnknownQuery.
239 240 241 242 243 244 245 246 247 248 249 250 251 252 |
# File 'lib/query/composer.rb', line 239 def _validate_dependencies!(name) raise UnknownQuery, "`#{name}`" unless @parts.key?(name) dependencies = [] @parts[name].parameters.each do |(_, pname)| unless @parts.key?(pname) raise UnknownQuery, "`#{pname}` referenced by `#{name}`" end dependencies << pname end dependencies end |
#alias(new_name, name) ⇒ Object
Aliases the given query component with the new name. This can be useful for redefining an existing component, where you still want to retain the old definition.
composer.use(:source) { Something.all }
composer.alias(:old_source, :source)
composer.use(:source) { |old_source| ... }
158 159 160 161 |
# File 'lib/query/composer.rb', line 158 def alias(new_name, name) @parts[new_name] = @parts[name] self end |
#build(root, options = {}) ⇒ Object
Return an Arel object representing the query starting at the component named ‘root`. Supported options are:
-
:use_cte (false) - the query should use common table expressions. If false, the query will use derived tables, instead.
-
:use_aliases (true) - the query will use short, opaque identifiers for aliases. If false, the query will use the full dependency names to identify the elements.
177 178 179 180 181 182 183 184 185 186 |
# File 'lib/query/composer.rb', line 177 def build(root, ={}) deps = _resolve(root) aliases = _alias_queries(deps, ) if _use_cte?() _query_with_cte(root, deps, aliases) else _query_with_derived_table(root, deps, aliases) end end |
#delete(name) ⇒ Object
Removes the named component from the composer.
164 165 166 167 |
# File 'lib/query/composer.rb', line 164 def delete(name) @parts.delete(name) self end |
#use(name, &definition) ⇒ Object
Indicate that the named identifier should be defined by the given block. The names used for the parameters of the block are significant, and must exactly match the identifiers of other elements in the query.
The block should return an Arel object, for use in composing the larger reporting query. If the return value of the block responds to :arel, the result of that method will be returned instead.
146 147 148 149 |
# File 'lib/query/composer.rb', line 146 def use(name, &definition) @parts[name] = definition self end |