DBrb 0.1.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.
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
+