m4dbi 0.6.2 → 0.7.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/{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