├── .gitignore ├── test ├── ruby_test.rb ├── count_test.rb ├── profiler.rb ├── helper.rb ├── source_test.rb ├── slice_test.rb ├── chaining_test.rb ├── sort_test.rb ├── join_test.rb ├── benchmark.rb ├── types_test.rb ├── enumerable_test.rb └── select_test.rb ├── lib └── ambition │ └── adapters │ ├── active_record.rb │ └── active_record │ ├── slice.rb │ ├── statements.rb │ ├── sort.rb │ ├── base.rb │ ├── query.rb │ └── select.rb ├── Manifest ├── Rakefile ├── LICENSE ├── ambitious-activerecord.gemspec └── README /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | -------------------------------------------------------------------------------- /test/ruby_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "ActiveRecord Adapter" do 4 | context "Inline Ruby" do 5 | xspecify "should know what to return" do 6 | name = 'David' 7 | sql = User.select { |u| name.nil? || u.name == name }.to_s 8 | sql.should == "SELECT * FROM users WHERE (users.name = 'David')" 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/count_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "ActiveRecord Adapter" do 4 | context "Count" do 5 | setup do 6 | hash = { :conditions => "users.name = 'jon'" } 7 | User.expects(:count).with(hash) 8 | @sql = User.select { |m| m.name == 'jon' } 9 | end 10 | 11 | specify "size" do 12 | @sql.size 13 | end 14 | 15 | specify "length" do 16 | @sql.length 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/ambition/adapters/active_record.rb: -------------------------------------------------------------------------------- 1 | require 'ambition' 2 | require 'active_record' 3 | require 'ambition/adapters/active_record/query' 4 | require 'ambition/adapters/active_record/base' 5 | require 'ambition/adapters/active_record/select' 6 | require 'ambition/adapters/active_record/sort' 7 | require 'ambition/adapters/active_record/slice' 8 | require 'ambition/adapters/active_record/statements' 9 | 10 | ActiveRecord::Base.extend Ambition::API 11 | ActiveRecord::Base.ambition_adapter = Ambition::Adapters::ActiveRecord 12 | -------------------------------------------------------------------------------- /lib/ambition/adapters/active_record/slice.rb: -------------------------------------------------------------------------------- 1 | module Ambition 2 | module Adapters 3 | module ActiveRecord 4 | class Slice < Base 5 | def slice(start, length=nil) 6 | if start.is_a? Range 7 | length = start.end 8 | length -= 1 if start.exclude_end? 9 | start = start.first - 1 10 | length -= start 11 | end 12 | out = "LIMIT #{length} " 13 | out << "OFFSET #{start}" if start.to_i.nonzero? 14 | out 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /Manifest: -------------------------------------------------------------------------------- 1 | lib/ambition/adapters/active_record/base.rb 2 | lib/ambition/adapters/active_record/query.rb 3 | lib/ambition/adapters/active_record/select.rb 4 | lib/ambition/adapters/active_record/slice.rb 5 | lib/ambition/adapters/active_record/sort.rb 6 | lib/ambition/adapters/active_record/statements.rb 7 | lib/ambition/adapters/active_record.rb 8 | test/benchmark.rb 9 | test/chaining_test.rb 10 | test/count_test.rb 11 | test/enumerable_test.rb 12 | test/helper.rb 13 | test/join_test.rb 14 | test/profiler.rb 15 | test/ruby_test.rb 16 | test/select_test.rb 17 | test/slice_test.rb 18 | test/sort_test.rb 19 | test/source_test.rb 20 | test/types_test.rb 21 | Manifest 22 | -------------------------------------------------------------------------------- /lib/ambition/adapters/active_record/statements.rb: -------------------------------------------------------------------------------- 1 | module Ambition 2 | module Adapters 3 | module ActiveRecord 4 | module DatabaseStatements 5 | def self.const_missing(*args) 6 | Abstract 7 | end 8 | 9 | class Abstract 10 | def regexp(regexp) 11 | 'REGEXP' 12 | end 13 | 14 | def not_regexp(regexp) 15 | 'NOT REGEXP' 16 | end 17 | end 18 | 19 | class PostgreSQL < Abstract 20 | def regexp(regexp) 21 | if regexp.options == 1 22 | '~*' 23 | else 24 | '~' 25 | end 26 | end 27 | 28 | def not_regexp(regexp) 29 | if regexp.options == 1 30 | '!~*' 31 | else 32 | '!~' 33 | end 34 | end 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | 3 | Version = '0.1.3' 4 | 5 | begin 6 | require 'rubygems' 7 | gem 'echoe', '>=2.7' 8 | ENV['RUBY_FLAGS'] = "" 9 | require 'echoe' 10 | 11 | Echoe.new('ambitious-activerecord') do |p| 12 | p.dependencies << 'activerecord >=1.15.6' 13 | p.summary = "An ambitious adapter for ActiveRecord" 14 | p.author = 'Chris Wanstrath' 15 | p.email = "chris@ozmm.org" 16 | 17 | p.project = 'ambition' 18 | p.url = "http://ambition.rubyforge.org/" 19 | p.test_pattern = 'test/*_test.rb' 20 | p.version = Version 21 | p.dependencies << 'ambition >=0.5.3' 22 | end 23 | 24 | rescue LoadError 25 | puts "Not doing any of the Echoe gemmy stuff, because you don't have the specified gem versions" 26 | 27 | require 'rake/testtask' 28 | Rake::TestTask.new do |t| 29 | t.pattern = "test/*_test.rb" 30 | end 31 | end 32 | 33 | desc 'Install as a gem' 34 | task :install_gem do 35 | puts `rake manifest package && gem install pkg/ambitious-activerecord-#{Version}.gem` 36 | end 37 | 38 | task :default => :test 39 | 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2007 Chris Wanstrath 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /lib/ambition/adapters/active_record/sort.rb: -------------------------------------------------------------------------------- 1 | module Ambition 2 | module Adapters 3 | module ActiveRecord 4 | class Sort < Base 5 | def sort_by(method) 6 | "#{owner.table_name}.#{quote_column_name method}" 7 | end 8 | 9 | def reverse_sort_by(method) 10 | "#{owner.table_name}.#{quote_column_name method} DESC" 11 | end 12 | 13 | def chained_sort_by(receiver, method) 14 | if reflection = owner.reflections[receiver] 15 | stash[:include] ||= [] 16 | stash[:include] << receiver 17 | "#{reflection.table_name}.#{quote_column_name method}" 18 | else 19 | raise [ receiver, method ].inspect 20 | end 21 | end 22 | 23 | def chained_reverse_sort_by(receiver, method) 24 | if reflection = owner.reflections[receiver] 25 | stash[:include] ||= [] 26 | stash[:include] << receiver 27 | "#{reflection.table_name}.#{quote_column_name method} DESC" 28 | else 29 | raise [ receiver, method ].inspect 30 | end 31 | end 32 | 33 | def to_proc(symbol) 34 | "#{owner.table_name}.#{symbol}" 35 | end 36 | 37 | def rand 38 | 'RAND()' 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/profiler.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.dirname(__FILE__) + '/../../../lib' 2 | require 'rubygems' 3 | require 'ambition' 4 | require 'ambition/adapters/active_record' 5 | require 'ruby-prof' 6 | 7 | class User < ActiveRecord::Base 8 | def self.reflections 9 | return @reflections if @reflections 10 | @reflections = {} 11 | @reflections[:ideas] = Reflection.new(:has_many, 'user_id', :ideas, 'ideas') 12 | @reflections[:invites] = Reflection.new(:has_many, 'referrer_id', :invites, 'invites') 13 | @reflections[:profile] = Reflection.new(:has_one, 'user_id', :profile, 'profiles') 14 | @reflections[:account] = Reflection.new(:belongs_to, 'account_id', :account, 'accounts') 15 | @reflections 16 | end 17 | 18 | def self.table_name 19 | 'users' 20 | end 21 | end 22 | 23 | class Reflection < Struct.new(:macro, :primary_key_name, :name, :table_name) 24 | end 25 | 26 | result = RubyProf.profile do 27 | 1000.times do 28 | User.select { |u| (u.id == 20 && u.age > 20) || u.profile.name == 'Jon' }.sort_by { |u| [u.id, -u.name] }.first(20).to_hash 29 | end 30 | end 31 | 32 | printer = RubyProf::FlatPrinter.new(result) 33 | #printer = RubyProf::GraphPrinter.new(result) 34 | printer.print(STDOUT, 0) 35 | 36 | puts User.select { |u| (u.id == 20 && u.age > 20) || u.profile.name == 'Jon' }.sort_by { |u| [u.id, -u.name] }.first(20).to_hash.inspect 37 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | %w( rubygems test/spec mocha redgreen English ).each { |f| require f } 2 | 3 | $LOAD_PATH.unshift *[ File.dirname(__FILE__) + '/../lib', File.dirname(__FILE__) + '/../../../lib' ] 4 | require 'ambition/adapters/active_record' 5 | 6 | class User < ActiveRecord::Base 7 | def self.reflections 8 | return @reflections if @reflections 9 | @reflections = {} 10 | @reflections[:ideas] = Reflection.new(:has_many, 'user_id', :ideas, 'ideas') 11 | @reflections[:invites] = Reflection.new(:has_many, 'referrer_id', :invites, 'invites') 12 | @reflections[:profile] = Reflection.new(:has_one, 'user_id', :profile, 'profiles') 13 | @reflections[:account] = Reflection.new(:belongs_to, 'account_id', :account, 'accounts') 14 | @reflections 15 | end 16 | 17 | def self.table_name 18 | 'users' 19 | end 20 | end 21 | 22 | class Reflection < Struct.new(:macro, :primary_key_name, :name, :table_name) 23 | end 24 | 25 | module ActiveRecord 26 | module ConnectionAdapters 27 | class MysqlAdapter 28 | def initialize(*args) 29 | super 30 | end 31 | 32 | def connect(*args) 33 | true 34 | end 35 | end 36 | 37 | class PostgreSQLAdapter 38 | def connect(*args) 39 | true 40 | end 41 | class PGError; end 42 | end 43 | 44 | class FakeAdapter < AbstractAdapter 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/source_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | require 'ostruct' 3 | 4 | context "Setting the ambition_source" do 5 | setup do 6 | # @users = [ 7 | # OpenStruct.new(:name => 'Chris', :age => 22), 8 | # OpenStruct.new(:name => 'PJ', :age => 24), 9 | # OpenStruct.new(:name => 'Kevin', :age => 23), 10 | # OpenStruct.new(:name => '_why', :age => 65) 11 | # ] 12 | # User.ambition_source = @users 13 | end 14 | 15 | teardown do 16 | # User.ambition_source = nil 17 | end 18 | 19 | xspecify "should run all selects / detects against that collection" do 20 | User.detect { |u| u.name == 'Chris' }.should == @users.first 21 | end 22 | 23 | xspecify "should run all sorts against that collection" do 24 | User.sort_by { |u| -u.age }.entries.should == @users.sort_by { |u| -u.age } 25 | end 26 | 27 | xspecify "should chain successfully" do 28 | User.select { |u| u.age > 22 }.sort_by { |u| -u.age }.entries.should == [ @users[3], @users[1], @users[2] ] 29 | end 30 | 31 | xspecify "should be able to revert to normal" do 32 | block = proc { User.select { |m| m.name == 'PJ' }.first } 33 | 34 | User.expects(:find).never 35 | block.call.should == @users[1] 36 | 37 | conditions = { :conditions => "users.name = 'PJ'", :limit => 1 } 38 | User.expects(:find).with(:first, conditions) 39 | 40 | User.ambition_source = nil 41 | block.call 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/slice_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "ActiveRecord Adapter" do 4 | context "Slice" do 5 | setup do 6 | @sql = User.select { |m| m.name == 'jon' } 7 | end 8 | 9 | specify "first" do 10 | conditions = { :conditions => "users.name = 'jon'", :limit => 1 } 11 | User.expects(:find).with(:all, conditions) 12 | @sql.first 13 | end 14 | 15 | specify "first with argument" do 16 | conditions = { :conditions => "users.name = 'jon'", :limit => 5 } 17 | User.expects(:find).with(:all, conditions) 18 | @sql.first(5).entries 19 | end 20 | 21 | specify "[] with two elements" do 22 | conditions = { :conditions => "users.name = 'jon'", :limit => 20, :offset => 10 } 23 | User.expects(:find).with(:all, conditions) 24 | @sql[10, 20].entries 25 | 26 | conditions = { :conditions => "users.name = 'jon'", :limit => 20, :offset => 20 } 27 | User.expects(:find).with(:all, conditions) 28 | @sql[20, 20].entries 29 | end 30 | 31 | specify "slice is an alias of []" do 32 | conditions = { :conditions => "users.name = 'jon'", :limit => 20, :offset => 10 } 33 | User.expects(:find).with(:all, conditions) 34 | @sql.slice(10, 20).entries 35 | end 36 | 37 | specify "[] with range" do 38 | conditions = { :conditions => "users.name = 'jon'", :limit => 10, :offset => 10 } 39 | User.expects(:find).with(:all, conditions) 40 | @sql[11..20].entries 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/ambition/adapters/active_record/base.rb: -------------------------------------------------------------------------------- 1 | require 'active_record/connection_adapters/abstract/quoting' 2 | 3 | module Ambition 4 | module Adapters 5 | module ActiveRecord 6 | class Base 7 | include ::ActiveRecord::ConnectionAdapters::Quoting 8 | 9 | def sanitize(value) 10 | if value.is_a? Array 11 | return value.map { |v| sanitize(v) }.join(', ') 12 | end 13 | 14 | case value 15 | when true, 'true' 16 | '1' 17 | when false, 'false' 18 | '0' 19 | when Regexp 20 | "'#{value.source}'" 21 | else 22 | if active_connection? 23 | ::ActiveRecord::Base.connection.quote(value) 24 | else 25 | quote(value) 26 | end 27 | end 28 | rescue 29 | "'#{value}'" 30 | end 31 | 32 | def quote_column_name(value) 33 | if active_connection? 34 | ::ActiveRecord::Base.connection.quote_column_name(value) 35 | else 36 | value.to_s 37 | end 38 | end 39 | 40 | def active_connection? 41 | ::ActiveRecord::Base.active_connection_name 42 | end 43 | 44 | def dbadapter_name 45 | ::ActiveRecord::Base.connection.adapter_name 46 | rescue ::ActiveRecord::ConnectionNotEstablished 47 | 'Abstract' 48 | end 49 | 50 | def statement(*args) 51 | @statement_instance ||= DatabaseStatements.const_get(dbadapter_name).new 52 | @statement_instance.send(*args) 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/ambition/adapters/active_record/query.rb: -------------------------------------------------------------------------------- 1 | module Ambition 2 | module Adapters 3 | module ActiveRecord 4 | class Query 5 | @@select = 'SELECT * FROM %s %s' 6 | 7 | def kick 8 | owner.find(:all, to_hash) 9 | end 10 | 11 | def size 12 | owner.count(to_hash) 13 | end 14 | alias_method :length, :size 15 | 16 | def to_hash 17 | hash = {} 18 | 19 | unless (where = clauses[:select]).blank? 20 | hash[:conditions] = Array(where) 21 | hash[:conditions] *= ' AND ' 22 | end 23 | 24 | if order = clauses[:sort] 25 | hash[:order] = order.join(', ') 26 | end 27 | 28 | if Array(clauses[:slice]).last =~ /LIMIT (\d+)/ 29 | hash[:limit] = $1.to_i 30 | end 31 | 32 | if Array(clauses[:slice]).last =~ /OFFSET (\d+)/ 33 | hash[:offset] = $1.to_i 34 | end 35 | 36 | hash[:include] = stash[:include] if stash[:include] 37 | 38 | hash 39 | end 40 | 41 | def to_s 42 | hash = to_hash 43 | 44 | raise "Sorry, I can't construct SQL with complex joins (yet)" unless hash[:include].blank? 45 | 46 | sql = [] 47 | sql << "WHERE #{hash[:conditions]}" unless hash[:conditions].blank? 48 | sql << "ORDER BY #{hash[:order]}" unless hash[:order].blank? 49 | sql << clauses[:slice].last unless hash[:slice].blank? 50 | 51 | @@select % [ owner.table_name, sql.join(' ') ] 52 | end 53 | alias_method :to_sql, :to_s 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test/chaining_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "ActiveRecord Adapter" do 4 | context "Chaining" do 5 | specify "should join selects with AND" do 6 | sql = User.select { |m| m.name == 'jon' } 7 | sql = sql.select { |m| m.age == 22 } 8 | sql.to_s.should == "SELECT * FROM users WHERE users.name = 'jon' AND users.age = 22" 9 | end 10 | 11 | specify "should join sort_bys with a comma" do 12 | sql = User.select { |m| m.name == 'jon' } 13 | sql = sql.sort_by { |m| m.name } 14 | sql = sql.sort_by { |m| m.age } 15 | sql.to_s.should == "SELECT * FROM users WHERE users.name = 'jon' ORDER BY users.name, users.age" 16 | end 17 | 18 | specify "should join selects and sorts intelligently" do 19 | sql = User.select { |m| m.name == 'jon' } 20 | sql = sql.select { |m| m.age == 22 } 21 | sql = sql.sort_by { |m| -m.name } 22 | sql = sql.sort_by { |m| m.age } 23 | sql.to_s.should == "SELECT * FROM users WHERE users.name = 'jon' AND users.age = 22 ORDER BY users.name DESC, users.age" 24 | end 25 | 26 | specify "should join lots of selects and sorts intelligently" do 27 | sql = User.select { |m| m.name == 'jon' } 28 | sql = sql.select { |m| m.age == 22 } 29 | sql = sql.sort_by { |m| m.name } 30 | sql = sql.select { |m| m.power == true } 31 | sql = sql.sort_by { |m| m.email } 32 | sql = sql.select { |m| m.admin == true && m.email == 'chris@ozmm.org' } 33 | sql.to_s.should == "SELECT * FROM users WHERE users.name = 'jon' AND users.age = 22 AND users.power = 1 AND (users.admin = 1 AND users.email = 'chris@ozmm.org') ORDER BY users.name, users.email" 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/sort_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "ActiveRecord Adapter" do 4 | context "Sort" do 5 | setup do 6 | @sql = User.select { |m| m.name == 'jon' } 7 | end 8 | 9 | specify "simple order" do 10 | string = @sql.sort_by { |m| m.name }.to_s 11 | string.should == "SELECT * FROM users WHERE users.name = 'jon' ORDER BY users.name" 12 | end 13 | 14 | specify "simple combined order" do 15 | string = @sql.sort_by { |m| [ m.name, m.age ] }.to_s 16 | string.should == "SELECT * FROM users WHERE users.name = 'jon' ORDER BY users.name, users.age" 17 | end 18 | 19 | specify "simple combined order with single reverse" do 20 | string = @sql.sort_by { |m| [ m.name, -m.age ] }.to_s 21 | string.should == "SELECT * FROM users WHERE users.name = 'jon' ORDER BY users.name, users.age DESC" 22 | end 23 | 24 | specify "simple combined order with two reverses" do 25 | string = @sql.sort_by { |m| [ -m.name, -m.age ] }.to_s 26 | string.should == "SELECT * FROM users WHERE users.name = 'jon' ORDER BY users.name DESC, users.age DESC" 27 | end 28 | 29 | specify "reverse order with -" do 30 | string = @sql.sort_by { |m| -m.age }.to_s 31 | string.should == "SELECT * FROM users WHERE users.name = 'jon' ORDER BY users.age DESC" 32 | end 33 | 34 | xspecify "reverse order with #reverse" do 35 | # TODO: not implemented 36 | string = @sql.sort_by { |m| m.age }.reverse.to_s 37 | string.should == "SELECT * FROM users WHERE users.name = 'jon' ORDER BY users.age DESC" 38 | end 39 | 40 | specify "random order" do 41 | string = @sql.sort_by { rand }.to_s 42 | string.should == "SELECT * FROM users WHERE users.name = 'jon' ORDER BY RAND()" 43 | end 44 | 45 | specify "non-existent method to sort by" do 46 | should.raise(NoMethodError) { @sql.sort_by { foo }.to_s } 47 | end 48 | 49 | specify "Symbol#to_proc" do 50 | string = User.sort_by(&:name).to_s 51 | string.should == "SELECT * FROM users ORDER BY users.name" 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/join_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "ActiveRecord Adapter" do 4 | context "Joins" do 5 | specify "simple == on an association" do 6 | sql = User.select { |m| m.account.email == 'chris@ozmm.org' } 7 | sql.to_hash.should == { 8 | :conditions => "accounts.email = 'chris@ozmm.org'", 9 | :include => [:account] 10 | } 11 | end 12 | 13 | specify "simple mixed == on an association" do 14 | sql = User.select { |m| m.name == 'chris' && m.account.email == 'chris@ozmm.org' } 15 | sql.to_hash.should == { 16 | :conditions => "(users.name = 'chris' AND accounts.email = 'chris@ozmm.org')", 17 | :include => [:account] 18 | } 19 | end 20 | 21 | specify "multiple associations" do 22 | sql = User.select { |m| m.ideas.title == 'New Freezer' || m.invites.email == 'pj@hyett.com' } 23 | sql.to_hash.should == { 24 | :conditions => "(ideas.title = 'New Freezer' OR invites.email = 'pj@hyett.com')", 25 | :include => [:ideas, :invites] 26 | } 27 | end 28 | 29 | specify "belongs_to" do 30 | sql = User.select { |m| m.account.id > 20 } 31 | sql.to_hash.should == { 32 | :conditions => "accounts.id > 20", 33 | :include => [:account] 34 | } 35 | end 36 | 37 | specify "complex joins have no to_s" do 38 | sql = User.select { |m| m.account.id > 20 } 39 | should.raise { sql.to_s } 40 | end 41 | 42 | specify "non-existant associations" do 43 | should.raise { User.select { |m| m.liquor.brand == 'Jack' } } 44 | end 45 | 46 | specify "in order" do 47 | sql = User.sort_by { |m| m.ideas.title } 48 | sql.to_hash.should == { 49 | :order => "ideas.title", 50 | :include => [:ideas] 51 | } 52 | end 53 | 54 | specify "in a more complex order" do 55 | sql = User.sort_by { |m| [ m.ideas.title, -m.invites.email ] } 56 | sql.to_hash.should == { 57 | :order => "ideas.title, invites.email DESC", 58 | :include => [:ideas, :invites] 59 | } 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/benchmark.rb: -------------------------------------------------------------------------------- 1 | unless defined? Test 2 | $:.unshift File.dirname(__FILE__) + '/../../../lib' 3 | %w( rubygems ambition/adapters/active_record benchmark ).each { |f| require f } 4 | 5 | class User < ActiveRecord::Base 6 | def self.reflections 7 | return @reflections if @reflections 8 | @reflections = {} 9 | @reflections[:ideas] = Reflection.new(:has_many, 'user_id', :ideas, 'ideas') 10 | @reflections[:invites] = Reflection.new(:has_many, 'referrer_id', :invites, 'invites') 11 | @reflections[:profile] = Reflection.new(:has_one, 'user_id', :profile, 'profiles') 12 | @reflections[:account] = Reflection.new(:belongs_to, 'account_id', :account, 'accounts') 13 | @reflections 14 | end 15 | 16 | def self.table_name 17 | 'users' 18 | end 19 | end 20 | 21 | class Reflection < Struct.new(:macro, :primary_key_name, :name, :table_name) 22 | end 23 | 24 | Times = 10000 25 | 26 | Benchmark.bm(30) do |x| 27 | x.report 'simple select' do 28 | Times.times do 29 | User.select { |u| u.id == 20 }.to_hash 30 | end 31 | end 32 | 33 | x.report 'simple select w/ eval' do 34 | Times.times do 35 | User.select { |u| u.created_at == Time.now }.to_hash 36 | end 37 | end 38 | 39 | x.report 'dual select' do 40 | Times.times do 41 | User.select { |u| u.id == 20 && u.age > 20 }.to_hash 42 | end 43 | end 44 | 45 | x.report 'join select' do 46 | Times.times do 47 | User.select { |u| u.id == 20 && u.ideas.name =~ /stuff/ }.to_hash 48 | end 49 | end 50 | 51 | x.report 'dual select w/ sort' do 52 | Times.times do 53 | User.select { |u| u.id == 20 && u.age > 20 }.sort_by { |u| u.id }.to_hash 54 | end 55 | end 56 | 57 | x.report 'dual select w/ sort & first' do 58 | Times.times do 59 | User.select { |u| u.id == 20 && u.age > 20 }.sort_by { |u| u.id }.first(20).to_hash 60 | end 61 | end 62 | 63 | x.report "it's complicated" do 64 | Times.times do 65 | User.select { |u| (u.id == 20 && u.age > 20) || u.profile.name == 'Jon' }.sort_by { |u| [u.id, -u.name] }.first(20).to_hash 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /test/types_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | ## 4 | # Once dynamically, once hardcoded 5 | context "Different types" do 6 | types_hash = { 7 | 'string' => "'string'", 8 | :symbol => "'--- :symbol\n'", 9 | 1 => '1', 10 | 1.2 => '1.2', 11 | true => '1', 12 | false => '0', 13 | Time.now => "'#{Time.now.to_s(:db)}'", 14 | DateTime.now => "'#{DateTime.now.to_s(:db)}'", 15 | Date.today => "'#{Date.today.to_s(:db)}'" 16 | } 17 | 18 | types_hash.each do |type, translation| 19 | specify "simple using #{type}" do 20 | sql = User.select { |m| m.name == type }.to_s 21 | sql.should == "SELECT * FROM users WHERE users.name = #{translation}" 22 | end 23 | end 24 | 25 | specify "float" do 26 | sql = User.select { |m| m.name == 1.2 }.to_s 27 | sql.should == "SELECT * FROM users WHERE users.name = 1.2" 28 | end 29 | 30 | specify "integer" do 31 | sql = User.select { |m| m.name == 1 }.to_s 32 | sql.should == "SELECT * FROM users WHERE users.name = 1" 33 | end 34 | 35 | specify "true" do 36 | sql = User.select { |m| m.name == true }.to_s 37 | sql.should == "SELECT * FROM users WHERE users.name = 1" 38 | end 39 | 40 | specify "false" do 41 | sql = User.select { |m| m.name == false }.to_s 42 | sql.should == "SELECT * FROM users WHERE users.name = 0" 43 | end 44 | 45 | specify "nil" do 46 | sql = User.select { |m| m.name == nil }.to_s 47 | sql.should == "SELECT * FROM users WHERE users.name IS NULL" 48 | end 49 | 50 | specify "not nil" do 51 | sql = User.select { |m| m.name != nil }.to_s 52 | sql.should == "SELECT * FROM users WHERE users.name IS NOT NULL" 53 | end 54 | 55 | specify "nil?" do 56 | sql = User.select { |m| m.name.nil? }.to_s 57 | sql.should == "SELECT * FROM users WHERE users.name IS NULL" 58 | end 59 | 60 | specify "!nil?" do 61 | sql = User.select { |m| !m.name.nil? }.to_s 62 | sql.should == "SELECT * FROM users WHERE users.name IS NOT NULL" 63 | end 64 | 65 | specify "Time" do 66 | sql = User.select { |m| m.name == Time.now }.to_s 67 | sql.should == "SELECT * FROM users WHERE users.name = '#{Time.now.to_s(:db)}'" 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/ambition/adapters/active_record/select.rb: -------------------------------------------------------------------------------- 1 | module Ambition 2 | module Adapters 3 | module ActiveRecord 4 | class Select < Base 5 | def call(method) 6 | "#{owner.table_name}.#{quote_column_name method}" 7 | end 8 | 9 | def chained_call(*methods) 10 | if reflection = owner.reflections[methods.first] 11 | stash[:include] ||= [] 12 | stash[:include] << methods.first 13 | "#{reflection.table_name}.#{quote_column_name methods.last}" 14 | elsif respond_to? methods[1] 15 | send(methods[1], methods.first) 16 | else 17 | raise "I don't understand: #{methods.inspect}" 18 | end 19 | end 20 | 21 | def both(left, right) 22 | "(#{left} AND #{right})" 23 | end 24 | 25 | def either(left, right) 26 | "(#{left} OR #{right})" 27 | end 28 | 29 | def ==(left, right) 30 | if right.nil? 31 | "#{left} IS NULL" 32 | else 33 | "#{left} = #{sanitize right}" 34 | end 35 | end 36 | 37 | # != 38 | def not_equal(left, right) 39 | if right.nil? 40 | "#{left} IS NOT NULL" 41 | else 42 | "#{left} <> #{sanitize right}" 43 | end 44 | end 45 | 46 | def =~(left, right) 47 | if right.is_a? Regexp 48 | "#{left} #{statement(:regexp, right)} #{sanitize right}" 49 | else 50 | "#{left} LIKE #{sanitize right}" 51 | end 52 | end 53 | 54 | # !~ 55 | def not_regexp(left, right) 56 | if right.is_a? Regexp 57 | "#{left} #{statement(:not_regexp, right)} #{sanitize right}" 58 | else 59 | "#{left} NOT LIKE #{sanitize right}" 60 | end 61 | end 62 | 63 | def <(left, right) 64 | "#{left} < #{sanitize right}" 65 | end 66 | 67 | def >(left, right) 68 | "#{left} > #{sanitize right}" 69 | end 70 | 71 | def >=(left, right) 72 | "#{left} >= #{sanitize right}" 73 | end 74 | 75 | def <=(left, right) 76 | "#{left} <= #{sanitize right}" 77 | end 78 | 79 | def include?(left, right) 80 | left = left.map { |element| sanitize element }.join(', ') 81 | "#{right} IN (#{left})" 82 | end 83 | 84 | def nil?(column) 85 | left = "#{owner.table_name}.#{quote_column_name column}" 86 | negated? ? not_equal(left, nil) : self.==(left, nil) 87 | end 88 | 89 | def downcase(column) 90 | "LOWER(#{owner.table_name}.#{quote_column_name column})" 91 | end 92 | 93 | def upcase(column) 94 | "UPPER(#{owner.table_name}.#{quote_column_name column})" 95 | end 96 | end 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /test/enumerable_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "ActiveRecord Adapter" do 4 | context "Each" do 5 | specify "simple ==" do 6 | hash = { :conditions => "users.age = 21" } 7 | User.expects(:find).with(:all, hash).returns([]) 8 | User.select { |m| m.age == 21 }.each do |user| 9 | puts user.name 10 | end 11 | end 12 | 13 | specify "limit and conditions" do 14 | hash = { :limit => 5, :conditions => "users.age = 21" } 15 | User.expects(:find).with(:all, hash).returns([]) 16 | User.select { |m| m.age == 21 }.first(5).each do |user| 17 | puts user.name 18 | end 19 | end 20 | 21 | specify "limit and conditions and order" do 22 | hash = { :limit => 5, :conditions => "users.age = 21", :order => 'users.name' } 23 | User.expects(:find).with(:all, hash).returns([]) 24 | User.select { |m| m.age == 21 }.sort_by { |m| m.name }.first(5).each do |user| 25 | puts user.name 26 | end 27 | end 28 | 29 | specify "limit and order" do 30 | hash = { :limit => 5, :order => 'users.name' } 31 | User.expects(:find).with(:all, hash).returns([]) 32 | User.sort_by { |m| m.name }.first(5).each do |user| 33 | puts user.name 34 | end 35 | end 36 | end 37 | 38 | context "Enumerable Methods" do 39 | specify "map" do 40 | hash = { :conditions => "users.age = 21" } 41 | User.expects(:find).with(:all, hash).returns([]) 42 | User.select { |m| m.age == 21 }.map { |u| u.name } 43 | end 44 | 45 | specify "each_with_index" do 46 | hash = { :conditions => "users.age = 21" } 47 | User.expects(:find).with(:all, hash).returns([]) 48 | User.select { |m| m.age == 21 }.each_with_index do |user, i| 49 | puts "#{i}: #{user.name}" 50 | end 51 | end 52 | 53 | specify "any?" do 54 | User.expects(:count).with(:conditions => "users.age > 21").returns(1) 55 | User.any? { |u| u.age > 21 }.should == true 56 | end 57 | 58 | specify "all?" do 59 | User.expects(:count).at_least_once.returns(10, 20) 60 | User.all? { |u| u.age > 21 }.should == false 61 | 62 | User.expects(:count).at_least_once.returns(10, 10) 63 | User.all? { |u| u.age > 21 }.should == true 64 | end 65 | 66 | specify "empty?" do 67 | User.expects(:count).with(:conditions => "users.age > 21").returns(1) 68 | User.select { |u| u.age > 21 }.empty?.should.equal false 69 | 70 | User.expects(:count).with(:conditions => "users.age > 21").returns(0) 71 | User.select { |u| u.age > 21 }.empty?.should.equal true 72 | end 73 | 74 | specify "entries" do 75 | User.expects(:find).with(:all, {}) 76 | User.entries 77 | 78 | hash = { :conditions => "users.age = 21" } 79 | User.expects(:find).with(:all, hash).returns([]) 80 | User.select { |m| m.age == 21 }.entries 81 | end 82 | 83 | specify "to_a" do 84 | User.expects(:find).with(:all, {}) 85 | User.to_a 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /ambitious-activerecord.gemspec: -------------------------------------------------------------------------------- 1 | 2 | # Gem::Specification for Ambitious-activerecord-0.1.3 3 | # Originally generated by Echoe 4 | 5 | Gem::Specification.new do |s| 6 | s.name = %q{ambitious-activerecord} 7 | s.version = "0.1.3" 8 | 9 | s.specification_version = 2 if s.respond_to? :specification_version= 10 | 11 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 12 | s.authors = ["Chris Wanstrath"] 13 | s.date = %q{2008-04-26} 14 | s.description = %q{An ambitious adapter for ActiveRecord} 15 | s.email = %q{chris@ozmm.org} 16 | s.extra_rdoc_files = ["lib/ambition/adapters/active_record/base.rb", "lib/ambition/adapters/active_record/query.rb", "lib/ambition/adapters/active_record/select.rb", "lib/ambition/adapters/active_record/slice.rb", "lib/ambition/adapters/active_record/sort.rb", "lib/ambition/adapters/active_record/statements.rb", "lib/ambition/adapters/active_record.rb"] 17 | s.files = ["lib/ambition/adapters/active_record/base.rb", "lib/ambition/adapters/active_record/query.rb", "lib/ambition/adapters/active_record/select.rb", "lib/ambition/adapters/active_record/slice.rb", "lib/ambition/adapters/active_record/sort.rb", "lib/ambition/adapters/active_record/statements.rb", "lib/ambition/adapters/active_record.rb", "test/benchmark.rb", "test/chaining_test.rb", "test/count_test.rb", "test/enumerable_test.rb", "test/helper.rb", "test/join_test.rb", "test/profiler.rb", "test/ruby_test.rb", "test/select_test.rb", "test/slice_test.rb", "test/sort_test.rb", "test/source_test.rb", "test/types_test.rb", "Manifest", "ambitious-activerecord.gemspec"] 18 | s.has_rdoc = true 19 | s.homepage = %q{http://ambition.rubyforge.org/} 20 | s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Ambitious-activerecord", "--main", "README"] 21 | s.require_paths = ["lib"] 22 | s.rubyforge_project = %q{ambition} 23 | s.rubygems_version = %q{1.0.1} 24 | s.summary = %q{An ambitious adapter for ActiveRecord} 25 | s.test_files = ["test/chaining_test.rb", "test/count_test.rb", "test/enumerable_test.rb", "test/join_test.rb", "test/ruby_test.rb", "test/select_test.rb", "test/slice_test.rb", "test/sort_test.rb", "test/source_test.rb", "test/types_test.rb"] 26 | 27 | s.add_dependency(%q, [">= 1.15.6"]) 28 | s.add_dependency(%q, [">= 0.5.3"]) 29 | end 30 | 31 | 32 | # # Original Rakefile source (requires the Echoe gem): 33 | # 34 | # require 'rake' 35 | # 36 | # Version = '0.1.3' 37 | # 38 | # begin 39 | # require 'rubygems' 40 | # gem 'echoe', '>=2.7' 41 | # ENV['RUBY_FLAGS'] = "" 42 | # require 'echoe' 43 | # 44 | # Echoe.new('ambitious-activerecord') do |p| 45 | # p.dependencies << 'activerecord >=1.15.6' 46 | # p.summary = "An ambitious adapter for ActiveRecord" 47 | # p.author = 'Chris Wanstrath' 48 | # p.email = "chris@ozmm.org" 49 | # 50 | # p.project = 'ambition' 51 | # p.url = "http://ambition.rubyforge.org/" 52 | # p.test_pattern = 'test/*_test.rb' 53 | # p.version = Version 54 | # p.dependencies << 'ambition >=0.5.3' 55 | # end 56 | # 57 | # rescue LoadError 58 | # puts "Not doing any of the Echoe gemmy stuff, because you don't have the specified gem versions" 59 | # 60 | # require 'rake/testtask' 61 | # Rake::TestTask.new do |t| 62 | # t.pattern = "test/*_test.rb" 63 | # end 64 | # end 65 | # 66 | # desc 'Install as a gem' 67 | # task :install_gem do 68 | # puts `rake manifest package && gem install pkg/ambitious-activerecord-#{Version}.gem` 69 | # end 70 | # 71 | # task :default => :test 72 | # 73 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | h2. An Ambitious ActiveRecord Adapter 2 | 3 | I could tell you all about how awesome the internals are, or 4 | how fun it was to write, or how it'll make you rich and famous, 5 | but instead I'm just going to show you some examples. 6 | 7 | h2. Get It 8 | 9 | @$ sudo gem install ambitious-activerecord@ 10 | 11 | This will suck in the adapter and its dependencies (ActiveRecord & Ambition). 12 | It's fully usable outside of Rails (I use it in a Camping app or two), as long 13 | as you're riding ActiveRecord. 14 | 15 | Now require it in your app: 16 | 17 |
 18 | require 'rubygems'
 19 | require 'ambition/adapters/activerecord'
 20 | 
21 | 22 | h2. Examples 23 | 24 | Basically, you write your SQL in Ruby. No, not in Ruby. As Ruby. 25 | 26 | 27 | User.select { |u| u.city == 'San Francisco' }.each do |user| 28 | puts user.name 29 | end 30 | 31 | 32 | And that's it. 33 | 34 | The key is that queries aren't actually run until the data they represent is 35 | requested. Usually this is done with what I call a kicker method. You can call them 36 | that, too. 37 | 38 | Kicker methods are guys like @detect@, @each@, @each_with_index@, @map@, @entries@, 39 | @to_a@, and @first@ (with no argument). Methods like @select@, @sort_by@, and @first@ 40 | (with an argument) are not kicker methods and return a @Context@ object without running any SQL. 41 | 42 | Our @Context@ object has two useful methods: @to_s@ and @to_hash@. With these, 43 | we can check out what exactly we're building. Not everyone has @to_s@, 44 | though. Mostly ignore these methods and treat everything like you normally 45 | would. 46 | 47 | See, @to_s@: 48 | 49 | 50 | >> User.select { |m| m.name == 'jon' }.to_s 51 | => "SELECT * FROM users WHERE users.name = 'jon'" 52 | 53 | 54 | See, @to_hash@: 55 | 56 | 57 | >> User.select { |m| m.name == 'jon' }.to_hash 58 | => { :conditions => "users.name = 'jon'" } 59 | 60 | 61 | h2. Equality - select { |u| u.field == 'bob' } 62 | 63 | 64 | User.select { |m| m.name == 'jon' } 65 | "SELECT * FROM users WHERE users.name = 'jon'" 66 | 67 | User.select { |m| m.created_at > 2.days.ago } 68 | "SELECT * FROM users WHERE users.created_at > '2007-09-26 20:37:47'" 69 | 70 | User.select { |m| m.name == 'jon' } 71 | "SELECT * FROM users WHERE users.name = 'jon'" 72 | 73 | User.select { |m| m.name != 'jon' } 74 | "SELECT * FROM users WHERE users.name <> 'jon'" 75 | 76 | User.select { |m| m.name == 'jon' && m.age == 21 } 77 | "SELECT * FROM users WHERE (users.name = 'jon' AND users.age = 21)" 78 | 79 | User.select { |m| m.name == 'jon' || m.age == 21 } 80 | "SELECT * FROM users WHERE (users.name = 'jon' OR users.age = 21)" 81 | 82 | User.select { |m| m.name == 'jon' || m.age == 21 && m.password == 'pass' } 83 | "SELECT * FROM users WHERE 84 | (users.name = 'jon' OR (users.age = 21 AND users.password = 'pass'))" 85 | 86 | User.select { |m| (m.name == 'jon' || m.name == 'rick') && m.age == 21 } 87 | "SELECT * FROM users WHERE 88 | ((users.name = 'jon' OR users.name = 'rick') AND users.age = 21)" 89 | 90 | 91 | h2. Associations - select { |u| u.field == 'bob' && u.association.field == 'bob@bob.com' } 92 | 93 | The @to_s@ method doesn't work on associations yet, but that's okay: they can 94 | still query through ActiveRecord just fine. 95 | 96 | 97 | User.select do |u| 98 | u.email == 'chris@ozmm.org' && u.profile.name == 'chris wanstrath' 99 | end.map(&:title) 100 | 101 | "SELECT users.id AS t0_r0, ... FROM users 102 | LEFT OUTER JOIN profiles ON profiles.user_id = users.id 103 | WHERE ((users.email = 'chris@ozmm.org' AND profiles.name = 'chris wanstrath'))" 104 | 105 | 106 | h2. Comparisons - select { |u| u.age > 21 } 107 | 108 | 109 | User.select { |m| m.age > 21 } 110 | "SELECT * FROM users WHERE users.age > 21" 111 | 112 | User.select { |m| m.age < 21 }.to_s 113 | "SELECT * FROM users WHERE users.age < 21" 114 | 115 | User.select { |m| [1, 2, 3, 4].include? m.id } 116 | "SELECT * FROM users WHERE users.id IN (1, 2, 3, 4)" 117 | 118 | 119 | h2. LIKE and REGEXP (RLIKE) - select { |m| m.name =~ 'chris' } 120 | 121 | 122 | User.select { |m| m.name =~ 'chris' } 123 | "SELECT * FROM users WHERE users.name LIKE 'chris'" 124 | 125 | User.select { |m| m.name =~ 'chri%' } 126 | "SELECT * FROM users WHERE users.name LIKE 'chri%'" 127 | 128 | User.select { |m| m.name !~ 'chris' } 129 | "SELECT * FROM users WHERE users.name NOT LIKE 'chris'" 130 | 131 | User.select { |m| !(m.name =~ 'chris') } 132 | "SELECT * FROM users WHERE users.name NOT LIKE 'chris'" 133 | 134 | User.select { |m| m.name =~ /chris/ } 135 | "SELECT * FROM users WHERE users.name REGEXP 'chris'" 136 | 137 | 138 | h2. #detect 139 | 140 | 141 | User.detect { |m| m.name == 'chris' } 142 | "SELECT * FROM users WHERE users.name = 'chris' LIMIT 1" 143 | 144 | 145 | h2. LIMITs - first, first(x), [offset, limit], [range], slice 146 | 147 | 148 | User.select { |m| m.name == 'jon' }.first 149 | "SELECT * FROM users WHERE users.name = 'jon' LIMIT 1" 150 | 151 | User.select { |m| m.name == 'jon' }.first(5) 152 | "SELECT * FROM users WHERE users.name = 'jon' LIMIT 5" 153 | 154 | User.select { |m| m.name == 'jon' }[10, 20] 155 | "SELECT * FROM users WHERE users.name = 'jon' LIMIT 10, 20" 156 | 157 | User.select { |m| m.name == 'jon' }[10..20] 158 | "SELECT * FROM users WHERE users.name = 'jon' LIMIT 10, 10" 159 | 160 | 161 | h2. ORDER - sort_by { |u| u.field } 162 | 163 | 164 | User.select { |m| m.name == 'jon' }.sort_by { |m| m.name } 165 | "SELECT * FROM users WHERE users.name = 'jon' ORDER BY users.name" 166 | 167 | User.select { |m| m.name == 'jon' }.sort_by { |m| [ m.name, m.age ] } 168 | "SELECT * FROM users WHERE users.name = 'jon' ORDER BY users.name, users.age" 169 | 170 | User.select { |m| m.name == 'jon' }.sort_by { |m| [ m.name, -m.age ] } 171 | "SELECT * FROM users WHERE users.name = 'jon' 172 | ORDER BY users.name, users.age DESC" 173 | 174 | User.select { |m| m.name == 'jon' }.sort_by { |m| [ -m.name, -m.age ] } 175 | "SELECT * FROM users WHERE users.name = 'jon' 176 | ORDER BY users.name DESC, users.age DESC" 177 | 178 | User.select { |m| m.name == 'jon' }.sort_by { |m| -m.age } 179 | "SELECT * FROM users WHERE users.name = 'jon' ORDER BY users.age DESC" 180 | 181 | User.select { |m| m.name == 'jon' }.sort_by { |m| -m.profiles.title } 182 | "SELECT users.id AS t0_r0, ... FROM users 183 | LEFT OUTER JOIN profiles ON profiles.user_id = users.id 184 | WHERE (users.name = 'jon') ORDER BY profiles.title DESC" 185 | 186 | User.select { |m| m.name == 'jon' }.sort_by { rand } 187 | "SELECT * FROM users WHERE users.name = 'jon' ORDER BY RAND()" 188 | 189 | 190 | h2. COUNT - select { |u| u.name == 'jon' }.size 191 | 192 | 193 | User.select { |m| m.name == 'jon' }.size 194 | "SELECT count(*) AS count_all FROM users WHERE (users.name = 'jon')" 195 | 196 | >> User.select { |m| m.name == 'jon' }.size 197 | => 21 198 | 199 | 200 | h2. Other Enumerables 201 | 202 | These methods perform COUNT() operations rather than loading your array into memory. They're all 203 | kickers. 204 | 205 | 206 | User.any? { |m| m.name == 'jon' } 207 | User.all? { |m| m.name == 'jon' } 208 | User.select { |m| m.name == 'jon' }.empty? 209 | 210 | 211 | h2. More Sugar 212 | 213 | The @downcase@ and @upcase@ methods will map to LOWER() and UPPER(), respectively. 214 | 215 | 216 | >> User.select { |m| m.name.downcase =~ 'jon%' }.to_s 217 | => "SELECT * FROM users WHERE LOWER(users.name) LIKE 'jon%'" 218 | 219 | 220 | h2. Quoting 221 | 222 | Columns and values will be quoted using ActiveRecord's quote_column_name and quote methods, if 223 | possible. 224 | 225 | h2. SELECT * FROM bugs 226 | 227 | Found a bug? Sweet. Add it at "the Lighthouse":http://err.lighthouseapp.com/projects/466-plugins/tickets/new. 228 | 229 | More information on Ambition: 230 | 231 | * "http://ambition.rubyforge.org":http://ambition.rubyforge.org 232 | * "http://groups.google.com/group/ambition-rb/":http://groups.google.com/group/ambition-rb/ 233 | 234 | - Chris Wanstrath [ chris@ozmm.org ] 235 | -------------------------------------------------------------------------------- /test/select_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "ActiveRecord Adapter" do 4 | context "Select" do 5 | specify "simple ==" do 6 | sql = User.select { |m| m.name == 'jon' }.to_s 7 | sql.should == "SELECT * FROM users WHERE users.name = 'jon'" 8 | end 9 | 10 | specify "simple !=" do 11 | sql = User.select { |m| m.name != 'jon' }.to_s 12 | sql.should == "SELECT * FROM users WHERE users.name <> 'jon'" 13 | end 14 | 15 | specify "simple == && ==" do 16 | sql = User.select { |m| m.name == 'jon' && m.age == 21 }.to_s 17 | sql.should == "SELECT * FROM users WHERE (users.name = 'jon' AND users.age = 21)" 18 | end 19 | 20 | specify "simple == || ==" do 21 | sql = User.select { |m| m.name == 'jon' || m.age == 21 }.to_s 22 | sql.should == "SELECT * FROM users WHERE (users.name = 'jon' OR users.age = 21)" 23 | end 24 | 25 | specify "mixed && and ||" do 26 | sql = User.select { |m| m.name == 'jon' || m.age == 21 && m.password == 'pass' }.to_s 27 | sql.should == "SELECT * FROM users WHERE (users.name = 'jon' OR (users.age = 21 AND users.password = 'pass'))" 28 | end 29 | 30 | specify "grouped && and ||" do 31 | sql = User.select { |m| (m.name == 'jon' || m.name == 'rick') && m.age == 21 }.to_s 32 | sql.should == "SELECT * FROM users WHERE ((users.name = 'jon' OR users.name = 'rick') AND users.age = 21)" 33 | end 34 | 35 | specify "simple >/<" do 36 | sql = User.select { |m| m.age > 21 }.to_s 37 | sql.should == "SELECT * FROM users WHERE users.age > 21" 38 | 39 | sql = User.select { |m| m.age >= 21 }.to_s 40 | sql.should == "SELECT * FROM users WHERE users.age >= 21" 41 | 42 | sql = User.select { |m| m.age < 21 }.to_s 43 | sql.should == "SELECT * FROM users WHERE users.age < 21" 44 | end 45 | 46 | specify "array.include? item" do 47 | sql = User.select { |m| [1, 2, 3, 4].include? m.id }.to_s 48 | sql.should == "SELECT * FROM users WHERE users.id IN (1, 2, 3, 4)" 49 | end 50 | 51 | specify "variable'd array.include? item" do 52 | array = [1, 2, 3, 4] 53 | sql = User.select { |m| array.include? m.id }.to_s 54 | sql.should == "SELECT * FROM users WHERE users.id IN (1, 2, 3, 4)" 55 | end 56 | 57 | specify "simple == with variables" do 58 | me = 'chris' 59 | sql = User.select { |m| m.name == me }.to_s 60 | sql.should == "SELECT * FROM users WHERE users.name = '#{me}'" 61 | end 62 | 63 | specify "simple == with method arguments" do 64 | def test_it(name) 65 | sql = User.select { |m| m.name == name }.to_s 66 | sql.should == "SELECT * FROM users WHERE users.name = '#{name}'" 67 | end 68 | 69 | test_it('chris') 70 | end 71 | 72 | specify "simple == with instance variables" do 73 | @me = 'chris' 74 | sql = User.select { |m| m.name == @me }.to_s 75 | sql.should == "SELECT * FROM users WHERE users.name = '#{@me}'" 76 | end 77 | 78 | specify "simple == with instance variable method call" do 79 | require 'ostruct' 80 | @person = OpenStruct.new(:name => 'chris') 81 | 82 | sql = User.select { |m| m.name == @person.name }.to_s 83 | sql.should == "SELECT * FROM users WHERE users.name = '#{@person.name}'" 84 | end 85 | 86 | specify "simple == with global variables" do 87 | $my_name = 'boston' 88 | sql = User.select { |m| m.name == $my_name }.to_s 89 | sql.should == "SELECT * FROM users WHERE users.name = '#{$my_name}'" 90 | end 91 | 92 | specify "simple == with method call" do 93 | def band 94 | 'megadeth' 95 | end 96 | 97 | sql = User.select { |m| m.name == band }.to_s 98 | sql.should == "SELECT * FROM users WHERE users.name = '#{band}'" 99 | end 100 | 101 | specify "simple =~ with string" do 102 | sql = User.select { |m| m.name =~ 'chris' }.to_s 103 | sql.should == "SELECT * FROM users WHERE users.name LIKE 'chris'" 104 | 105 | sql = User.select { |m| m.name =~ 'chri%' }.to_s 106 | sql.should == "SELECT * FROM users WHERE users.name LIKE 'chri%'" 107 | end 108 | 109 | specify "simple !~ with string" do 110 | sql = User.select { |m| m.name !~ 'chris' }.to_s 111 | sql.should == "SELECT * FROM users WHERE users.name NOT LIKE 'chris'" 112 | 113 | sql = User.select { |m| !(m.name =~ 'chris') }.to_s 114 | sql.should == "SELECT * FROM users WHERE users.name NOT LIKE 'chris'" 115 | end 116 | 117 | specify "simple =~ with regexp" do 118 | sql = User.select { |m| m.name =~ /chris/ }.to_s 119 | sql.should == "SELECT * FROM users WHERE users.name REGEXP 'chris'" 120 | end 121 | 122 | specify "simple =~ with regexp flags" do 123 | sql = User.select { |m| m.name =~ /chris/i }.to_s 124 | sql.should == "SELECT * FROM users WHERE users.name REGEXP 'chris'" 125 | end 126 | 127 | specify "simple LOWER()" do 128 | sql = User.select { |m| m.name.downcase =~ 'chris%' }.to_s 129 | sql.should == "SELECT * FROM users WHERE LOWER(users.name) LIKE 'chris%'" 130 | end 131 | 132 | specify "simple UPPER()" do 133 | sql = User.select { |m| m.name.upcase =~ 'chris%' }.to_s 134 | sql.should == "SELECT * FROM users WHERE UPPER(users.name) LIKE 'chris%'" 135 | end 136 | 137 | specify "undefined equality symbol" do 138 | should.raise { User.select { |m| m.name =* /chris/ }.to_s } 139 | end 140 | 141 | specify "block variable / assigning variable conflict" do 142 | m = User.select { |m| m.name == 'chris' }.to_s 143 | m.should == "SELECT * FROM users WHERE users.name = 'chris'" 144 | end 145 | 146 | specify "simple == with inline ruby" do 147 | sql = User.select { |m| m.created_at == 2.days.ago.to_s(:db) }.to_s 148 | sql.should == "SELECT * FROM users WHERE users.created_at = '#{2.days.ago.to_s(:db)}'" 149 | end 150 | 151 | specify "deep chains" do 152 | should.raise do 153 | User.select { |m| m.created_at.something.else.perhaps }.to_s 154 | end 155 | end 156 | 157 | specify "inspect" do 158 | User.select { |u| u.name }.inspect.should.match %r(call #to_s or #to_hash) 159 | end 160 | end 161 | 162 | context "Detect" do 163 | specify "simple ==" do 164 | User.expects(:select).returns(mock(:first => true)) 165 | User.detect { |m| m.name == 'chris' } 166 | end 167 | 168 | specify "nothing found" do 169 | User.expects(:select).returns(mock(:first => nil)) 170 | User.detect { |m| m.name == 'chris' }.should.be.nil 171 | end 172 | end 173 | 174 | xcontext "[]" do 175 | specify "finds a single row" do 176 | User.expects(:find).with(1) 177 | User[1] 178 | end 179 | end 180 | 181 | context "PostgreSQL specific" do 182 | setup do 183 | ActiveRecord::Base.connection = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.new 'fake_connection', 'fake_logger' 184 | end 185 | 186 | teardown do 187 | ActiveRecord::Base.remove_connection 188 | end 189 | 190 | specify "quoting of column name" do 191 | me = 'chris' 192 | sql = User.select { |m| m.name == me }.to_s 193 | sql.should == %(SELECT * FROM users WHERE users."name" = '#{me}') 194 | end 195 | 196 | specify "simple =~ with regexp" do 197 | sql = User.select { |m| m.name =~ /chris/ }.to_s 198 | sql.should == %(SELECT * FROM users WHERE users."name" ~ 'chris') 199 | end 200 | 201 | specify "insensitive =~" do 202 | sql = User.select { |m| m.name =~ /chris/i }.to_s 203 | sql.should == %(SELECT * FROM users WHERE users."name" ~* 'chris') 204 | end 205 | 206 | specify "negated =~" do 207 | sql = User.select { |m| m.name !~ /chris/ }.to_s 208 | sql.should == %(SELECT * FROM users WHERE users."name" !~ 'chris') 209 | end 210 | 211 | specify "negated insensitive =~" do 212 | sql = User.select { |m| m.name !~ /chris/i }.to_s 213 | sql.should == %(SELECT * FROM users WHERE users."name" !~* 'chris') 214 | end 215 | end 216 | 217 | context "MySQL specific" do 218 | setup do 219 | ActiveRecord::Base.connection = ActiveRecord::ConnectionAdapters::MysqlAdapter.new('connection', 'logger') 220 | end 221 | 222 | teardown do 223 | ActiveRecord::Base.connection = ActiveRecord::ConnectionAdapters::FakeAdapter.new('connection', 'logger') 224 | end 225 | 226 | specify "quoting of column name" do 227 | me = 'chris' 228 | sql = User.select { |m| m.name == me }.to_s 229 | sql.should == "SELECT * FROM users WHERE users.`name` = '#{me}'" 230 | end 231 | 232 | specify "simple =~ with regexp" do 233 | sql = User.select { |m| m.name =~ /chris/ }.to_s 234 | sql.should == "SELECT * FROM users WHERE users.`name` REGEXP 'chris'" 235 | end 236 | 237 | specify "negated =~" do 238 | sql = User.select { |m| m.name !~ 'chris' }.to_s 239 | sql.should == "SELECT * FROM users WHERE users.`name` NOT LIKE 'chris'" 240 | end 241 | 242 | specify "negated =~ with regexp" do 243 | sql = User.select { |m| m.name !~ /chris/ }.to_s 244 | sql.should == "SELECT * FROM users WHERE users.`name` NOT REGEXP 'chris'" 245 | end 246 | end 247 | 248 | context "Adapter without overrides" do 249 | setup do 250 | ActiveRecord::Base.connection = ActiveRecord::ConnectionAdapters::FakeAdapter.new('connection', 'logger') 251 | end 252 | 253 | specify "quoting of column name" do 254 | me = 'chris' 255 | sql = User.select { |m| m.name == me }.to_s 256 | sql.should == "SELECT * FROM users WHERE users.name = '#{me}'" 257 | end 258 | end 259 | end 260 | --------------------------------------------------------------------------------