squeel 0.5.0 → 0.5.5
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.
- 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
|