mao 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,417 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe Mao::Query do
5
+ before { prepare_spec }
6
+
7
+ let(:empty) { Mao.query(:empty) }
8
+ let(:one) { Mao.query(:one) }
9
+ let(:some) { Mao.query(:some) }
10
+ let(:typey) { Mao.query(:typey) }
11
+ let(:autoid) { Mao.query(:autoid) }
12
+ let(:times) { Mao.query(:times) }
13
+
14
+ describe ".new" do
15
+ subject { Mao::Query.new(:table, {}, {}) }
16
+
17
+ its(:table) { should be_an_instance_of Symbol }
18
+ its(:options) { should be_frozen }
19
+ its(:col_types) { should be_frozen }
20
+
21
+ context "no such table" do
22
+ it { expect { Mao::Query.new("nonextant")
23
+ }.to raise_exception(ArgumentError) }
24
+ end
25
+ end
26
+
27
+ describe "#with_options" do
28
+ subject { one.with_options(:blah => 99) }
29
+
30
+ its(:table) { should be one.table }
31
+ its(:options) { should eq({:blah => 99}) }
32
+ end
33
+
34
+ describe "#limit" do
35
+ subject { some.limit(2) }
36
+
37
+ its(:options) { should include(:limit => 2) }
38
+ its(:sql) { should eq 'SELECT * FROM "some" LIMIT 2' }
39
+
40
+ context "invalid argument" do
41
+ it { expect { some.limit("2")
42
+ }.to raise_exception(ArgumentError) }
43
+
44
+ it { expect { some.limit(false)
45
+ }.to raise_exception(ArgumentError) }
46
+ end
47
+ end
48
+
49
+ describe "#order" do
50
+ let(:asc) { some.order(:id, :asc) }
51
+ it { asc.options.should include(:order => [:id, 'ASC']) }
52
+ it { asc.sql.should eq 'SELECT * FROM "some" ORDER BY "id" ASC' }
53
+
54
+ let(:desc) { one.order(:value, :desc) }
55
+ it { desc.options.should include(:order => [:value, 'DESC']) }
56
+ it { desc.sql.should eq 'SELECT * FROM "one" ORDER BY "value" DESC' }
57
+
58
+ it { expect { one.order(:huh, :asc) }.to raise_exception(ArgumentError) }
59
+ it { expect { one.order(:value) }.to raise_exception(ArgumentError) }
60
+ it { expect { one.order(:id, 'ASC') }.to raise_exception(ArgumentError) }
61
+ it { expect { one.order(:id, :xyz) }.to raise_exception(ArgumentError) }
62
+ end
63
+
64
+ describe "#only" do
65
+ subject { some.only(:id, [:value]) }
66
+
67
+ its(:options) { should include(:only => [:id, :value]) }
68
+ its(:sql) { should eq 'SELECT "id", "value" FROM "some"' }
69
+
70
+ context "invalid argument" do
71
+ it { expect { some.only(42)
72
+ }.to raise_exception(ArgumentError) }
73
+
74
+ it { expect { some.only(nil)
75
+ }.to raise_exception(ArgumentError) }
76
+
77
+ it { expect { some.only("id")
78
+ }.to raise_exception(ArgumentError) }
79
+
80
+ it { expect { some.only(:j)
81
+ }.to raise_exception(ArgumentError) }
82
+ end
83
+
84
+ context "with #join" do
85
+ subject { some.join(:one) { one.value == some.value }.
86
+ only(:one => [:id]) }
87
+
88
+ its(:options) { should include(:only => {:one => [:id]}) }
89
+ its(:sql) { should eq 'SELECT "one"."id" "c3" ' +
90
+ 'FROM "some" ' +
91
+ 'INNER JOIN "one" ' +
92
+ 'ON ("one"."value" = "some"."value")' }
93
+ its(:select!) { should eq [{:one => {:id => 42}}] }
94
+
95
+ context "before #join" do
96
+ it { expect { some.only(:some => [:id])
97
+ }.to raise_exception(ArgumentError) }
98
+ end
99
+
100
+ context "poor arguments" do
101
+ it { expect { some.join(:one) { one.value }.only(:some => :id)
102
+ }.to raise_exception(ArgumentError) }
103
+ end
104
+ end
105
+ end
106
+
107
+ describe "#returning" do
108
+ subject { some.returning([:id], :value).
109
+ with_options(:insert => [{:value => "q"}]) }
110
+
111
+ its(:options) { should include(:returning => [:id, :value]) }
112
+ its(:sql) { should eq 'INSERT INTO "some" ("value") ' +
113
+ 'VALUES (\'q\') RETURNING "id", "value"' }
114
+
115
+ context "invalid argument" do
116
+ it { expect { some.returning(42)
117
+ }.to raise_exception(ArgumentError) }
118
+
119
+ it { expect { some.returning(nil)
120
+ }.to raise_exception(ArgumentError) }
121
+
122
+ it { expect { some.returning("id")
123
+ }.to raise_exception(ArgumentError) }
124
+
125
+ it { expect { some.returning("j")
126
+ }.to raise_exception(ArgumentError) }
127
+ end
128
+ end
129
+
130
+ describe "#where" do
131
+ subject { some.where { (id == 1).or(id > 10_000) } }
132
+
133
+ its(:options) do
134
+ should include(:where => [:Binary,
135
+ 'OR',
136
+ [:Binary, '=', [:Column, :id], "1"],
137
+ [:Binary, '>', [:Column, :id], "10000"]])
138
+ end
139
+
140
+ its(:sql) { should eq 'SELECT * FROM "some" WHERE ' \
141
+ '(("id" = 1) OR ("id" > 10000))' }
142
+
143
+ context "non-extant column" do
144
+ it { expect { some.where { non_extant_column == 42 }
145
+ }.to raise_exception(ArgumentError) }
146
+ end
147
+
148
+ context "with #join" do
149
+ subject { some.join(:one) { one.value == some.value }.
150
+ where { one.id == 42 } }
151
+
152
+ its(:options) { should include(:where => [:Binary,
153
+ '=',
154
+ [:Column, :one, :id],
155
+ "42"]) }
156
+ its(:sql) { should eq 'SELECT "some"."id" "c1", ' +
157
+ '"some"."value" "c2", ' +
158
+ '"one"."id" "c3", ' +
159
+ '"one"."value" "c4" ' +
160
+ 'FROM "some" ' +
161
+ 'INNER JOIN "one" ' +
162
+ 'ON ("one"."value" = "some"."value") ' +
163
+ 'WHERE ("one"."id" = 42)' }
164
+
165
+ its(:select!) { should eq(
166
+ [{:some => {:id => 3, :value => "你好, Dave."},
167
+ :one => {:id => 42, :value => "你好, Dave."}}]) }
168
+ end
169
+
170
+ context "with time values" do
171
+ it { times.select_first!.should eq(
172
+ {:id => 1, :time => Time.new(2012, 11, 10, 19, 45, 0, 0)}) }
173
+
174
+ it { times.where { time == Time.new(2012, 11, 11, 6, 45, 0, 11 * 3600) }.
175
+ select!.length.should eq 1 }
176
+ it { times.where { time == Time.new(2012, 11, 10, 19, 45, 0, 0) }.
177
+ select!.length.should eq 1 }
178
+ it { times.where { time == "2012-11-10 19:45:00" }.
179
+ select!.length.should eq 1 }
180
+ it { times.where { time == "2012-11-10 19:45:00 Z" }.
181
+ select!.length.should eq 1 }
182
+ it { times.where { time == "2012-11-10 19:45:00 +00" }.
183
+ select!.length.should eq 1 }
184
+ it { times.where { time == "2012-11-10 19:45:00 +00:00" }.
185
+ select!.length.should eq 1 }
186
+ it { times.where { time == "2012-11-10 19:45:00 -00" }.
187
+ select!.length.should eq 1 }
188
+ it { times.where { time == "2012-11-10 19:45:00 -00:00" }.
189
+ select!.length.should eq 1 }
190
+
191
+ it { times.where { time < Time.new(2012, 11, 11, 6, 45, 0, 11 * 3600) }.
192
+ select!.length.should eq 0 }
193
+ context "surprising results" do
194
+ # Timestamps are IGNORED for comparisons with "timestamp without time
195
+ # zone". See:
196
+ # http://postgresql.org/docs/9.1/static/datatype-datetime.html#AEN5714
197
+ it { times.where { time < "2012-11-11 6:45:00 +11" }.
198
+ select!.length.should eq 1 }
199
+ it { times.where { time < "2012-11-11 6:45:00 +1100" }.
200
+ select!.length.should eq 1 }
201
+ it { times.where { time < "2012-11-11 6:45:00 +11:00" }.
202
+ select!.length.should eq 1 }
203
+ end
204
+ it { times.where { time <= Time.new(2012, 11, 11, 6, 45, 0, 11 * 3600) }.
205
+ select!.length.should eq 1 }
206
+ it { times.where { time <= "2012-11-11 6:45:00 +11" }.
207
+ select!.length.should eq 1 }
208
+ it { times.where { time <= "2012-11-11 6:45:00 +1100" }.
209
+ select!.length.should eq 1 }
210
+ it { times.where { time <= "2012-11-11 6:45:00 +11:00" }.
211
+ select!.length.should eq 1 }
212
+ end
213
+ end
214
+
215
+ describe "#join" do
216
+ subject { some.join(:one) { one.value == some.value } }
217
+
218
+ its(:options) do
219
+ should include(:join => [:one,
220
+ [:Binary,
221
+ '=',
222
+ [:Column, :one, :value],
223
+ [:Column, :some, :value]]])
224
+ end
225
+
226
+ its(:sql) { should eq(
227
+ 'SELECT ' +
228
+ '"some"."id" "c1", ' +
229
+ '"some"."value" "c2", ' +
230
+ '"one"."id" "c3", ' +
231
+ '"one"."value" "c4" ' +
232
+ 'FROM "some" ' +
233
+ 'INNER JOIN "one" ' +
234
+ 'ON ("one"."value" = "some"."value")') }
235
+
236
+ its(:select!) { should eq [{:some => {:id => 3,
237
+ :value => "你好, Dave."},
238
+ :one => {:id => 42,
239
+ :value => "你好, Dave."}}] }
240
+
241
+ context "simple Hash joins" do
242
+ subject { some.join({:one => {:value => :id}}) }
243
+
244
+ its(:options) do
245
+ should include(:join => [:one,
246
+ [:Binary,
247
+ '=',
248
+ [:Column, :some, :value],
249
+ [:Column, :one, :id]]])
250
+ end
251
+ end
252
+ end
253
+
254
+ describe "#select!" do
255
+ context "use of #sql" do
256
+ # HACK: construct empty manually, otherwise it'll try to look up column
257
+ # info and ruin our assertions.
258
+ let(:empty) { Mao::Query.new("empty",
259
+ {},
260
+ {}) }
261
+ let(:empty_sure) { double("empty_sure") }
262
+ let(:empty_sql) { double("empty_sql") }
263
+ before { empty.should_receive(:with_options).
264
+ with(:update => nil).
265
+ and_return(empty_sure) }
266
+ before { empty_sure.should_receive(:sql).
267
+ and_return(empty_sql) }
268
+ before { PG::Connection.any_instance.should_receive(:exec).
269
+ with(empty_sql).and_return(:ok) }
270
+ it { empty.select!.should eq :ok }
271
+ end
272
+
273
+ context "no results" do
274
+ it { empty.select!.should eq [] }
275
+ end
276
+
277
+ context "one result" do
278
+ subject { one.select! }
279
+
280
+ it { should be_an_instance_of Array }
281
+ it { should have(1).item }
282
+ its([0]) { should eq({:id => 42, :value => "你好, Dave."}) }
283
+ end
284
+
285
+ context "some results" do
286
+ subject { some.select! }
287
+
288
+ it { should be_an_instance_of Array }
289
+ it { should have(3).items }
290
+
291
+ its([0]) { should eq({:id => 1, :value => "Bah"}) }
292
+ its([1]) { should eq({:id => 2, :value => "Hah"}) }
293
+ its([2]) { should eq({:id => 3, :value => "你好, Dave."}) }
294
+ end
295
+
296
+ context "various types" do
297
+ subject { typey.select! }
298
+
299
+ it { should have(2).items }
300
+ its([0]) { should eq(
301
+ {:korea => true,
302
+ :japan => BigDecimal.new("1234567890123456.789"),
303
+ :china => "WHAT\x00".force_encoding(Encoding::ASCII_8BIT)}) }
304
+ its([1]) { should eq(
305
+ {:korea => false,
306
+ :japan => BigDecimal.new("-1234567890123456.789"),
307
+ :china => "HUH\x01\x02".force_encoding(Encoding::ASCII_8BIT)}) }
308
+ end
309
+ end
310
+
311
+ describe "#select_first!" do
312
+ # HACK: construct empty manually, otherwise it'll try to look up column
313
+ # info and ruin our assertions.
314
+ let(:empty) { Mao::Query.new("empty",
315
+ {},
316
+ {}) }
317
+ before { empty.should_receive(:limit).with(1).and_return(empty) }
318
+ before { empty.should_receive(:select!).and_return([:ok]) }
319
+ it { empty.select_first!.should eq :ok }
320
+ end
321
+
322
+ describe "#update!" do
323
+ context "use of #sql" do
324
+ let(:empty) { Mao::Query.new("empty",
325
+ {},
326
+ {}) }
327
+ let(:empty_update) { double("empty_update") }
328
+ let(:empty_sql) { double("empty_sql") }
329
+ before { empty.should_receive(:with_options).
330
+ with(:update => {:x => :y}).
331
+ and_return(empty_update) }
332
+ before { empty_update.should_receive(:sql).and_return(empty_sql) }
333
+ before { PG::Connection.any_instance.should_receive(:exec).
334
+ with(empty_sql).and_return(:ok) }
335
+ it { empty.update!(:x => :y).should eq :ok }
336
+ end
337
+
338
+ context "#sql result" do
339
+ subject { empty.with_options(:update => {:id => 44}).sql }
340
+ it { should eq 'UPDATE "empty" SET "id" = 44' }
341
+ end
342
+
343
+ context "no matches" do
344
+ it { empty.update!(:value => "y").should eq 0 }
345
+ end
346
+
347
+ context "no such column" do
348
+ it { expect { empty.update!(:x => "y")
349
+ }.to raise_exception(ArgumentError, /is not a column/) }
350
+ end
351
+
352
+ context "some matches" do
353
+ it { some.where { id <= 2 }.update!(:value => 'Meh').should eq 2 }
354
+ end
355
+ end
356
+
357
+ describe "#insert!" do
358
+ context "use of #sql" do
359
+ let(:empty) { Mao::Query.new("empty",
360
+ {},
361
+ {}) }
362
+ let(:empty_insert) { double("empty_insert") }
363
+ let(:empty_sql) { double("empty_sql") }
364
+ before { empty.should_receive(:with_options).
365
+ with(:insert => [{:x => :y}]).
366
+ and_return(empty_insert) }
367
+ before { empty_insert.should_receive(:sql).and_return(empty_sql) }
368
+ before { PG::Connection.any_instance.should_receive(:exec).
369
+ with(empty_sql).and_return(:ok) }
370
+ it { empty.insert!([:x => :y]).should eq :ok }
371
+ end
372
+
373
+ context "#sql result" do
374
+ context "all columns alike" do
375
+ subject { empty.with_options(
376
+ :insert => [{:id => 44}, {:id => 38}]).sql }
377
+ it { should eq 'INSERT INTO "empty" ("id") VALUES (44), (38)' }
378
+ end
379
+
380
+ context "not all columns alike" do
381
+ subject { empty.with_options(
382
+ :insert => [{:id => 1}, {:value => 'z', :id => 2}]).sql }
383
+ it { should eq 'INSERT INTO "empty" ("id", "value") ' +
384
+ 'VALUES (1, DEFAULT), (2, \'z\')' }
385
+ end
386
+ end
387
+
388
+ context "result" do
389
+ context "number of rows" do
390
+ it { autoid.insert!(:value => "quox").should eq 1 }
391
+ it { autoid.insert!({:value => "lol"}, {:value => "x"}).should eq 2 }
392
+ end
393
+
394
+ context "#returning" do
395
+ it do
396
+ autoid.returning(:id).insert!(:value => "nanana").
397
+ should eq([{:id => 1}])
398
+ autoid.returning(:id).insert!(:value => "ha").
399
+ should eq([{:id => 2}])
400
+ autoid.returning(:id).insert!(:value => "bah").
401
+ should eq([{:id => 3}])
402
+ autoid.returning(:id).insert!({:value => "a"}, {:value => "b"}).
403
+ should eq([{:id => 4}, {:id => 5}])
404
+ end
405
+ end
406
+ end
407
+ end
408
+
409
+ describe "reconnection" do
410
+ it do
411
+ Mao.query(:one).select!.should be_an_instance_of Array
412
+ pending
413
+ end
414
+ end
415
+ end
416
+
417
+ # vim: set sw=2 cc=80 et:
@@ -0,0 +1,13 @@
1
+ require 'mao'
2
+
3
+ def relative_to_spec(filename)
4
+ File.join(File.dirname(File.absolute_path(__FILE__)),
5
+ filename)
6
+ end
7
+
8
+ def prepare_spec
9
+ `psql mao_testing -f #{relative_to_spec("fixture.sql")} 2>&1 | grep -v ^NOTICE`
10
+ Mao.connect!(:dbname => 'mao_testing')
11
+ end
12
+
13
+ # vim: set sw=2 cc=80 et:
data/thoughts.rb ADDED
@@ -0,0 +1,42 @@
1
+ X.trans do
2
+ X.query(:tblSamuraiUser).join { ... }.where {
3
+ upp = tblSamuraiUserProduct
4
+
5
+ upp.userProductId == 188...
6
+
7
+
8
+ first_cond = (email == "arlen@noblesamurai.com").and(userId > 10000)
9
+ second_cond = x.and(y).and(z)
10
+
11
+ blah = tblSamuraiUserProduct.columnName == xyzzy
12
+
13
+ first_cond.or second_cond.or blah
14
+ }
15
+
16
+ my_new_record = my_old_record.merge(changes)
17
+
18
+ X.update(:tblSamuraiUser).where { userId == 610610 }.update(changes)
19
+ end
20
+
21
+ X.query(:tblSamuraiUser).where(lambda {email == "arlen@noblesamurai.com"}, lambda {userId > 10000})
22
+
23
+ #Tim's thoughts:
24
+ #
25
+ #- We need to know what we think of statically defined r'ships b/w tables vs defining everything in place where the query is performed.
26
+ # Is the ActiveRecord way of defining r'ships b/w tables a good model for us?
27
+ # Or do we have some alternative way of statically defining this stuff? Or do we dynamically infer it?
28
+ #
29
+ # I am thinking we need a means to join which is based again on a simple hash. For example, the hash
30
+ # could look like: {:from => :tblSamuraiUser.id, :to => tblSamuraiUserProduct.user_id}
31
+ # Then, the user could define these hash as constants, eg UserToUserProduct = blah
32
+ # The joins could then be brought in to the query object (perhaps a .joins method, which can either take a single hash or an array),
33
+ # as with a :joins key, which might map to an array of joins options hashes.
34
+ #
35
+ # Anyway, those are my thoughts for now, we can chat further!
36
+
37
+
38
+
39
+ # Tenet:
40
+ #
41
+ # From the code you have written, it should be sufficiently declarative and map
42
+ # sufficiently predictably to SQL that you can predict the SQL produced.
metadata ADDED
@@ -0,0 +1,120 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mao
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Timothy Leslie Allen
9
+ - Arlen Christian Mart Cuss
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2012-11-15 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: pg
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ! '>='
29
+ - !ruby/object:Gem::Version
30
+ version: '0'
31
+ - !ruby/object:Gem::Dependency
32
+ name: rake
33
+ requirement: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - ! '>='
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ type: :development
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: rspec
49
+ requirement: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ description: Mao Ain't an ORM
64
+ email:
65
+ - allen.timothy.email@gmail.com
66
+ - ar@len.me
67
+ executables: []
68
+ extensions: []
69
+ extra_rdoc_files: []
70
+ files:
71
+ - .autotest
72
+ - .gitignore
73
+ - .rspec
74
+ - .travis.yml
75
+ - Gemfile
76
+ - Gemfile.lock
77
+ - README.md
78
+ - Rakefile
79
+ - TODO
80
+ - lib/mao.rb
81
+ - lib/mao/filter.rb
82
+ - lib/mao/query.rb
83
+ - lib/mao/version.rb
84
+ - mao.gemspec
85
+ - spec/filter_spec.rb
86
+ - spec/fixture.sql
87
+ - spec/mao_spec.rb
88
+ - spec/query_spec.rb
89
+ - spec/spec_helper.rb
90
+ - thoughts.rb
91
+ homepage: https://github.com/unnali/mao
92
+ licenses: []
93
+ post_install_message:
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ! '>='
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ none: false
105
+ requirements:
106
+ - - ! '>='
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ requirements: []
110
+ rubyforge_project:
111
+ rubygems_version: 1.8.23
112
+ signing_key:
113
+ specification_version: 3
114
+ summary: A database access layer. Currently supports PG.
115
+ test_files:
116
+ - spec/filter_spec.rb
117
+ - spec/fixture.sql
118
+ - spec/mao_spec.rb
119
+ - spec/query_spec.rb
120
+ - spec/spec_helper.rb