m4dbi 0.5.0 → 0.6.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/CHANGELOG CHANGED
@@ -1,3 +1,19 @@
1
+ ## 0.6.0
2
+
3
+ - Compatibility with DBI 0.4.0 ensured.
4
+ - Support for multi-column primary keys added.
5
+ - MySQL support improved and spec'ed.
6
+ - SQLite support improved and spec'ed.
7
+ - MIT licence adopted.
8
+
9
+ ## 0.5.5
10
+
11
+ - Improved and added numerous specs.
12
+ - More precise NoMethodError? thrown on Model instances. (r2601)
13
+ - DBI::Timestamp now behaves like Time. (r2602)
14
+ - Model instance writers now properly modify object values (not just database).
15
+ - nil values in condition clauses ([] and "where") no longer generate wrong SQL.
16
+
1
17
  ## 0.5.0
2
18
 
3
- First official public release.
19
+ - First official public release.
data/HIM CHANGED
@@ -21,17 +21,17 @@ M4DBI is a Ruby library that provides ORM modelling to the Ruby DBI package
21
21
 
22
22
  ==== Repository
23
23
 
24
- To use the repository version, you need Subversion (http://subversion.tigris.org).
25
- Chances are, there is a Subversion package for your Linux/UNIX flavour.
24
+ To use the repository version, you need git ( http://git.or.cz ).
25
+ Chances are, there is a git package for your Linux/UNIX flavour.
26
26
 
27
27
  cd /where/you/want/m4dbi
28
- svn co http://rome.purepistos.net/svn/m4dbi
28
+ git clone git://github.com/Pistos/m4dbi.git
29
29
 
30
30
  Change the following to whatever the equivalent paths are for your system:
31
31
 
32
32
  cd /usr/lib/ruby/site_ruby/1.8
33
- ln -s /path/to/checked/out/m4dbi/trunk/lib/m4dbi
34
- ln -s /path/to/checked/out/m4dbi/trunk/lib/m4dbi.rb
33
+ ln -s /path/to/cloned/m4dbi/lib/m4dbi
34
+ ln -s /path/to/cloned/m4dbi/lib/m4dbi.rb
35
35
 
36
36
  === Usage
37
37
 
@@ -40,7 +40,7 @@ generated from the spec files under the spec/ dir.
40
40
 
41
41
  === Source Code
42
42
 
43
- Browse source at http://rome.purepistos.net/issues/m4dbi/browser/trunk .
43
+ Browse source at http://github.com/Pistos/m4dbi/tree/master .
44
44
  See coverage at http://rome.purepistos.net/m4dbi/rcov .
45
45
  Very limited rdocs at http://rome.purepistos.net/m4dbi/rdoc .
46
46
 
data/LICENCE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT Licence
2
+
3
+ Copyright (c) 2008-2009 Pistos
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/lib/m4dbi.rb CHANGED
@@ -1,8 +1,14 @@
1
1
  require 'rubygems'
2
2
 
3
- require 'm4dbi/traits'
4
- require 'm4dbi/hash'
5
- require 'm4dbi/database-handle'
6
- require 'm4dbi/row'
7
- require 'm4dbi/model'
8
- require 'm4dbi/collection'
3
+ M4DBI_VERSION = '0.6.0'
4
+
5
+ __DIR__ = File.expand_path( File.dirname( __FILE__ ) )
6
+
7
+ require "#{__DIR__}/m4dbi/traits"
8
+ require "#{__DIR__}/m4dbi/hash"
9
+ require "#{__DIR__}/m4dbi/array"
10
+ require "#{__DIR__}/m4dbi/database-handle"
11
+ require "#{__DIR__}/m4dbi/row"
12
+ require "#{__DIR__}/m4dbi/timestamp"
13
+ require "#{__DIR__}/m4dbi/model"
14
+ require "#{__DIR__}/m4dbi/collection"
@@ -0,0 +1,5 @@
1
+ class Array
2
+ def to_placeholders
3
+ map { '?' }.join( ', ' )
4
+ end
5
+ end
@@ -20,17 +20,40 @@ module DBI
20
20
  end
21
21
  end; end
22
22
 
23
+ module DBD; module Mysql
24
+ module DatabaseNameAccessor
25
+ def dbname
26
+ select_column( "SELECT DATABASE()" )
27
+ end
28
+ end
29
+ end; end
30
+
31
+ module DBD; module SQLite3
32
+ module DatabaseNameAccessor
33
+ def dbname
34
+ select_one( "PRAGMA database_list" )[ 2 ]
35
+ end
36
+ end
37
+ end; end
38
+
23
39
  class DatabaseHandle
40
+ attr_reader :transactions
41
+
24
42
  alias old_initialize initialize
25
- def initialize( handle )
43
+ def initialize( *args )
26
44
  DBI::DatabaseHandle.last_handle = self
27
- handle = old_initialize( handle )
45
+ handle = old_initialize( *args )
28
46
  @mutex = Mutex.new
47
+ @transactions = Array.new
29
48
 
30
49
  # Hackery to expose dbname.
31
50
  if defined?( DBI::DBD::Pg::Database ) and ( DBI::DBD::Pg::Database === @handle )
32
51
  @handle.extend DBI::DBD::Pg::ConnectionDatabaseNameAccessor
33
52
  extend DBI::DBD::Pg::DatabaseNameAccessor
53
+ elsif defined?( DBI::DBD::Mysql::Database ) and ( DBI::DBD::Mysql::Database === @handle )
54
+ extend DBI::DBD::Mysql::DatabaseNameAccessor
55
+ elsif defined?( DBI::DBD::SQLite3::Database ) and ( DBI::DBD::SQLite3::Database === @handle )
56
+ extend DBI::DBD::SQLite3::DatabaseNameAccessor
34
57
  end
35
58
  # TODO: more DBDs
36
59
 
@@ -44,12 +67,18 @@ module DBI
44
67
  # database handle.
45
68
  def one_transaction
46
69
  @mutex.synchronize do
70
+ # Keep track of transactions for debugging purposes
71
+ transaction = { :time => ::Time.now, :stack => caller }
72
+ @transactions << transaction
73
+
47
74
  auto_commit = self[ 'AutoCommit' ]
48
75
  self[ 'AutoCommit' ] = false
49
76
  result = transaction do
50
77
  yield self
51
78
  end
52
79
  self[ 'AutoCommit' ] = auto_commit
80
+
81
+ @transactions.delete transaction
53
82
  result
54
83
  end
55
84
  end
@@ -58,6 +87,8 @@ module DBI
58
87
  row = select_one( statement, *bindvars )
59
88
  if row
60
89
  row[ 0 ]
90
+ else
91
+ raise DBI::DataError.new( "Query returned no rows." )
61
92
  end
62
93
  end
63
94
 
data/lib/m4dbi/hash.rb CHANGED
@@ -1,21 +1,49 @@
1
1
  class Hash
2
- def to_clause( join_string )
2
+ COMPACT_NILS = true
3
+ DONT_COMPACT_NILS = false
4
+
5
+ # Takes an optional block to provide a single "field = ?" type subclause
6
+ # for each key-value pair.
7
+ def to_clause( join_string, compact_nils = DONT_COMPACT_NILS )
3
8
  # The clause items and the values have to be in the same order.
4
9
  keys_ = keys
5
- clause = keys_.map { |field|
6
- "#{field} = ?"
7
- }.join( join_string )
10
+ if block_given?
11
+ mapping = keys_.map { |field| yield field }
12
+ else
13
+ mapping = keys_.map { |field| "#{field} = ?" }
14
+ end
15
+ clause = mapping.join( join_string )
8
16
  values_ = keys_.map { |key|
9
17
  self[ key ]
10
18
  }
19
+ if compact_nils
20
+ values_.compact!
21
+ end
11
22
  [ clause, values_ ]
12
23
  end
13
24
 
14
25
  def to_where_clause
15
- to_clause( " AND " )
26
+ to_clause( " AND ", COMPACT_NILS ) { |field|
27
+ if self[ field ].nil?
28
+ "#{field} IS NULL"
29
+ else
30
+ "#{field} = ?"
31
+ end
32
+ }
16
33
  end
17
34
 
18
35
  def to_set_clause
19
36
  to_clause( ", " )
20
37
  end
38
+
39
+ if method_defined? :slice
40
+ warn "Hash#slice already defined; redefining."
41
+ end
42
+ def slice( *desired_keys )
43
+ Hash[
44
+ select { |key,value|
45
+ desired_keys.include? key
46
+ }
47
+ ]
48
+ end
21
49
  end
data/lib/m4dbi/model.rb CHANGED
@@ -11,26 +11,39 @@ module DBI
11
11
 
12
12
  extend Enumerable
13
13
 
14
- def self.[]( hash_or_pk_value )
15
- case hash_or_pk_value
16
- when Hash
17
- clause, values = hash_or_pk_value.to_where_clause
18
- row = dbh.select_one(
19
- "SELECT * FROM #{table} WHERE #{clause}",
20
- *values
21
- )
22
- else
23
- row = dbh.select_one(
24
- "SELECT * FROM #{table} WHERE #{pk} = ?",
25
- hash_or_pk_value
26
- )
14
+ def self.[]( first_arg, *args )
15
+ if args.size == 0
16
+ case first_arg
17
+ when Hash
18
+ clause, values = first_arg.to_where_clause
19
+ when NilClass
20
+ clause = pk_clause
21
+ values = [ first_arg ]
22
+ else # single value
23
+ clause = pk_clause
24
+ values = Array( first_arg )
25
+ end
26
+ else
27
+ clause = pk_clause
28
+ values = [ first_arg ] + args
27
29
  end
28
30
 
31
+ row = dbh.select_one(
32
+ "SELECT * FROM #{table} WHERE #{clause}",
33
+ *values
34
+ )
35
+
29
36
  if row
30
37
  self.new( row )
31
38
  end
32
39
  end
33
40
 
41
+ def self.pk_clause
42
+ pk.map { |col|
43
+ "#{col} = ?"
44
+ }.join( ' AND ' )
45
+ end
46
+
34
47
  def self.from_rows( rows )
35
48
  rows.map { |r| self.new( r ) }
36
49
  end
@@ -85,7 +98,7 @@ module DBI
85
98
  end
86
99
 
87
100
  def self.count
88
- dbh.select_column( "SELECT COUNT(*) FROM #{table}" )
101
+ dbh.select_column( "SELECT COUNT(*) FROM #{table}" ).to_i
89
102
  end
90
103
 
91
104
  def self.create( hash = {} )
@@ -115,9 +128,16 @@ module DBI
115
128
  *values
116
129
  )
117
130
  if num_inserted > 0
118
- pk_value = hash[ self.pk.to_sym ] || hash[ self.pk.to_s ]
119
- if pk_value
120
- rec = self.one_where( self.pk => pk_value )
131
+ pk_hash = hash.slice( *(
132
+ self.pk.map { |pk_col| pk_col.to_sym }
133
+ ) )
134
+ if pk_hash.empty?
135
+ pk_hash = hash.slice( *(
136
+ self.pk.map { |pk_col| pk_col.to_s }
137
+ ) )
138
+ end
139
+ if not pk_hash.empty?
140
+ rec = self.one_where( pk_hash )
121
141
  else
122
142
  begin
123
143
  rec = last_record( dbh_ )
@@ -189,11 +209,12 @@ module DBI
189
209
  )
190
210
  end
191
211
 
192
- def self.update_one( pk_value, set_hash )
193
- set_clause, set_params = set_hash.to_set_clause
194
- params = set_params + [ pk_value ]
212
+ def self.update_one( *args )
213
+ set_clause, set_params = args[ -1 ].to_set_clause
214
+ pk_values = args[ 0..-2 ]
215
+ params = set_params + pk_values
195
216
  dbh.do(
196
- "UPDATE #{table} SET #{set_clause} WHERE #{pk} = ?",
217
+ "UPDATE #{table} SET #{set_clause} WHERE #{pk_clause}",
197
218
  *params
198
219
  )
199
220
  end
@@ -233,6 +254,7 @@ module DBI
233
254
  j.#{m1_fk} = ?
234
255
  AND m2.id = j.#{m2_fk}
235
256
  },
257
+ # TODO: m2.id? Should be m2.pk or something
236
258
  pk
237
259
  )
238
260
  end
@@ -249,6 +271,7 @@ module DBI
249
271
  j.#{m2_fk} = ?
250
272
  AND m1.id = j.#{m1_fk}
251
273
  },
274
+ # TODO: Should be m1.pk not m1.id
252
275
  pk
253
276
  )
254
277
  end
@@ -267,17 +290,44 @@ module DBI
267
290
  end
268
291
 
269
292
  def method_missing( method, *args )
270
- @row.send( method, *args )
293
+ begin
294
+ @row.send( method, *args )
295
+ rescue NoMethodError => e
296
+ raise NoMethodError.new(
297
+ "undefined method '#{method}' for #{self}",
298
+ method,
299
+ args
300
+ )
301
+ end
271
302
  end
272
303
 
304
+ # Returns a single value for single-column primary keys,
305
+ # returns an Array for multi-column primary keys.
273
306
  def pk
274
- @row[ self.class.pk ]
307
+ if pk_columns.size == 1
308
+ @row[ pk_columns[ 0 ] ]
309
+ else
310
+ pk_values
311
+ end
275
312
  end
276
313
 
277
- def pk_column
314
+ # Always returns an Array of values, even for single-column primary keys.
315
+ def pk_values
316
+ pk_columns.map { |col|
317
+ @row[ col ]
318
+ }
319
+ end
320
+
321
+ def pk_columns
278
322
  self.class.pk
279
323
  end
280
324
 
325
+ def pk_clause
326
+ pk_columns.map { |col|
327
+ "#{col} = ?"
328
+ }.join( ' AND ' )
329
+ end
330
+
281
331
  def ==( other )
282
332
  other and ( pk == other.pk )
283
333
  end
@@ -293,17 +343,23 @@ module DBI
293
343
  def set( hash )
294
344
  set_clause, set_params = hash.to_set_clause
295
345
  set_params << pk
296
- dbh.do(
297
- "UPDATE #{table} SET #{set_clause} WHERE #{pk_column} = ?",
346
+ num_updated = dbh.do(
347
+ "UPDATE #{table} SET #{set_clause} WHERE #{pk_clause}",
298
348
  *set_params
299
349
  )
350
+ if num_updated > 0
351
+ hash.each do |key,value|
352
+ @row[ key ] = value
353
+ end
354
+ end
355
+ num_updated
300
356
  end
301
357
 
302
358
  # Returns true iff the record and only the record was successfully deleted.
303
359
  def delete
304
360
  num_deleted = dbh.do(
305
- "DELETE FROM #{table} WHERE #{pk_column} = ?",
306
- pk
361
+ "DELETE FROM #{table} WHERE #{pk_clause}",
362
+ *pk_values
307
363
  )
308
364
  num_deleted == 1
309
365
  end
@@ -316,18 +372,21 @@ module DBI
316
372
 
317
373
  # Define a new DBI::Model like this:
318
374
  # class Post < DBI::Model( :posts ); end
319
- # You can specify the primary key column like so:
320
- # class Author < DBI::Model( :authors, 'id' ); end
321
- def self.Model( table, pk_ = 'id' )
375
+ # You can specify the primary key column(s) using an array, like so:
376
+ # class Author < DBI::Model( :authors, [ 'auth_num' ] ); end
377
+ def self.Model( table, pk_ = [ 'id' ] )
322
378
  h = DBI::DatabaseHandle.last_handle
323
379
  if h.nil? or not h.connected?
324
380
  raise DBI::Error.new( "Attempted to create a Model class without first connecting to a database." )
325
381
  end
382
+ if not pk_.respond_to? :each
383
+ raise DBI::Error.new( "Primary key must be enumerable (was given #{pk_.inspect})" )
384
+ end
326
385
 
327
386
  model_key =
328
- if defined?( DBI::DBD::Pg::Database ) and DBI::DBD::Pg::Database === h.handle
387
+ # DBD-dependent. Not all DBDs have dbname implemented by M4DBI.
388
+ if h.respond_to? :dbname
329
389
  "#{h.dbname}::#{table}"
330
- # TODO: more DBDs
331
390
  else
332
391
  table
333
392
  end
@@ -342,24 +401,35 @@ module DBI
342
401
  } )
343
402
 
344
403
  if defined?( DBI::DBD::Pg::Database ) and DBI::DBD::Pg::Database === h.handle
404
+ # TODO: This is broken for non-SERIAL or multi-column primary keys
345
405
  meta_def( "last_record".to_sym ) do |dbh_|
346
406
  self.s1 "SELECT * FROM #{table} WHERE #{pk} = currval( '#{table}_#{pk}_seq' );"
347
407
  end
408
+ elsif defined?( DBI::DBD::Mysql::Database ) and DBI::DBD::Mysql::Database === h.handle
409
+ meta_def( "last_record".to_sym ) do |dbh_|
410
+ self.s1 "SELECT * FROM #{table} WHERE #{pk} = LAST_INSERT_ID();"
411
+ end
412
+ elsif defined?( DBI::DBD::SQLite3::Database ) and DBI::DBD::SQLite3::Database === h.handle
413
+ meta_def( "last_record".to_sym ) do |dbh_|
414
+ self.s1 "SELECT * FROM #{table} WHERE #{pk} = last_insert_rowid();"
415
+ end
348
416
  # TODO: more DBDs
349
417
  end
350
418
 
351
419
  klass.trait[ :columns ].each do |col|
352
420
  colname = col[ 'name' ]
353
421
 
422
+ # Column readers
354
423
  class_def( colname.to_sym ) do
355
424
  @row[ colname ]
356
425
  end
357
426
 
427
+ # Column writers
358
428
  class_def( "#{colname}=".to_sym ) do |new_value|
359
429
  num_changed = dbh.do(
360
- "UPDATE #{table} SET #{colname} = ? WHERE #{pk_column} = ?",
430
+ "UPDATE #{table} SET #{colname} = ? WHERE #{pk_clause}",
361
431
  new_value,
362
- pk
432
+ *pk_values
363
433
  )
364
434
  if num_changed > 0
365
435
  @row[ colname ] = new_value
@@ -0,0 +1,20 @@
1
+ module DBI
2
+ class Timestamp
3
+ def method_missing( method, *args )
4
+ t = to_time
5
+ begin
6
+ t.send( method, *args )
7
+ rescue NoMethodError => e
8
+ raise NoMethodError.new(
9
+ "undefined method '#{method}' for #{self}",
10
+ method,
11
+ args
12
+ )
13
+ end
14
+ end
15
+
16
+ def <=>( other )
17
+ to_time <=> other.to_time
18
+ end
19
+ end
20
+ end
data/spec/dbi.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'spec/helper'
2
2
 
3
- $dbh = DBI.connect( "DBI:Pg:m4dbi", "m4dbi", "m4dbi" )
4
- # See test-schema.sql and test-data.sql
3
+ $dbh = connect_to_spec_database
4
+ reset_data
5
5
 
6
6
  describe 'DBI::DatabaseHandle#select_column' do
7
7
 
@@ -11,6 +11,15 @@ describe 'DBI::DatabaseHandle#select_column' do
11
11
  )
12
12
  name.class.should.not.equal Array
13
13
  name.should.equal 'author1'
14
+
15
+ null = $dbh.select_column(
16
+ "SELECT c4 FROM many_col_table WHERE c3 = 40"
17
+ )
18
+ null.should.be.nil
19
+
20
+ should.raise( DBI::DataError ) do
21
+ $dbh.select_column( "SELECT name FROM authors WHERE 1+1 = 3" )
22
+ end
14
23
  end
15
24
 
16
25
  it 'selects one column of first row' do
@@ -26,7 +35,6 @@ describe 'DBI::DatabaseHandle#select_column' do
26
35
  )
27
36
  name.should.equal 'author3'
28
37
  end
29
-
30
38
  end
31
39
 
32
40
  describe 'DBI::DatabaseHandle#one_transaction' do
@@ -87,6 +95,8 @@ describe 'DBI::DatabaseHandle#one_transaction' do
87
95
 
88
96
  value = $dbh.sc "SELECT c1 FROM many_col_table WHERE id = 1;"
89
97
  value.should.equal( 11 + 1 + 1 )
98
+
99
+ reset_data
90
100
  end
91
101
 
92
102
  end
data/spec/hash.rb CHANGED
@@ -5,23 +5,43 @@ describe 'Hash' do
5
5
  h = {
6
6
  :a => 2,
7
7
  :b => 'foo',
8
+ :the_nil => nil,
8
9
  :abc => Time.now,
9
10
  :xyz => 9.02,
10
11
  }
11
12
  clause, values = h.to_clause( " AND " )
12
13
  where_clause, where_values = h.to_where_clause
13
14
 
14
- s = "a = ? AND b = ? AND abc = ? AND xyz = ?"
15
- clause.length.should.equal s.length
16
- where_clause.length.should.equal s.length
15
+ str = "a = ? AND b = ? AND the_nil = ? AND abc = ? AND xyz = ?"
16
+ where_str = "a = ? AND b = ? AND the_nil IS NULL AND abc = ? AND xyz = ?"
17
+ clause.length.should.equal str.length
18
+ where_clause.length.should.equal where_str.length
17
19
 
18
- h.each do |key,value|
20
+ h.each_key do |key|
19
21
  clause.should.match /#{key} = ?/
20
- where_clause.should.match /#{key} = ?/
21
- values.find { |v| v == value }.should.not.be.nil
22
- where_values.find { |v| v == value }.should.not.be.nil
22
+ if h[ key ].nil?
23
+ where_clause.should.match /#{key} IS NULL/
24
+ else
25
+ where_clause.should.match /#{key} = ?/
26
+ end
23
27
  end
24
- values.size.should.equal h.keys.size
25
- where_values.size.should.equal h.keys.size
28
+
29
+ values.should.equal h.values
30
+ where_values.should.equal h.values.compact
31
+ end
32
+
33
+ it 'offers a means to get subhashes via #slice' do
34
+ h = { :a => 1, :b => 2, :c => 3, :d => 4, :e => 5, :f => 6 }
35
+ h.slice( :b, :c ).should.equal(
36
+ { :b => 2, :c => 3 }
37
+ )
38
+ h.slice( :a, :b, :f ).should.equal(
39
+ { :a => 1, :b => 2, :f => 6 }
40
+ )
41
+ h.slice( :c, :e, :q ).should.equal(
42
+ { :c => 3, :e => 5 }
43
+ )
44
+ h.slice.should.equal Hash.new
45
+ h.slice( :g, :h ).should.equal Hash.new
26
46
  end
27
47
  end
data/spec/helper.rb CHANGED
@@ -11,3 +11,22 @@ $LOAD_PATH.unshift(
11
11
  )
12
12
 
13
13
  require 'm4dbi'
14
+
15
+ puts "DBI version: #{DBI::VERSION}"
16
+ puts "M4DBI version: #{M4DBI_VERSION}"
17
+
18
+ # See test-schema*.sql and test-data.sql
19
+ def connect_to_spec_database( database = ( ENV[ 'M4DBI_DATABASE' ] || 'm4dbi' ) )
20
+ driver = ENV[ 'M4DBI_DRIVER' ] || "DBI:Pg"
21
+ puts "Using DBI driver: '#{driver}'"
22
+ DBI.connect( "#{driver}:#{database}", "m4dbi", "m4dbi" )
23
+ end
24
+
25
+ def reset_data( dbh = $dbh, datafile = "test-data.sql" )
26
+ dir = File.dirname( __FILE__ )
27
+ File.read( "#{dir}/#{datafile}" ).split( /;/ ).each do |command|
28
+ if not command.strip.empty?
29
+ dbh.do command
30
+ end
31
+ end
32
+ end
data/spec/model.rb CHANGED
@@ -1,14 +1,5 @@
1
1
  require 'spec/helper'
2
2
 
3
- # See test-schema.sql and test-data.sql
4
-
5
- def reset_data( dbh = $dbh, datafile = "test-data.sql" )
6
- dir = File.dirname( __FILE__ )
7
- File.read( "#{dir}/#{datafile}" ).split( /;/ ).each do |command|
8
- dbh.do( command )
9
- end
10
- end
11
-
12
3
  describe 'DBI::Model' do
13
4
  it 'raises an exception when trying to define a model before connecting to a database' do
14
5
  dbh = DBI::DatabaseHandle.last_handle
@@ -21,7 +12,7 @@ describe 'DBI::Model' do
21
12
  end
22
13
  end
23
14
 
24
- $dbh = DBI.connect( "DBI:Pg:m4dbi", "m4dbi", "m4dbi" )
15
+ $dbh = connect_to_spec_database
25
16
  reset_data
26
17
 
27
18
  class ManyCol < DBI::Model( :many_col_table )
@@ -43,6 +34,7 @@ describe 'A DBI::Model subclass' do
43
34
  @m_post = Class.new( DBI::Model( :posts ) )
44
35
  @m_empty = Class.new( DBI::Model( :empty_table ) )
45
36
  @m_mc = Class.new( DBI::Model( :many_col_table ) )
37
+ @m_mcpk = Class.new( DBI::Model( :mcpk, [ :kc1, :kc2 ] ) )
46
38
  class Author < DBI::Model( :authors ); end
47
39
  end
48
40
 
@@ -78,7 +70,7 @@ describe 'A DBI::Model subclass' do
78
70
 
79
71
  class Author < DBI::Model( :authors ); end
80
72
 
81
- dbh = DBI.connect( "DBI:Pg:m4dbi", "m4dbi", "m4dbi" )
73
+ dbh = connect_to_spec_database
82
74
  new_handle = DBI::DatabaseHandle.last_handle
83
75
  new_handle.should.equal dbh
84
76
  new_handle.should.not.equal original_handle
@@ -93,7 +85,7 @@ describe 'A DBI::Model subclass' do
93
85
  a1.should.not.be.nil
94
86
  a1.name.should.equal 'author1'
95
87
 
96
- dbh = DBI.connect( "DBI:Pg:m4dbi2", "m4dbi", "m4dbi" )
88
+ dbh = connect_to_spec_database( 'm4dbi2' )
97
89
  reset_data( dbh, "test-data2.sql" )
98
90
 
99
91
  @m_author2 = Class.new( DBI::Model( :authors ) )
@@ -109,7 +101,7 @@ describe 'A DBI::Model subclass' do
109
101
  ensure
110
102
  # Clean up handles for later specs
111
103
  dbh.disconnect if dbh and dbh.connected?
112
- DBI.connect( "DBI:Pg:m4dbi", "m4dbi", "m4dbi" )
104
+ connect_to_spec_database
113
105
  end
114
106
  end
115
107
 
@@ -135,6 +127,26 @@ describe 'A DBI::Model subclass' do
135
127
  o.should.not.be.nil
136
128
  o.class.should.equal @m_author
137
129
  o.name.should.equal 'author2'
130
+
131
+ o = @m_mcpk[ [ 2, 2 ] ]
132
+ o.should.not.be.nil
133
+ o.class.should.equal @m_mcpk
134
+ o.val.should.equal 'two two'
135
+
136
+ o = @m_mcpk[ 1, 1 ]
137
+ o.should.not.be.nil
138
+ o.class.should.equal @m_mcpk
139
+ o.val.should.equal 'one one'
140
+
141
+ o = @m_mcpk[ { :kc1 => 5, :kc2 => 6 } ]
142
+ o.should.not.be.nil
143
+ o.class.should.equal @m_mcpk
144
+ o.val.should.equal 'five six'
145
+
146
+ should.not.raise( DBI::Error ) do
147
+ o = @m_author[ nil ]
148
+ o.should.be.nil
149
+ end
138
150
  end
139
151
 
140
152
  it 'provides hash-like single-record access via #[ field_hash ]' do
@@ -147,6 +159,16 @@ describe 'A DBI::Model subclass' do
147
159
  o.should.not.be.nil
148
160
  o.class.should.equal @m_post
149
161
  o.text.should.equal 'First post.'
162
+
163
+ o = @m_mc[ :c1 => 100, :c2 => 50 ]
164
+ o.should.not.be.nil
165
+ o.class.should.equal @m_mc
166
+ o.c3.should.equal 20
167
+
168
+ o = @m_mc[ :c1 => 100, :c2 => nil ]
169
+ o.should.not.be.nil
170
+ o.class.should.equal @m_mc
171
+ o.c3.should.equal 40
150
172
  end
151
173
 
152
174
  it 'returns nil from #[] when no record is found' do
@@ -158,9 +180,7 @@ describe 'A DBI::Model subclass' do
158
180
  end
159
181
 
160
182
  it 'provides multi-record access via #where( Hash )' do
161
- posts = @m_post.where(
162
- :author_id => 1
163
- )
183
+ posts = @m_post.where( :author_id => 1 )
164
184
  posts.should.not.be.nil
165
185
  posts.should.not.be.empty
166
186
  posts.size.should.equal 2
@@ -171,6 +191,24 @@ describe 'A DBI::Model subclass' do
171
191
  }
172
192
  p = sorted_posts.first
173
193
  p.text.should.equal 'First post.'
194
+
195
+ rows = @m_mc.where( :c1 => 100, :c2 => 50 )
196
+ rows.should.not.be.nil
197
+ rows.should.not.be.empty
198
+ rows.size.should.equal 1
199
+ row = rows[ 0 ]
200
+ row.class.should.equal @m_mc
201
+ row.c1.should.equal 100
202
+ row.c3.should.equal 20
203
+
204
+ rows = @m_mc.where( :c1 => 100, :c2 => nil )
205
+ rows.should.not.be.nil
206
+ rows.should.not.be.empty
207
+ rows.size.should.equal 1
208
+ row = rows[ 0 ]
209
+ row.class.should.equal @m_mc
210
+ row.c1.should.equal 100
211
+ row.c3.should.equal 40
174
212
  end
175
213
 
176
214
  it 'provides multi-record access via #where( String )' do
@@ -187,6 +225,20 @@ describe 'A DBI::Model subclass' do
187
225
  p.text.should.equal 'Second post.'
188
226
  end
189
227
 
228
+ it 'provides multi-record access via #where( String, param, param... )' do
229
+ posts = @m_post.where( "id < ?", 3 )
230
+ posts.should.not.be.nil
231
+ posts.should.not.be.empty
232
+ posts.size.should.equal 2
233
+ posts[ 0 ].class.should.equal @m_post
234
+
235
+ sorted_posts = posts.sort { |p1,p2|
236
+ p2._id <=> p1._id
237
+ }
238
+ p = sorted_posts.first
239
+ p.text.should.equal 'Second post.'
240
+ end
241
+
190
242
  it 'returns an empty array from #where when no records are found' do
191
243
  a = @m_author.where( :id => 999 )
192
244
  a.should.be.empty
@@ -200,6 +252,12 @@ describe 'A DBI::Model subclass' do
200
252
  post.should.not.be.nil
201
253
  post.class.should.equal @m_post
202
254
  post.text.should.equal 'Second post.'
255
+
256
+ row = @m_mc.one_where( :c1 => 100, :c2 => nil )
257
+ row.should.not.be.nil
258
+ row.class.should.equal @m_mc
259
+ row.c1.should.equal 100
260
+ row.c3.should.equal 40
203
261
  end
204
262
 
205
263
  it 'provides single-record access via #one_where( String )' do
@@ -209,6 +267,13 @@ describe 'A DBI::Model subclass' do
209
267
  post.id.should.equal 3
210
268
  end
211
269
 
270
+ it 'provides single-record access via #one_where( String, param, param... )' do
271
+ post = @m_post.one_where( "text LIKE ?", '%Third%' )
272
+ post.should.not.be.nil
273
+ post.class.should.equal @m_post
274
+ post.id.should.equal 3
275
+ end
276
+
212
277
  it 'returns nil from #one_where when no record is found' do
213
278
  a = @m_author.one_where( :id => 999 )
214
279
  a.should.be.nil
@@ -217,7 +282,6 @@ describe 'A DBI::Model subclass' do
217
282
  p.should.be.nil
218
283
  end
219
284
 
220
-
221
285
  it 'returns all table records via #all' do
222
286
  rows = @m_author.all
223
287
  rows.should.not.be.nil
@@ -225,8 +289,10 @@ describe 'A DBI::Model subclass' do
225
289
  rows.size.should.equal 3
226
290
 
227
291
  rows[ 0 ].id.should.equal 1
292
+ rows[ 0 ].class.should.equal @m_author
228
293
  rows[ 0 ].name.should.equal 'author1'
229
294
  rows[ 1 ].id.should.equal 2
295
+ rows[ 1 ].class.should.equal @m_author
230
296
  rows[ 1 ].name.should.equal 'author2'
231
297
  end
232
298
 
@@ -236,7 +302,7 @@ describe 'A DBI::Model subclass' do
236
302
  rows.should.be.empty
237
303
  end
238
304
 
239
- it 'returns any single record from #one' do
305
+ it 'returns a random single record from #one' do
240
306
  one = @m_author.one
241
307
  one.should.not.be.nil
242
308
  one.class.should.equal @m_author
@@ -324,6 +390,14 @@ describe 'A DBI::Model subclass' do
324
390
  a.should.not.respond_to :no_column_by_this_name
325
391
  a.name.should.equal 'author1'
326
392
  @m_author.count.should.equal n
393
+
394
+ n = @m_mc.count
395
+ row = @m_mc.find_or_create( :c1 => 100, :c2 => nil )
396
+ row.should.not.be.nil
397
+ row.class.should.equal @m_mc
398
+ row.c1.should.equal 100
399
+ row.c3.should.equal 40
400
+ @m_mc.count.should.equal n
327
401
  end
328
402
 
329
403
  it 'creates a record via #find_or_create( Hash )' do
@@ -373,7 +447,7 @@ describe 'A DBI::Model subclass' do
373
447
  posts[ 1 ].text.should.equal 'Third post.'
374
448
  posts[ 1 ].class.should.equal @m_post
375
449
 
376
- no_posts = @m_post.s( "SELECT * FROM posts WHERE FALSE" )
450
+ no_posts = @m_post.s( "SELECT * FROM posts WHERE 1+1 = 3" )
377
451
  no_posts.should.not.be.nil
378
452
  no_posts.should.be.empty
379
453
  end
@@ -402,7 +476,7 @@ describe 'A DBI::Model subclass' do
402
476
  post.author_id.should.equal 1
403
477
  post.text.should.equal 'Third post.'
404
478
 
405
- no_post = @m_post.s1( "SELECT * FROM posts WHERE FALSE" )
479
+ no_post = @m_post.s1( "SELECT * FROM posts WHERE 1+1 = 3" )
406
480
  no_post.should.be.nil
407
481
  end
408
482
 
@@ -426,16 +500,26 @@ describe 'A DBI::Model subclass' do
426
500
  end
427
501
 
428
502
  it 'provides a means to update records referred to by primary key value' do
429
- new_text = 'This is some new text.'
503
+ new_text = 'Some new text.'
430
504
 
431
505
  p2 = @m_post[ 2 ]
432
506
  p2.text.should.not.equal new_text
433
-
434
507
  @m_post.update_one( 2, { :text => new_text } )
435
-
436
508
  p2_ = @m_post[ 2 ]
437
509
  p2_.text.should.equal new_text
438
510
 
511
+ row = @m_mcpk[ 1, 1 ]
512
+ row.val.should.not.equal new_text
513
+ @m_mcpk.update_one( 1, 1, { :val => new_text } )
514
+ row = @m_mcpk[ 1, 1 ]
515
+ row.val.should.equal new_text
516
+
517
+ row = @m_mcpk[ 3, 4 ]
518
+ row.val.should.not.equal new_text
519
+ @m_mcpk.update_one( 3, 4, { :val => new_text } )
520
+ row = @m_mcpk[ 3, 4 ]
521
+ row.val.should.equal new_text
522
+
439
523
  reset_data
440
524
  end
441
525
 
@@ -560,6 +644,8 @@ describe 'A found DBI::Model subclass instance' do
560
644
  before do
561
645
  @m_author = Class.new( DBI::Model( :authors ) )
562
646
  @m_post = Class.new( DBI::Model( :posts ) )
647
+ @m_mc = Class.new( DBI::Model( :many_col_table ) )
648
+ @m_mcpk = Class.new( DBI::Model( :mcpk, [ :kc1, :kc2 ] ) )
563
649
  end
564
650
 
565
651
  it 'provides access to primary key value' do
@@ -568,6 +654,12 @@ describe 'A found DBI::Model subclass instance' do
568
654
 
569
655
  p = @m_post[ 3 ]
570
656
  p.pk.should.equal 3
657
+
658
+ r = @m_mcpk[ 1, 1 ]
659
+ r.pk.should.equal [ 1, 1 ]
660
+
661
+ r = @m_mcpk[ { :kc1 => 3, :kc2 => 4 } ]
662
+ r.pk.should.equal [ 3, 4 ]
571
663
  end
572
664
 
573
665
  it 'provides read access to fields via identically-named readers' do
@@ -595,6 +687,7 @@ describe 'A found DBI::Model subclass instance' do
595
687
 
596
688
  p3 = @m_post[ 3 ]
597
689
  p3.text = the_new_text
690
+ p3.text.should.equal the_new_text
598
691
 
599
692
  p3_ = @m_post[ 3 ]
600
693
  p3_.text.should.equal the_new_text
@@ -603,6 +696,16 @@ describe 'A found DBI::Model subclass instance' do
603
696
  p2_ = @m_post[ 2 ]
604
697
  p2_.text.should.equal p2.text
605
698
 
699
+ mc1 = @m_mc.create(
700
+ :id => 1,
701
+ :c1 => 2
702
+ )
703
+ mc1.c1.should.equal 2
704
+ mc1.c1 = nil
705
+ mc1.c1.should.be.nil
706
+ mc1_ = @m_mc[ 1 ]
707
+ mc1_.c1.should.be.nil
708
+
606
709
  reset_data
607
710
  end
608
711
 
@@ -620,11 +723,28 @@ describe 'A found DBI::Model subclass instance' do
620
723
  :author_id => 2,
621
724
  :text => the_new_text
622
725
  )
726
+ p.author_id.should.equal 2
727
+ p.text.should.equal the_new_text
623
728
 
624
729
  p_ = @m_post[ 1 ]
625
730
  p_.author_id.should.equal 2
626
731
  p_.text.should.equal the_new_text
627
732
 
733
+ mc1 = @m_mc.create(
734
+ :id => 1,
735
+ :c1 => 2,
736
+ :c2 => 3
737
+ )
738
+ mc1.set(
739
+ :c1 => nil,
740
+ :c2 => 4
741
+ )
742
+ mc1.c1.should.be.nil
743
+ mc1.c2.should.equal 4
744
+ mc1_ = @m_mc[ 1 ]
745
+ mc1_.c1.should.be.nil
746
+ mc1_.c2.should.equal 4
747
+
628
748
  reset_data
629
749
  end
630
750
 
@@ -645,18 +765,6 @@ describe 'A found DBI::Model subclass instance' do
645
765
  end
646
766
  end
647
767
 
648
- it 'allows a field to be incremented' do
649
- mc = ManyCol.create( :c1 => 50 )
650
- should.not.raise do
651
- mc.inc
652
- end
653
- end
654
- it 'allows a field to be decremented' do
655
- mc = ManyCol.create( :c1 => 50 )
656
- should.not.raise do
657
- mc.dec
658
- end
659
- end
660
768
  end
661
769
 
662
770
  describe 'DBI::Model (relationships)' do
@@ -740,7 +848,6 @@ describe 'DBI::Collection' do
740
848
  before do
741
849
  @m_author = Class.new( DBI::Model( :authors ) )
742
850
  @m_post = Class.new( DBI::Model( :posts ) )
743
- @m_fan = Class.new( DBI::Model( :fans ) )
744
851
 
745
852
  DBI::Model.one_to_many(
746
853
  @m_author, @m_post, :posts, :author, :author_id
metadata CHANGED
@@ -1,19 +1,30 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: m4dbi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
5
- platform: ""
4
+ version: 0.6.0
5
+ platform: ruby
6
6
  authors:
7
7
  - Pistos
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-04-01 00:00:00 -04:00
12
+ date: 2008-11-10 00:00:00 -05:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: metaid
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: dbi
27
+ type: :runtime
17
28
  version_requirement:
18
29
  version_requirements: !ruby/object:Gem::Requirement
19
30
  requirements:
@@ -31,21 +42,25 @@ extra_rdoc_files:
31
42
  - HIM
32
43
  - READHIM
33
44
  - CHANGELOG
45
+ - LICENCE
34
46
  files:
35
47
  - HIM
36
48
  - READHIM
37
- - lib/m4dbi/database-handle.rb
49
+ - CHANGELOG
50
+ - LICENCE
51
+ - lib/m4dbi/array.rb
52
+ - lib/m4dbi/collection.rb
38
53
  - lib/m4dbi/row.rb
39
- - lib/m4dbi/model.rb
54
+ - lib/m4dbi/timestamp.rb
40
55
  - lib/m4dbi/traits.rb
41
- - lib/m4dbi/collection.rb
56
+ - lib/m4dbi/database-handle.rb
42
57
  - lib/m4dbi/hash.rb
58
+ - lib/m4dbi/model.rb
43
59
  - lib/m4dbi.rb
44
- - spec/helper.rb
45
- - spec/dbi.rb
46
60
  - spec/model.rb
61
+ - spec/dbi.rb
47
62
  - spec/hash.rb
48
- - CHANGELOG
63
+ - spec/helper.rb
49
64
  has_rdoc: false
50
65
  homepage: http://purepistos.net/m4dbi
51
66
  post_install_message:
@@ -66,15 +81,14 @@ required_rubygems_version: !ruby/object:Gem::Requirement
66
81
  version: "0"
67
82
  version:
68
83
  requirements:
69
- - dbi
70
84
  - bacon (optional)
71
- rubyforge_project:
72
- rubygems_version: 0.9.5
85
+ rubyforge_project: m4dbi
86
+ rubygems_version: 1.2.0
73
87
  signing_key:
74
88
  specification_version: 2
75
89
  summary: Models (and More) for DBI
76
90
  test_files:
77
- - spec/helper.rb
78
- - spec/dbi.rb
79
91
  - spec/model.rb
92
+ - spec/dbi.rb
80
93
  - spec/hash.rb
94
+ - spec/helper.rb