ambition 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: