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} +0 -0
- data/lib/m4dbi.rb +16 -4
- data/lib/m4dbi/collection.rb +7 -8
- data/lib/m4dbi/database.rb +73 -0
- data/lib/m4dbi/error.rb +4 -0
- data/lib/m4dbi/model.rb +55 -41
- data/spec/database.rb +84 -0
- data/spec/hash.rb +5 -5
- data/spec/helper.rb +18 -6
- data/spec/model.rb +211 -96
- metadata +25 -35
- data/HIM +0 -50
- data/lib/m4dbi/database-handle.rb +0 -117
- data/lib/m4dbi/row.rb +0 -35
- data/lib/m4dbi/timestamp.rb +0 -20
- data/spec/dbi.rb +0 -142
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
|
+
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
|
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
|
data/lib/m4dbi/collection.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
module
|
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
|
-
|
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.
|
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.
|
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
|
+
|
data/lib/m4dbi/error.rb
ADDED
data/lib/m4dbi/model.rb
CHANGED
@@ -1,7 +1,4 @@
|
|
1
|
-
|
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.
|
124
|
-
num_inserted = dbh_.
|
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 =
|
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.
|
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.
|
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
|
-
#
|
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
|
-
|
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
|
-
#
|
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
|
282
|
-
raise
|
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
|
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.
|
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.
|
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
|
385
|
-
# class Post <
|
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 <
|
384
|
+
# class Author < M4DBI::Model( :authors, pk: [ 'auth_num' ] ); end
|
388
385
|
def self.Model( table, options = Hash.new )
|
389
|
-
h = options[ :dbh ] ||
|
390
|
-
if h.nil?
|
391
|
-
raise
|
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
|
392
|
+
raise M4DBI::Error.new( "Primary key must be enumerable (was given #{pk_.inspect})" )
|
396
393
|
end
|
397
394
|
|
398
395
|
model_key =
|
399
|
-
|
400
|
-
|
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(
|
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.
|
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?(
|
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?(
|
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?(
|
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
|
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(
|
444
|
+
class_def( method ) do
|
444
445
|
@row[ colname ]
|
445
446
|
end
|
446
447
|
|
447
448
|
# Column writers
|
448
|
-
|
449
|
-
|
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
|