squeel 0.5.0 → 0.5.5
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +115 -39
- data/lib/squeel/adapters/active_record.rb +22 -5
- data/lib/squeel/adapters/active_record/3.0/association_preload.rb +15 -0
- data/lib/squeel/adapters/active_record/3.0/compat.rb +143 -0
- data/lib/squeel/adapters/active_record/3.0/context.rb +67 -0
- data/lib/squeel/adapters/active_record/3.0/join_association.rb +54 -0
- data/lib/squeel/adapters/active_record/3.0/join_dependency.rb +84 -0
- data/lib/squeel/adapters/active_record/3.0/relation.rb +327 -0
- data/lib/squeel/adapters/active_record/context.rb +67 -0
- data/lib/squeel/adapters/active_record/join_association.rb +10 -56
- data/lib/squeel/adapters/active_record/join_dependency.rb +22 -7
- data/lib/squeel/adapters/active_record/preloader.rb +21 -0
- data/lib/squeel/adapters/active_record/relation.rb +84 -38
- data/lib/squeel/context.rb +38 -0
- data/lib/squeel/dsl.rb +1 -1
- data/lib/squeel/nodes/join.rb +18 -0
- data/lib/squeel/nodes/key_path.rb +2 -2
- data/lib/squeel/nodes/stub.rb +5 -1
- data/lib/squeel/version.rb +1 -1
- data/lib/squeel/visitors.rb +2 -2
- data/lib/squeel/visitors/{order_visitor.rb → attribute_visitor.rb} +1 -2
- data/lib/squeel/visitors/predicate_visitor.rb +13 -11
- data/lib/squeel/visitors/symbol_visitor.rb +48 -0
- data/spec/helpers/squeel_helper.rb +17 -1
- data/spec/spec_helper.rb +31 -0
- data/spec/squeel/adapters/active_record/context_spec.rb +50 -0
- data/spec/squeel/adapters/active_record/join_association_spec.rb +1 -1
- data/spec/squeel/adapters/active_record/join_depdendency_spec.rb +1 -1
- data/spec/squeel/adapters/active_record/relation_spec.rb +166 -25
- data/spec/squeel/dsl_spec.rb +6 -6
- data/spec/squeel/nodes/join_spec.rb +16 -3
- data/spec/squeel/nodes/stub_spec.rb +12 -0
- data/spec/squeel/visitors/{order_visitor_spec.rb → attribute_visitor_spec.rb} +4 -5
- data/spec/squeel/visitors/predicate_visitor_spec.rb +18 -6
- data/spec/squeel/visitors/symbol_visitor_spec.rb +42 -0
- data/squeel.gemspec +2 -2
- metadata +21 -13
- data/lib/squeel/contexts/join_dependency_context.rb +0 -74
- data/lib/squeel/visitors/select_visitor.rb +0 -103
- data/spec/squeel/contexts/join_dependency_context_spec.rb +0 -43
- data/spec/squeel/visitors/select_visitor_spec.rb +0 -115
data/README.rdoc
CHANGED
@@ -1,41 +1,117 @@
|
|
1
1
|
=Squeel
|
2
2
|
|
3
|
-
|
4
|
-
for
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
==
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
3
|
+
Squeel is a (Rails 3.1-only for now) rewrite of MetaWhere. It's rapidly approaching
|
4
|
+
a point where I could recommend it for daily use. Once it hits feature completion, I'll
|
5
|
+
work on backporting to Rails 3.0.x. In the meantime, please feel free to clone this repo
|
6
|
+
and give it a test drive using <tt>rake console</tt> and the models in the
|
7
|
+
<tt>spec/support/schema.rb</tt> file.
|
8
|
+
|
9
|
+
== Getting started
|
10
|
+
|
11
|
+
In your Gemfile:
|
12
|
+
|
13
|
+
gem "squeel" # Last officially released gem
|
14
|
+
# gem "squeel", :git => "git://github.com/ernie/squeel.git" # Track git repo
|
15
|
+
|
16
|
+
In an intitializer:
|
17
|
+
|
18
|
+
Squeel.configure do |config|
|
19
|
+
# To load hash extensions (to allow for AND (&), OR (|), and NOT (-) against
|
20
|
+
# hashes of conditions)
|
21
|
+
config.load_core_extensions :hash
|
22
|
+
|
23
|
+
# To load symbol extensions (for a subset of the old MetaWhere functionality,
|
24
|
+
# via ARel predicate methods on Symbols: :name.matches, etc)
|
25
|
+
# config.load_core_extensions :symbol
|
26
|
+
|
27
|
+
# To load both hash and symbol extensions
|
28
|
+
# config.load_core_extensions :hash, :symbol
|
29
|
+
end
|
30
|
+
|
31
|
+
== The Squeel Query DSL
|
32
|
+
|
33
|
+
Squeel enhances the normal ActiveRecord query methods by enabling them to accept
|
34
|
+
blocks. Inside a block, the Squeel query DSL can be used. Note the use of curly braces
|
35
|
+
in these examples instead of parentheses. {} denotes a Squeel DSL query.
|
36
|
+
|
37
|
+
=== Stubs
|
38
|
+
|
39
|
+
Stubs are, for most intents and purposes, just like Symbols in a normal call to
|
40
|
+
Relation#where (note the need for doubling up on the curly braces here, the first ones
|
41
|
+
start the block, the second are the hash braces):
|
42
|
+
|
43
|
+
Person.where{{name => 'Ernie'}}
|
44
|
+
=> SELECT "people".* FROM "people" WHERE "people"."name" = 'Ernie'
|
45
|
+
|
46
|
+
You normally wouldn't bother using the DSL in this case, as a simple hash would
|
47
|
+
suffice. However, stubs serve as a building block for keypaths, and keypaths are
|
48
|
+
very handy.
|
49
|
+
|
50
|
+
=== KeyPaths
|
51
|
+
|
52
|
+
A Squeel keypath is essentially a more concise and readable alternative to a
|
53
|
+
deeply nested hash. For instance, in standard ActiveRecord, you might join several
|
54
|
+
associations like this to perform a query:
|
55
|
+
|
56
|
+
Person.joins(:articles => {:comments => :person})
|
57
|
+
=> SELECT "people".* FROM "people"
|
58
|
+
INNER JOIN "articles" ON "articles"."person_id" = "people"."id"
|
59
|
+
INNER JOIN "comments" ON "comments"."article_id" = "articles"."id"
|
60
|
+
INNER JOIN "people" "people_comments" ON "people_comments"."id" = "comments"."person_id"
|
61
|
+
|
62
|
+
With a keypath, this would look like:
|
63
|
+
|
64
|
+
Person.joins{articles.comments.person}
|
65
|
+
|
66
|
+
A keypath can exist in the context of a hash, and is normally interpreted relative to
|
67
|
+
the current level of nesting. It can be forced into an "absolute" path by anchoring it with
|
68
|
+
a ~, like:
|
69
|
+
|
70
|
+
~articles.comments.person
|
71
|
+
|
72
|
+
This isn't quite so useful in the typical hash context, but can be very useful when it comes
|
73
|
+
to interpreting functions and the like. We'll cover those later.
|
74
|
+
|
75
|
+
=== Joins
|
76
|
+
|
77
|
+
As you saw above, keypaths can be used as shorthand for joins. Additionally, you can
|
78
|
+
specify join types (or join classes, in the case of polymorphic belongs_to joins):
|
79
|
+
|
80
|
+
Person.joins{articles.outer}
|
81
|
+
=> SELECT "people".* FROM "people"
|
82
|
+
LEFT OUTER JOIN "articles" ON "articles"."person_id" = "people"."id"
|
83
|
+
Note.joins{notable(Person).outer}
|
84
|
+
=> SELECT "notes".* FROM "notes"
|
85
|
+
LEFT OUTER JOIN "people"
|
86
|
+
ON "people"."id" = "notes"."notable_id"
|
87
|
+
AND "notes"."notable_type" = 'Person'
|
88
|
+
|
89
|
+
These can also be used inside keypaths:
|
90
|
+
|
91
|
+
Note.joins{notable(Person).articles}
|
92
|
+
=> SELECT "notes".* FROM "notes"
|
93
|
+
INNER JOIN "people" ON "people"."id" = "notes"."notable_id"
|
94
|
+
AND "notes"."notable_type" = 'Person'
|
95
|
+
INNER JOIN "articles" ON "articles"."person_id" = "people"."id"
|
96
|
+
|
97
|
+
=== Functions
|
98
|
+
|
99
|
+
You can call SQL functions just like you would call a method in Ruby...
|
100
|
+
|
101
|
+
Person.select{coalesce(name, '<no name given>')}
|
102
|
+
=> SELECT coalesce("people"."name", '<no name given>') FROM "people"
|
103
|
+
|
104
|
+
...and you can easily give it an alias:
|
105
|
+
|
106
|
+
person = Person.select{
|
107
|
+
coalesce(name, '<no name given>').as(name_with_default)
|
108
|
+
}.first
|
109
|
+
person.name_with_default # name or <no name given>, depending on data
|
110
|
+
|
111
|
+
=== Operators
|
112
|
+
|
113
|
+
You can use the standard mathematical operators (+, -, *, /)inside the Squeel DSL to
|
114
|
+
specify operators in the resulting SQL, or the <tt>op</tt> method to specify another
|
115
|
+
custom operator, such as the standard SQL concatenation operator, ||:
|
116
|
+
|
117
|
+
...more docs to come...
|
@@ -1,6 +1,23 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require 'squeel/adapters/active_record/
|
1
|
+
case ActiveRecord::VERSION::STRING
|
2
|
+
when /^3\.0\./
|
3
|
+
require 'squeel/adapters/active_record/3.0/compat'
|
4
|
+
require 'squeel/adapters/active_record/3.0/relation'
|
5
|
+
require 'squeel/adapters/active_record/3.0/join_dependency'
|
6
|
+
require 'squeel/adapters/active_record/3.0/join_association'
|
7
|
+
require 'squeel/adapters/active_record/3.0/association_preload'
|
8
|
+
require 'squeel/adapters/active_record/3.0/context'
|
4
9
|
|
5
|
-
ActiveRecord::Relation.send :include, Squeel::Adapters::ActiveRecord::Relation
|
6
|
-
ActiveRecord::Associations::JoinDependency.send :include, Squeel::Adapters::ActiveRecord::JoinDependency
|
10
|
+
ActiveRecord::Relation.send :include, Squeel::Adapters::ActiveRecord::Relation
|
11
|
+
ActiveRecord::Associations::ClassMethods::JoinDependency.send :include, Squeel::Adapters::ActiveRecord::JoinDependency
|
12
|
+
ActiveRecord::Base.extend Squeel::Adapters::ActiveRecord::AssociationPreload
|
13
|
+
else
|
14
|
+
require 'squeel/adapters/active_record/relation'
|
15
|
+
require 'squeel/adapters/active_record/join_dependency'
|
16
|
+
require 'squeel/adapters/active_record/join_association'
|
17
|
+
require 'squeel/adapters/active_record/preloader'
|
18
|
+
require 'squeel/adapters/active_record/context'
|
19
|
+
|
20
|
+
ActiveRecord::Relation.send :include, Squeel::Adapters::ActiveRecord::Relation
|
21
|
+
ActiveRecord::Associations::JoinDependency.send :include, Squeel::Adapters::ActiveRecord::JoinDependency
|
22
|
+
ActiveRecord::Associations::Preloader.send :include, Squeel::Adapters::ActiveRecord::Preloader
|
23
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Squeel
|
2
|
+
module Adapters
|
3
|
+
module ActiveRecord
|
4
|
+
module AssociationPreload
|
5
|
+
|
6
|
+
def preload_associations(records, associations, preload_options={})
|
7
|
+
records = Array.wrap(records).compact.uniq
|
8
|
+
return if records.empty?
|
9
|
+
super(records, Visitors::SymbolVisitor.new.accept(associations), preload_options)
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
module Arel
|
2
|
+
|
3
|
+
class Table
|
4
|
+
alias :table_name :name
|
5
|
+
|
6
|
+
def [] name
|
7
|
+
::Arel::Attribute.new self, name.to_sym
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module Nodes
|
12
|
+
class Node
|
13
|
+
def not
|
14
|
+
Nodes::Not.new self
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
remove_const :And
|
19
|
+
class And < Arel::Nodes::Node
|
20
|
+
attr_reader :children
|
21
|
+
|
22
|
+
def initialize children, right = nil
|
23
|
+
unless Array === children
|
24
|
+
children = [children, right]
|
25
|
+
end
|
26
|
+
@children = children
|
27
|
+
end
|
28
|
+
|
29
|
+
def left
|
30
|
+
children.first
|
31
|
+
end
|
32
|
+
|
33
|
+
def right
|
34
|
+
children[1]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class NamedFunction < Arel::Nodes::Function
|
39
|
+
attr_accessor :name, :distinct
|
40
|
+
|
41
|
+
include Arel::Predications
|
42
|
+
|
43
|
+
def initialize name, expr, aliaz = nil
|
44
|
+
super(expr, aliaz)
|
45
|
+
@name = name
|
46
|
+
@distinct = false
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class InfixOperation < Binary
|
51
|
+
include Arel::Expressions
|
52
|
+
include Arel::Predications
|
53
|
+
|
54
|
+
attr_reader :operator
|
55
|
+
|
56
|
+
def initialize operator, left, right
|
57
|
+
super(left, right)
|
58
|
+
@operator = operator
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class Multiplication < InfixOperation
|
63
|
+
def initialize left, right
|
64
|
+
super(:*, left, right)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class Division < InfixOperation
|
69
|
+
def initialize left, right
|
70
|
+
super(:/, left, right)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class Addition < InfixOperation
|
75
|
+
def initialize left, right
|
76
|
+
super(:+, left, right)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class Subtraction < InfixOperation
|
81
|
+
def initialize left, right
|
82
|
+
super(:-, left, right)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
module Visitors
|
88
|
+
class ToSql
|
89
|
+
def column_for attr
|
90
|
+
name = attr.name.to_s
|
91
|
+
table = attr.relation.table_name
|
92
|
+
|
93
|
+
column_cache[table][name]
|
94
|
+
end
|
95
|
+
|
96
|
+
# This isn't really very cachey at all. Good enough for now.
|
97
|
+
def column_cache
|
98
|
+
@column_cache ||= Hash.new do |hash, key|
|
99
|
+
Hash[
|
100
|
+
@engine.connection.columns(key, "#{key} Columns").map do |c|
|
101
|
+
[c.name, c]
|
102
|
+
end
|
103
|
+
]
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def visit_Arel_Nodes_InfixOperation o
|
108
|
+
"#{visit o.left} #{o.operator} #{visit o.right}"
|
109
|
+
end
|
110
|
+
|
111
|
+
def visit_Arel_Nodes_NamedFunction o
|
112
|
+
"#{o.name}(#{o.distinct ? 'DISTINCT ' : ''}#{o.expressions.map { |x|
|
113
|
+
visit x
|
114
|
+
}.join(', ')})#{o.alias ? " AS #{visit o.alias}" : ''}"
|
115
|
+
end
|
116
|
+
|
117
|
+
def visit_Arel_Nodes_And o
|
118
|
+
o.children.map { |x| visit x }.join ' AND '
|
119
|
+
end
|
120
|
+
|
121
|
+
def visit_Arel_Nodes_Not o
|
122
|
+
"NOT (#{visit o.expr})"
|
123
|
+
end
|
124
|
+
|
125
|
+
def visit_Arel_Nodes_Values o
|
126
|
+
"VALUES (#{o.expressions.zip(o.columns).map { |value, attr|
|
127
|
+
if Nodes::SqlLiteral === value
|
128
|
+
visit_Arel_Nodes_SqlLiteral value
|
129
|
+
else
|
130
|
+
quote(value, attr && column_for(attr))
|
131
|
+
end
|
132
|
+
}.join ', '})"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
module Predications
|
138
|
+
def as other
|
139
|
+
Nodes::As.new self, Nodes::SqlLiteral.new(other)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'squeel/context'
|
2
|
+
|
3
|
+
module Squeel
|
4
|
+
module Adapters
|
5
|
+
module ActiveRecord
|
6
|
+
class Context < ::Squeel::Context
|
7
|
+
# Because the AR::Associations namespace is insane
|
8
|
+
JoinBase = ::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinBase
|
9
|
+
|
10
|
+
def initialize(object)
|
11
|
+
@base = object.join_base
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
def find(object, parent = @base)
|
16
|
+
if JoinBase === parent
|
17
|
+
object = object.to_sym if String === object
|
18
|
+
case object
|
19
|
+
when Symbol, Nodes::Stub
|
20
|
+
@object.join_associations.detect { |j|
|
21
|
+
j.reflection.name == object.to_sym && j.parent == parent
|
22
|
+
}
|
23
|
+
when Nodes::Join
|
24
|
+
@object.join_associations.detect { |j|
|
25
|
+
j.reflection.name == object.name && j.parent == parent &&
|
26
|
+
(object.polymorphic? ? j.reflection.klass == object.klass : true)
|
27
|
+
}
|
28
|
+
else
|
29
|
+
@object.join_associations.detect { |j|
|
30
|
+
j.reflection == object && j.parent == parent
|
31
|
+
}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def traverse(keypath, parent = @base, include_endpoint = false)
|
37
|
+
parent = @base if keypath.absolute?
|
38
|
+
keypath.path.each do |key|
|
39
|
+
parent = find(key, parent) || key
|
40
|
+
end
|
41
|
+
parent = find(keypath.endpoint, parent) if include_endpoint
|
42
|
+
|
43
|
+
parent
|
44
|
+
end
|
45
|
+
|
46
|
+
def sanitize_sql(conditions, parent)
|
47
|
+
parent.active_record.send(:sanitize_sql, conditions, parent.aliased_table_name)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def get_table(object)
|
53
|
+
if [Symbol, Nodes::Stub].include?(object.class)
|
54
|
+
Arel::Table.new(object.to_sym, :engine => @engine)
|
55
|
+
elsif Nodes::Join === object
|
56
|
+
object.klass ? object.klass.arel_table : Arel::Table.new(object.name, :engine => @engine)
|
57
|
+
elsif object.respond_to?(:aliased_table_name)
|
58
|
+
Arel::Table.new(object.table_name, :as => object.aliased_table_name, :engine => @engine)
|
59
|
+
else
|
60
|
+
raise ArgumentError, "Unable to get table for #{object}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
module Squeel
|
4
|
+
module Adapters
|
5
|
+
module ActiveRecord
|
6
|
+
class JoinAssociation < ::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation
|
7
|
+
|
8
|
+
def initialize(reflection, join_dependency, parent = nil, polymorphic_class = nil)
|
9
|
+
if polymorphic_class && ::ActiveRecord::Base > polymorphic_class
|
10
|
+
swapping_reflection_klass(reflection, polymorphic_class) do |reflection|
|
11
|
+
super(reflection, join_dependency, parent)
|
12
|
+
end
|
13
|
+
else
|
14
|
+
super(reflection, join_dependency, parent)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def swapping_reflection_klass(reflection, klass)
|
19
|
+
reflection = reflection.clone
|
20
|
+
original_polymorphic = reflection.options.delete(:polymorphic)
|
21
|
+
reflection.instance_variable_set(:@klass, klass)
|
22
|
+
yield reflection
|
23
|
+
ensure
|
24
|
+
reflection.options[:polymorphic] = original_polymorphic
|
25
|
+
end
|
26
|
+
|
27
|
+
def ==(other)
|
28
|
+
super && active_record == other.active_record
|
29
|
+
end
|
30
|
+
|
31
|
+
def association_join
|
32
|
+
return @join if @Join
|
33
|
+
|
34
|
+
@join = super
|
35
|
+
|
36
|
+
if reflection.macro == :belongs_to && reflection.options[:polymorphic]
|
37
|
+
aliased_table = Arel::Table.new(table_name, :as => @aliased_table_name,
|
38
|
+
:engine => arel_engine,
|
39
|
+
:columns => klass.columns)
|
40
|
+
|
41
|
+
parent_table = Arel::Table.new(parent.table_name, :as => parent.aliased_table_name,
|
42
|
+
:engine => arel_engine,
|
43
|
+
:columns => parent.active_record.columns)
|
44
|
+
|
45
|
+
@join << parent_table[reflection.options[:foreign_type]].eq(reflection.klass.name)
|
46
|
+
end
|
47
|
+
|
48
|
+
@join
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|