m4dbi 0.5.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/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