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.
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