m4dbi 0.6.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/{READHIM → README} RENAMED
File without changes
data/lib/m4dbi.rb CHANGED
@@ -1,14 +1,26 @@
1
1
  require 'rubygems'
2
+ require 'rdbi'
3
+ require 'metaid'
4
+ require 'thread'
2
5
 
3
- M4DBI_VERSION = '0.6.2'
6
+ M4DBI_VERSION = '0.7.0'
4
7
 
5
8
  __DIR__ = File.expand_path( File.dirname( __FILE__ ) )
6
9
 
10
+ require "#{__DIR__}/m4dbi/error"
7
11
  require "#{__DIR__}/m4dbi/traits"
8
12
  require "#{__DIR__}/m4dbi/hash"
9
13
  require "#{__DIR__}/m4dbi/array"
10
- require "#{__DIR__}/m4dbi/database-handle"
11
- require "#{__DIR__}/m4dbi/row"
12
- require "#{__DIR__}/m4dbi/timestamp"
14
+ require "#{__DIR__}/m4dbi/database"
13
15
  require "#{__DIR__}/m4dbi/model"
14
16
  require "#{__DIR__}/m4dbi/collection"
17
+
18
+ module M4DBI
19
+ ancestral_trait_class_reader :last_dbh
20
+
21
+ def self.connect( *args )
22
+ dbh = M4DBI::Database.new( RDBI.connect( *args ) )
23
+ trait :last_dbh => dbh
24
+ dbh
25
+ end
26
+ end
@@ -1,4 +1,4 @@
1
- module DBI
1
+ module M4DBI
2
2
  class Collection
3
3
  def initialize( the_one, the_many_model, the_one_fk )
4
4
  @the_one = the_one
@@ -25,7 +25,7 @@ module DBI
25
25
  def delete( arg )
26
26
  case arg
27
27
  when @the_many_model
28
- result = @the_many_model.dbh.do(
28
+ @the_many_model.dbh.execute(
29
29
  %{
30
30
  DELETE FROM #{@the_many_model.table}
31
31
  WHERE
@@ -34,15 +34,14 @@ module DBI
34
34
  },
35
35
  @the_one.pk,
36
36
  arg.pk
37
- )
38
- result > 0
37
+ ).affected_count > 0
39
38
  when Hash
40
39
  hash = arg
41
40
  keys = hash.keys
42
41
  where_subclause = keys.map { |k|
43
42
  "#{k} = ?"
44
43
  }.join( " AND " )
45
- @the_many_model.dbh.do(
44
+ @the_many_model.dbh.execute(
46
45
  %{
47
46
  DELETE FROM #{@the_many_model.table}
48
47
  WHERE
@@ -51,19 +50,19 @@ module DBI
51
50
  },
52
51
  @the_one.pk,
53
52
  *( keys.map { |k| hash[ k ] } )
54
- )
53
+ ).affected_count
55
54
  end
56
55
  end
57
56
 
58
57
  # Returns the number of records deleted
59
58
  def clear
60
- @the_many_model.dbh.do(
59
+ @the_many_model.dbh.execute(
61
60
  %{
62
61
  DELETE FROM #{@the_many_model.table}
63
62
  WHERE #{@the_one_fk} = ?
64
63
  },
65
64
  @the_one.pk
66
- )
65
+ ).affected_count
67
66
  end
68
67
  end
69
68
  end
@@ -0,0 +1,73 @@
1
+ module M4DBI
2
+
3
+ class Database
4
+
5
+ def initialize( rdbi_dbh )
6
+ @dbh = rdbi_dbh
7
+ end
8
+
9
+ def execute( *args )
10
+ @dbh.execute *args
11
+ end
12
+
13
+ def select( sql, *bindvars )
14
+ execute( sql, *bindvars ).fetch( :all, RDBI::Result::Driver::Struct )
15
+ end
16
+
17
+ def select_one( sql, *bindvars )
18
+ select( sql, *bindvars )[ 0 ]
19
+ end
20
+
21
+ def select_column( sql, *bindvars )
22
+ rows = execute( sql, *bindvars ).fetch( 1, RDBI::Result::Driver::Array )
23
+ if rows.any?
24
+ rows[ 0 ][ 0 ]
25
+ else
26
+ raise RDBI::Error.new( "Query returned no rows. SQL: #{@dbh.last_query}" )
27
+ end
28
+ end
29
+
30
+ alias select_all select
31
+ alias s select
32
+ alias s1 select_one
33
+ alias sc select_column
34
+ alias update execute
35
+ alias u execute
36
+ alias insert execute
37
+ alias i execute
38
+ alias delete execute
39
+ alias d execute
40
+
41
+ def connected?
42
+ @dbh.connected?
43
+ end
44
+
45
+ def disconnect
46
+ @dbh.disconnect
47
+ end
48
+
49
+ def table_schema( *args )
50
+ @dbh.table_schema( *args )
51
+ end
52
+
53
+ def database_name
54
+ @dbh.database_name
55
+ end
56
+
57
+ def transaction( &block )
58
+ @dbh.transaction &block
59
+ end
60
+
61
+ def last_query
62
+ @dbh.last_query
63
+ end
64
+
65
+ def driver
66
+ @dbh.driver
67
+ end
68
+ end
69
+ end
70
+
71
+
72
+
73
+
@@ -0,0 +1,4 @@
1
+ module M4DBI
2
+ class Error < ::RDBI::Error
3
+ end
4
+ end
data/lib/m4dbi/model.rb CHANGED
@@ -1,7 +1,4 @@
1
- require 'dbi'
2
- require 'metaid'
3
-
4
- module DBI
1
+ module M4DBI
5
2
  class Model
6
3
  #attr_reader :row
7
4
  ancestral_trait_reader :dbh, :table
@@ -120,11 +117,11 @@ module DBI
120
117
  value_placeholders = values.map { |v| '?' }.join( ',' )
121
118
  rec = nil
122
119
 
123
- dbh.one_transaction do |dbh_|
124
- num_inserted = dbh_.do(
120
+ dbh.transaction do |dbh_|
121
+ num_inserted = dbh_.execute(
125
122
  "INSERT INTO #{table} ( #{cols} ) VALUES ( #{value_placeholders} )",
126
123
  *values
127
- )
124
+ ).affected_count
128
125
  if num_inserted > 0
129
126
  pk_hash = hash.slice( *(
130
127
  self.pk.map { |pk_col| pk_col.to_sym }
@@ -152,7 +149,7 @@ module DBI
152
149
 
153
150
  def self.find_or_create( hash = nil )
154
151
  item = nil
155
- error = DBI::Error.new( "Failed to find_or_create( #{hash.inspect} )" )
152
+ error = M4DBI::Error.new( "Failed to find_or_create( #{hash.inspect} )" )
156
153
  item = self.one_where( hash )
157
154
  if item.nil?
158
155
  item =
@@ -201,7 +198,7 @@ module DBI
201
198
 
202
199
  set_clause, set_params = set_hash.to_set_clause
203
200
  params = set_params + where_params
204
- dbh.do(
201
+ dbh.execute(
205
202
  "UPDATE #{table} SET #{set_clause} WHERE #{where_clause}",
206
203
  *params
207
204
  )
@@ -211,19 +208,19 @@ module DBI
211
208
  set_clause, set_params = args[ -1 ].to_set_clause
212
209
  pk_values = args[ 0..-2 ]
213
210
  params = set_params + pk_values
214
- dbh.do(
211
+ dbh.execute(
215
212
  "UPDATE #{table} SET #{set_clause} WHERE #{pk_clause}",
216
213
  *params
217
214
  )
218
215
  end
219
216
 
220
217
  # Example:
221
- # DBI::Model.one_to_many( Author, Post, :posts, :author, :author_id )
218
+ # M4DBI::Model.one_to_many( Author, Post, :posts, :author, :author_id )
222
219
  # her_posts = some_author.posts
223
220
  # the_author = some_post.author
224
221
  def self.one_to_many( the_one, the_many, many_as, one_as, the_one_fk )
225
222
  the_one.class_def( many_as.to_sym ) do
226
- DBI::Collection.new( self, the_many, the_one_fk )
223
+ M4DBI::Collection.new( self, the_many, the_one_fk )
227
224
  end
228
225
  the_many.class_def( one_as.to_sym ) do
229
226
  the_one[ @row[ the_one_fk ] ]
@@ -234,7 +231,7 @@ module DBI
234
231
  end
235
232
 
236
233
  # Example:
237
- # DBI::Model.many_to_many(
234
+ # M4DBI::Model.many_to_many(
238
235
  # @m_author, @m_fan, :authors_liked, :fans, :authors_fans, :author_id, :fan_id
239
236
  # )
240
237
  # her_fans = some_author.fans
@@ -278,11 +275,11 @@ module DBI
278
275
  # ------------------- :nodoc:
279
276
 
280
277
  def initialize( row )
281
- if not row.respond_to?( "[]".to_sym ) or not row.respond_to?( "[]=".to_sym )
282
- raise DBI::Error.new( "Attempted to instantiate DBI::Model with an invalid argument (#{row.inspect}). (Expecting DBI::Row.)" )
278
+ if ! row.respond_to?( "[]".to_sym ) || ! row.respond_to?( "[]=".to_sym )
279
+ raise M4DBI::Error.new( "Attempted to instantiate M4DBI::Model with an invalid argument (#{row.inspect}). (Expecting something accessible [] and []= .)" )
283
280
  end
284
281
  if caller[ 1 ] !~ %r{/m4dbi/model\.rb:}
285
- warn "Do not call DBI::Model#new directly; use DBI::Model#create instead."
282
+ warn "Do not call M4DBI::Model#new directly; use M4DBI::Model#create instead."
286
283
  end
287
284
  @row = row
288
285
  end
@@ -354,10 +351,10 @@ module DBI
354
351
  def set( hash )
355
352
  set_clause, set_params = hash.to_set_clause
356
353
  set_params << pk
357
- num_updated = dbh.do(
354
+ num_updated = dbh.execute(
358
355
  "UPDATE #{table} SET #{set_clause} WHERE #{pk_clause}",
359
356
  *set_params
360
- )
357
+ ).affected_count
361
358
  if num_updated > 0
362
359
  hash.each do |key,value|
363
360
  @row[ key ] = value
@@ -368,10 +365,10 @@ module DBI
368
365
 
369
366
  # Returns true iff the record and only the record was successfully deleted.
370
367
  def delete
371
- num_deleted = dbh.do(
368
+ num_deleted = dbh.execute(
372
369
  "DELETE FROM #{table} WHERE #{pk_clause}",
373
370
  *pk_values
374
- )
371
+ ).affected_count
375
372
  num_deleted == 1
376
373
  end
377
374
 
@@ -381,35 +378,34 @@ module DBI
381
378
  end
382
379
  end
383
380
 
384
- # Define a new DBI::Model like this:
385
- # class Post < DBI::Model( :posts ); end
381
+ # Define a new M4DBI::Model like this:
382
+ # class Post < M4DBI::Model( :posts ); end
386
383
  # You can specify the primary key column(s) using an option, like so:
387
- # class Author < DBI::Model( :authors, pk: [ 'auth_num' ] ); end
384
+ # class Author < M4DBI::Model( :authors, pk: [ 'auth_num' ] ); end
388
385
  def self.Model( table, options = Hash.new )
389
- h = options[ :dbh ] || DBI::DatabaseHandle.last_handle
390
- if h.nil? or not h.connected?
391
- raise DBI::Error.new( "Attempted to create a Model class without first connecting to a database." )
386
+ h = options[ :dbh ] || M4DBI.last_dbh
387
+ if h.nil? || ! h.connected?
388
+ raise M4DBI::Error.new( "Attempted to create a Model class without first connecting to a database." )
392
389
  end
393
390
  pk_ = options[ :pk ] || [ 'id' ]
394
391
  if not pk_.respond_to? :each
395
- raise DBI::Error.new( "Primary key must be enumerable (was given #{pk_.inspect})" )
392
+ raise M4DBI::Error.new( "Primary key must be enumerable (was given #{pk_.inspect})" )
396
393
  end
397
394
 
398
395
  model_key =
399
- # DBD-dependent. Not all DBDs have dbname implemented by M4DBI.
400
- if h.respond_to? :dbname
401
- "#{h.dbname}::#{table}"
396
+ if h.respond_to? :database_name
397
+ "#{h.database_name}::#{table}"
402
398
  else
403
399
  table
404
400
  end
405
401
 
406
402
  @models ||= Hash.new
407
- @models[ model_key ] ||= Class.new( DBI::Model ) do |klass|
403
+ @models[ model_key ] ||= Class.new( M4DBI::Model ) do |klass|
408
404
  klass.trait( {
409
405
  :dbh => h,
410
406
  :table => table,
411
407
  :pk => pk_,
412
- :columns => h.columns( table.to_s ),
408
+ :columns => h.table_schema( table.to_sym ).columns,
413
409
  } )
414
410
 
415
411
  meta_def( 'pk_str'.to_sym ) do
@@ -420,43 +416,61 @@ module DBI
420
416
  end
421
417
  end
422
418
 
423
- if defined?( DBI::DBD::Pg::Database ) and DBI::DBD::Pg::Database === h.handle
419
+ if defined?( RDBI::Driver::PostgreSQL ) && RDBI::Driver::PostgreSQL === h.driver
424
420
  # TODO: This is broken for non-SERIAL or multi-column primary keys
425
421
  meta_def( "last_record".to_sym ) do |dbh_|
426
422
  self.s1 "SELECT * FROM #{table} WHERE #{pk_str} = currval( '#{table}_#{pk_str}_seq' );"
427
423
  end
428
- elsif defined?( DBI::DBD::Mysql::Database ) and DBI::DBD::Mysql::Database === h.handle
424
+ elsif defined?( RDBI::Driver::MySQL ) && RDBI::Driver::MySQL === h.driver
429
425
  meta_def( "last_record".to_sym ) do |dbh_|
430
426
  self.s1 "SELECT * FROM #{table} WHERE #{pk_str} = LAST_INSERT_ID();"
431
427
  end
432
- elsif defined?( DBI::DBD::SQLite3::Database ) and DBI::DBD::SQLite3::Database === h.handle
428
+ elsif defined?( RDBI::Driver::SQLite3 ) && RDBI::Driver::SQLite3 === h.driver
433
429
  meta_def( "last_record".to_sym ) do |dbh_|
434
430
  self.s1 "SELECT * FROM #{table} WHERE #{pk_str} = last_insert_rowid();"
435
431
  end
436
- # TODO: more DBDs
432
+ # TODO: more DB drivers
437
433
  end
438
434
 
439
435
  klass.trait[ :columns ].each do |col|
436
+
440
437
  colname = col[ 'name' ]
438
+ method = colname.to_sym
439
+ while klass.method_defined? method
440
+ method = "#{method}_".to_sym
441
+ end
441
442
 
442
443
  # Column readers
443
- class_def( colname.to_sym ) do
444
+ class_def( method ) do
444
445
  @row[ colname ]
445
446
  end
446
447
 
447
448
  # Column writers
448
- class_def( "#{colname}=".to_sym ) do |new_value|
449
- num_changed = dbh.do(
449
+
450
+ class_def( "#{method}=".to_sym ) do |new_value|
451
+ num_changed = dbh.execute(
452
+ "UPDATE #{table} SET #{colname} = ? WHERE #{pk_clause}",
453
+ new_value,
454
+ *pk_values
455
+ ).affected_count
456
+ if num_changed > 0
457
+ @row[ colname ] = new_value
458
+ end
459
+ end
460
+
461
+ class_def( '[]='.to_sym ) do |colname, new_value|
462
+ num_changed = dbh.execute(
450
463
  "UPDATE #{table} SET #{colname} = ? WHERE #{pk_clause}",
451
464
  new_value,
452
465
  *pk_values
453
- )
466
+ ).affected_count
454
467
  if num_changed > 0
455
468
  @row[ colname ] = new_value
456
469
  end
457
470
  end
471
+
458
472
  end
459
473
  end
460
474
  end
461
475
 
462
- end
476
+ end
data/spec/database.rb ADDED
@@ -0,0 +1,84 @@
1
+ require_relative 'helper'
2
+
3
+ $dbh = connect_to_spec_database
4
+ reset_data
5
+
6
+ describe 'M4DBI.last_dbh' do
7
+ it 'provides the last database handle connected to' do
8
+ M4DBI.last_dbh.should.equal $dbh
9
+ end
10
+ end
11
+
12
+ describe 'M4DBI::Database#select_column' do
13
+
14
+ it 'selects one column' do
15
+ name = $dbh.select_column(
16
+ "SELECT name FROM authors LIMIT 1"
17
+ )
18
+ name.class.should.not.equal Array
19
+ name.should.equal 'author1'
20
+
21
+ null = $dbh.select_column(
22
+ "SELECT c4 FROM many_col_table WHERE c3 = 40"
23
+ )
24
+ null.should.be.nil
25
+
26
+ should.raise( RDBI::Error ) do
27
+ $dbh.select_column( "SELECT name FROM authors WHERE 1+1 = 3" )
28
+ end
29
+
30
+ begin
31
+ $dbh.select_column( "SELECT name FROM authors WHERE 1+1 = 3" )
32
+ rescue RDBI::Error => e
33
+ e.message.should.match /SELECT name FROM authors WHERE 1\+1 = 3/
34
+ end
35
+ end
36
+
37
+ it 'selects one column of first row' do
38
+ name = $dbh.select_column(
39
+ "SELECT name FROM authors ORDER BY name DESC"
40
+ )
41
+ name.should.equal 'author3'
42
+ end
43
+
44
+ it 'selects first column of first row' do
45
+ name = $dbh.select_column(
46
+ "SELECT name, id FROM authors ORDER BY name DESC"
47
+ )
48
+ name.should.equal 'author3'
49
+ end
50
+ end
51
+
52
+ describe 'row accessors' do
53
+
54
+ it 'provide read access via #fieldname' do
55
+ row = $dbh.select_one(
56
+ "SELECT * FROM posts ORDER BY author_id DESC LIMIT 1"
57
+ )
58
+ row.should.not.equal nil
59
+
60
+ row.author_id.should.be.same_as row[ 'author_id' ]
61
+ row.text.should.be.same_as row[ 'text' ]
62
+
63
+ row.text.should.equal 'Second post.'
64
+ end
65
+
66
+ it 'provide in-memory (non-syncing) write access via #fieldname=' do
67
+ row = $dbh.select_one(
68
+ "SELECT * FROM posts ORDER BY author_id DESC LIMIT 1"
69
+ )
70
+ row.should.not.equal nil
71
+
72
+ old_id = row[ :id ]
73
+ row[ :id ] = old_id + 1
74
+ row[ :id ].should.not.equal old_id
75
+ row[ :id ].should.equal( old_id + 1 )
76
+
77
+ old_text = row.text
78
+ new_text = 'This is the new post text.'
79
+ row.text = new_text
80
+ row.text.should.not.equal old_text
81
+ row.text.should.equal new_text
82
+ end
83
+
84
+ end