m4dbi 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1,3 @@
1
+ ## 0.5.0
2
+
3
+ First official public release.
data/HIM ADDED
@@ -0,0 +1,50 @@
1
+ == M4DBI - Models (and more) For DBI
2
+
3
+ http://purepistos.net/m4dbi
4
+
5
+ M4DBI is a Ruby library that provides ORM modelling to the Ruby DBI package
6
+ ( http://ruby-dbi.rubyforge.org/ ).
7
+
8
+ === Dependencies
9
+
10
+ - dbi (of course) http://ruby-dbi.rubyforge.org/
11
+ - DBDs for your choice of DBs
12
+ - metaid (gem install metaid)
13
+ - bacon (optional, for running tests; gem install bacon)
14
+
15
+ === Installation
16
+
17
+ ==== Nightly Gem
18
+
19
+ wget http://rome.purepistos.net/m4dbi/m4dbi-nightly.gem
20
+ gem install m4dbi-nightly.gem
21
+
22
+ ==== Repository
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.
26
+
27
+ cd /where/you/want/m4dbi
28
+ svn co http://rome.purepistos.net/svn/m4dbi
29
+
30
+ Change the following to whatever the equivalent paths are for your system:
31
+
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
35
+
36
+ === Usage
37
+
38
+ See http://rome.purepistos.net/m4dbi/examples/ . These are automatically
39
+ generated from the spec files under the spec/ dir.
40
+
41
+ === Source Code
42
+
43
+ Browse source at http://rome.purepistos.net/issues/m4dbi/browser/trunk .
44
+ See coverage at http://rome.purepistos.net/m4dbi/rcov .
45
+ Very limited rdocs at http://rome.purepistos.net/m4dbi/rdoc .
46
+
47
+ === Feedback and Support
48
+
49
+ On IRC: irc.freenode.net ##mathetes or ##ramaze .
50
+ Use http://mibbit.com if you don't have an IRC client.
data/READHIM ADDED
@@ -0,0 +1 @@
1
+ Don't read ME; read HIM!
@@ -0,0 +1,69 @@
1
+ module DBI
2
+ class Collection
3
+ def initialize( the_one, the_many_model, the_one_fk )
4
+ @the_one = the_one
5
+ @the_many_model = the_many_model
6
+ @the_one_fk = the_one_fk
7
+ end
8
+
9
+ def elements
10
+ @the_many_model.where( @the_one_fk => @the_one.pk )
11
+ end
12
+ alias copy elements
13
+
14
+ def method_missing( method, *args, &blk )
15
+ elements.send( method, *args, &blk )
16
+ end
17
+
18
+ def push( new_item_hash )
19
+ new_item_hash[ @the_one_fk ] = @the_one.pk
20
+ @the_many_model.create( new_item_hash )
21
+ end
22
+ alias << push
23
+ alias add push
24
+
25
+ def delete( arg )
26
+ case arg
27
+ when @the_many_model
28
+ result = @the_many_model.dbh.do(
29
+ %{
30
+ DELETE FROM #{@the_many_model.table}
31
+ WHERE
32
+ #{@the_one_fk} = ?
33
+ AND #{@the_many_model.pk} = ?
34
+ },
35
+ @the_one.pk,
36
+ arg.pk
37
+ )
38
+ result > 0
39
+ when Hash
40
+ hash = arg
41
+ keys = hash.keys
42
+ where_subclause = keys.map { |k|
43
+ "#{k} = ?"
44
+ }.join( " AND " )
45
+ @the_many_model.dbh.do(
46
+ %{
47
+ DELETE FROM #{@the_many_model.table}
48
+ WHERE
49
+ #{@the_one_fk} = ?
50
+ AND #{where_subclause}
51
+ },
52
+ @the_one.pk,
53
+ *( keys.map { |k| hash[ k ] } )
54
+ )
55
+ end
56
+ end
57
+
58
+ # Returns the number of records deleted
59
+ def clear
60
+ @the_many_model.dbh.do(
61
+ %{
62
+ DELETE FROM #{@the_many_model.table}
63
+ WHERE #{@the_one_fk} = ?
64
+ },
65
+ @the_one.pk
66
+ )
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,86 @@
1
+ require 'dbi'
2
+ require 'thread'
3
+
4
+ module DBI
5
+
6
+ # Here, we engage in some hackery to get database handles to provide us
7
+ # with the name of the database connected to. For mystical reasons, this
8
+ # is hidden in normal DBI.
9
+ # Retrieve the database name with DatabaseHandle#dbname.
10
+ module DBD; module Pg
11
+ module ConnectionDatabaseNameAccessor
12
+ def dbname
13
+ @connection.db
14
+ end
15
+ end
16
+ module DatabaseNameAccessor
17
+ def dbname
18
+ @handle.dbname
19
+ end
20
+ end
21
+ end; end
22
+
23
+ class DatabaseHandle
24
+ alias old_initialize initialize
25
+ def initialize( handle )
26
+ DBI::DatabaseHandle.last_handle = self
27
+ handle = old_initialize( handle )
28
+ @mutex = Mutex.new
29
+
30
+ # Hackery to expose dbname.
31
+ if defined?( DBI::DBD::Pg::Database ) and ( DBI::DBD::Pg::Database === @handle )
32
+ @handle.extend DBI::DBD::Pg::ConnectionDatabaseNameAccessor
33
+ extend DBI::DBD::Pg::DatabaseNameAccessor
34
+ end
35
+ # TODO: more DBDs
36
+
37
+ handle
38
+ end
39
+
40
+ # Atomically disable autocommit, do transaction, and reenable.
41
+ # Used for a single transaction when autocommit is normally left on.
42
+ # Only one thread can execute one_transaction at a time,
43
+ # since we need to thread protect the AutoCommit property of the
44
+ # database handle.
45
+ def one_transaction
46
+ @mutex.synchronize do
47
+ auto_commit = self[ 'AutoCommit' ]
48
+ self[ 'AutoCommit' ] = false
49
+ result = transaction do
50
+ yield self
51
+ end
52
+ self[ 'AutoCommit' ] = auto_commit
53
+ result
54
+ end
55
+ end
56
+
57
+ def select_column( statement, *bindvars )
58
+ row = select_one( statement, *bindvars )
59
+ if row
60
+ row[ 0 ]
61
+ end
62
+ end
63
+
64
+ alias s select_all
65
+ alias s1 select_one
66
+ alias sc select_column
67
+ alias u do
68
+ alias i do
69
+ alias d do
70
+
71
+ class << self
72
+ def last_handle
73
+ @handle# ||= create_handle
74
+ end
75
+
76
+ def last_handle=( handle )
77
+ @handle = handle
78
+ end
79
+ end
80
+
81
+ end
82
+ end
83
+
84
+
85
+
86
+
data/lib/m4dbi/hash.rb ADDED
@@ -0,0 +1,21 @@
1
+ class Hash
2
+ def to_clause( join_string )
3
+ # The clause items and the values have to be in the same order.
4
+ keys_ = keys
5
+ clause = keys_.map { |field|
6
+ "#{field} = ?"
7
+ }.join( join_string )
8
+ values_ = keys_.map { |key|
9
+ self[ key ]
10
+ }
11
+ [ clause, values_ ]
12
+ end
13
+
14
+ def to_where_clause
15
+ to_clause( " AND " )
16
+ end
17
+
18
+ def to_set_clause
19
+ to_clause( ", " )
20
+ end
21
+ end
@@ -0,0 +1,372 @@
1
+ require 'dbi'
2
+ require 'metaid'
3
+
4
+ module DBI
5
+ class Model
6
+ #attr_reader :row
7
+ ancestral_trait_reader :dbh, :table
8
+ ancestral_trait_class_reader :dbh, :table, :pk, :columns
9
+
10
+ M4DBI_UNASSIGNED = '__m4dbi_unassigned__'
11
+
12
+ extend Enumerable
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
+ )
27
+ end
28
+
29
+ if row
30
+ self.new( row )
31
+ end
32
+ end
33
+
34
+ def self.from_rows( rows )
35
+ rows.map { |r| self.new( r ) }
36
+ end
37
+
38
+ def self.where( conditions, *args )
39
+ case conditions
40
+ when String
41
+ sql = "SELECT * FROM #{table} WHERE #{conditions}"
42
+ params = args
43
+ when Hash
44
+ cond, params = conditions.to_where_clause
45
+ sql = "SELECT * FROM #{table} WHERE #{cond}"
46
+ end
47
+
48
+ self.from_rows(
49
+ dbh.select_all( sql, *params )
50
+ )
51
+ end
52
+
53
+ def self.one_where( conditions, *args )
54
+ case conditions
55
+ when String
56
+ sql = "SELECT * FROM #{table} WHERE #{conditions} LIMIT 1"
57
+ params = args
58
+ when Hash
59
+ cond, params = conditions.to_where_clause
60
+ sql = "SELECT * FROM #{table} WHERE #{cond} LIMIT 1"
61
+ end
62
+
63
+ row = dbh.select_one( sql, *params )
64
+ if row
65
+ self.new( row )
66
+ end
67
+ end
68
+
69
+ def self.all
70
+ self.from_rows(
71
+ dbh.select_all( "SELECT * FROM #{table}" )
72
+ )
73
+ end
74
+
75
+ # TODO: Perhaps we'll use cursors for Model#each.
76
+ def self.each( &block )
77
+ self.all.each( &block )
78
+ end
79
+
80
+ def self.one
81
+ row = dbh.select_one( "SELECT * FROM #{table} LIMIT 1" )
82
+ if row
83
+ self.new( row )
84
+ end
85
+ end
86
+
87
+ def self.count
88
+ dbh.select_column( "SELECT COUNT(*) FROM #{table}" )
89
+ end
90
+
91
+ def self.create( hash = {} )
92
+ if block_given?
93
+ row = DBI::Row.new(
94
+ columns.collect { |c| c[ 'name' ] },
95
+ [ M4DBI_UNASSIGNED ] * columns.size
96
+ )
97
+ yield row
98
+ hash = row.to_h
99
+ hash.to_a.each do |key,value|
100
+ if value == M4DBI_UNASSIGNED
101
+ hash.delete( key )
102
+ end
103
+ end
104
+ end
105
+
106
+ keys = hash.keys
107
+ cols = keys.join( ',' )
108
+ values = keys.map { |key| hash[ key ] }
109
+ value_placeholders = values.map { |v| '?' }.join( ',' )
110
+ rec = nil
111
+
112
+ dbh.one_transaction do |dbh_|
113
+ num_inserted = dbh_.do(
114
+ "INSERT INTO #{table} ( #{cols} ) VALUES ( #{value_placeholders} )",
115
+ *values
116
+ )
117
+ 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 )
121
+ else
122
+ begin
123
+ rec = last_record( dbh_ )
124
+ rescue NoMethodError => e
125
+ # ignore
126
+ #puts "not implemented: #{e.message}"
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ rec
133
+ end
134
+
135
+ def self.find_or_create( hash = nil )
136
+ item = nil
137
+ error = nil
138
+ item = self.one_where( hash )
139
+ if item.nil?
140
+ item =
141
+ begin
142
+ self.create( hash )
143
+ rescue => error
144
+ self.one_where( hash )
145
+ end
146
+ end
147
+ if item
148
+ item
149
+ else
150
+ raise error
151
+ end
152
+ end
153
+
154
+ def self.select_all( *args )
155
+ self.from_rows(
156
+ dbh.select_all( *args )
157
+ )
158
+ end
159
+
160
+ def self.select_one( *args )
161
+ row = dbh.select_one( *args )
162
+ if row
163
+ self.new( row )
164
+ end
165
+ end
166
+
167
+ class << self
168
+ alias s select_all
169
+ alias s1 select_one
170
+ end
171
+
172
+ def self.update( where_hash_or_clause, set_hash )
173
+ where_clause = nil
174
+ set_clause = nil
175
+ where_params = nil
176
+
177
+ if where_hash_or_clause.respond_to? :keys
178
+ where_clause, where_params = where_hash_or_clause.to_where_clause
179
+ else
180
+ where_clause = where_hash_or_clause
181
+ where_params = []
182
+ end
183
+
184
+ set_clause, set_params = set_hash.to_set_clause
185
+ params = set_params + where_params
186
+ dbh.do(
187
+ "UPDATE #{table} SET #{set_clause} WHERE #{where_clause}",
188
+ *params
189
+ )
190
+ end
191
+
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 ]
195
+ dbh.do(
196
+ "UPDATE #{table} SET #{set_clause} WHERE #{pk} = ?",
197
+ *params
198
+ )
199
+ end
200
+
201
+ # Example:
202
+ # DBI::Model.one_to_many( Author, :posts, Post, :author, :author_id )
203
+ # her_posts = some_author.posts
204
+ # the_author = some_post.author
205
+ def self.one_to_many( the_one, the_many, many_as, one_as, the_one_fk )
206
+ the_one.class_def( many_as.to_sym ) do
207
+ DBI::Collection.new( self, the_many, the_one_fk )
208
+ end
209
+ the_many.class_def( one_as.to_sym ) do
210
+ the_one[ @row[ the_one_fk ] ]
211
+ end
212
+ the_many.class_def( "#{one_as}=".to_sym ) do |new_one|
213
+ send( "#{the_one_fk}=".to_sym, new_one.pk )
214
+ end
215
+ end
216
+
217
+ # Example:
218
+ # DBI::Model.many_to_many(
219
+ # @m_author, @m_fan, :authors_liked, :fans, :authors_fans, :author_id, :fan_id
220
+ # )
221
+ # her_fans = some_author.fans
222
+ # favourite_authors = fan.authors_liked
223
+ def self.many_to_many( model1, model2, m1_as, m2_as, join_table, m1_fk, m2_fk )
224
+ model1.class_def( m2_as.to_sym ) do
225
+ model2.select_all(
226
+ %{
227
+ SELECT
228
+ m2.*
229
+ FROM
230
+ #{model2.table} m2,
231
+ #{join_table} j
232
+ WHERE
233
+ j.#{m1_fk} = ?
234
+ AND m2.id = j.#{m2_fk}
235
+ },
236
+ pk
237
+ )
238
+ end
239
+
240
+ model2.class_def( m1_as.to_sym ) do
241
+ model1.select_all(
242
+ %{
243
+ SELECT
244
+ m1.*
245
+ FROM
246
+ #{model1.table} m1,
247
+ #{join_table} j
248
+ WHERE
249
+ j.#{m2_fk} = ?
250
+ AND m1.id = j.#{m1_fk}
251
+ },
252
+ pk
253
+ )
254
+ end
255
+ end
256
+
257
+ # ------------------- :nodoc:
258
+
259
+ def initialize( row )
260
+ if not row.respond_to?( "[]".to_sym ) or not row.respond_to?( "[]=".to_sym )
261
+ raise DBI::Error.new( "Attempted to instantiate DBI::Model with an invalid argument (#{row.inspect}). (Expecting DBI::Row.)" )
262
+ end
263
+ if caller[ 1 ] !~ %r{/m4dbi/model\.rb:}
264
+ warn "Do not call DBI::Model#new directly; use DBI::Model#create instead."
265
+ end
266
+ @row = row
267
+ end
268
+
269
+ def method_missing( method, *args )
270
+ @row.send( method, *args )
271
+ end
272
+
273
+ def pk
274
+ @row[ self.class.pk ]
275
+ end
276
+
277
+ def pk_column
278
+ self.class.pk
279
+ end
280
+
281
+ def ==( other )
282
+ other and ( pk == other.pk )
283
+ end
284
+
285
+ def hash
286
+ "#{self.class.hash}#{pk}".to_i
287
+ end
288
+
289
+ def eql?( other )
290
+ hash == other.hash
291
+ end
292
+
293
+ def set( hash )
294
+ set_clause, set_params = hash.to_set_clause
295
+ set_params << pk
296
+ dbh.do(
297
+ "UPDATE #{table} SET #{set_clause} WHERE #{pk_column} = ?",
298
+ *set_params
299
+ )
300
+ end
301
+
302
+ # Returns true iff the record and only the record was successfully deleted.
303
+ def delete
304
+ num_deleted = dbh.do(
305
+ "DELETE FROM #{table} WHERE #{pk_column} = ?",
306
+ pk
307
+ )
308
+ num_deleted == 1
309
+ end
310
+
311
+ # save does nothing. It exists to provide compatibility with other ORMs.
312
+ def save
313
+ nil
314
+ end
315
+ end
316
+
317
+ # Define a new DBI::Model like this:
318
+ # 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' )
322
+ h = DBI::DatabaseHandle.last_handle
323
+ if h.nil? or not h.connected?
324
+ raise DBI::Error.new( "Attempted to create a Model class without first connecting to a database." )
325
+ end
326
+
327
+ model_key =
328
+ if defined?( DBI::DBD::Pg::Database ) and DBI::DBD::Pg::Database === h.handle
329
+ "#{h.dbname}::#{table}"
330
+ # TODO: more DBDs
331
+ else
332
+ table
333
+ end
334
+
335
+ @models ||= Hash.new
336
+ @models[ model_key ] ||= Class.new( DBI::Model ) do |klass|
337
+ klass.trait( {
338
+ :dbh => h,
339
+ :table => table,
340
+ :pk => pk_,
341
+ :columns => h.columns( table.to_s ),
342
+ } )
343
+
344
+ if defined?( DBI::DBD::Pg::Database ) and DBI::DBD::Pg::Database === h.handle
345
+ meta_def( "last_record".to_sym ) do |dbh_|
346
+ self.s1 "SELECT * FROM #{table} WHERE #{pk} = currval( '#{table}_#{pk}_seq' );"
347
+ end
348
+ # TODO: more DBDs
349
+ end
350
+
351
+ klass.trait[ :columns ].each do |col|
352
+ colname = col[ 'name' ]
353
+
354
+ class_def( colname.to_sym ) do
355
+ @row[ colname ]
356
+ end
357
+
358
+ class_def( "#{colname}=".to_sym ) do |new_value|
359
+ num_changed = dbh.do(
360
+ "UPDATE #{table} SET #{colname} = ? WHERE #{pk_column} = ?",
361
+ new_value,
362
+ pk
363
+ )
364
+ if num_changed > 0
365
+ @row[ colname ] = new_value
366
+ end
367
+ end
368
+ end
369
+ end
370
+ end
371
+
372
+ end
data/lib/m4dbi/row.rb ADDED
@@ -0,0 +1,35 @@
1
+ require 'dbi'
2
+
3
+ module DBI
4
+ class Row
5
+ def method_missing( method, *args )
6
+ if method.to_s =~ /^(.+)=$/
7
+ field = $1
8
+ if not @column_names.include?( field )
9
+ field = convert_alternate_fieldname( field )
10
+ end
11
+ if @column_names.include?( field )
12
+ self[ field ] = *args
13
+ else
14
+ super
15
+ end
16
+ else
17
+ field = method.to_s
18
+ # We shouldn't use by_field directly and test for nil,
19
+ # because nil may be a valid value for the column.
20
+ if not @column_names.include?( field )
21
+ field = convert_alternate_fieldname( field )
22
+ end
23
+ if @column_names.include?( field )
24
+ by_field field
25
+ else
26
+ super
27
+ end
28
+ end
29
+ end
30
+
31
+ def convert_alternate_fieldname( field )
32
+ field.gsub( /(^_)|(_$)/ , '' )
33
+ end
34
+ end
35
+ end