ambition 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,18 @@
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.
data/Manifest ADDED
@@ -0,0 +1,23 @@
1
+ ./init.rb
2
+ ./lib/ambition/count.rb
3
+ ./lib/ambition/enumerable.rb
4
+ ./lib/ambition/limit.rb
5
+ ./lib/ambition/order.rb
6
+ ./lib/ambition/processor.rb
7
+ ./lib/ambition/query.rb
8
+ ./lib/ambition/where.rb
9
+ ./lib/ambition.rb
10
+ ./lib/proc_to_ruby.rb
11
+ ./LICENSE
12
+ ./Rakefile
13
+ ./README
14
+ ./test/chaining_test.rb
15
+ ./test/count_test.rb
16
+ ./test/enumerable_test.rb
17
+ ./test/helper.rb
18
+ ./test/join_test.rb
19
+ ./test/limit_test.rb
20
+ ./test/order_test.rb
21
+ ./test/types_test.rb
22
+ ./test/where_test.rb
23
+ ./Manifest
data/README ADDED
@@ -0,0 +1,175 @@
1
+ == Ambitious SQL
2
+
3
+ A simple experiment and even simpler query library.
4
+
5
+ I could tell you all about how awesome the internals are, or
6
+ how fun it was to write, or how it'll make you rich and famous,
7
+ but instead I'm just going to show you some examples.
8
+
9
+ The goal is this: write once, run with ActiveRecord, Sequel, DataMapper, whatever. Kind
10
+ of like Rack for databases.
11
+
12
+ == Git It (Not with Git, though)
13
+
14
+ $ sudo gem install ambition -y
15
+
16
+ This will suck in Ambition and its dependencies (ParseTree & ActiveRecord). It's fully usable
17
+ outside of Rails (I use it in a Camping app or two), as long as you're riding ActiveRecord.
18
+
19
+ To use with Rails, after installing the gem:
20
+
21
+ $ cd vendor/plugins
22
+ $ gem unpack ambition
23
+
24
+ RDoc exists: http://rock.errtheblog.com/ambition
25
+
26
+ == Examples
27
+
28
+ Basically, you write your SQL in Ruby. No, not in Ruby. As Ruby.
29
+
30
+ User.select { |u| u.city == 'San Francisco' }.each do |user|
31
+ puts user.name
32
+ end
33
+
34
+ And that's it.
35
+
36
+ The key is the +each+ method. You build up a +Query+ using +select+, +first+, and +sort_by+,
37
+ then call +each+ on it. This'll run the query and enumerate through the results. Really, you
38
+ can use any Enumerable method: +map+, +each_with_index+, etc.
39
+
40
+ Our +Query+ object has two useful methods: +to_sql+ and +to_hash+. With these, we can
41
+ check out what exactly we're building. Not everyone has +to_sql+, though. Mostly ignore
42
+ these methods and treat everything like you normally would.
43
+
44
+ See, +to_sql+:
45
+ >> User.select { |m| m.name == 'jon' }.to_sql
46
+ => "SELECT * FROM users WHERE users.`name` = 'jon'"
47
+
48
+ See, +to_hash+:
49
+ >> User.select { |m| m.name == 'jon' }.to_hash
50
+ => {:conditions=>"users.`name` = 'jon'"}
51
+
52
+ == Limitations
53
+
54
+ You can use variables, but any more complex Ruby (right now) won't work
55
+ inside your blocks. Just do it outside the block and assign it to a variable, okay?
56
+
57
+ Instead of:
58
+ User.select { |m| m.date == 2.days.ago }
59
+
60
+ Just do:
61
+ date = 2.days.ago
62
+ User.select { |m| m.date == date }
63
+
64
+ Instance variables and globals work, too. Same with method calls.
65
+
66
+ == Equality -- select { |u| u.field == 'bob' }
67
+
68
+ User.select { |m| m.name == 'jon' }
69
+ "SELECT * FROM users WHERE users.`name` = 'jon'"
70
+
71
+ User.select { |m| m.name != 'jon' }
72
+ "SELECT * FROM users WHERE users.`name` <> 'jon'"
73
+
74
+ User.select { |m| m.name == 'jon' && m.age == 21 }
75
+ "SELECT * FROM users WHERE (users.`name` = 'jon' AND users.`age` = 21)"
76
+
77
+ User.select { |m| m.name == 'jon' || m.age == 21 }
78
+ "SELECT * FROM users WHERE (users.`name` = 'jon' OR users.`age` = 21)"
79
+
80
+ User.select { |m| m.name == 'jon' || m.age == 21 && m.password == 'pass' }
81
+ "SELECT * FROM users WHERE (users.`name` = 'jon' OR (users.`age` = 21 AND users.`password` = 'pass'))"
82
+
83
+ User.select { |m| (m.name == 'jon' || m.name == 'rick') && m.age == 21 }
84
+ "SELECT * FROM users WHERE ((users.`name` = 'jon' OR users.`name` = 'rick') AND users.`age` = 21)"
85
+
86
+ == Associations -- select { |u| u.field == 'bob' && u.association.field == 'bob@bob.com' }
87
+
88
+ The +to_sql+ method doesn't work on associations yet, but that's okay: they can still query through
89
+ ActiveRecord just fine.
90
+
91
+ User.select { |u| u.email == 'chris@ozmm.org' && u.profile.name == 'chris wanstrath' }.map(&:title)
92
+ SELECT users.`id` AS t0_r0, ... FROM users LEFT OUTER JOIN profiles ON profiles.user_id = users.id WHERE ((users.`email` = 'chris@ozmm.org' AND profiles.name = 'chris wanstrath'))
93
+
94
+ == Comparisons -- select { |u| u.age > 21 }
95
+
96
+ User.select { |m| m.age > 21 }
97
+ "SELECT * FROM users WHERE users.`age` > 21"
98
+
99
+ User.select { |m| m.age < 21 }.to_sql
100
+ "SELECT * FROM users WHERE users.`age` < 21"
101
+
102
+ User.select { |m| [1, 2, 3, 4].include? m.id }
103
+ "SELECT * FROM users WHERE users.`id` IN (1, 2, 3, 4)"
104
+
105
+ == LIKE and REGEXP (RLIKE) -- select { |m| m.name =~ 'chris' }
106
+
107
+ User.select { |m| m.name =~ 'chris' }
108
+ "SELECT * FROM users WHERE users.`name` LIKE 'chris'"
109
+
110
+ User.select { |m| m.name =~ 'chri%' }
111
+ "SELECT * FROM users WHERE users.`name` LIKE 'chri%'"
112
+
113
+ User.select { |m| m.name !~ 'chris' }
114
+ "SELECT * FROM users WHERE users.`name` NOT LIKE 'chris'"
115
+
116
+ User.select { |m| !(m.name =~ 'chris') }
117
+ "SELECT * FROM users WHERE users.`name` NOT LIKE 'chris'"
118
+
119
+ User.select { |m| m.name =~ /chris/ }
120
+ "SELECT * FROM users WHERE users.`name` REGEXP 'chris'"
121
+
122
+ == #detect
123
+
124
+ User.detect { |m| m.name == 'chris' }
125
+ "SELECT * FROM users WHERE users.`name` = 'chris' LIMIT 1"
126
+
127
+ == LIMITs -- first, first(x), [offset, limit], [range]
128
+
129
+ User.select { |m| m.name == 'jon' }.first
130
+ "SELECT * FROM users WHERE users.`name` = 'jon' LIMIT 1"
131
+
132
+ User.select { |m| m.name == 'jon' }.first(5)
133
+ "SELECT * FROM users WHERE users.`name` = 'jon' LIMIT 5"
134
+
135
+ User.select { |m| m.name == 'jon' }[10, 20]
136
+ "SELECT * FROM users WHERE users.`name` = 'jon' LIMIT 10, 20"
137
+
138
+ User.select { |m| m.name == 'jon' }[10..20]
139
+ "SELECT * FROM users WHERE users.`name` = 'jon' LIMIT 10, 10"
140
+
141
+ == ORDER -- sort_by { |u| u.field }
142
+
143
+ User.select { |m| m.name == 'jon' }.sort_by { |m| m.name }
144
+ "SELECT * FROM users WHERE users.`name` = 'jon' ORDER BY users.name"
145
+
146
+ User.select { |m| m.name == 'jon' }.sort_by { |m| [ m.name, m.age ] }
147
+ "SELECT * FROM users WHERE users.`name` = 'jon' ORDER BY users.name, users.age"
148
+
149
+ User.select { |m| m.name == 'jon' }.sort_by { |m| [ m.name, -m.age ] }
150
+ "SELECT * FROM users WHERE users.`name` = 'jon' ORDER BY users.name, users.age DESC"
151
+
152
+ User.select { |m| m.name == 'jon' }.sort_by { |m| [ -m.name, -m.age ] }
153
+ "SELECT * FROM users WHERE users.`name` = 'jon' ORDER BY users.name DESC, users.age DESC"
154
+
155
+ User.select { |m| m.name == 'jon' }.sort_by { |m| -m.age }
156
+ "SELECT * FROM users WHERE users.`name` = 'jon' ORDER BY users.age DESC"
157
+
158
+ User.select { |m| m.name == 'jon' }.sort_by { rand }
159
+ "SELECT * FROM users WHERE users.`name` = 'jon' ORDER BY RAND()"
160
+
161
+ == COUNT -- select { |u| u.name == 'jon' }.size
162
+
163
+ User.select { |m| m.name == 'jon' }.size
164
+ SELECT count(*) AS count_all FROM users WHERE (users.`name` = 'jon')
165
+
166
+ >> User.select { |m| m.name == 'jon' }.size
167
+ => 21
168
+
169
+ == SELECT * FROM bugs
170
+
171
+ Found a bug? Sweet. Add it at the Lighthouse: http://err.lighthouseapp.com/projects/466-plugins/tickets/new
172
+
173
+ Feature requests are welcome.
174
+
175
+ * Chris Wanstrath [ chris@ozmm.org ]
data/Rakefile ADDED
@@ -0,0 +1,52 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test it!'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.pattern = 'test/**/*_test.rb'
11
+ t.verbose = true
12
+ end
13
+
14
+ desc 'Generate RDoc documentation'
15
+ Rake::RDocTask.new(:rdoc) do |rdoc|
16
+ files = ['README', 'LICENSE', 'lib/**/*.rb']
17
+ rdoc.rdoc_files.add(files)
18
+ rdoc.main = "README" # page to start on
19
+ rdoc.title = "ambition"
20
+ rdoc.template = File.exists?(t="/Users/chris/ruby/projects/err/rock/template.rb") ? t : "/var/www/rock/template.rb"
21
+ rdoc.rdoc_dir = 'doc' # rdoc output folder
22
+ rdoc.options << '--inline-source'
23
+ end
24
+
25
+ desc 'Generate coverage reports'
26
+ task :rcov do
27
+ `rcov -e gems test/*_test.rb`
28
+ puts 'Generated coverage reports.'
29
+ end
30
+
31
+
32
+ begin
33
+ require 'rubygems'
34
+ gem 'echoe', '=1.3'
35
+ ENV['RUBY_FLAGS'] = ""
36
+ require 'echoe'
37
+
38
+ Echoe.new('ambition', '0.1.0') do |p|
39
+ p.rubyforge_name = 'err'
40
+ p.summary = "Ambition builds SQL from plain jane Ruby."
41
+ p.description = "Ambition builds SQL from plain jane Ruby."
42
+ p.url = "http://errtheblog.com/"
43
+ p.author = 'Chris Wanstrath'
44
+ p.email = "chris@ozmm.org"
45
+ p.extra_deps << ['ParseTree', '=2.0.1']
46
+ p.extra_deps << ['activerecord', '>=1.15.0']
47
+ end
48
+
49
+ rescue LoadError => boom
50
+ puts "You are missing a dependency required for meta-operations on this gem."
51
+ puts "#{boom.to_s.capitalize}."
52
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'ambition'
data/lib/ambition.rb ADDED
@@ -0,0 +1,21 @@
1
+ require 'rubygems'
2
+ require 'proc_to_ruby'
3
+ require 'ambition/processor'
4
+ require 'ambition/query'
5
+ require 'ambition/where'
6
+ require 'ambition/order'
7
+ require 'ambition/limit'
8
+ require 'ambition/count'
9
+ require 'ambition/enumerable'
10
+
11
+ module Ambition
12
+ include Where, Order, Limit, Enumerable, Count
13
+
14
+ attr_accessor :query_context
15
+
16
+ def query_context
17
+ @query_context || Query.new(self)
18
+ end
19
+ end
20
+
21
+ ActiveRecord::Base.extend Ambition
@@ -0,0 +1,8 @@
1
+ module Ambition
2
+ module Count
3
+ def size
4
+ count(query_context.to_hash)
5
+ end
6
+ alias_method :length, :size
7
+ end
8
+ end
@@ -0,0 +1,9 @@
1
+ module Ambition
2
+ module Enumerable
3
+ include ::Enumerable
4
+
5
+ def each(&block)
6
+ find(:all, query_context.to_hash).each(&block)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,34 @@
1
+ module Ambition
2
+ module Limit
3
+ def first(limit = 1, offset = nil)
4
+ query_context.add LimitProcessor.new(limit, offset)
5
+ find(limit == 1 ? :first : :all, query_context.to_hash)
6
+ end
7
+
8
+ def [](offset, limit = nil)
9
+ return first(offset, limit) if limit
10
+
11
+ if offset.is_a? Range
12
+ limit = offset.end
13
+ limit -= 1 if offset.exclude_end?
14
+ first(offset.first, limit - offset.first)
15
+ else
16
+ first(offset, 1)
17
+ end
18
+ end
19
+ end
20
+
21
+ class LimitProcessor
22
+ def initialize(*args)
23
+ @args = args
24
+ end
25
+
26
+ def key
27
+ :limit
28
+ end
29
+
30
+ def to_s
31
+ @args.compact * ', '
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,52 @@
1
+ module Ambition
2
+ module Order
3
+ def sort_by(&block)
4
+ query_context.add OrderProcessor.new(table_name, block)
5
+ end
6
+ end
7
+
8
+ class OrderProcessor < Processor
9
+ def initialize(table_name, block)
10
+ super()
11
+ @receiver = nil
12
+ @table_name = table_name
13
+ @block = block
14
+ @key = :order
15
+ end
16
+
17
+ ##
18
+ # Sexp Processing Methods
19
+ def process_call(exp)
20
+ receiver, method, other = *exp
21
+ exp.clear
22
+
23
+ translation(receiver, method, other)
24
+ end
25
+
26
+ def process_vcall(exp)
27
+ if (method = exp.shift) == :rand
28
+ 'RAND()'
29
+ else
30
+ raise "Not implemented: :vcall for #{method}"
31
+ end
32
+ end
33
+
34
+ def process_masgn(exp)
35
+ exp.clear
36
+ ''
37
+ end
38
+
39
+ ##
40
+ # Helpers!
41
+ def translation(receiver, method, other)
42
+ case method
43
+ when :-@
44
+ "#{process(receiver)} DESC"
45
+ when :__send__
46
+ "#{@table_name}.#{eval('to_s', @block)}"
47
+ else
48
+ "#{@table_name}.#{method}"
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,54 @@
1
+ require 'active_record/connection_adapters/abstract/quoting'
2
+
3
+ module Ambition
4
+ class Processor < SexpProcessor
5
+ include ActiveRecord::ConnectionAdapters::Quoting
6
+
7
+ attr_reader :key, :join_string, :prefix
8
+
9
+ def initialize
10
+ super()
11
+ @strict = false
12
+ @expected = String
13
+ @auto_shift_type = true
14
+ @warn_on_default = false
15
+ @default_method = :process_error
16
+ end
17
+
18
+ ##
19
+ # Processing methods
20
+ def process_error(exp)
21
+ raise "Missing process method for sexp: #{exp.inspect}"
22
+ end
23
+
24
+ def process_proc(exp)
25
+ receiver, body = process(exp.shift), exp.shift
26
+ return process(body)
27
+ end
28
+
29
+ def process_dasgn_curr(exp)
30
+ @receiver = exp.shift
31
+ return @receiver.to_s
32
+ end
33
+
34
+ def process_array(exp)
35
+ arrayed = exp.map { |m| process(m) }
36
+ exp.clear
37
+ return arrayed.join(', ')
38
+ end
39
+
40
+ ##
41
+ # Helper methods
42
+ def to_s
43
+ process(@block.to_sexp).squeeze(' ')
44
+ end
45
+
46
+ def sanitize(value)
47
+ case value.to_s
48
+ when 'true' then '1'
49
+ when 'false' then '0'
50
+ else ActiveRecord::Base.connection.quote(value) rescue quote(value)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,81 @@
1
+ module Ambition
2
+ class Query
3
+ @@select = 'SELECT * FROM %s %s'
4
+
5
+ def initialize(owner)
6
+ @table_name = owner.table_name
7
+ @owner = owner
8
+ @clauses = []
9
+ end
10
+
11
+ def add(clause)
12
+ @clauses << clause
13
+ self
14
+ end
15
+
16
+ def method_missing(method, *args, &block)
17
+ with_context do
18
+ @owner.send(method, *args, &block)
19
+ end
20
+ end
21
+
22
+ def with_context
23
+ @owner.query_context = self
24
+ ret = yield
25
+ ensure
26
+ @owner.query_context = nil
27
+ ret
28
+ end
29
+
30
+ def to_hash
31
+ keyed = keyed_clauses
32
+ hash = {}
33
+
34
+ unless (where = keyed[:conditions]).blank?
35
+ hash[:conditions] = Array(where)
36
+ hash[:conditions] *= ' AND '
37
+ end
38
+
39
+ unless (includes = keyed[:includes]).blank?
40
+ hash[:include] = includes.flatten
41
+ end
42
+
43
+ if order = keyed[:order]
44
+ hash[:order] = order.join(', ')
45
+ end
46
+
47
+ if limit = keyed[:limit]
48
+ hash[:limit] = limit.join(', ')
49
+ end
50
+
51
+ hash
52
+ end
53
+
54
+ def to_s
55
+ hash = keyed_clauses
56
+
57
+ sql = []
58
+ sql << "JOIN #{hash[:includes].join(', ')}" unless hash[:includes].blank?
59
+ sql << "WHERE #{hash[:conditions].join(' AND ')}" unless hash[:conditions].blank?
60
+ sql << "ORDER BY #{hash[:order].join(', ')}" unless hash[:order].blank?
61
+ sql << "LIMIT #{hash[:limit].join(', ')}" unless hash[:limit].blank?
62
+
63
+ @@select % [ @table_name, sql.join(' ') ]
64
+ end
65
+ alias_method :to_sql, :to_s
66
+
67
+ def keyed_clauses
68
+ @clauses.inject({}) do |hash, clause|
69
+ hash[clause.key] ||= []
70
+ hash[clause.key] << clause.to_s
71
+
72
+ if clause.respond_to?(:includes) && !clause.includes.blank?
73
+ hash[:includes] ||= []
74
+ hash[:includes] << clause.includes
75
+ end
76
+
77
+ hash
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,158 @@
1
+ module Ambition
2
+ module Where
3
+ def select(*args, &block)
4
+ query_context.add WhereProcessor.new(self, block)
5
+ end
6
+
7
+ def detect(&block)
8
+ select(&block).first
9
+ end
10
+ end
11
+
12
+ class WhereProcessor < Processor
13
+ attr_reader :includes
14
+
15
+ def initialize(owner, block)
16
+ super()
17
+ @receiver = nil
18
+ @owner = owner
19
+ @table_name = owner.table_name
20
+ @block = block
21
+ @key = :conditions
22
+ @includes = []
23
+ end
24
+
25
+ ##
26
+ # Sexp Processing Methods
27
+ def process_and(exp)
28
+ joined_expressions 'AND', exp
29
+ end
30
+
31
+ def process_or(exp)
32
+ joined_expressions 'OR', exp
33
+ end
34
+
35
+ def process_not(exp)
36
+ _, receiver, method, other = *exp.first
37
+ exp.clear
38
+ return translation(receiver, negate(method), other)
39
+ end
40
+
41
+ def process_call(exp)
42
+ receiver, method, other = *exp
43
+ exp.clear
44
+
45
+ return translation(receiver, method, other)
46
+ end
47
+
48
+ def process_lit(exp)
49
+ exp.shift.to_s
50
+ end
51
+
52
+ def process_str(exp)
53
+ sanitize exp.shift
54
+ end
55
+
56
+ def process_nil(exp)
57
+ 'NULL'
58
+ end
59
+
60
+ def process_false(exp)
61
+ sanitize 'false'
62
+ end
63
+
64
+ def process_true(exp)
65
+ sanitize 'true'
66
+ end
67
+
68
+ def process_match3(exp)
69
+ regexp, target = exp.shift.last.inspect.gsub('/',''), process(exp.shift)
70
+ "#{target} REGEXP '#{regexp}'"
71
+ end
72
+
73
+ def process_dvar(exp)
74
+ target = exp.shift
75
+ if target == @receiver
76
+ return @table_name
77
+ else
78
+ return value(target.to_s[0..-1])
79
+ end
80
+ end
81
+
82
+ def process_ivar(exp)
83
+ value(exp.shift.to_s[0..-1])
84
+ end
85
+
86
+ def process_lvar(exp)
87
+ value(exp.shift.to_s)
88
+ end
89
+
90
+ def process_vcall(exp)
91
+ value(exp.shift.to_s)
92
+ end
93
+
94
+ def process_gvar(exp)
95
+ value(exp.shift.to_s)
96
+ end
97
+
98
+ def process_attrasgn(exp)
99
+ exp.clear
100
+ raise "Assignment not supported. Maybe you meant ==?"
101
+ end
102
+
103
+ ##
104
+ # Processor helper methods
105
+ def joined_expressions(with, exp)
106
+ clauses = []
107
+ while clause = exp.shift
108
+ clauses << clause
109
+ end
110
+ return "(" + clauses.map { |c| process(c) }.join(" #{with} ") + ")"
111
+ end
112
+
113
+ def value(variable)
114
+ sanitize eval(variable, @block)
115
+ end
116
+
117
+ def negate(method)
118
+ case method
119
+ when :==
120
+ '<>'
121
+ when :=~
122
+ '!~'
123
+ else
124
+ raise "Not implemented: #{method}"
125
+ end
126
+ end
127
+
128
+ def translation(receiver, method, other)
129
+ case method.to_s
130
+ when '=='
131
+ "#{process(receiver)} = #{process(other)}"
132
+ when '<>', '>', '<'
133
+ "#{process(receiver)} #{method} #{process(other)}"
134
+ when 'include?'
135
+ "#{process(other)} IN (#{process(receiver)})"
136
+ when '=~'
137
+ "#{process(receiver)} LIKE #{process(other)}"
138
+ when '!~'
139
+ "#{process(receiver)} NOT LIKE #{process(other)}"
140
+ else
141
+ build_condition(receiver, method, other)
142
+ end
143
+ end
144
+
145
+ def build_condition(receiver, method, other)
146
+ if receiver.first == :call && receiver[1].last == @receiver
147
+ if reflection = @owner.reflections[receiver.last]
148
+ @includes << reflection.name unless @includes.include? reflection.name
149
+ "#{reflection.table_name}.#{method}"
150
+ else
151
+ raise "No reflection `#{receiver.last}' found on #{@owner}"
152
+ end
153
+ else
154
+ "#{process(receiver)}.`#{method}` #{process(other)}"
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,36 @@
1
+ ##
2
+ # Taken from ruby2ruby, Copyright (c) 2006 Ryan Davis under the MIT License
3
+ require 'parse_tree'
4
+ require 'unique'
5
+ require 'sexp_processor'
6
+
7
+ class Method
8
+ def with_class_and_method_name
9
+ if self.inspect =~ /<Method: (.*)\#(.*)>/ then
10
+ klass = eval $1
11
+ method = $2.intern
12
+ raise "Couldn't determine class from #{self.inspect}" if klass.nil?
13
+ return yield(klass, method)
14
+ else
15
+ raise "Can't parse signature: #{self.inspect}"
16
+ end
17
+ end
18
+
19
+ def to_sexp
20
+ with_class_and_method_name do |klass, method|
21
+ ParseTree.new(false).parse_tree_for_method(klass, method)
22
+ end
23
+ end
24
+ end
25
+
26
+ class Proc
27
+ def to_method
28
+ Unique.send(:define_method, :proc_to_method, self)
29
+ Unique.new.method(:proc_to_method)
30
+ end
31
+
32
+ def to_sexp
33
+ body = self.to_method.to_sexp[2][1..-1]
34
+ [:proc, *body]
35
+ end
36
+ end
@@ -0,0 +1,34 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ context "Chaining" do
4
+ specify "should join selects with AND" do
5
+ sql = User.select { |m| m.name == 'jon' }
6
+ sql = sql.select { |m| m.age == 22 }
7
+ sql.to_sql.should == "SELECT * FROM users WHERE users.`name` = 'jon' AND users.`age` = 22"
8
+ end
9
+
10
+ specify "should join sort_bys with a comma" do
11
+ sql = User.select { |m| m.name == 'jon' }
12
+ sql = sql.sort_by { |m| m.name }
13
+ sql = sql.sort_by { |m| m.age }
14
+ sql.to_sql.should == "SELECT * FROM users WHERE users.`name` = 'jon' ORDER BY users.name, users.age"
15
+ end
16
+
17
+ specify "should join selects and sorts intelligently" do
18
+ sql = User.select { |m| m.name == 'jon' }
19
+ sql = sql.select { |m| m.age == 22 }
20
+ sql = sql.sort_by { |m| -m.name }
21
+ sql = sql.sort_by { |m| m.age }
22
+ sql.to_sql.should == "SELECT * FROM users WHERE users.`name` = 'jon' AND users.`age` = 22 ORDER BY users.name DESC, users.age"
23
+ end
24
+
25
+ specify "should join lots of selects and sorts intelligently" do
26
+ sql = User.select { |m| m.name == 'jon' }
27
+ sql = sql.select { |m| m.age == 22 }
28
+ sql = sql.sort_by { |m| m.name }
29
+ sql = sql.select { |m| m.power == true }
30
+ sql = sql.sort_by { |m| m.email }
31
+ sql = sql.select { |m| m.admin == true && m.email == 'chris@ozmm.org' }
32
+ sql.to_sql.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"
33
+ end
34
+ end
@@ -0,0 +1,17 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ context "Count" do
4
+ setup do
5
+ hash = { :conditions => "users.`name` = 'jon'" }
6
+ User.expects(:count).with(hash)
7
+ @sql = User.select { |m| m.name == 'jon' }
8
+ end
9
+
10
+ specify "size" do
11
+ @sql.size
12
+ end
13
+
14
+ specify "length" do
15
+ @sql.length
16
+ end
17
+ end
@@ -0,0 +1,51 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ context "Each" do
4
+ specify "simple ==" do
5
+ hash = { :conditions => "users.`age` = 21" }
6
+ User.expects(:find).with(:all, hash).returns([])
7
+ User.select { |m| m.age == 21 }.each do |user|
8
+ puts user.name
9
+ end
10
+ end
11
+
12
+ specify "limit and conditions" do
13
+ hash = { :limit => '5', :conditions => "users.`age` = 21" }
14
+ User.expects(:find).with(:all, hash).returns([])
15
+ User.select { |m| m.age == 21 }.first(5).each do |user|
16
+ puts user.name
17
+ end
18
+ end
19
+
20
+ specify "limit and conditions and order" do
21
+ hash = { :limit => '5', :conditions => "users.`age` = 21", :order => 'users.name' }
22
+ User.expects(:find).with(:all, hash).returns([])
23
+ User.select { |m| m.age == 21 }.sort_by { |m| m.name }.first(5).each do |user|
24
+ puts user.name
25
+ end
26
+ end
27
+
28
+ specify "limit and order" do
29
+ hash = { :limit => '5', :order => 'users.name' }
30
+ User.expects(:find).with(:all, hash).returns([])
31
+ User.sort_by { |m| m.name }.first(5).each do |user|
32
+ puts user.name
33
+ end
34
+ end
35
+ end
36
+
37
+ context "Enumerable Methods" do
38
+ specify "map" do
39
+ hash = { :conditions => "users.`age` = 21" }
40
+ User.expects(:find).with(:all, hash).returns([])
41
+ User.select { |m| m.age == 21 }.map { |u| u.name }
42
+ end
43
+
44
+ specify "each_with_index" do
45
+ hash = { :conditions => "users.`age` = 21" }
46
+ User.expects(:find).with(:all, hash).returns([])
47
+ User.select { |m| m.age == 21 }.each_with_index do |user, i|
48
+ puts "#{i}: #{user.name}"
49
+ end
50
+ end
51
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,30 @@
1
+ require 'rubygems'
2
+ require 'test/spec'
3
+ require 'mocha'
4
+ require 'redgreen'
5
+ require 'active_support'
6
+ require 'active_record'
7
+
8
+ $:.unshift File.dirname(__FILE__) + '/../lib'
9
+ require 'ambition'
10
+
11
+ class User
12
+ extend Ambition
13
+
14
+ def self.reflections
15
+ return @reflections if @reflections
16
+ @reflections = {}
17
+ @reflections[:ideas] = Reflection.new(:has_many, 'user_id', :ideas, 'ideas')
18
+ @reflections[:invites] = Reflection.new(:has_many, 'referrer_id', :invites, 'invites')
19
+ @reflections[:profile] = Reflection.new(:has_one, 'user_id', :profile, 'profiles')
20
+ @reflections[:account] = Reflection.new(:belongs_to, 'account_id', :account, 'accounts')
21
+ @reflections
22
+ end
23
+
24
+ def self.table_name
25
+ 'users'
26
+ end
27
+ end
28
+
29
+ class Reflection < Struct.new(:macro, :primary_key_name, :name, :table_name)
30
+ end
data/test/join_test.rb ADDED
@@ -0,0 +1,32 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ context "Joins" do
4
+ specify "simple == on an association" do
5
+ sql = User.select { |m| m.account.email == 'chris@ozmm.org' }
6
+ sql.to_hash.should == {
7
+ :conditions => "accounts.email = 'chris@ozmm.org'",
8
+ :include => [:account]
9
+ }
10
+ end
11
+
12
+ specify "simple mixed == on an association" do
13
+ sql = User.select { |m| m.name == 'chris' && m.account.email == 'chris@ozmm.org' }
14
+ sql.to_hash.should == {
15
+ :conditions => "(users.`name` = 'chris' AND accounts.email = 'chris@ozmm.org')",
16
+ :include => [:account]
17
+ }
18
+ end
19
+
20
+ specify "multiple associations" do
21
+ sql = User.select { |m| m.ideas.title == 'New Freezer' || m.invites.email == 'pj@hyett.com' }
22
+ sql.to_hash.should == {
23
+ :conditions => "(ideas.title = 'New Freezer' OR invites.email = 'pj@hyett.com')",
24
+ :include => [:ideas, :invites]
25
+ }
26
+ end
27
+
28
+ specify "non-existant associations" do
29
+ sql = User.select { |m| m.liquor.brand == 'Jack' }
30
+ should.raise { sql.to_hash }
31
+ end
32
+ end
@@ -0,0 +1,37 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ context "Limit" do
4
+ setup do
5
+ @sql = User.select { |m| m.name == 'jon' }
6
+ end
7
+
8
+ specify "first" do
9
+ conditions = { :conditions => "users.`name` = 'jon'", :limit => '1' }
10
+ User.expects(:find).with(:first, conditions)
11
+ @sql.first
12
+ end
13
+
14
+ specify "first with argument" do
15
+ conditions = { :conditions => "users.`name` = 'jon'", :limit => '5' }
16
+ User.expects(:find).with(:all, conditions)
17
+ @sql.first(5)
18
+ end
19
+
20
+ specify "[] with one element" do
21
+ conditions = { :conditions => "users.`name` = 'jon'", :limit => '10, 1' }
22
+ User.expects(:find).with(:all, conditions)
23
+ @sql[10]
24
+ end
25
+
26
+ specify "[] with two elements" do
27
+ conditions = { :conditions => "users.`name` = 'jon'", :limit => '10, 20' }
28
+ User.expects(:find).with(:all, conditions)
29
+ @sql[10, 20]
30
+ end
31
+
32
+ specify "[] with range" do
33
+ conditions = { :conditions => "users.`name` = 'jon'", :limit => '10, 10' }
34
+ User.expects(:find).with(:all, conditions)
35
+ @sql[10..20]
36
+ end
37
+ end
@@ -0,0 +1,52 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ context "Order" do
4
+ setup do
5
+ @sql = User.select { |m| m.name == 'jon' }
6
+ end
7
+
8
+ specify "simple order" do
9
+ string = @sql.sort_by { |m| m.name }.to_sql
10
+ string.should == "SELECT * FROM users WHERE users.`name` = 'jon' ORDER BY users.name"
11
+ end
12
+
13
+ specify "simple combined order" do
14
+ string = @sql.sort_by { |m| [ m.name, m.age ] }.to_sql
15
+ string.should == "SELECT * FROM users WHERE users.`name` = 'jon' ORDER BY users.name, users.age"
16
+ end
17
+
18
+ specify "simple combined order with single reverse" do
19
+ string = @sql.sort_by { |m| [ m.name, -m.age ] }.to_sql
20
+ string.should == "SELECT * FROM users WHERE users.`name` = 'jon' ORDER BY users.name, users.age DESC"
21
+ end
22
+
23
+ specify "simple combined order with two reverses" do
24
+ string = @sql.sort_by { |m| [ -m.name, -m.age ] }.to_sql
25
+ string.should == "SELECT * FROM users WHERE users.`name` = 'jon' ORDER BY users.name DESC, users.age DESC"
26
+ end
27
+
28
+ specify "reverse order with -" do
29
+ string = @sql.sort_by { |m| -m.age }.to_sql
30
+ string.should == "SELECT * FROM users WHERE users.`name` = 'jon' ORDER BY users.age DESC"
31
+ end
32
+
33
+ xspecify "reverse order with #reverse" do
34
+ # TODO: not implemented
35
+ string = @sql.sort_by { |m| m.age }.reverse.to_sql
36
+ string.should == "SELECT * FROM users WHERE users.`name` = 'jon' ORDER BY users.age DESC"
37
+ end
38
+
39
+ specify "random order" do
40
+ string = @sql.sort_by { rand }.to_sql
41
+ string.should == "SELECT * FROM users WHERE users.`name` = 'jon' ORDER BY RAND()"
42
+ end
43
+
44
+ specify "non-existent method to sort by" do
45
+ should.raise { @sql.sort_by { foo }.to_sql }
46
+ end
47
+
48
+ specify "Symbol#to_proc" do
49
+ string = User.sort_by(&:name).to_sql
50
+ string.should == "SELECT * FROM users ORDER BY users.name"
51
+ end
52
+ end
@@ -0,0 +1,56 @@
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
+ nil => 'NULL',
12
+ true => '1',
13
+ false => '0',
14
+ Time.now => "'#{Time.now.to_s(:db)}'",
15
+ DateTime.now => "'#{DateTime.now.to_s(:db)}'",
16
+ Date.today => "'#{Date.today.to_s(:db)}'"
17
+ }
18
+
19
+ types_hash.each do |type, translation|
20
+ specify "simple using #{type}" do
21
+ sql = User.select { |m| m.name == type }.to_sql
22
+ sql.should == "SELECT * FROM users WHERE users.`name` = #{translation}"
23
+ end
24
+ end
25
+
26
+ specify "float" do
27
+ sql = User.select { |m| m.name == 1.2 }.to_sql
28
+ sql.should == "SELECT * FROM users WHERE users.`name` = 1.2"
29
+ end
30
+
31
+ specify "integer" do
32
+ sql = User.select { |m| m.name == 1 }.to_sql
33
+ sql.should == "SELECT * FROM users WHERE users.`name` = 1"
34
+ end
35
+
36
+ specify "true" do
37
+ sql = User.select { |m| m.name == true }.to_sql
38
+ sql.should == "SELECT * FROM users WHERE users.`name` = 1"
39
+ end
40
+
41
+ specify "false" do
42
+ sql = User.select { |m| m.name == false }.to_sql
43
+ sql.should == "SELECT * FROM users WHERE users.`name` = 0"
44
+ end
45
+
46
+ specify "nil" do
47
+ sql = User.select { |m| m.name == nil }.to_sql
48
+ sql.should == "SELECT * FROM users WHERE users.`name` = NULL"
49
+ end
50
+
51
+ xspecify "Time" do
52
+ # TODO: nothing but variables inside blocks for now
53
+ sql = User.select { |m| m.name == Time.now }.to_sql
54
+ sql.should == "SELECT * FROM users WHERE users.`name` = NULL"
55
+ end
56
+ end
@@ -0,0 +1,139 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ context "Where (using select)" do
4
+ specify "simple ==" do
5
+ sql = User.select { |m| m.name == 'jon' }.to_sql
6
+ sql.should == "SELECT * FROM users WHERE users.`name` = 'jon'"
7
+ end
8
+
9
+ specify "simple !=" do
10
+ sql = User.select { |m| m.name != 'jon' }.to_sql
11
+ sql.should == "SELECT * FROM users WHERE users.`name` <> 'jon'"
12
+ end
13
+
14
+ specify "simple == && ==" do
15
+ sql = User.select { |m| m.name == 'jon' && m.age == 21 }.to_sql
16
+ sql.should == "SELECT * FROM users WHERE (users.`name` = 'jon' AND users.`age` = 21)"
17
+ end
18
+
19
+ specify "simple == || ==" do
20
+ sql = User.select { |m| m.name == 'jon' || m.age == 21 }.to_sql
21
+ sql.should == "SELECT * FROM users WHERE (users.`name` = 'jon' OR users.`age` = 21)"
22
+ end
23
+
24
+ specify "mixed && and ||" do
25
+ sql = User.select { |m| m.name == 'jon' || m.age == 21 && m.password == 'pass' }.to_sql
26
+ sql.should == "SELECT * FROM users WHERE (users.`name` = 'jon' OR (users.`age` = 21 AND users.`password` = 'pass'))"
27
+ end
28
+
29
+ specify "grouped && and ||" do
30
+ sql = User.select { |m| (m.name == 'jon' || m.name == 'rick') && m.age == 21 }.to_sql
31
+ sql.should == "SELECT * FROM users WHERE ((users.`name` = 'jon' OR users.`name` = 'rick') AND users.`age` = 21)"
32
+ end
33
+
34
+ specify "simple >/<" do
35
+ sql = User.select { |m| m.age > 21 }.to_sql
36
+ sql.should == "SELECT * FROM users WHERE users.`age` > 21"
37
+
38
+ sql = User.select { |m| m.age < 21 }.to_sql
39
+ sql.should == "SELECT * FROM users WHERE users.`age` < 21"
40
+ end
41
+
42
+ specify "array.include? item" do
43
+ sql = User.select { |m| [1, 2, 3, 4].include? m.id }.to_sql
44
+ sql.should == "SELECT * FROM users WHERE users.`id` IN (1, 2, 3, 4)"
45
+ end
46
+
47
+ specify "simple == with variables" do
48
+ me = 'chris'
49
+ sql = User.select { |m| m.name == me }.to_sql
50
+ sql.should == "SELECT * FROM users WHERE users.`name` = '#{me}'"
51
+ end
52
+
53
+ specify "simple == with method arguments" do
54
+ def test_it(name)
55
+ sql = User.select { |m| m.name == name }.to_sql
56
+ sql.should == "SELECT * FROM users WHERE users.`name` = '#{name}'"
57
+ end
58
+
59
+ test_it('chris')
60
+ end
61
+
62
+ specify "simple == with instance variables" do
63
+ @me = 'chris'
64
+ sql = User.select { |m| m.name == @me }.to_sql
65
+ sql.should == "SELECT * FROM users WHERE users.`name` = '#{@me}'"
66
+ end
67
+
68
+ xspecify "simple == with instance variable method call" do
69
+ require 'ostruct'
70
+ @person = OpenStruct.new(:name => 'chris')
71
+
72
+ sql = User.select { |m| m.name == @person.name }.to_sql
73
+ sql.should == "SELECT * FROM users WHERE users.`name` = '#{@person.name}'"
74
+ end
75
+
76
+ specify "simple == with global variables" do
77
+ $my_name = 'boston'
78
+ sql = User.select { |m| m.name == $my_name }.to_sql
79
+ sql.should == "SELECT * FROM users WHERE users.`name` = '#{$my_name}'"
80
+ end
81
+
82
+ specify "simple == with method call" do
83
+ def band
84
+ 'megadeth'
85
+ end
86
+
87
+ sql = User.select { |m| m.name == band }.to_sql
88
+ sql.should == "SELECT * FROM users WHERE users.`name` = '#{band}'"
89
+ end
90
+
91
+ specify "simple =~ with string" do
92
+ sql = User.select { |m| m.name =~ 'chris' }.to_sql
93
+ sql.should == "SELECT * FROM users WHERE users.`name` LIKE 'chris'"
94
+
95
+ sql = User.select { |m| m.name =~ 'chri%' }.to_sql
96
+ sql.should == "SELECT * FROM users WHERE users.`name` LIKE 'chri%'"
97
+ end
98
+
99
+ specify "simple !~ with string" do
100
+ sql = User.select { |m| m.name !~ 'chris' }.to_sql
101
+ sql.should == "SELECT * FROM users WHERE users.`name` NOT LIKE 'chris'"
102
+
103
+ sql = User.select { |m| !(m.name =~ 'chris') }.to_sql
104
+ sql.should == "SELECT * FROM users WHERE users.`name` NOT LIKE 'chris'"
105
+ end
106
+
107
+ specify "simple =~ with regexp" do
108
+ sql = User.select { |m| m.name =~ /chris/ }.to_sql
109
+ sql.should == "SELECT * FROM users WHERE users.`name` REGEXP 'chris'"
110
+ end
111
+
112
+ specify "undefined equality symbol" do
113
+ should.raise { User.select { |m| m.name =* /chris/ }.to_sql }
114
+ end
115
+
116
+ specify "undefined inequality symbol" do
117
+ should.raise { User.select { |m| m.name !+ 'chris' }.to_sql }
118
+ end
119
+
120
+ xspecify "simple == with inline ruby" do
121
+ # TODO: implement this
122
+ sql = User.select { |m| m.created_at == 2.days.ago.to_s(:db) }.to_sql
123
+ sql.should == "SELECT * FROM users WHERE users.`created_at` = #{2.days.ago.to_s(:db)}"
124
+ end
125
+ end
126
+
127
+ context "Where (using detect)" do
128
+ specify "simple ==" do
129
+ conditions = { :conditions => "users.`name` = 'chris'", :limit => '1' }
130
+ User.expects(:find).with(:first, conditions)
131
+ User.detect { |m| m.name == 'chris' }
132
+ end
133
+
134
+ specify "nothing found" do
135
+ conditions = { :conditions => "users.`name` = 'chris'", :limit => '1' }
136
+ User.expects(:find).with(:first, conditions).returns(nil)
137
+ User.detect { |m| m.name == 'chris' }.should.be.nil
138
+ end
139
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.0
3
+ specification_version: 1
4
+ name: ambition
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.1.0
7
+ date: 2007-08-29 00:00:00 -07:00
8
+ summary: Ambition builds SQL from plain jane Ruby.
9
+ require_paths:
10
+ - lib
11
+ email: chris@ozmm.org
12
+ homepage: http://errtheblog.com/
13
+ rubyforge_project: err
14
+ description: Ambition builds SQL from plain jane Ruby.
15
+ autorequire:
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - Chris Wanstrath
31
+ files:
32
+ - ./init.rb
33
+ - ./lib/ambition/count.rb
34
+ - ./lib/ambition/enumerable.rb
35
+ - ./lib/ambition/limit.rb
36
+ - ./lib/ambition/order.rb
37
+ - ./lib/ambition/processor.rb
38
+ - ./lib/ambition/query.rb
39
+ - ./lib/ambition/where.rb
40
+ - ./lib/ambition.rb
41
+ - ./lib/proc_to_ruby.rb
42
+ - ./LICENSE
43
+ - ./Rakefile
44
+ - ./README
45
+ - ./test/chaining_test.rb
46
+ - ./test/count_test.rb
47
+ - ./test/enumerable_test.rb
48
+ - ./test/helper.rb
49
+ - ./test/join_test.rb
50
+ - ./test/limit_test.rb
51
+ - ./test/order_test.rb
52
+ - ./test/types_test.rb
53
+ - ./test/where_test.rb
54
+ - ./Manifest
55
+ test_files: []
56
+
57
+ rdoc_options: []
58
+
59
+ extra_rdoc_files: []
60
+
61
+ executables: []
62
+
63
+ extensions: []
64
+
65
+ requirements: []
66
+
67
+ dependencies:
68
+ - !ruby/object:Gem::Dependency
69
+ name: ParseTree
70
+ version_requirement:
71
+ version_requirements: !ruby/object:Gem::Version::Requirement
72
+ requirements:
73
+ - - "="
74
+ - !ruby/object:Gem::Version
75
+ version: 2.0.1
76
+ version:
77
+ - !ruby/object:Gem::Dependency
78
+ name: activerecord
79
+ version_requirement:
80
+ version_requirements: !ruby/object:Gem::Version::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: 1.15.0
85
+ version: