squeel 0.5.5 → 0.6.0

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