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} +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
|