squeel 0.5.5 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/.yardopts +3 -0
  2. data/Gemfile +8 -3
  3. data/README.md +368 -0
  4. data/lib/core_ext/hash.rb +8 -8
  5. data/lib/core_ext/symbol.rb +7 -6
  6. data/lib/squeel.rb +2 -0
  7. data/lib/squeel/adapters/active_record.rb +25 -20
  8. data/lib/squeel/adapters/active_record/3.0/compat.rb +1 -2
  9. data/lib/squeel/adapters/active_record/3.0/context.rb +6 -7
  10. data/lib/squeel/adapters/active_record/3.0/join_dependency.rb +5 -5
  11. data/lib/squeel/adapters/active_record/context.rb +6 -7
  12. data/lib/squeel/adapters/active_record/join_dependency.rb +5 -5
  13. data/lib/squeel/configuration.rb +29 -0
  14. data/lib/squeel/constants.rb +1 -0
  15. data/lib/squeel/context.rb +36 -7
  16. data/lib/squeel/dsl.rb +57 -2
  17. data/lib/squeel/nodes.rb +6 -0
  18. data/lib/squeel/nodes/and.rb +1 -0
  19. data/lib/squeel/nodes/binary.rb +11 -2
  20. data/lib/squeel/nodes/function.rb +30 -48
  21. data/lib/squeel/nodes/join.rb +56 -12
  22. data/lib/squeel/nodes/key_path.rb +68 -2
  23. data/lib/squeel/nodes/nary.rb +12 -2
  24. data/lib/squeel/nodes/not.rb +1 -0
  25. data/lib/squeel/nodes/operation.rb +9 -0
  26. data/lib/squeel/nodes/operators.rb +16 -0
  27. data/lib/squeel/nodes/or.rb +1 -0
  28. data/lib/squeel/nodes/order.rb +19 -1
  29. data/lib/squeel/nodes/predicate.rb +25 -3
  30. data/lib/squeel/nodes/predicate_operators.rb +12 -0
  31. data/lib/squeel/nodes/stub.rb +55 -48
  32. data/lib/squeel/nodes/unary.rb +7 -1
  33. data/lib/squeel/predicate_methods.rb +2 -10
  34. data/lib/squeel/version.rb +1 -1
  35. data/lib/squeel/visitors/attribute_visitor.rb +80 -4
  36. data/lib/squeel/visitors/base.rb +70 -4
  37. data/lib/squeel/visitors/predicate_visitor.rb +28 -9
  38. data/lib/squeel/visitors/symbol_visitor.rb +1 -1
  39. data/spec/core_ext/symbol_spec.rb +2 -2
  40. data/spec/spec_helper.rb +6 -1
  41. data/spec/squeel/adapters/active_record/context_spec.rb +0 -7
  42. data/spec/squeel/adapters/active_record/relation_spec.rb +27 -0
  43. data/spec/squeel/dsl_spec.rb +20 -1
  44. data/spec/squeel/nodes/join_spec.rb +11 -4
  45. data/spec/squeel/nodes/key_path_spec.rb +1 -1
  46. data/spec/squeel/nodes/predicate_spec.rb +0 -42
  47. data/spec/squeel/nodes/stub_spec.rb +9 -8
  48. data/spec/squeel/visitors/predicate_visitor_spec.rb +34 -9
  49. data/squeel.gemspec +6 -9
  50. metadata +8 -10
  51. data/README.rdoc +0 -117
  52. data/lib/squeel/predicate_methods/function.rb +0 -9
  53. data/lib/squeel/predicate_methods/predicate.rb +0 -11
  54. data/lib/squeel/predicate_methods/stub.rb +0 -9
  55. data/lib/squeel/predicate_methods/symbol.rb +0 -9
@@ -93,10 +93,9 @@ module Arel
93
93
  column_cache[table][name]
94
94
  end
95
95
 
96
- # This isn't really very cachey at all. Good enough for now.
97
96
  def column_cache
98
97
  @column_cache ||= Hash.new do |hash, key|
99
- Hash[
98
+ hash[key] = Hash[
100
99
  @engine.connection.columns(key, "#{key} Columns").map do |c|
101
100
  [c.name, c]
102
101
  end
@@ -8,8 +8,11 @@ module Squeel
8
8
  JoinBase = ::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinBase
9
9
 
10
10
  def initialize(object)
11
- @base = object.join_base
12
11
  super
12
+ @base = object.join_base
13
+ @engine = @base.arel_engine
14
+ @arel_visitor = Arel::Visitors.visitor_for @engine
15
+ @default_table = Arel::Table.new(@base.table_name, :as => @base.aliased_table_name, :engine => @engine)
13
16
  end
14
17
 
15
18
  def find(object, parent = @base)
@@ -23,7 +26,7 @@ module Squeel
23
26
  when Nodes::Join
24
27
  @object.join_associations.detect { |j|
25
28
  j.reflection.name == object.name && j.parent == parent &&
26
- (object.polymorphic? ? j.reflection.klass == object.klass : true)
29
+ (object.polymorphic? ? j.reflection.klass == object._klass : true)
27
30
  }
28
31
  else
29
32
  @object.join_associations.detect { |j|
@@ -43,17 +46,13 @@ module Squeel
43
46
  parent
44
47
  end
45
48
 
46
- def sanitize_sql(conditions, parent)
47
- parent.active_record.send(:sanitize_sql, conditions, parent.aliased_table_name)
48
- end
49
-
50
49
  private
51
50
 
52
51
  def get_table(object)
53
52
  if [Symbol, Nodes::Stub].include?(object.class)
54
53
  Arel::Table.new(object.to_sym, :engine => @engine)
55
54
  elsif Nodes::Join === object
56
- object.klass ? object.klass.arel_table : Arel::Table.new(object.name, :engine => @engine)
55
+ object._klass ? object._klass.arel_table : Arel::Table.new(object._name, :engine => @engine)
57
56
  elsif object.respond_to?(:aliased_table_name)
58
57
  Arel::Table.new(object.table_name, :as => object.aliased_table_name, :engine => @engine)
59
58
  else
@@ -37,13 +37,13 @@ module Squeel
37
37
  case associations
38
38
  when Nodes::Join
39
39
  parent ||= @joins.last
40
- reflection = parent.reflections[associations.name] or
41
- raise ::ActiveRecord::ConfigurationError, "Association named '#{ associations.name }' was not found; perhaps you misspelled it?"
40
+ reflection = parent.reflections[associations._name] or
41
+ raise ::ActiveRecord::ConfigurationError, "Association named '#{ associations._name }' was not found; perhaps you misspelled it?"
42
42
 
43
- unless join_association = find_join_association_respecting_polymorphism(reflection, parent, associations.klass)
43
+ unless join_association = find_join_association_respecting_polymorphism(reflection, parent, associations._klass)
44
44
  @reflections << reflection
45
- join_association = build_join_association_respecting_polymorphism(reflection, parent, associations.klass)
46
- join_association.join_type = associations.type
45
+ join_association = build_join_association_respecting_polymorphism(reflection, parent, associations._klass)
46
+ join_association.join_type = associations._type
47
47
  @joins << join_association
48
48
  cache_joined_association(join_association)
49
49
  end
@@ -8,8 +8,11 @@ module Squeel
8
8
  JoinPart = ::ActiveRecord::Associations::JoinDependency::JoinPart
9
9
 
10
10
  def initialize(object)
11
- @base = object.join_base
12
11
  super
12
+ @base = object.join_base
13
+ @engine = @base.arel_engine
14
+ @arel_visitor = Arel::Visitors.visitor_for @engine
15
+ @default_table = Arel::Table.new(@base.table_name, :as => @base.aliased_table_name, :engine => @engine)
13
16
  end
14
17
 
15
18
  def find(object, parent = @base)
@@ -23,7 +26,7 @@ module Squeel
23
26
  when Nodes::Join
24
27
  @object.join_associations.detect { |j|
25
28
  j.reflection.name == object.name && j.parent == parent &&
26
- (object.polymorphic? ? j.reflection.klass == object.klass : true)
29
+ (object.polymorphic? ? j.reflection.klass == object._klass : true)
27
30
  }
28
31
  else
29
32
  @object.join_associations.detect { |j|
@@ -43,17 +46,13 @@ module Squeel
43
46
  parent
44
47
  end
45
48
 
46
- def sanitize_sql(conditions, parent)
47
- parent.active_record.send(:sanitize_sql, conditions, parent.aliased_table_name)
48
- end
49
-
50
49
  private
51
50
 
52
51
  def get_table(object)
53
52
  if [Symbol, Nodes::Stub].include?(object.class)
54
53
  Arel::Table.new(object.to_sym, :engine => @engine)
55
54
  elsif Nodes::Join === object
56
- object.klass ? object.klass.arel_table : Arel::Table.new(object.name, :engine => @engine)
55
+ object._klass ? object._klass.arel_table : Arel::Table.new(object._name, :engine => @engine)
57
56
  elsif object.respond_to?(:aliased_table_name)
58
57
  Arel::Table.new(object.table_name, :as => object.aliased_table_name, :engine => @engine)
59
58
  else
@@ -36,13 +36,13 @@ module Squeel
36
36
  case associations
37
37
  when Nodes::Join
38
38
  parent ||= join_parts.last
39
- reflection = parent.reflections[associations.name] or
40
- raise ::ActiveRecord::ConfigurationError, "Association named '#{ associations.name }' was not found; perhaps you misspelled it?"
39
+ reflection = parent.reflections[associations._name] or
40
+ raise ::ActiveRecord::ConfigurationError, "Association named '#{ associations._name }' was not found; perhaps you misspelled it?"
41
41
 
42
- unless join_association = find_join_association_respecting_polymorphism(reflection, parent, associations.klass)
42
+ unless join_association = find_join_association_respecting_polymorphism(reflection, parent, associations._klass)
43
43
  @reflections << reflection
44
- join_association = build_join_association_respecting_polymorphism(reflection, parent, associations.klass)
45
- join_association.join_type = associations.type
44
+ join_association = build_join_association_respecting_polymorphism(reflection, parent, associations._klass)
45
+ join_association.join_type = associations._type
46
46
  @join_parts << join_association
47
47
  cache_joined_association(join_association)
48
48
  end
@@ -2,18 +2,47 @@ require 'squeel/constants'
2
2
  require 'squeel/predicate_methods'
3
3
 
4
4
  module Squeel
5
+ # The Squeel configuration module. The Squeel module extends this to provide its
6
+ # configuration capability.
5
7
  module Configuration
6
8
 
9
+ # Start a Squeel configuration block in an initializer.
10
+ #
11
+ # @yield [config] A configuration block
12
+ #
13
+ # @example Load hash and symbol extensions
14
+ # Squeel.configure do |config|
15
+ # config.load_core_extensions :hash, :symbol
16
+ # end
17
+ #
18
+ # @example Alias a predicate
19
+ # Squeel.configure do |config|
20
+ # config.alias_ptedicate :is_less_than, :lt
21
+ # end
7
22
  def configure
8
23
  yield self
9
24
  end
10
25
 
26
+ # Load core extensions for Hash, Symbol, or both
27
+ #
28
+ # @overload load_core_extensions(sym)
29
+ # Load a single extension
30
+ # @param [Symbol] sym :hash or :symbol
31
+ # @overload load_core_extensions(sym1, sym2)
32
+ # Load both extensions
33
+ # @param [Symbol] sym1 :hash or :symbol
34
+ # @param [Symbol] sym2 :hash or :symbol
11
35
  def load_core_extensions(*exts)
12
36
  exts.each do |ext|
13
37
  require "core_ext/#{ext}"
14
38
  end
15
39
  end
16
40
 
41
+ # Create an alias to an existing predication method. The _any/_all variations will
42
+ # be created automatically.
43
+ # @param [Symbol] new_name The alias name
44
+ # @param [Symbol] existing_name The existing predicate name
45
+ # @raise [ArgumentError] The existing name is an _any/_all variation, and not the original predicate name
17
46
  def alias_predicate(new_name, existing_name)
18
47
  raise ArgumentError, 'the existing name should be the base name, not an _any/_all variation' if existing_name.to_s =~ /(_any|_all)$/
19
48
  ['', '_any', '_all'].each do |suffix|
@@ -1,4 +1,5 @@
1
1
  module Squeel
2
+ # Defines the default list of ARel predicates and predicate aliases
2
3
  module Constants
3
4
  PREDICATES = [
4
5
  :eq, :eq_any, :eq_all,
@@ -1,35 +1,64 @@
1
1
  require 'arel'
2
2
 
3
3
  module Squeel
4
+ # @abstract Subclass and implement {#traverse}, #{find} and {#get_table}
5
+ # to create a Context that supports a given ORM.
4
6
  class Context
5
7
  attr_reader :base, :engine, :arel_visitor
6
8
 
9
+ # The Squeel context expects some kind of context object that is
10
+ # representative of the current joins in a query in order to return
11
+ # appropriate tables. Again, in the case of an ActiveRecord context,
12
+ # this will be a JoinDependency. Subclasses are expected to set the
13
+ # <tt>@base</tt>, <tt>@engine</tt>, and <tt>@arel_visitor</tt>
14
+ # instance variables to appropriate values for use in their implementations
15
+ # of other required methods.
16
+ #
17
+ # @param object The object the context will use for contextualization
7
18
  def initialize(object)
8
19
  @object = object
9
- @engine = @base.arel_engine
10
- @arel_visitor = Arel::Visitors.visitor_for @engine
11
- @default_table = Arel::Table.new(@base.table_name, :as => @base.aliased_table_name, :engine => @engine)
12
20
  @tables = Hash.new {|hash, key| hash[key] = get_table(key)}
13
21
  end
14
22
 
23
+ # This method should find a given object inside the context.
24
+ #
25
+ # @param object The object to find
26
+ # @param parent The parent object, if applicable
27
+ # @return a valid "parent" or contextualizable object
15
28
  def find(object, parent = @base)
16
29
  raise NotImplementedError, "Subclasses must implement public method find"
17
30
  end
18
31
 
32
+ # This method should traverse a keypath and return an object for use
33
+ # in future calls to #traverse, #find, or #contextualize.
34
+ #
35
+ # @param [Nodes::KeyPath] keypath The keypath to traverse
36
+ # @param parent The parent object from which traversal should start.
37
+ # @param [Boolean] include_endpoint Whether or not the KeyPath's
38
+ # endpoint should be treated as a traversable key
39
+ # @return a valid "parent" or contextualizable object
19
40
  def traverse(keypath, parent = @base, include_endpoint = false)
20
41
  raise NotImplementedError, "Subclasses must implement public method traverse"
21
42
  end
22
43
 
44
+ # This method, as implemented, just makes use of the table cache, which will
45
+ # call get_table, where the real work of getting the ARel Table occurs.
46
+ #
47
+ # @param object A contextualizable object (this will depend on the subclass's implementation)
48
+ # @return [Arel::Table] A table corresponding to the object param
23
49
  def contextualize(object)
24
50
  @tables[object]
25
51
  end
26
52
 
27
- def sanitize_sql(conditions, parent)
28
- raise NotImplementedError, "Subclasses must implement public method sanitize_sql"
29
- end
30
-
31
53
  private
32
54
 
55
+ # Returns an Arel::Table that's appropriate for the object it's been sent.
56
+ # What's "appropriate"? Well, that's up to the implementation to decide, but
57
+ # it should probably generate a table that is least likely to result in invalid
58
+ # SQL.
59
+ #
60
+ # @param object A contextualizable object (this will depend on the subclass's implementation)
61
+ # @return [Arel::Table] A table corresponding to the object param.
33
62
  def get_table(object)
34
63
  raise NotImplementedError, "Subclasses must implement private method get_table"
35
64
  end
@@ -1,6 +1,9 @@
1
1
  module Squeel
2
+ # Interprets DSL blocks, generating various Squeel nodes as appropriate.
2
3
  class DSL
3
4
 
5
+ # We're creating a BlankSlate-type class here, since we want most
6
+ # method calls to fall through to method_missing.
4
7
  Squeel.evil_things do
5
8
  (instance_methods + private_instance_methods).each do |method|
6
9
  unless method.to_s =~ /^(__|instance_eval)/
@@ -9,15 +12,67 @@ module Squeel
9
12
  end
10
13
  end
11
14
 
15
+ # Called from an adapter, not directly.
16
+ # Evaluates a block of Squeel DSL code.
17
+ #
18
+ # @example A DSL block that uses instance_eval
19
+ # Post.where{title == 'Hello world!'}
20
+ #
21
+ # @example A DSL block with access to methods from the closure
22
+ # Post.where{|dsl| dsl.title == local_method(local_var)}
23
+ #
24
+ # @yield [dsl] A block of Squeel DSL code, with an optional argument if
25
+ # access to closure methods is desired.
26
+ # @return The results of the interpreted DSL code.
12
27
  def self.eval(&block)
13
28
  if block.arity > 0
14
- yield self.new
29
+ yield self.new(block.binding)
15
30
  else
16
- self.new.instance_eval(&block)
31
+ self.new(block.binding).instance_eval(&block)
17
32
  end
18
33
  end
19
34
 
35
+ private
36
+
37
+ # This isn't normally called directly, but via DSL.eval, which will
38
+ # pass the block's binding to the new instance, for use with #my.
39
+ #
40
+ # @param [Binding] The block's binding.
41
+ def initialize(caller_binding)
42
+ @caller = caller_binding.eval 'self'
43
+ end
44
+
45
+ # If you really need to get at an instance variable or method inside
46
+ # a DSL block, this method will let you do it. It passes a block back
47
+ # to the DSL's caller for instance_eval.
48
+ #
49
+ # It's also pretty evil, so I hope you enjoy using it while I'm burning in
50
+ # programmer hell.
51
+ #
52
+ # @param &block A block to instance_eval against the DSL's caller.
53
+ # @return
54
+ def my(&block)
55
+ @caller.instance_eval &block
56
+ end
57
+
58
+ # Node generation inside DSL blocks.
59
+ #
60
+ # @overload node_name
61
+ # Creates a Stub. Method calls chained from this Stub will determine
62
+ # what type of node we eventually end up with.
63
+ # @return [Nodes::Stub] A stub with the name of the method
64
+ # @overload node_name(klass)
65
+ # Creates a Join with a polymorphic class matching the given parameter
66
+ # @param [Class] klass The polymorphic class of the join node
67
+ # @return [Nodes::Join] A join node with the name of the method and the given class
68
+ # @overload node_name(first_arg, *other_args)
69
+ # Creates a Function with the given arguments becoming the function's arguments
70
+ # @param first_arg The first argument
71
+ # @param *other_args Optional additional arguments
72
+ # @return [Nodes::Function] A function node for the given method name with the given arguments
20
73
  def method_missing(method_id, *args)
74
+ super if method_id == :to_ary
75
+
21
76
  if args.empty?
22
77
  Nodes::Stub.new method_id
23
78
  elsif (args.size == 1) && (Class === args[0])
@@ -1,3 +1,9 @@
1
+ module Squeel
2
+ # Namespace for the nodes created by Squeel::DSL, and
3
+ # evaluated by Squeel::Visitors classes
4
+ module Nodes
5
+ end
6
+ end
1
7
  require 'squeel/nodes/stub'
2
8
  require 'squeel/nodes/key_path'
3
9
  require 'squeel/nodes/predicate'
@@ -2,6 +2,7 @@ require 'squeel/nodes/nary'
2
2
 
3
3
  module Squeel
4
4
  module Nodes
5
+ # A grouping of nodes that will be converted to an Arel::Nodes::And upon visitation.
5
6
  class And < Nary
6
7
  end
7
8
  end
@@ -2,22 +2,31 @@ require 'squeel/nodes/predicate_operators'
2
2
 
3
3
  module Squeel
4
4
  module Nodes
5
+ # A node that represents an operation with two operands.
5
6
  class Binary
7
+
6
8
  include PredicateOperators
7
9
 
8
- attr_reader :left, :right
10
+ # The left operand
11
+ attr_reader :left
12
+
13
+ # The right operand
14
+ attr_reader :right
9
15
 
16
+ # @param left The left operand
17
+ # @param right The right operand
10
18
  def initialize(left, right)
11
19
  @left, @right = left, right
12
20
  end
13
21
 
22
+ # Comparison with other nodes
14
23
  def eql?(other)
15
24
  self.class == other.class &&
16
25
  self.left == other.left &&
17
26
  self.right == other.right
18
27
  end
19
-
20
28
  alias :== :eql?
29
+
21
30
  end
22
31
  end
23
32
  end
@@ -2,26 +2,49 @@ require 'squeel/predicate_methods'
2
2
 
3
3
  module Squeel
4
4
  module Nodes
5
+ # A node that represents an SQL function call
5
6
  class Function
6
7
 
7
8
  include PredicateMethods
8
9
  include Operators
9
10
 
10
- attr_reader :name, :args, :alias
11
-
11
+ alias :== :eq
12
+ alias :'^' :not_eq
13
+ alias :'!=' :not_eq if respond_to?(:'!=')
14
+ alias :>> :in
15
+ alias :<< :not_in
16
+ alias :=~ :matches
17
+ alias :'!~' :does_not_match if respond_to?(:'!~')
18
+ alias :> :gt
19
+ alias :>= :gteq
20
+ alias :< :lt
21
+ alias :<= :lteq
22
+
23
+ # @return [Symbol] The name of the SQL function to be called
24
+ attr_reader :name
25
+
26
+ # @return [Array] The arguments to be passed to the SQL function
27
+ attr_reader :args
28
+
29
+ # @return [String] The SQL function's alias
30
+ # @return [NilClass] If no alias
31
+ attr_reader :alias
32
+
33
+ # Create a node representing an SQL Function with the given name and arguments
34
+ # @param [Symbol] name The function name
35
+ # @param [Array] args The array of arguments to pass to the function.
12
36
  def initialize(name, args)
13
37
  @name, @args = name, args
14
38
  end
15
39
 
40
+ # Set an alias for the function
41
+ # @param [String, Symbol] The alias name
42
+ # @return [Function] This function with the new alias value.
16
43
  def as(alias_name)
17
44
  @alias = alias_name.to_s
18
45
  self
19
46
  end
20
47
 
21
- def ==(value)
22
- Predicate.new self, :eq, value
23
- end
24
-
25
48
  def asc
26
49
  Order.new self, 1
27
50
  end
@@ -30,51 +53,10 @@ module Squeel
30
53
  Order.new self, -1
31
54
  end
32
55
 
33
- # Won't work on Ruby 1.8.x so need to do this conditionally
34
- define_method('!=') do |value|
35
- Predicate.new(self, :not_eq, value)
36
- end if respond_to?('!=')
37
-
38
- def ^(value)
39
- Predicate.new self, :not_eq, value
40
- end
41
-
42
- def >>(value)
43
- Predicate.new self, :in, value
44
- end
45
-
46
- def <<(value)
47
- Predicate.new self, :not_in, value
48
- end
49
-
50
- def =~(value)
51
- Predicate.new self, :matches, value
52
- end
53
-
54
- # Won't work on Ruby 1.8.x so need to do this conditionally
55
- define_method('!~') do |value|
56
- Predicate.new(self, :does_not_match, value)
57
- end if respond_to?('!~')
58
-
59
- def >(value)
60
- Predicate.new self, :gt, value
61
- end
62
-
63
- def >=(value)
64
- Predicate.new self, :gteq, value
65
- end
66
-
67
- def <(value)
68
- Predicate.new self, :lt, value
69
- end
70
-
71
- def <=(value)
72
- Predicate.new self, :lteq, value
73
- end
74
-
75
56
  # expand_hash_conditions_for_aggregates assumes our hash keys can be
76
57
  # converted to symbols, so this has to be implemented, but it doesn't
77
58
  # really have to do anything useful.
59
+ # @return [NilClass] Just to avoid bombing out on expand_hash_conditions_for_aggregates
78
60
  def to_sym
79
61
  nil
80
62
  end