mao 0.0.3

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.
@@ -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