squeel 0.5.0 → 0.5.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/README.rdoc +115 -39
  2. data/lib/squeel/adapters/active_record.rb +22 -5
  3. data/lib/squeel/adapters/active_record/3.0/association_preload.rb +15 -0
  4. data/lib/squeel/adapters/active_record/3.0/compat.rb +143 -0
  5. data/lib/squeel/adapters/active_record/3.0/context.rb +67 -0
  6. data/lib/squeel/adapters/active_record/3.0/join_association.rb +54 -0
  7. data/lib/squeel/adapters/active_record/3.0/join_dependency.rb +84 -0
  8. data/lib/squeel/adapters/active_record/3.0/relation.rb +327 -0
  9. data/lib/squeel/adapters/active_record/context.rb +67 -0
  10. data/lib/squeel/adapters/active_record/join_association.rb +10 -56
  11. data/lib/squeel/adapters/active_record/join_dependency.rb +22 -7
  12. data/lib/squeel/adapters/active_record/preloader.rb +21 -0
  13. data/lib/squeel/adapters/active_record/relation.rb +84 -38
  14. data/lib/squeel/context.rb +38 -0
  15. data/lib/squeel/dsl.rb +1 -1
  16. data/lib/squeel/nodes/join.rb +18 -0
  17. data/lib/squeel/nodes/key_path.rb +2 -2
  18. data/lib/squeel/nodes/stub.rb +5 -1
  19. data/lib/squeel/version.rb +1 -1
  20. data/lib/squeel/visitors.rb +2 -2
  21. data/lib/squeel/visitors/{order_visitor.rb → attribute_visitor.rb} +1 -2
  22. data/lib/squeel/visitors/predicate_visitor.rb +13 -11
  23. data/lib/squeel/visitors/symbol_visitor.rb +48 -0
  24. data/spec/helpers/squeel_helper.rb +17 -1
  25. data/spec/spec_helper.rb +31 -0
  26. data/spec/squeel/adapters/active_record/context_spec.rb +50 -0
  27. data/spec/squeel/adapters/active_record/join_association_spec.rb +1 -1
  28. data/spec/squeel/adapters/active_record/join_depdendency_spec.rb +1 -1
  29. data/spec/squeel/adapters/active_record/relation_spec.rb +166 -25
  30. data/spec/squeel/dsl_spec.rb +6 -6
  31. data/spec/squeel/nodes/join_spec.rb +16 -3
  32. data/spec/squeel/nodes/stub_spec.rb +12 -0
  33. data/spec/squeel/visitors/{order_visitor_spec.rb → attribute_visitor_spec.rb} +4 -5
  34. data/spec/squeel/visitors/predicate_visitor_spec.rb +18 -6
  35. data/spec/squeel/visitors/symbol_visitor_spec.rb +42 -0
  36. data/squeel.gemspec +2 -2
  37. metadata +21 -13
  38. data/lib/squeel/contexts/join_dependency_context.rb +0 -74
  39. data/lib/squeel/visitors/select_visitor.rb +0 -103
  40. data/spec/squeel/contexts/join_dependency_context_spec.rb +0 -43
  41. data/spec/squeel/visitors/select_visitor_spec.rb +0 -115
@@ -1,41 +1,117 @@
1
1
  =Squeel
2
2
 
3
- This is a complete rewrite of the library formerly called MetaWhere. It's Rails 3.1-only
4
- for now.
5
-
6
- It's not really suitable for actual use yet, but you're welcome to test it and send
7
- me feedback.
8
-
9
- ==What's new?
10
-
11
- A lot.
12
-
13
- * Symbol and hash methods aren't loaded by default. To enable them, do this in
14
- your Squeel.configure block: <tt>config.load_core_extensions :hash, :symbol</tt>
15
- * Speaking of, you can call Squeel.configure do |config| ... end and do
16
- another bit of configuration, setting up your own aliases.
17
- <tt>config.alias_predicate :new_name, :old_name</tt>
18
- * The preferred way to use the various enhancements is now by passing a block to
19
- the relation method you're calling. For example:
20
-
21
- Person.select{max(id).as(max_id)} # Call SQL functions
22
- Person.where{(name == 'bob') & (salary == 100000)} # Compounds & and | work
23
-
24
- * Operators have changed. As before, operators starting with ! are only available
25
- on Ruby 1.9. Upgrade, for the love of all that is good and holy.
26
-
27
- * == - Equality
28
- * != - Inequality
29
- * ^ - Inequality, for those poor souls on 1.8.x
30
- * >> - In, (mnemonic: value >> [1,2,3], the value is running INTO the array)
31
- * << - Not in, (mnemonic: value << [1,2,3], the value is running OUT of the array)
32
- * =~ - Matches (SQL LIKE)
33
- * !~ - Not matches (SQL NOT LIKE) Again, only in Ruby 1.9
34
- * > - Greater than
35
- * >= - Greater than or equal to
36
- * < - Less than
37
- * <= - Less than or equal to
38
- * [] - Alternative function syntax. Just use parentheses, not sure I'm gonna keep [].
39
-
40
- There's more -- have a read through the specs for a better idea of what you can do,
41
- or clone the repo, run <tt>bundle install</tt> and play around in <tt>rake console</tt>.
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
- require 'squeel/adapters/active_record/relation'
2
- require 'squeel/adapters/active_record/join_dependency'
3
- require 'squeel/adapters/active_record/join_association'
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