Permalink
Browse files

Initial commit

  • Loading branch information...
0 parents commit 2b203aca2947ca7f3c0f5f06e819ca06abff7bc8 @ernie ernie committed Apr 9, 2011
Showing with 4,240 additions and 0 deletions.
  1. +4 −0 .gitignore
  2. +8 −0 Gemfile
  3. +20 −0 LICENSE
  4. +41 −0 README.rdoc
  5. +19 −0 Rakefile
  6. +13 −0 lib/core_ext/hash.rb
  7. +36 −0 lib/core_ext/symbol.rb
  8. +26 −0 lib/squeel.rb
  9. +6 −0 lib/squeel/adapters/active_record.rb
  10. +90 −0 lib/squeel/adapters/active_record/join_association.rb
  11. +68 −0 lib/squeel/adapters/active_record/join_dependency.rb
  12. +292 −0 lib/squeel/adapters/active_record/relation.rb
  13. +25 −0 lib/squeel/configuration.rb
  14. +23 −0 lib/squeel/constants.rb
  15. +74 −0 lib/squeel/contexts/join_dependency_context.rb
  16. +31 −0 lib/squeel/dsl.rb
  17. +10 −0 lib/squeel/nodes.rb
  18. +8 −0 lib/squeel/nodes/and.rb
  19. +23 −0 lib/squeel/nodes/binary.rb
  20. +84 −0 lib/squeel/nodes/function.rb
  21. +51 −0 lib/squeel/nodes/join.rb
  22. +127 −0 lib/squeel/nodes/key_path.rb
  23. +35 −0 lib/squeel/nodes/nary.rb
  24. +8 −0 lib/squeel/nodes/not.rb
  25. +23 −0 lib/squeel/nodes/operation.rb
  26. +27 −0 lib/squeel/nodes/operators.rb
  27. +8 −0 lib/squeel/nodes/or.rb
  28. +35 −0 lib/squeel/nodes/order.rb
  29. +49 −0 lib/squeel/nodes/predicate.rb
  30. +17 −0 lib/squeel/nodes/predicate_operators.rb
  31. +113 −0 lib/squeel/nodes/stub.rb
  32. +22 −0 lib/squeel/nodes/unary.rb
  33. +22 −0 lib/squeel/predicate_methods.rb
  34. +9 −0 lib/squeel/predicate_methods/function.rb
  35. +11 −0 lib/squeel/predicate_methods/predicate.rb
  36. +9 −0 lib/squeel/predicate_methods/stub.rb
  37. +9 −0 lib/squeel/predicate_methods/symbol.rb
  38. +3 −0 lib/squeel/version.rb
  39. +3 −0 lib/squeel/visitors.rb
  40. +46 −0 lib/squeel/visitors/base.rb
  41. +107 −0 lib/squeel/visitors/order_visitor.rb
  42. +179 −0 lib/squeel/visitors/predicate_visitor.rb
  43. +103 −0 lib/squeel/visitors/select_visitor.rb
  44. +5 −0 spec/blueprints/articles.rb
  45. +5 −0 spec/blueprints/comments.rb
  46. +3 −0 spec/blueprints/notes.rb
  47. +4 −0 spec/blueprints/people.rb
  48. +3 −0 spec/blueprints/tags.rb
  49. +22 −0 spec/console.rb
  50. +68 −0 spec/core_ext/symbol_spec.rb
  51. +5 −0 spec/helpers/squeel_helper.rb
  52. +30 −0 spec/spec_helper.rb
  53. +18 −0 spec/squeel/adapters/active_record/join_association_spec.rb
  54. +60 −0 spec/squeel/adapters/active_record/join_depdendency_spec.rb
  55. +437 −0 spec/squeel/adapters/active_record/relation_spec.rb
  56. +43 −0 spec/squeel/contexts/join_dependency_context_spec.rb
  57. +73 −0 spec/squeel/dsl_spec.rb
  58. +149 −0 spec/squeel/nodes/function_spec.rb
  59. +27 −0 spec/squeel/nodes/join_spec.rb
  60. +92 −0 spec/squeel/nodes/key_path_spec.rb
  61. +149 −0 spec/squeel/nodes/operation_spec.rb
  62. +87 −0 spec/squeel/nodes/operators_spec.rb
  63. +30 −0 spec/squeel/nodes/order_spec.rb
  64. +88 −0 spec/squeel/nodes/predicate_operators_spec.rb
  65. +92 −0 spec/squeel/nodes/predicate_spec.rb
  66. +178 −0 spec/squeel/nodes/stub_spec.rb
  67. +128 −0 spec/squeel/visitors/order_visitor_spec.rb
  68. +267 −0 spec/squeel/visitors/predicate_visitor_spec.rb
  69. +115 −0 spec/squeel/visitors/select_visitor_spec.rb
  70. +101 −0 spec/support/schema.rb
  71. +44 −0 squeel.gemspec
4 .gitignore
@@ -0,0 +1,4 @@
+*.gem
+.bundle
+Gemfile.lock
+pkg/*
8 Gemfile
@@ -0,0 +1,8 @@
+source "http://rubygems.org"
+gemspec
+
+gem 'arel', :git => 'git://github.com/rails/arel.git'
+git 'git://github.com/rails/rails.git' do
+ gem 'activesupport'
+ gem 'activerecord'
+end
20 LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2010-2011 Ernie Miller
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
41 README.rdoc
@@ -0,0 +1,41 @@
+=Squeel
+
+This is a complete rewrite of the library formerly called MetaWhere. It's Rails 3.1-only
+for now.
+
+It's not really suitable for actual use yet, but you're welcome to test it and send
+me feedback.
+
+==What's new?
+
+A lot.
+
+* Symbol and hash methods aren't loaded by default. To enable them, do this in
+ your Squeel.configure block: <tt>config.load_core_extensions!</tt>
+* Speaking of, you can call Squeel.configure do |config| ... end and do
+ another bit of configuration, setting up your own aliases.
+ <tt>config.alias_predicate :new_name, :old_name</tt>
+* The preferred way to use the various enhancements is now by passing a block to
+ the relation method you're calling. For example:
+
+ Person.select{max(id).as(max_id)} # Call SQL functions
+ Person.where{(name == 'bob') & (salary == 100000)} # Compounds & and | work
+
+* Operators have changed. As before, operators starting with ! are only available
+ on Ruby 1.9. Upgrade, for the love of all that is good and holy.
+
+ * == - Equality
+ * != - Inequality
+ * ^ - Inequality, for those poor souls on 1.8.x
+ * >> - In, (mnemonic: value >> [1,2,3], the value is running INTO the array)
+ * << - Not in, (mnemonic: value << [1,2,3], the value is running OUT of the array)
+ * =~ - Matches (SQL LIKE)
+ * !~ - Not matches (SQL NOT LIKE) Again, only in Ruby 1.9
+ * > - Greater than
+ * >= - Greater than or equal to
+ * < - Less than
+ * <= - Less than or equal to
+ * [] - Alternative function syntax. Just use parentheses, not sure I'm gonna keep [].
+
+There's more -- have a read through the specs for a better idea of what you can do,
+or clone the repo, run <tt>bundle install</tt> and play around in <tt>rake console</tt>.
19 Rakefile
@@ -0,0 +1,19 @@
+require 'bundler'
+require 'rspec/core/rake_task'
+
+Bundler::GemHelper.install_tasks
+
+RSpec::Core::RakeTask.new(:spec) do |rspec|
+ rspec.rspec_opts = ['--backtrace']
+end
+
+task :default => :spec
+
+desc "Open an irb session with Squeel and the sample data used in specs"
+task :console do
+ require 'irb'
+ require 'irb/completion'
+ require 'console'
+ ARGV.clear
+ IRB.start
+end
13 lib/core_ext/hash.rb
@@ -0,0 +1,13 @@
+require 'squeel/nodes/predicate_operators'
+
+class Hash
+ # Hashes are "acceptable" by PredicateVisitor, so they
+ # can be treated like nodes for the purposes of and/or/not
+ # if you load core extensions with:
+ #
+ # Squeel.configure do |config|
+ # config.load_core_extensions :hash
+ # end
+
+ include Squeel::Nodes::PredicateOperators
+end
36 lib/core_ext/symbol.rb
@@ -0,0 +1,36 @@
+require 'squeel/predicate_methods'
+
+class Symbol
+ # These extensions to Symbol are loaded optionally, with:
+ #
+ # Squeel.configure do |config|
+ # config.load_core_extensions :symbol
+ # end
+
+ include Squeel::PredicateMethods
+
+ def asc
+ Squeel::Nodes::Order.new self, 1
+ end
+
+ def desc
+ Squeel::Nodes::Order.new self, -1
+ end
+
+ def func(*args)
+ Squeel::Nodes::Function.new(self, args)
+ end
+
+ def inner
+ Squeel::Nodes::Join.new(self, Arel::InnerJoin)
+ end
+
+ def outer
+ Squeel::Nodes::Join.new(self, Arel::OuterJoin)
+ end
+
+ def of_class(klass)
+ Squeel::Nodes::Join.new(self, Arel::InnerJoin, klass)
+ end
+
+end
26 lib/squeel.rb
@@ -0,0 +1,26 @@
+require 'squeel/configuration'
+
+module Squeel
+
+ extend Configuration
+
+ def self.evil_things
+ original_verbosity = $VERBOSE
+ $VERBOSE = nil
+ yield
+ ensure
+ $VERBOSE = original_verbosity
+ end
+
+ Constants::PREDICATE_ALIASES.each do |original, aliases|
+ aliases.each do |aliaz|
+ alias_predicate aliaz, original
+ end
+ end
+
+end
+
+require 'squeel/nodes'
+require 'squeel/dsl'
+require 'squeel/visitors'
+require 'squeel/adapters/active_record'
6 lib/squeel/adapters/active_record.rb
@@ -0,0 +1,6 @@
+require 'squeel/adapters/active_record/relation'
+require 'squeel/adapters/active_record/join_dependency'
+require 'squeel/adapters/active_record/join_association'
+
+ActiveRecord::Relation.send :include, Squeel::Adapters::ActiveRecord::Relation
+ActiveRecord::Associations::JoinDependency.send :include, Squeel::Adapters::ActiveRecord::JoinDependency
90 lib/squeel/adapters/active_record/join_association.rb
@@ -0,0 +1,90 @@
+require 'active_record'
+
+module Squeel
+ module Adapters
+ module ActiveRecord
+
+ class JoinAssociation < ::ActiveRecord::Associations::JoinDependency::JoinAssociation
+
+ def initialize(reflection, join_dependency, parent = nil, polymorphic_class = nil)
+ if polymorphic_class && ::ActiveRecord::Base > polymorphic_class
+ swapping_reflection_klass(reflection, polymorphic_class) do |reflection|
+ super(reflection, join_dependency, parent)
+ end
+ else
+ super(reflection, join_dependency, parent)
+ end
+ end
+
+ def swapping_reflection_klass(reflection, klass)
+ reflection = reflection.clone
+ original_polymorphic = reflection.options.delete(:polymorphic)
+ reflection.instance_variable_set(:@klass, klass)
+ yield reflection
+ ensure
+ reflection.options[:polymorphic] = original_polymorphic
+ end
+
+ def join_to(relation)
+ tables = @tables.dup
+ foreign_table = parent_table
+
+ # The chain starts with the target table, but we want to end with it here (makes
+ # more sense in this context), so we reverse
+ chain.reverse.each_with_index do |reflection, i|
+ table = tables.shift
+
+ case reflection.source_macro
+ when :belongs_to
+ key = reflection.association_primary_key
+ foreign_key = reflection.foreign_key
+ when :has_and_belongs_to_many
+ # Join the join table first...
+ relation.from(join(
+ table,
+ table[reflection.foreign_key].
+ eq(foreign_table[reflection.active_record_primary_key])
+ ))
+
+ foreign_table, table = table, tables.shift
+
+ key = reflection.association_primary_key
+ foreign_key = reflection.association_foreign_key
+ else
+ key = reflection.foreign_key
+ foreign_key = reflection.active_record_primary_key
+ end
+
+ constraint = table[key].eq(foreign_table[foreign_key])
+
+ if reflection.options[:polymorphic]
+ constraint = constraint.and(
+ foreign_table[reflection.foreign_type].eq(reflection.klass.name)
+ )
+ end
+
+ if reflection.klass.finder_needs_type_condition?
+ constraint = table.create_and([
+ constraint,
+ reflection.klass.send(:type_condition, table)
+ ])
+ end
+
+ relation.from(join(table, constraint))
+
+ unless conditions[i].empty?
+ relation.where(sanitize(conditions[i], table))
+ end
+
+ # The current table in this iteration becomes the foreign table in the next
+ foreign_table = table
+ end
+
+ relation
+ end
+
+ end
+
+ end
+ end
+end
68 lib/squeel/adapters/active_record/join_dependency.rb
@@ -0,0 +1,68 @@
+require 'active_record'
+
+module Squeel
+ module Adapters
+ module ActiveRecord
+ module JoinDependency
+
+ # Yes, I'm using alias_method_chain here. No, I don't feel too
+ # bad about it. JoinDependency, or, to call it by its full proper
+ # name, ::ActiveRecord::Associations::JoinDependency, is one of the
+ # most "for internal use only" chunks of ActiveRecord.
+ def self.included(base)
+ base.class_eval do
+ alias_method_chain :build, :squeel
+ end
+ end
+
+ def build_with_squeel(associations, parent = nil, join_type = Arel::InnerJoin)
+ associations = associations.symbol if Nodes::Stub === associations
+
+ case associations
+ when Nodes::Join
+ parent ||= join_parts.last
+ reflection = parent.reflections[associations.name] or
+ raise ::ActiveRecord::ConfigurationError, "Association named '#{ associations.name }' was not found; perhaps you misspelled it?"
+
+ unless join_association = find_join_association_respecting_polymorphism(reflection, parent, associations)
+ @reflections << reflection
+ join_association = build_join_association_respecting_polymorphism(reflection, parent, associations)
+ join_association.join_type = associations.type
+ @join_parts << join_association
+ cache_joined_association(join_association)
+ end
+
+ join_association
+ when Nodes::KeyPath
+ parent ||= join_parts.last
+ associations.path_with_endpoint.each do |key|
+ parent = build(key, parent, join_type)
+ end
+ parent
+ else
+ build_without_squeel(associations, parent, join_type)
+ end
+ end
+
+ def find_join_association_respecting_polymorphism(reflection, parent, join)
+ if association = find_join_association(reflection, parent)
+ unless reflection.options[:polymorphic]
+ association
+ else
+ association if association.active_record == join.klass
+ end
+ end
+ end
+
+ def build_join_association_respecting_polymorphism(reflection, parent, join)
+ if reflection.options[:polymorphic] && join.polymorphic?
+ JoinAssociation.new(reflection, self, parent, join.klass)
+ else
+ JoinAssociation.new(reflection, self, parent)
+ end
+ end
+
+ end
+ end
+ end
+end
292 lib/squeel/adapters/active_record/relation.rb
@@ -0,0 +1,292 @@
+require 'active_record'
+
+module Squeel
+ module Adapters
+ module ActiveRecord
+ module Relation
+
+ JoinAssociation = ::ActiveRecord::Associations::JoinDependency::JoinAssociation
+ JoinDependency = ::ActiveRecord::Associations::JoinDependency
+
+ attr_writer :join_dependency
+ private :join_dependency=
+
+ def join_dependency
+ @join_dependency ||= (build_join_dependency(table.from(table), @joins_values) && @join_dependency)
+ end
+
+ def select_visitor
+ Visitors::SelectVisitor.new(
+ Contexts::JoinDependencyContext.new(join_dependency)
+ )
+ end
+
+ def predicate_visitor
+ Visitors::PredicateVisitor.new(
+ Contexts::JoinDependencyContext.new(join_dependency)
+ )
+ end
+
+ def order_visitor
+ Visitors::OrderVisitor.new(
+ Contexts::JoinDependencyContext.new(join_dependency)
+ )
+ end
+
+ def merge(r, association_name = nil)
+ if association_name || relation_with_different_base?(r)
+ r = r.clone
+ association_name ||= infer_association_for_relation_merge(r)
+ prepare_relation_for_association_merge!(r, association_name)
+ self.joins_values += [association_name] if reflect_on_association(association_name)
+ end
+
+ super(r)
+ end
+
+ def relation_with_different_base?(r)
+ ::ActiveRecord::Relation === r &&
+ base_class.name != r.klass.base_class.name
+ end
+
+ def infer_association_for_relation_merge(r)
+ default_association = reflect_on_all_associations.detect {|a| a.class_name == r.klass.name}
+ default_association ? default_association.name : r.table_name.to_sym
+ end
+
+ def prepare_relation_for_association_merge!(r, association_name)
+ r.where_values.map! {|w| Squeel::Visitors::PredicateVisitor.can_accept?(w) ? {association_name => w} : w}
+ r.having_values.map! {|h| Squeel::Visitors::PredicateVisitor.can_accept?(h) ? {association_name => h} : h}
+ r.joins_values.map! {|j| [Symbol, Hash, Nodes::Stub, Nodes::Join].include?(j.class) ? {association_name => j} : j}
+ end
+
+ def build_arel
+ arel = table.from table
+
+ build_join_dependency(arel, @joins_values) unless @joins_values.empty?
+
+ predicate_viz = predicate_visitor
+
+ collapse_wheres(arel, predicate_viz.accept((@where_values - ['']).uniq))
+
+ arel.having(*predicate_viz.accept(@having_values.uniq.reject{|h| h.blank?})) unless @having_values.empty?
+
+ arel.take(connection.sanitize_limit(@limit_value)) if @limit_value
+ arel.skip(@offset_value) if @offset_value
+
+ arel.group(*@group_values.uniq.reject{|g| g.blank?}) unless @group_values.empty?
+
+ unless @order_values.empty?
+ order_viz = order_visitor
+ arel.order(*order_viz.accept(@order_values.uniq.reject{|o| o.blank?}))
+ end
+
+ build_select(arel, select_visitor.accept(@select_values.uniq))
+
+ arel.from(@from_value) if @from_value
+ arel.lock(@lock_value) if @lock_value
+
+ arel
+ end
+
+ def build_join_dependency(manager, joins)
+ buckets = joins.group_by do |join|
+ case join
+ when String
+ 'string_join'
+ when Hash, Symbol, Array, Nodes::Stub, Nodes::Join, Nodes::KeyPath
+ 'association_join'
+ when JoinAssociation
+ 'stashed_join'
+ when Arel::Nodes::Join
+ 'join_node'
+ else
+ raise 'unknown class: %s' % join.class.name
+ end
+ end
+
+ association_joins = buckets['association_join'] || []
+ stashed_association_joins = buckets['stashed_join'] || []
+ join_nodes = buckets['join_node'] || []
+ string_joins = (buckets['string_join'] || []).map { |x|
+ x.strip
+ }.uniq
+
+ join_list = custom_join_ast(manager, string_joins)
+
+ # All of this duplication just to add
+ self.join_dependency = JoinDependency.new(
+ @klass,
+ association_joins,
+ join_list
+ )
+
+ join_nodes.each do |join|
+ join_dependency.alias_tracker.aliased_name_for(join.left.name.downcase)
+ end
+
+ join_dependency.graft(*stashed_association_joins)
+
+ @implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty?
+
+ join_dependency.join_associations.each do |association|
+ association.join_to(manager)
+ end
+
+ manager.join_sources.concat join_nodes.uniq
+ manager.join_sources.concat join_list
+
+ manager
+ end
+
+ def select(value = Proc.new)
+ if block_given? && Proc === value
+ if value.arity > 0
+ to_a.select {|*block_args| value.call(*block_args)}
+ else
+ relation = clone
+ relation.select_values += Array.wrap(DSL.evaluate &value)
+ relation
+ end
+ else
+ super
+ end
+ end
+
+ def where(opts = Proc.new, *rest)
+ if block_given? && Proc === opts
+ super(DSL.evaluate &opts)
+ else
+ super
+ end
+ end
+
+ def build_where(opts, other = [])
+ case opts
+ when String, Array
+ super
+ else # Let's prevent PredicateBuilder from doing its thing
+ [opts, *other].map do |arg|
+ case arg
+ when Array # Just in case there's an array in there somewhere
+ @klass.send(:sanitize_sql, arg)
+ when Hash
+ @klass.send(:expand_hash_conditions_for_aggregates, arg)
+ else
+ arg
+ end
+ end
+ end
+ end
+
+ def order(*args)
+ if block_given? && args.empty?
+ super(DSL.evaluate &Proc.new)
+ else
+ super
+ end
+ end
+
+ def joins(*args)
+ if block_given? && args.empty?
+ super(DSL.evaluate &Proc.new)
+ else
+ super
+ end
+ end
+
+ def having(*args)
+ if block_given? && args.empty?
+ super(DSL.evaluate &Proc.new)
+ else
+ super
+ end
+ end
+
+ def collapse_wheres(arel, wheres)
+ wheres = [wheres] unless Array === wheres
+ binaries = wheres.grep(Arel::Nodes::Binary)
+
+ groups = binaries.group_by {|b| [b.class, b.left]}
+
+ groups.each do |_, bins|
+ arel.where(Arel::Nodes::And.new(bins))
+ end
+
+ (wheres - binaries).each do |where|
+ where = Arel.sql(where) if String === where
+ arel.where(Arel::Nodes::Grouping.new(where))
+ end
+ end
+
+ def find_equality_predicates(nodes)
+ nodes.map { |node|
+ case node
+ when Arel::Nodes::Equality
+ node
+ when Arel::Nodes::Grouping
+ find_equality_predicates([node.expr])
+ when Arel::Nodes::And
+ find_equality_predicates(node.children)
+ else
+ nil
+ end
+ }.compact.flatten
+ end
+
+ # Simulate the logic that occurs in #to_a
+ #
+ # This will let us get a dump of the SQL that will be run against the
+ # DB for debug purposes without actually running the query.
+ def debug_sql
+ if eager_loading?
+ including = (@eager_load_values + @includes_values).uniq
+ join_dependency = JoinDependency.new(@klass, including, [])
+ construct_relation_for_association_find(join_dependency).to_sql
+ else
+ arel.to_sql
+ end
+ end
+
+ ### ZOMG ALIAS_METHOD_CHAIN IS BELOW. HIDE YOUR EYES!
+ # ...
+ # ...
+ # ...
+ # Since you're still looking, let me explain this horrible
+ # transgression you see before you.
+ # You see, Relation#where_values_hash is defined on the
+ # ActiveRecord::Relation class. Since it's defined there, but
+ # I would very much like to modify its behavior, I have three
+ # choices.
+ #
+ # 1. Inherit from ActiveRecord::Relation in a Squeel::Relation
+ # class, and make an attempt to usurp all of the various calls
+ # to methods on ActiveRecord::Relation by doing some really
+ # evil stuff with constant reassignment, all for the sake of
+ # being able to use super().
+ #
+ # 2. Submit a patch to Rails core, breaking this method off into
+ # another module, all for my own selfish desire to use super()
+ # while mucking about in Rails internals.
+ #
+ # 3. Use alias_method_chain, and say 10 hail Hanssons as penance.
+ #
+ # I opted to go with #3. Except for the hail Hansson thing.
+ # Unless you're DHH, in which case, I totally said them.
+
+ def self.included(base)
+ base.class_eval do
+ alias_method_chain :where_values_hash, :squeel
+ end
+ end
+
+ def where_values_hash_with_squeel
+ equalities = find_equality_predicates(predicate_visitor.accept(@where_values))
+
+ Hash[equalities.map { |where| [where.left.name, where.right] }]
+ end
+
+ end
+ end
+ end
+end
25 lib/squeel/configuration.rb
@@ -0,0 +1,25 @@
+require 'squeel/constants'
+require 'squeel/predicate_methods'
+
+module Squeel
+ module Configuration
+
+ def configure
+ yield self
+ end
+
+ def load_core_extensions(*exts)
+ exts.each do |ext|
+ require "core_ext/#{ext}"
+ end
+ end
+
+ def alias_predicate(new_name, existing_name)
+ raise ArgumentError, 'the existing name should be the base name, not an _any/_all variation' if existing_name.to_s =~ /(_any|_all)$/
+ ['', '_any', '_all'].each do |suffix|
+ PredicateMethods.class_eval "alias :#{new_name}#{suffix} :#{existing_name}#{suffix} unless defined?(#{new_name}#{suffix})"
+ end
+ end
+
+ end
+end
23 lib/squeel/constants.rb
@@ -0,0 +1,23 @@
+module Squeel
+ module Constants
+ PREDICATES = [
+ :eq, :eq_any, :eq_all,
+ :not_eq, :not_eq_any, :not_eq_all,
+ :matches, :matches_any, :matches_all,
+ :does_not_match, :does_not_match_any, :does_not_match_all,
+ :lt, :lt_any, :lt_all,
+ :lteq, :lteq_any, :lteq_all,
+ :gt, :gt_any, :gt_all,
+ :gteq, :gteq_any, :gteq_all,
+ :in, :in_any, :in_all,
+ :not_in, :not_in_any, :not_in_all
+ ].freeze
+
+ PREDICATE_ALIASES = {
+ :matches => [:like],
+ :does_not_match => [:not_like],
+ :lteq => [:lte],
+ :gteq => [:gte]
+ }.freeze
+ end
+end
74 lib/squeel/contexts/join_dependency_context.rb
@@ -0,0 +1,74 @@
+require 'active_record'
+
+module Squeel
+ # Because the AR::Associations namespace is insane
+ JoinPart = ActiveRecord::Associations::JoinDependency::JoinPart
+
+ module Contexts
+ class JoinDependencyContext
+ attr_reader :base, :engine, :arel_visitor
+
+ def initialize(join_dependency)
+ @join_dependency = join_dependency
+ @base = join_dependency.join_base
+ @engine = @base.arel_engine
+ @arel_visitor = Arel::Visitors.visitor_for @engine
+ @default_table = Arel::Table.new(@base.table_name, :as => @base.aliased_table_name, :engine => @engine)
+ @tables = Hash.new {|hash, key| hash[key] = get_table(key)}
+ end
+
+ def find(object, parent = @base)
+ if JoinPart === parent
+ object = object.to_sym if String === object
+ case object
+ when Symbol, Nodes::Stub
+ @join_dependency.join_associations.detect { |j|
+ j.reflection.name == object.to_sym && j.parent == parent
+ }
+ when Nodes::Join
+ @join_dependency.join_associations.detect { |j|
+ j.reflection.name == object.name && j.parent == parent &&
+ (object.polymorphic? ? j.reflection.klass == object.klass : true)
+ }
+ else
+ @join_dependency.join_associations.detect { |j|
+ j.reflection == object && j.parent == parent
+ }
+ end
+ else
+ nil
+ end
+ end
+
+ def traverse(keypath, parent = @base, include_endpoint = false)
+ parent = @base if keypath.absolute?
+ keypath.path.each do |key|
+ parent = find(key, parent)
+ end
+ parent = find(keypath.endpoint, parent) if include_endpoint
+
+ parent
+ end
+
+ def contextualize(object)
+ @tables[object]
+ end
+
+ def sanitize_sql(conditions, parent)
+ parent.active_record.send(:sanitize_sql, conditions, parent.aliased_table_name)
+ end
+
+ private
+
+ def get_table(object)
+ if [Symbol, Nodes::Stub].include?(object.class)
+ Arel::Table.new(object.to_sym, :engine => @engine)
+ elsif object.respond_to?(:aliased_table_name)
+ Arel::Table.new(object.table_name, :as => object.aliased_table_name, :engine => @engine)
+ else
+ @default_table
+ end
+ end
+ end
+ end
+end
31 lib/squeel/dsl.rb
@@ -0,0 +1,31 @@
+module Squeel
+ class DSL
+
+ Squeel.evil_things do
+ (instance_methods + private_instance_methods).each do |method|
+ unless method.to_s =~ /^(__|instance_eval)/
+ undef_method method
+ end
+ end
+ end
+
+ def self.evaluate(&block)
+ if block.arity > 0
+ yield self.new
+ else
+ self.new.instance_eval(&block)
+ end
+ end
+
+ def method_missing(method_id, *args)
+ if args.empty?
+ Nodes::Stub.new method_id
+ elsif (args.size == 1) && (Class === args[0])
+ Nodes::Join.new(method_id, Arel::InnerJoin, args[0])
+ else
+ Nodes::Function.new method_id, args
+ end
+ end
+
+ end
+end
10 lib/squeel/nodes.rb
@@ -0,0 +1,10 @@
+require 'squeel/nodes/stub'
+require 'squeel/nodes/key_path'
+require 'squeel/nodes/predicate'
+require 'squeel/nodes/function'
+require 'squeel/nodes/operation'
+require 'squeel/nodes/order'
+require 'squeel/nodes/and'
+require 'squeel/nodes/or'
+require 'squeel/nodes/not'
+require 'squeel/nodes/join'
8 lib/squeel/nodes/and.rb
@@ -0,0 +1,8 @@
+require 'squeel/nodes/nary'
+
+module Squeel
+ module Nodes
+ class And < Nary
+ end
+ end
+end
23 lib/squeel/nodes/binary.rb
@@ -0,0 +1,23 @@
+require 'squeel/nodes/predicate_operators'
+
+module Squeel
+ module Nodes
+ class Binary
+ include PredicateOperators
+
+ attr_reader :left, :right
+
+ def initialize(left, right)
+ @left, @right = left, right
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.left == other.left &&
+ self.right == other.right
+ end
+
+ alias :== :eql?
+ end
+ end
+end
84 lib/squeel/nodes/function.rb
@@ -0,0 +1,84 @@
+require 'squeel/predicate_methods'
+
+module Squeel
+ module Nodes
+ class Function
+
+ include PredicateMethods
+ include Operators
+
+ attr_reader :name, :args, :alias
+
+ def initialize(name, args)
+ @name, @args = name, args
+ end
+
+ def as(alias_name)
+ @alias = alias_name.to_s
+ self
+ end
+
+ def ==(value)
+ Predicate.new self, :eq, value
+ end
+
+ def asc
+ Order.new self, 1
+ end
+
+ def desc
+ Order.new self, -1
+ end
+
+ # Won't work on Ruby 1.8.x so need to do this conditionally
+ define_method('!=') do |value|
+ Predicate.new(self, :not_eq, value)
+ end if respond_to?('!=')
+
+ def ^(value)
+ Predicate.new self, :not_eq, value
+ end
+
+ def >>(value)
+ Predicate.new self, :in, value
+ end
+
+ def <<(value)
+ Predicate.new self, :not_in, value
+ end
+
+ def =~(value)
+ Predicate.new self, :matches, value
+ end
+
+ # Won't work on Ruby 1.8.x so need to do this conditionally
+ define_method('!~') do |value|
+ Predicate.new(self, :does_not_match, value)
+ end if respond_to?('!~')
+
+ def >(value)
+ Predicate.new self, :gt, value
+ end
+
+ def >=(value)
+ Predicate.new self, :gteq, value
+ end
+
+ def <(value)
+ Predicate.new self, :lt, value
+ end
+
+ def <=(value)
+ Predicate.new self, :lteq, value
+ end
+
+ # expand_hash_conditions_for_aggregates assumes our hash keys can be
+ # converted to symbols, so this has to be implemented, but it doesn't
+ # really have to do anything useful.
+ def to_sym
+ nil
+ end
+
+ end
+ end
+end
51 lib/squeel/nodes/join.rb
@@ -0,0 +1,51 @@
+module Squeel
+ module Nodes
+ class Join
+ attr_reader :name, :type, :klass
+
+ def initialize(name, type = Arel::InnerJoin, klass = nil)
+ @name, @type = name, type
+ @klass = convert_to_class(klass) if klass
+ end
+
+ def inner
+ @type = Arel::InnerJoin
+ self
+ end
+
+ def outer
+ @type = Arel::OuterJoin
+ self
+ end
+
+ def klass=(class_or_class_name)
+ @klass = convert_to_class(class_or_class_name)
+ end
+
+ def polymorphic?
+ @klass
+ end
+
+ # expand_hash_conditions_for_aggregates assumes our hash keys can be
+ # converted to symbols, so this has to be implemented, but it doesn't
+ # really have to do anything useful.
+ def to_sym
+ nil
+ end
+
+ private
+
+ def convert_to_class(value)
+ case value
+ when String, Symbol
+ Kernel.const_get(value)
+ when Class
+ value
+ else
+ raise ArgumentError, "#{value} cannot be converted to a Class"
+ end
+ end
+
+ end
+ end
+end
127 lib/squeel/nodes/key_path.rb
@@ -0,0 +1,127 @@
+require 'squeel/nodes/operators'
+require 'squeel/nodes/predicate_operators'
+
+module Squeel
+ module Nodes
+ class KeyPath
+ include PredicateOperators
+ include Operators
+
+ attr_reader :path, :endpoint
+
+ def initialize(path, endpoint, absolute = false)
+ @path, @endpoint = path, endpoint
+ @path = [@path] unless Array === @path
+ @endpoint = Stub.new(@endpoint) if Symbol === @endpoint
+ @absolute = absolute
+ end
+
+ def absolute?
+ @absolute
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.path == other.path &&
+ self.endpoint.eql?(other.endpoint) &&
+ self.absolute? == other.absolute?
+ end
+
+ def |(other)
+ endpoint.respond_to?(:|) ? super : no_method_error(:|)
+ end
+
+ def &(other)
+ endpoint.respond_to?(:&) ? super : no_method_error(:&)
+ end
+
+ def -@
+ endpoint.respond_to?(:-@) ? super : no_method_error(:-@)
+ end
+
+ def +(other)
+ endpoint.respond_to?(:+) ? super : no_method_error(:+)
+ end
+
+ def -(other)
+ endpoint.respond_to?(:-) ? super : no_method_error(:-)
+ end
+
+ def *(other)
+ endpoint.respond_to?(:*) ? super : no_method_error(:*)
+ end
+
+ def /(other)
+ endpoint.respond_to?(:/) ? super : no_method_error(:/)
+ end
+
+ def op(operator, other)
+ endpoint.respond_to?(:op) ? super : no_method_error(:/)
+ end
+
+ def ~
+ @absolute = true
+ self
+ end
+
+ # To let these fall through to the endpoint via method_missing
+ instance_methods.grep(/^(==|=~|!~)$/) do |operator|
+ undef_method operator
+ end
+
+ def hash
+ [self.class, endpoint, *path].hash
+ end
+
+ def to_sym
+ nil
+ end
+
+ def %(val)
+ case endpoint
+ when Stub, Function
+ eq(val)
+ self
+ else
+ endpoint % val
+ self
+ end
+ end
+
+ def path_with_endpoint
+ path + [endpoint]
+ end
+
+ def to_s
+ path.map(&:to_s).join('.') << ".#{endpoint}"
+ end
+
+ def method_missing(method_id, *args)
+ super if method_id == :to_ary
+ if endpoint.respond_to? method_id
+ @endpoint = @endpoint.send(method_id, *args)
+ self
+ elsif Stub === endpoint
+ @path << endpoint.symbol
+ if args.empty?
+ @endpoint = Stub.new(method_id)
+ elsif (args.size == 1) && (Class === args[0])
+ @endpoint = Join.new(method_id, Arel::InnerJoin, args[0])
+ else
+ @endpoint = Nodes::Function.new method_id, args
+ end
+ self
+ else
+ super
+ end
+ end
+
+ private
+
+ def no_method_error(method_id)
+ raise NoMethodError, "undefined method `#{method_id}' for #{self}:#{self.class}"
+ end
+
+ end
+ end
+end
35 lib/squeel/nodes/nary.rb
@@ -0,0 +1,35 @@
+module Squeel
+ module Nodes
+ class Nary
+ include PredicateOperators
+
+ attr_reader :children
+
+ def initialize(children)
+ raise ArgumentError, '#{self.class} requires an array' unless Array === children
+ # We don't dup here, as incoming arrays should be created by the
+ # Operators#& method on other nodes. If you're creating And nodes
+ # manually, by sure that they're new arays.
+ @children = children
+ end
+
+ def &(other)
+ @children << other
+ self
+ end
+
+ def -(other)
+ @children << Not.new(other)
+ self
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.children == other.children
+ end
+
+ alias :== :eql?
+
+ end
+ end
+end
8 lib/squeel/nodes/not.rb
@@ -0,0 +1,8 @@
+require 'squeel/nodes/unary'
+
+module Squeel
+ module Nodes
+ class Not < Unary
+ end
+ end
+end
23 lib/squeel/nodes/operation.rb
@@ -0,0 +1,23 @@
+require 'squeel/nodes/function'
+
+module Squeel
+ module Nodes
+ class Operation < Function
+
+ def initialize(left, operator, right)
+ super(operator, [left, right])
+ end
+
+ alias :operator :name
+
+ def left
+ args[0]
+ end
+
+ def right
+ args[1]
+ end
+
+ end
+ end
+end
27 lib/squeel/nodes/operators.rb
@@ -0,0 +1,27 @@
+module Squeel
+ module Nodes
+ module Operators
+
+ def +(value)
+ Operation.new(self, :+, value)
+ end
+
+ def -(value)
+ Operation.new(self, :-, value)
+ end
+
+ def *(value)
+ Operation.new(self, :*, value)
+ end
+
+ def /(value)
+ Operation.new(self, :/, value)
+ end
+
+ def op(operator, value)
+ Operation.new(self, operator, value)
+ end
+
+ end
+ end
+end
8 lib/squeel/nodes/or.rb
@@ -0,0 +1,8 @@
+require 'squeel/nodes/binary'
+
+module Squeel
+ module Nodes
+ class Or < Binary
+ end
+ end
+end
35 lib/squeel/nodes/order.rb
@@ -0,0 +1,35 @@
+module Squeel
+ module Nodes
+ class Order
+ attr_reader :expr, :direction
+
+ def initialize(expr, direction = 1)
+ raise ArgumentError, "Direction #{direction} is not valid. Must be -1 or 1." unless [-1,1].include? direction
+ @expr, @direction = expr, direction
+ end
+
+ def asc
+ @direction = 1
+ self
+ end
+
+ def desc
+ @direction = -1
+ self
+ end
+
+ def ascending?
+ @direction == 1
+ end
+
+ def descending?
+ @direction == -1
+ end
+
+ def reverse!
+ @direction = - @direction
+ self
+ end
+ end
+ end
+end
49 lib/squeel/nodes/predicate.rb
@@ -0,0 +1,49 @@
+require 'squeel/predicate_methods'
+require 'squeel/nodes/predicate_operators'
+
+module Squeel
+ module Nodes
+ class Predicate
+
+ include PredicateMethods
+ include PredicateOperators
+
+ attr_accessor :value
+ attr_reader :expr, :method_name
+
+ def initialize(expr, method_name = :eq, value = :__undefined__)
+ @expr, @method_name, @value = expr, method_name, value
+ end
+
+ def eql?(other)
+ self.class.eql?(other.class) &&
+ self.expr.eql?(other.expr) &&
+ self.method_name.eql?(other.method_name) &&
+ self.value.eql?(other.value)
+ end
+
+ alias :== :eql?
+
+ def hash
+ [self.class, expr, method_name, value].hash
+ end
+
+ def value?
+ @value != :__undefined__
+ end
+
+ def %(val)
+ @value = val
+ self
+ end
+
+ # expand_hash_conditions_for_aggregates assumes our hash keys can be
+ # converted to symbols, so this has to be implemented, but it doesn't
+ # really have to do anything useful.
+ def to_sym
+ nil
+ end
+
+ end
+ end
+end
17 lib/squeel/nodes/predicate_operators.rb
@@ -0,0 +1,17 @@
+module Squeel
+ module Nodes
+ module PredicateOperators
+ def |(other)
+ Or.new(self, other)
+ end
+
+ def &(other)
+ And.new([self, other])
+ end
+
+ def -@
+ Not.new(self)
+ end
+ end
+ end
+end
113 lib/squeel/nodes/stub.rb
@@ -0,0 +1,113 @@
+require 'squeel/predicate_methods'
+require 'squeel/nodes/operators'
+
+module Squeel
+ module Nodes
+ class Stub
+
+ include PredicateMethods
+ include Operators
+
+ attr_reader :symbol
+
+ def initialize(symbol)
+ @symbol = symbol
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.symbol == other.symbol
+ end
+
+ def hash
+ symbol.hash
+ end
+
+ def to_sym
+ symbol
+ end
+
+ def to_s
+ symbol.to_s
+ end
+
+ def method_missing(method_id, *args)
+ super if method_id == :to_ary
+ KeyPath.new(self.symbol, method_id)
+ end
+
+ def ==(value)
+ Predicate.new self.symbol, :eq, value
+ end
+
+ # Won't work on Ruby 1.8.x so need to do this conditionally
+ define_method('!=') do |value|
+ Predicate.new(self.symbol, :not_eq, value)
+ end if respond_to?('!=')
+
+ def ^(value)
+ Predicate.new self.symbol, :not_eq, value
+ end
+
+ def >>(value)
+ Predicate.new self.symbol, :in, value
+ end
+
+ def <<(value)
+ Predicate.new self.symbol, :not_in, value
+ end
+
+ def =~(value)
+ Predicate.new self.symbol, :matches, value
+ end
+
+ # Won't work on Ruby 1.8.x so need to do this conditionally
+ define_method('!~') do |value|
+ Predicate.new(self.symbol, :does_not_match, value)
+ end if respond_to?('!~')
+
+ def >(value)
+ Predicate.new self.symbol, :gt, value
+ end
+
+ def >=(value)
+ Predicate.new self.symbol, :gteq, value
+ end
+
+ def <(value)
+ Predicate.new self.symbol, :lt, value
+ end
+
+ def <=(value)
+ Predicate.new self.symbol, :lteq, value
+ end
+
+ def asc
+ Order.new self.symbol, 1
+ end
+
+ def desc
+ Order.new self.symbol, -1
+ end
+
+ def func(*args)
+ Function.new(self.symbol, args)
+ end
+
+ alias :[] :func
+
+ def inner
+ Join.new(self.symbol, Arel::InnerJoin)
+ end
+
+ def outer
+ Join.new(self.symbol, Arel::OuterJoin)
+ end
+
+ def of_class(klass)
+ Join.new(self.symbol, Arel::InnerJoin, klass)
+ end
+
+ end
+ end
+end
22 lib/squeel/nodes/unary.rb
@@ -0,0 +1,22 @@
+require 'squeel/nodes/predicate_operators'
+
+module Squeel
+ module Nodes
+ class Unary
+ include PredicateOperators
+
+ attr_reader :expr
+
+ def initialize(expr)
+ @expr = expr
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.expr == other.expr
+ end
+
+ alias :== :eql?
+ end
+ end
+end
22 lib/squeel/predicate_methods.rb
@@ -0,0 +1,22 @@
+require 'squeel/predicate_methods/symbol'
+require 'squeel/predicate_methods/stub'
+require 'squeel/predicate_methods/predicate'
+require 'squeel/predicate_methods/function'
+
+module Squeel
+ module PredicateMethods
+
+ def self.included(base)
+ base.send :include, const_get(base.name.split(/::/)[-1].to_sym)
+ end
+
+ Constants::PREDICATES.each do |method_name|
+ class_eval <<-RUBY
+ def #{method_name}(value = :__undefined__)
+ predicate :#{method_name}, value
+ end
+ RUBY
+ end
+
+ end
+end
9 lib/squeel/predicate_methods/function.rb
@@ -0,0 +1,9 @@
+module Squeel
+ module PredicateMethods
+ module Function
+ def predicate(method_name, value = :__undefined__)
+ Nodes::Predicate.new self, method_name, value
+ end
+ end
+ end
+end
11 lib/squeel/predicate_methods/predicate.rb
@@ -0,0 +1,11 @@
+module Squeel
+ module PredicateMethods
+ module Predicate
+ def predicate(method_name, value = :__undefined__)
+ @method_name = method_name
+ @value = value unless value == :__undefined__
+ self
+ end
+ end
+ end
+end
9 lib/squeel/predicate_methods/stub.rb
@@ -0,0 +1,9 @@
+module Squeel
+ module PredicateMethods
+ module Stub
+ def predicate(method_name, value = :__undefined__)
+ Nodes::Predicate.new self.symbol, method_name, value
+ end
+ end
+ end
+end
9 lib/squeel/predicate_methods/symbol.rb
@@ -0,0 +1,9 @@
+module Squeel
+ module PredicateMethods
+ module Symbol
+ def predicate(method_name, value = :__undefined__)
+ Nodes::Predicate.new self, method_name, value
+ end
+ end
+ end
+end
3 lib/squeel/version.rb
@@ -0,0 +1,3 @@
+module Squeel
+ VERSION = "0.5.0"
+end
3 lib/squeel/visitors.rb
@@ -0,0 +1,3 @@
+require 'squeel/visitors/predicate_visitor'
+require 'squeel/visitors/order_visitor'
+require 'squeel/visitors/select_visitor'
46 lib/squeel/visitors/base.rb
@@ -0,0 +1,46 @@
+require 'active_support/core_ext/module'
+require 'squeel/nodes'
+
+module Squeel
+ module Visitors
+ class Base
+ attr_accessor :context
+ delegate :contextualize, :find, :traverse, :sanitize_sql, :engine, :arel_visitor, :to => :context
+
+ def initialize(context = nil)
+ @context = context
+ end
+
+ def accept(object, parent = context.base)
+ visit(object, parent)
+ end
+
+ def can_accept?(object)
+ respond_to? DISPATCH[object.class]
+ end
+
+ def self.can_accept?(object)
+ method_defined? DISPATCH[object.class]
+ end
+
+ private
+
+ DISPATCH = Hash.new do |hash, klass|
+ hash[klass] = "visit_#{klass.name.gsub('::', '_')}"
+ end
+
+ def quoted?(object)
+ case object
+ when Arel::Nodes::SqlLiteral, Bignum, Fixnum
+ false
+ else
+ true
+ end
+ end
+
+ def visit(object, parent)
+ send(DISPATCH[object.class], object, parent)
+ end
+ end
+ end
+end
107 lib/squeel/visitors/order_visitor.rb
@@ -0,0 +1,107 @@
+require 'squeel/visitors/base'
+require 'squeel/contexts/join_dependency_context'
+
+module Squeel
+ module Visitors
+ class OrderVisitor < Base
+
+ def visit_Hash(o, parent)
+ o.map do |k, v|
+ if implies_context_change?(v)
+ visit_with_context_change(k, v, parent)
+ else
+ visit_without_context_change(k, v, parent)
+ end
+ end.flatten
+ end
+
+ def implies_context_change?(v)
+ Hash === v || can_accept?(v) ||
+ (Array === v && !v.empty? && v.all? {|val| can_accept?(val)})
+ end
+
+ def visit_with_context_change(k, v, parent)
+ parent = case k
+ when Nodes::KeyPath
+ traverse(k, parent, true)
+ else
+ find(k, parent)
+ end
+
+ if Array === v
+ v.map {|val| accept(val, parent || k)}
+ else
+ can_accept?(v) ? accept(v, parent || k) : v
+ end
+ end
+
+ def visit_without_context_change(k, v, parent)
+ v
+ end
+
+ def visit_Array(o, parent)
+ o.map { |v| can_accept?(v) ? accept(v, parent) : v }.flatten
+ end
+
+ def visit_Symbol(o, parent)
+ contextualize(parent)[o]
+ end
+
+ def visit_Squeel_Nodes_Stub(o, parent)
+ contextualize(parent)[o.symbol]
+ end
+
+ def visit_Squeel_Nodes_KeyPath(o, parent)
+ parent = traverse(o, parent)
+
+ accept(o.endpoint, parent)
+ end
+
+ def visit_Squeel_Nodes_Order(o, parent)
+ accept(o.expr, parent).send(o.descending? ? :desc : :asc)
+ end
+
+ def visit_Squeel_Nodes_Function(o, parent)
+ args = o.args.map do |arg|
+ case arg
+ when Nodes::Function, Nodes::KeyPath
+ accept(arg, parent)
+ when Symbol, Nodes::Stub
+ Arel.sql(arel_visitor.accept contextualize(parent)[arg.to_sym])
+ else
+ quoted?(arg) ? Arel.sql(arel_visitor.accept arg) : arg
+ end
+ end
+ Arel::Nodes::NamedFunction.new(o.name, args, o.alias)
+ end
+
+ def visit_Squeel_Nodes_Operation(o, parent)
+ args = o.args.map do |arg|
+ case arg
+ when Nodes::Function
+ accept(arg, parent)
+ when Symbol, Nodes::Stub
+ Arel.sql(arel_visitor.accept contextualize(parent)[arg.to_sym])
+ else
+ quoted?(arg) ? Arel.sql(arel_visitor.accept arg) : arg
+ end
+ end
+
+ op = case o.operator
+ when :+
+ Arel::Nodes::Addition.new(args[0], args[1])
+ when :-
+ Arel::Nodes::Subtraction.new(args[0], args[1])
+ when :*
+ Arel::Nodes::Multiplication.new(args[0], args[1])
+ when :/
+ Arel::Nodes::Division.new(args[0], args[1])
+ else
+ Arel.sql("#{arel_visitor.accept(args[0])} #{o.operator} #{arel_visitor.accept(args[1])}")
+ end
+ o.alias ? op.as(o.alias) : op
+ end
+
+ end
+ end
+end
179 lib/squeel/visitors/predicate_visitor.rb
@@ -0,0 +1,179 @@
+require 'squeel/visitors/base'
+require 'squeel/contexts/join_dependency_context'
+
+module Squeel
+ module Visitors
+ class PredicateVisitor < Base
+
+ def visit_Hash(o, parent)
+ predicates = o.map do |k, v|
+ if implies_context_change?(v)
+ visit_with_context_change(k, v, parent)
+ else
+ visit_without_context_change(k, v, parent)
+ end
+ end
+
+ predicates.flatten!
+
+ if predicates.size > 1
+ Arel::Nodes::Grouping.new(Arel::Nodes::And.new predicates)
+ else
+ predicates.first
+ end
+ end
+
+ def visit_Array(o, parent)
+ if o.first.is_a? String
+ sanitize_sql(o, parent)
+ else
+ o.map { |v| can_accept?(v) ? accept(v, parent) : v }.flatten
+ end
+ end
+
+ def visit_Squeel_Nodes_KeyPath(o, parent)
+ parent = traverse(o, parent)
+
+ accept(o.endpoint, parent)
+ end
+
+ def visit_Squeel_Nodes_Predicate(o, parent)
+ value = o.value
+ case value
+ when Nodes::Function
+ value = accept(value, parent)
+ when Nodes::KeyPath
+ value = can_accept?(value.endpoint) ? accept(value, parent) : contextualize(traverse(value, parent))[value.endpoint.to_sym]
+ end
+ if Nodes::Function === o.expr
+ accept(o.expr, parent).send(o.method_name, value)
+ else
+ contextualize(parent)[o.expr].send(o.method_name, value)
+ end
+ end
+
+ def visit_Squeel_Nodes_Function(o, parent)
+ args = o.args.map do |arg|
+ case arg
+ when Nodes::Function
+ accept(arg, parent)
+ when Nodes::KeyPath
+ can_accept?(arg.endpoint) ? accept(arg, parent) : contextualize(traverse(arg, parent))[arg.endpoint.to_sym]
+ when Symbol, Nodes::Stub
+ Arel.sql(arel_visitor.accept contextualize(parent)[arg.to_sym])
+ else
+ quoted?(arg) ? Arel.sql(arel_visitor.accept arg) : arg
+ end
+ end
+ Arel::Nodes::NamedFunction.new(o.name, args, o.alias)
+ end
+
+ def visit_Squeel_Nodes_Operation(o, parent)
+ args = o.args.map do |arg|
+ case arg
+ when Nodes::Function
+ accept(arg, parent)
+ when Nodes::KeyPath
+ can_accept?(arg.endpoint) ? accept(arg, parent) : contextualize(traverse(arg, parent))[arg.endpoint.to_sym]
+ when Symbol, Nodes::Stub
+ Arel.sql(arel_visitor.accept contextualize(parent)[arg.to_sym])
+ else
+ quoted?(arg) ? Arel.sql(arel_visitor.accept arg) : arg
+ end
+ end
+
+ op = case o.operator
+ when :+
+ Arel::Nodes::Addition.new(args[0], args[1])
+ when :-
+ Arel::Nodes::Subtraction.new(args[0], args[1])
+ when :*
+ Arel::Nodes::Multiplication.new(args[0], args[1])
+ when :/
+ Arel::Nodes::Division.new(args[0], args[1])
+ else
+ Arel::Nodes::InfixOperation(o.operator, args[0], args[1])
+ end
+ o.alias ? op.as(o.alias) : op
+ end
+
+ def visit_Squeel_Nodes_And(o, parent)
+ Arel::Nodes::Grouping.new(Arel::Nodes::And.new(accept(o.children, parent)))
+ end
+
+ def visit_Squeel_Nodes_Or(o, parent)
+ accept(o.left, parent).or(accept(o.right, parent))
+ end
+
+ def visit_Squeel_Nodes_Not(o, parent)
+ accept(o.expr, parent).not
+ end
+
+ def implies_context_change?(v)
+ case v
+ when Hash, Nodes::Predicate, Nodes::Unary, Nodes::Binary, Nodes::Nary
+ true
+ when Nodes::KeyPath
+ can_accept?(v.endpoint)
+ when Array
+ (!v.empty? && v.all? {|val| can_accept?(val)})
+ else
+ false
+ end
+ end
+
+ def visit_with_context_change(k, v, parent)
+ parent = case k
+ when Nodes::KeyPath
+ traverse(k, parent, true)
+ else
+ find(k, parent)
+ end
+
+ case v
+ when Hash, Nodes::KeyPath, Nodes::Predicate, Nodes::Unary, Nodes::Binary, Nodes::Nary
+ accept(v, parent || k)
+ when Array
+ v.map {|val| accept(val, parent || k)}
+ else
+ raise ArgumentError, <<-END
+ Hashes, Predicates, and arrays of visitables as values imply that their
+ corresponding keys are a parent. This didn't work out so well in the case
+ of key = #{k} and value = #{v}"
+ END
+ end
+ end
+
+ def visit_without_context_change(k, v, parent)
+ case v
+ when Nodes::Stub, Symbol
+ v = contextualize(parent)[v.to_sym]
+ when Nodes::KeyPath # If we could accept the endpoint, we wouldn't be here
+ v = contextualize(traverse(v, parent))[v.endpoint.to_sym]
+ end
+
+ case k
+ when Nodes::Predicate
+ accept(k % v, parent)
+ when Nodes::Function
+ arel_predicate_for(accept(k, parent), v, parent)
+ when Nodes::KeyPath
+ accept(k % v, parent)
+ else
+ attribute = contextualize(parent)[k.to_sym]
+ arel_predicate_for(attribute, v, parent)
+ end
+ end
+
+ def arel_predicate_for(attribute, value, parent)
+ if [Array, Range, Arel::SelectManager].include?(value.class)
+ attribute.in(value)
+ else
+ value = can_accept?(value) ? accept(value, parent) : value
+ attribute.eq(value)
+ end
+ end
+
+ end
+ end
+end
103 lib/squeel/visitors/select_visitor.rb
@@ -0,0 +1,103 @@
+require 'squeel/visitors/base'
+require 'squeel/contexts/join_dependency_context'
+
+module Squeel
+ module Visitors
+ class SelectVisitor < Base
+
+ def visit_Hash(o, parent)
+ o.map do |k, v|
+ if implies_context_change?(v)
+ visit_with_context_change(k, v, parent)
+ else
+ visit_without_context_change(k, v, parent)
+ end
+ end.flatten
+ end
+
+ def implies_context_change?(v)
+ Hash === v || can_accept?(v) ||
+ (Array === v && !v.empty? && v.all? {|val| can_accept?(val)})
+ end
+
+ def visit_with_context_change(k, v, parent)
+ parent = case k
+ when Nodes::KeyPath
+ traverse(k, parent, true)
+ else
+ find(k, parent)
+ end
+
+ if Array === v
+ v.map {|val| accept(val, parent || k)}
+ else
+ can_accept?(v) ? accept(v, parent || k) : v
+ end
+ end
+
+ def visit_without_context_change(k, v, parent)
+ v
+ end
+
+ def visit_Array(o, parent)
+ o.map { |v| can_accept?(v) ? accept(v, parent) : v }.flatten
+ end
+
+ def visit_Symbol(o, parent)
+ contextualize(parent)[o]
+ end
+
+ def visit_Squeel_Nodes_Stub(o, parent)
+ contextualize(parent)[o.symbol]
+ end
+
+ def visit_Squeel_Nodes_KeyPath(o, parent)
+ parent = traverse(o, parent)
+
+ accept(o.endpoint, parent)
+ end
+
+ def visit_Squeel_Nodes_Function(o, parent)
+ args = o.args.map do |arg|
+ case arg
+ when Nodes::Function, Nodes::KeyPath
+ accept(arg, parent)
+ when Symbol, Nodes::Stub
+ Arel.sql(arel_visitor.accept contextualize(parent)[arg.to_sym])
+ else
+ quoted?(arg) ? Arel.sql(arel_visitor.accept arg) : arg
+ end
+ end
+ Arel::Nodes::NamedFunction.new(o.name, args, o.alias)
+ end
+
+ def visit_Squeel_Nodes_Operation(o, parent)
+ args = o.args.map do |arg|
+ case arg
+ when Nodes::Function
+ accept(arg, parent)
+ when Symbol, Nodes::Stub
+ Arel.sql(arel_visitor.accept contextualize(parent)[arg.to_sym])
+ else
+ quoted?(arg) ? Arel.sql(arel_visitor.accept arg) : arg
+ end
+ end
+
+ op = case o.operator
+ when :+
+ Arel::Nodes::Addition.new(args[0], args[1])
+ when :-
+ Arel::Nodes::Subtraction.new(args[0], args[1])
+ when :*
+ Arel::Nodes::Multiplication.new(args[0], args[1])
+ when :/
+ Arel::Nodes::Division.new(args[0], args[1])
+ else
+ Arel.sql("#{arel_visitor.accept(args[0])} #{o.operator} #{arel_visitor.accept(args[1])}")
+ end
+ o.alias ? op.as(o.alias) : op
+ end
+
+ end
+ end
+end
5 spec/blueprints/articles.rb
@@ -0,0 +1,5 @@
+Article.blueprint do
+ person
+ title
+ body
+end
5 spec/blueprints/comments.rb
@@ -0,0 +1,5 @@
+Comment.blueprint do
+ article
+ person
+ body
+end
3 spec/blueprints/notes.rb
@@ -0,0 +1,3 @@
+Note.blueprint do
+ note
+end
4 spec/blueprints/people.rb
@@ -0,0 +1,4 @@
+Person.blueprint do
+ name
+ salary
+end
3 spec/blueprints/tags.rb
@@ -0,0 +1,3 @@
+Tag.blueprint do
+ name { Sham.tag_name }
+end
22 spec/console.rb
@@ -0,0 +1,22 @@
+Bundler.setup
+require 'machinist/active_record'
+require 'sham'
+require 'faker'
+
+Dir[File.expand_path('../../spec/{helpers,support,blueprints}/*.rb', __FILE__)].each do |f|
+ require f
+end
+
+Sham.define do
+ name { Faker::Name.name }
+ title { Faker::Lorem.sentence }
+ body { Faker::Lorem.paragraph }
+ salary {|index| 30000 + (index * 1000)}
+ tag_name { Faker::Lorem.words(3).join(' ') }
+ note { Faker::Lorem.words(7).join(' ') }
+end
+
+Schema.create
+
+require 'squeel'
+
68 spec/core_ext/symbol_spec.rb
@@ -0,0 +1,68 @@
+require 'spec_helper'
+
+describe Symbol do
+
+ Squeel::Constants::PREDICATES.each do |method_name|
+ it "creates #{method_name} predicates with no value" do
+ predicate = :attribute.send(method_name)
+ predicate.expr.should eq :attribute
+ predicate.method_name.should eq method_name
+ predicate.value?.should be_false
+ end
+
+ it "creates #{method_name} predicates with a value" do
+ predicate = :attribute.send(method_name, 'value')
+ predicate.expr.should eq :attribute
+ predicate.method_name.should eq method_name
+ predicate.value.should eq 'value'
+ end
+ end
+
+ Squeel::Constants::PREDICATE_ALIASES.each do |method_name, aliases|
+ aliases.each do |aliaz|
+ ['', '_any', '_all'].each do |suffix|
+ it "creates #{method_name.to_s + suffix} predicates with no value using the alias #{aliaz.to_s + suffix}" do
+ predicate = :attribute.send(aliaz.to_s + suffix)
+ predicate.expr.should eq :attribute
+ predicate.method_name.should eq "#{method_name}#{suffix}".to_sym
+ predicate.value?.should be_false
+ end
+
+ it "creates #{method_name.to_s + suffix} predicates with a value using the alias #{aliaz.to_s + suffix}" do
+ predicate = :attribute.send((aliaz.to_s + suffix), 'value')
+ predicate.expr.should eq :attribute
+ predicate.method_name.should eq "#{method_name}#{suffix}".to_sym
+ predicate.value.should eq 'value'
+ end
+ end
+ end
+ end
+
+ it 'creates ascending orders' do
+ order = :attribute.asc
+ order.should be_ascending
+ end
+
+ it 'creates descending orders' do
+ order = :attribute.desc
+ order.should be_descending
+ end
+
+ it 'creates functions' do
+ function = :function.func
+ function.should be_a Squeel::Nodes::Function
+ end
+
+ it 'creates inner joins' do
+ join = :join.inner
+ join.should be_a Squeel::Nodes::Join
+ join.type.should eq Arel::InnerJoin
+ end
+
+ it 'creates outer joins' do
+ join = :join.outer
+ join.should be_a Squeel::Nodes::Join
+ join.type.should eq Arel::OuterJoin
+ end
+
+end
5 spec/helpers/squeel_helper.rb
@@ -0,0 +1,5 @@
+module SqueelHelper
+ def dsl(&block)
+ Squeel::DSL.evaluate(&block)
+ end
+end
30 spec/spec_helper.rb
@@ -0,0 +1,30 @@
+require 'machinist/active_record'
+require 'sham'
+require 'faker'
+
+Dir[File.expand_path('../{helpers,support,blueprints}/*.rb', __FILE__)].each do |f|
+ require f
+end
+
+Sham.define do
+ name { Faker::Name.name }
+ title { Faker::Lorem.sentence }
+ body { Faker::Lorem.paragraph }
+ salary {|index| 30000 + (index * 1000)}
+ tag_name { Faker::Lorem.words(3).join(' ') }
+ note { Faker::Lorem.words(7).join(' ') }
+end
+
+RSpec.configure do |config|
+ config.before(:suite) { Schema.create }
+ config.before(:all) { Sham.reset(:before_all) }
+ config.before(:each) { Sham.reset(:before_each) }
+
+ config.include SqueelHelper
+end
+
+require 'squeel'
+
+Squeel.configure do |config|
+ config.load_core_extensions :hash, :symbol
+end
18 spec/squeel/adapters/active_record/join_association_spec.rb
@@ -0,0 +1,18 @@
+module Squeel
+ module Adapters
+ module ActiveRecord
+ describe JoinAssociation do
+ before do
+ @jd = ::ActiveRecord::Associations::JoinDependency.new(Note, {}, [])
+ @notable = Note.reflect_on_association(:notable)
+ end
+
+ it 'accepts a 4th parameter to set a polymorphic class' do
+ join_association = JoinAssociation.new(@notable, @jd, @jd.join_base, Article)
+ join_association.reflection.klass.should eq Article
+ end
+
+ end
+ end
+ end
+end
60 spec/squeel/adapters/active_record/join_depdendency_spec.rb
@@ -0,0 +1,60 @@
+module Squeel
+ module Adapters
+ module ActiveRecord
+ describe JoinDependency do
+ before do
+ @jd = ::ActiveRecord::Associations::JoinDependency.new(Person, {}, [])
+ end
+
+ it 'joins with symbols' do
+ @jd.send(:build, :articles => :comments)
+ @jd.join_associations.should have(2).associations
+ @jd.join_associations.each do |association|
+ association.join_type.should eq Arel::InnerJoin
+ end
+ end
+
+ it 'joins with stubs' do
+ @jd.send(:build, Nodes::Stub.new(:articles) => Nodes::Stub.new(:comments))
+ @jd.join_associations.should have(2).associations
+ @jd.join_associations.each do |association|
+ association.join_type.should eq Arel::InnerJoin
+ end
+ @jd.join_associations[0].table_name.should eq 'articles'
+ @jd.join_associations[1].table_name.should eq 'comments'
+ end
+
+ it 'joins with key paths' do
+ @jd.send(:build, dsl{children.children.parent})
+ @jd.join_associations.should have(3).associations
+ @jd.join_associations.each do |association|
+ association.join_type.should eq Arel::InnerJoin
+ end
+ @jd.join_associations[0].aliased_table_name.should eq 'children_people'
+ @jd.join_associations[1].aliased_table_name.should eq 'children_people_2'
+ @jd.join_associat