DBrb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/lib/DB.rb +245 -0
  2. data/tests/DbrbTest.rb +204 -0
  3. metadata +46 -0
data/lib/DB.rb ADDED
@@ -0,0 +1,245 @@
1
+ # DBrb - database access library for Ruby.
2
+ #
3
+ # Copyright (c) 2006 Tim Becker
4
+ #
5
+ # Released under the same terms as Ruby
6
+ # (http://www.ruby-lang.org/en/LICENSE.txt)
7
+ #
8
+ #
9
+ # see DBrb for documentation.
10
+
11
+ require 'dbi'
12
+
13
+
14
+ #:title: DBrb : RDoc Documenation
15
+
16
+ # +DBrb+ is a wrapper to facilitate working with {Ruby
17
+ # DBI}[http://sourceforge.net/projects/ruby-dbi]. The intention is to
18
+ # make the most commonly used DB functionality (as determined by me) easy and
19
+ # intuitive to use.
20
+ #
21
+ # === Examples
22
+ #
23
+ # db = DBrb.new("my_dbi_driver_string", "usr", "credentials")
24
+ #
25
+ # # return single value or array if a single row is selected
26
+ # i = db.sql("select max(id) from some_table")
27
+ # name, last_name = db.sql("SELECT name, last_name FROM some_name WHERE id = ?", 1000)
28
+ #
29
+ # should the two examples above return more than one row, the first
30
+ # one is used.
31
+ #
32
+ # db.sql("SELECT name, last_name FROM some_name WHERE id < ?", 1000) do |row|
33
+ # puts "#{row.name} #{row.last_name}"
34
+ # # in case of conflicts with existing methods, you can use:
35
+ # row["last_name"]
36
+ # end
37
+ #
38
+ # The row that the +sql+ iterates over is the standard DBI::Row, which
39
+ # has been supplemented with accessor methods named after the columns
40
+ # in case these don't conflict with existing methods.
41
+
42
+
43
+ class DBrb
44
+
45
+ #
46
+ def initialize driver, usr=nil, pwd=nil, params=nil
47
+ @driver=driver
48
+ @usr=usr
49
+ @pwd=pwd
50
+ @params=params
51
+
52
+
53
+ @pstatements = {}
54
+
55
+ if block_given?
56
+ yield self
57
+ self.close
58
+ end
59
+
60
+ end
61
+
62
+ # +sql+ is the method that does all the work. The only required
63
+ # parameter is +stmt+, which passes a string with the SQL
64
+ # statement to be executed. The optional parameters in +args+
65
+ # get bound to '?'-placeholders in the statement.
66
+ #
67
+ # This method can be called in several ways.
68
+ # * Statements not returning results:
69
+ #
70
+ # db.sql "UPDATE sometable SET somevalue=somevalue+1"
71
+ #
72
+ # * If the statement returns a single value, that value is returned by the method call:
73
+ #
74
+ #
75
+ # value = db.sql "SELECT value FROM sometable LIMIT 1"
76
+ #
77
+ # * Statements returning several values in a single row:
78
+ # first_name, last_name =
79
+ # db.sql "SELECT first, last
80
+ # FROM sometable
81
+ # LIMIT 1"
82
+ #
83
+ # (It should go without saying: if a single row result is expected like in the
84
+ # examples above but the statement returns several rows, the first one is used.)
85
+ #
86
+ # * If a block is passed to the method, it can be used to iterate over the results.
87
+ #
88
+ # db.sql("SELECT first, last FROM sometable WHERE last=?", "Smith") do |row|
89
+ # puts "Name: #{row.first} #{row.last}
90
+ # end
91
+ #
92
+ # * or, in case the resulting rows only contain a single column :
93
+ #
94
+ # db.sql "SELECT first FROM sometable" do |firstname|
95
+ # puts "Hello #{firstname}!"
96
+ # end
97
+
98
+ def sql(stmt, *args, &block) #:yields: row
99
+ #optional args and an optional block
100
+ _sql_internal(stmt,false,*args, &block)
101
+ end
102
+
103
+ # Similar to the sql method. This method is meant for `INSERT`
104
+ # and `UPDATE` statements and returns the RPC (row processed
105
+ # count), i.e. the number of rows affected by the statement.
106
+ #
107
+
108
+ def sql_count (stmt, *args, &block)
109
+ _sql_internal(stmt,true,*args,&block)
110
+ end
111
+
112
+ # Closes all resources used by this object instance.
113
+ def close
114
+ @pstatements.each_value {|pstmt|
115
+ pstmt[0].finish
116
+ }
117
+ @pstatements = {}
118
+ @db.disconnect
119
+ @db = nil
120
+ end
121
+
122
+ # _Debug_ _method_. This method dumps statistics about how often
123
+ # each sql statement was executed by this instance. Statistics
124
+ # are reset after calls to `close`
125
+ def dumpStmtStats
126
+ @pstatements.each_pair { |stmt, arr|
127
+ puts "#{stmt} : #{arr[1]}"
128
+ } if @pstatements
129
+ end
130
+
131
+ def to_s
132
+ "DBrb: #{@driver}"
133
+ end
134
+
135
+ protected
136
+
137
+ # Returns a (cached) reference to the DBI driver. Default
138
+ # behaviour is to create a single connection, keeping it cached
139
+ # and reusing it for all DB statements executed by this object
140
+ # instance.
141
+ #
142
+ # This method is used internally by `sql` and could be
143
+ # overridden in case you require different behaviour, e.g.
144
+ # creating a new connection for each executed statement or
145
+ # maintaining several connections.
146
+
147
+ def get_db
148
+ return @db if @db && @db.connected?
149
+ @db = DBI.connect(@driver, @usr, @pwd, @params)
150
+ end
151
+
152
+ def _sql_internal (stmt, count, *args) #:yields: row
153
+ #optional args and an optional block
154
+ pstmt = get_pstmt stmt
155
+ pstmt.execute *args
156
+
157
+
158
+ ret_val = nil
159
+ begin
160
+ if block_given?
161
+ pstmt.each { |row|
162
+ if row.length==1
163
+ yield row[0]
164
+ else
165
+ yield row
166
+ end
167
+ }
168
+ else
169
+ unless count
170
+ ret_val = pstmt.fetch if pstmt.fetchable?
171
+ ret_val = ret_val[0] if ret_val && ret_val.length==1
172
+ end
173
+
174
+ end
175
+ rescue NoMethodError
176
+ # calling fetch for mysql drivers on stmts returning
177
+ # no rows (INSERT...) is fucked.
178
+
179
+ # Message: <"undefined method `fetch_row' for nil:NilClass">
180
+ # ---Backtrace---
181
+ # /usr/local/lib/ruby/site_ruby/1.8/DBD/Mysql/Mysql.rb:424:in `fetch'
182
+ # /usr/local/lib/ruby/site_ruby/1.8/dbi/dbi.rb:811:in `fetch'
183
+ # /usr/local/lib/ruby/site_ruby/1.8/dbi/dbi.rb:836:in `each'
184
+ end
185
+
186
+ ret_val = pstmt.rows if count
187
+ pstmt.cancel # clean up
188
+ return ret_val
189
+
190
+ end
191
+
192
+
193
+
194
+ private
195
+
196
+ # Maintains a cache of all prepared statments. Each prepared
197
+ # statement executed by `sql` is prepared only once and reused in
198
+ # case the same sql statement is executed a second time.
199
+ #
200
+ # Prepared statements are disposed when `close` is called.
201
+
202
+ def get_pstmt stmt #:doc:
203
+ #@pstatements[stmt] ||= get_db.prepare stmt
204
+ @pstatements[stmt] ||= [nil,0]
205
+ @pstatements[stmt][1] += 1
206
+ @pstatements[stmt][0] ||= get_db.prepare stmt
207
+ end
208
+
209
+
210
+ end
211
+ module DBI
212
+ # augments DBI::Row with a +method_missing+ method that lets the
213
+ # row behave as if it had accessor methods for each colum.
214
+ class Row
215
+ def method_missing meth_id
216
+ return self[meth_id]
217
+ end
218
+ end
219
+ end
220
+
221
+ if $0==__FILE__
222
+ sql = DBrb.new('DBI:PG:stats2', 'test', 'test')
223
+ row = sql.sql("SELECT * FROM post WHERE nick=? LIMIT 10", "muppet")
224
+ puts row.nick
225
+ row = sql.sql("SELECT * FROM post WHERE nick=? LIMIT 10", "Philo")
226
+ puts row.nick
227
+ puts sql.sql("SELECT count(*) from post")
228
+
229
+ puts sql.sql("SELECT count(*) from post")
230
+
231
+ puts sql.sql("SELECT nick from post limit 20") {|nick|
232
+ puts nick
233
+ }
234
+
235
+ puts sql.sql("SELECT nick, url from post limit 20") {|row|
236
+ puts row.nick, row.url
237
+ }
238
+
239
+ sql.dumpStmtStats
240
+
241
+ sql.close
242
+
243
+
244
+
245
+ end
data/tests/DbrbTest.rb ADDED
@@ -0,0 +1,204 @@
1
+
2
+
3
+ require 'DB'
4
+ require 'test/unit'
5
+
6
+ # Since each test needs to be executed for several different database
7
+ # engines, DBrbTest maintains an array of instances, each connected to
8
+ # one of the backends. the `test_...` methods iterate over the
9
+ # connections and call the actual test methods, which are named like the
10
+ # `test_...` methods without the `test_` part, with each DBrb instance.
11
+ # E.g. test_create_test_table calls `create_test_table` once with each
12
+ # connection.
13
+
14
+ class DbrbTest < Test::Unit::TestCase
15
+
16
+ # These are the DB logins to test. I'm currently using a
17
+ # postgres server, with DB, user and pwd all set to `test`. In
18
+ # case you'd like to test other databases, just add the
19
+ # necessary connection parameters.
20
+
21
+ @@credentials = [
22
+ ['DBI:PG:test', 'test', 'test'],
23
+ ['DBI:Mysql:test;socket=/var/run/mysqld/mysqld.sock;database=test', 'test', 'test'] # there must be a better way.
24
+ ]
25
+
26
+ def setup
27
+ @time = Time.new
28
+ @connections = []
29
+ @@credentials.each { |cred|
30
+ @connections.push DBrb.new(*cred)
31
+ }
32
+ end
33
+
34
+ def teardown
35
+ @connections.each {|conn|
36
+ conn.close
37
+ }
38
+ end
39
+
40
+ # Each test case follows the same scheme: the same tests are
41
+ # executed for each database and wrapped in a
42
+ # `assert_nothing_raised` assertion that fails in case of
43
+ # database exceptions.
44
+
45
+ def each_ok
46
+ @connections.each {|conn|
47
+ assert_nothing_raised {
48
+ yield conn
49
+ }
50
+ }
51
+ end
52
+
53
+ #Set up the example table for the tests.
54
+ def test_a_create_test_table
55
+ each_ok { |conn|
56
+ # This should be as generic as possible to
57
+ # accomodate different DB's
58
+ conn.sql "
59
+ CREATE TABLE test_table (
60
+ col_vc VARCHAR(50),
61
+ col_num NUMERIC,
62
+ col_bool BOOLEAN,
63
+ col_double DOUBLE PRECISION,
64
+ col_int INTEGER,
65
+ col_date DATE,
66
+ col_time TIME,
67
+ col_timestamp TIMESTAMP
68
+ )"
69
+ }
70
+ end
71
+
72
+
73
+
74
+ def test_b_insert_values
75
+ each_ok {|con|
76
+ %w{zero one two three four five six seven eight
77
+ nine ten}.each_with_index {|num, i|
78
+ con.sql("INSERT INTO test_table
79
+ VALUES (?,?,?,?,?,?,?,?)",
80
+ num, i, i%2==0,"#{i}.#{i}".to_f,i, @time, @time, @time)
81
+ }
82
+ }
83
+ end
84
+
85
+
86
+
87
+ def test_c_select_values
88
+ each_ok { |db|
89
+
90
+ tst = SelectTest.new db, self
91
+ tst.do "one", "SELECT col_vc FROM test_table WHERE col_num=?", 1
92
+ tst.do "2", "SELECT col_num FROM test_table WHERE col_vc=?", "two"
93
+ # postgres: 0 mysql: false
94
+ #tst.do 0, "SELECT col_bool FROM test_table WHERE col_vc=?", "three"
95
+
96
+ #postgres 4.4 mysql "4.4"
97
+ #tst.do 4.4, "SELECT col_double FROM test_table WHERE col_vc=?", "four"
98
+ tst.do 5, "SELECT col_int FROM test_table WHERE col_vc=?", "five"
99
+ tst.do ["six",6],"SELECT col_vc, col_int FROM test_table WHERE col_double=?", 6.6
100
+ }
101
+ end
102
+
103
+ # Date handling seems to be very implementation specific...
104
+ def test_c_select_date
105
+ each_ok { |db|
106
+ date = db.sql("SELECT col_date FROM test_table LIMIT 1")
107
+
108
+ assert_equal(@time.year, date.year, db.to_s)
109
+ assert_equal(@time.mon, date.month, db.to_s)
110
+ assert_equal(@time.day, date.day, db.to_s)
111
+
112
+ time = db.sql("SELECT col_time FROM test_table LIMIT 1")
113
+
114
+ if time.class != DBI::Time
115
+ # postgres driver doesn't return DBI::Time
116
+ puts "\nWarning, #{db} returning '#{time.class}' instead of DBI::TIME"
117
+ assert_equal(@time.strftime("%H:%M:%S"), time, db)
118
+ else
119
+ assert_equal(@time.hour, time.hour, db)
120
+ assert_equal(@time.min, time.min,db)
121
+ assert_equal(@time.sec, time.sec,db)
122
+ end
123
+
124
+ timestamp = db.sql("SELECT col_timestamp FROM test_table LIMIT 1")
125
+
126
+ if timestamp.class != DBI::Timestamp
127
+ # mysql driver doesn't return DBI::Timestamp...
128
+ puts "\nWarning, #{db} returning '#{timestamp.class}' instead of DBI::TIMESTAMP"
129
+ assert_equal(@time.strftime("%Y-%m-%d %H:%M:%S"), timestamp, db.to_s)
130
+ else
131
+ assert_equal(@time.year, timestamp.year, db.to_s)
132
+ assert_equal(@time.mon, timestamp.month, db.to_s)
133
+ assert_equal(@time.day, timestamp.day, db.to_s)
134
+ assert_equal(@time.hour, timestamp.hour, db.to_s)
135
+ assert_equal(@time.min, timestamp.min, db.to_s)
136
+ assert_equal(@time.sec, timestamp.sec, db.to_s)
137
+ end
138
+ }
139
+ end
140
+
141
+ def test_c_select_with_block
142
+ each_ok { |db|
143
+ comp_arr = [0,1,2,3,4,5,6,7,8,9,10]
144
+
145
+ arr = []
146
+ db.sql "SELECT col_int FROM test_table ORDER BY col_int" do |i|
147
+ arr.push i
148
+ end
149
+ assert_equal comp_arr, arr
150
+
151
+ arr=[]
152
+
153
+ db.sql "SELECT col_int, col_date FROM test_table ORDER BY col_int" do |row|
154
+ arr.push row.col_int
155
+ end
156
+ assert_equal comp_arr, arr
157
+ }
158
+ end
159
+
160
+
161
+ def test_d_update_values
162
+ each_ok do |db|
163
+ db.sql "UPDATE test_table SET col_int = col_int+1"
164
+ comp_arr = [1,2,3,4,5,6,7,8,9,10,11]
165
+
166
+ arr = []
167
+ db.sql "SELECT col_int FROM test_table ORDER BY col_int" do |i|
168
+ arr.push i
169
+ end
170
+ assert_equal comp_arr, arr
171
+ end
172
+ end
173
+
174
+ def test_e_count
175
+ each_ok do |db|
176
+ c = db.sql_count "UPDATE test_table SET col_int = col_int+2"
177
+ assert_equal(11, c, db)
178
+
179
+ c = db.sql_count "SELECT * FROM test_table" do |row|
180
+ arr=row
181
+ end
182
+
183
+ assert(c==0||c==11, db)
184
+ end
185
+ end
186
+
187
+ #Drop the test table
188
+ def test_z_drop_test_table
189
+ each_ok {|con|
190
+ con.sql "DROP TABLE test_table"
191
+ }
192
+ end
193
+
194
+ end
195
+
196
+ class SelectTest
197
+ def initialize db, tcase
198
+ @db=db
199
+ @tcase=tcase
200
+ end
201
+ def do val, sql, *args
202
+ @tcase.assert_equal(val, @db.sql(sql, *args), @db.to_s)
203
+ end
204
+ end
metadata ADDED
@@ -0,0 +1,46 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.11
3
+ specification_version: 1
4
+ name: DBrb
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.1.0
7
+ date: 2006-05-14 00:00:00 +02:00
8
+ summary: Easier access to databases.
9
+ require_paths:
10
+ - lib
11
+ email: tim@kuriositaet.de
12
+ homepage: http://www.kuriositaet.de/ruby/dbrb
13
+ rubyforge_project:
14
+ description: DBrb is a database access layer meant to be easier and more consistant to use than ruby DBI. Currently, and for the forseeable future it's implemented as a wrapper to the DBI lib.
15
+ autorequire: DB.rb
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ authors:
29
+ - Tim Becker
30
+ files:
31
+ - tests/DbrbTest.rb
32
+ - lib/DB.rb
33
+ test_files: []
34
+
35
+ rdoc_options: []
36
+
37
+ extra_rdoc_files: []
38
+
39
+ executables: []
40
+
41
+ extensions: []
42
+
43
+ requirements:
44
+ - DBI
45
+ dependencies: []
46
+