akitaonrails-activerecord-sqlserver-adapter 1.1.0 → 1.1.1
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/README +31 -0
- data/RUNNING_UNIT_TESTS +44 -0
- data/Rakefile +89 -0
- data/activerecord-sqlserver-adapter.gemspec +15 -0
- data/lib/activerecord-sqlserver-adapter.rb +3 -0
- data/lib/dbd/ADO.rb +229 -0
- data/lib/dbi.rb +1043 -0
- data/lib/dbi/columninfo.rb +158 -0
- data/lib/dbi/row.rb +205 -0
- data/lib/dbi/sql.rb +239 -0
- data/lib/dbi/trace.rb +90 -0
- data/lib/dbi/utils.rb +365 -0
- data/lib/dbi/version.rb +9 -0
- data/lib/rails_fcgi.rb +1 -0
- data/lib/rails_fcgi/fixes.rb +35 -0
- data/test/aaaa_create_tables_test_sqlserver.rb +43 -0
- data/test/affected_rows_test_sqlserver.rb +29 -0
- data/test/connections/native_sqlserver/connection.rb +23 -0
- data/test/connections/native_sqlserver_odbc/connection.rb +25 -0
- data/test/fixtures/db_definitions/sqlserver.drop.sql +35 -0
- data/test/fixtures/db_definitions/sqlserver.sql +247 -0
- data/test/fixtures/db_definitions/sqlserver2.drop.sql +1 -0
- data/test/fixtures/db_definitions/sqlserver2.sql +4 -0
- metadata +36 -1
data/README
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
activerecord-sqlserver-adapter
|
2
|
+
==============================
|
3
|
+
|
4
|
+
Originally forked from http://svn.rubyonrails.org/rails/adapters/sqlserver/
|
5
|
+
revision 9250
|
6
|
+
|
7
|
+
Incorporates changes from:
|
8
|
+
|
9
|
+
- Redmine's fixes:
|
10
|
+
http://www.redmine.org/attachments/817/sqlserver_adapter.rb
|
11
|
+
|
12
|
+
- Github's jrafanie:
|
13
|
+
http://github.com/jrafanie/rails-sqlserver-adapter/tree/master/sqlserver_adapter_rails.rb
|
14
|
+
|
15
|
+
- Github's adzap:
|
16
|
+
http://github.com/adzap/sqlserver_adapter_mods/tree/master/lib/sqlserver_adapter_mods
|
17
|
+
|
18
|
+
This gem should support SQL Server 2005 but not 2000 because of type changes.
|
19
|
+
|
20
|
+
Included ruby-dbi 0.2.2:
|
21
|
+
- http://rubyforge.org/frs/download.php/41304/dbi-0.2.2.zip
|
22
|
+
|
23
|
+
INSTALL
|
24
|
+
=======
|
25
|
+
|
26
|
+
gem install akitaonrails-activerecord-sqlserver-adapter --source=http://gems.github.com
|
27
|
+
|
28
|
+
Contact:
|
29
|
+
========
|
30
|
+
|
31
|
+
Fabio Akita (fabioakita@gmail.com)
|
data/RUNNING_UNIT_TESTS
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
== Creating the test database
|
2
|
+
|
3
|
+
The default names for the test databases are "activerecord_unittest" and
|
4
|
+
"activerecord_unittest2". If you want to use another database name then be sure
|
5
|
+
to update the connection adapter setups you want to test with in
|
6
|
+
test/connections/<your database>/connection.rb.
|
7
|
+
|
8
|
+
|
9
|
+
== Requirements
|
10
|
+
|
11
|
+
The tests of this adapter depend on the existence of rails edge. All the tests
|
12
|
+
defined by rails edge are re-used. For this to work the following directory
|
13
|
+
structure is assumed to exist:
|
14
|
+
|
15
|
+
#{RAILS_ROOT}/vendor/plugins/adapters/sqlserver
|
16
|
+
#{RAILS_ROOT}/vendor/rails/activerecord/test
|
17
|
+
|
18
|
+
Define a user named 'rails' in SQL Server with all privileges granted. Use an empty
|
19
|
+
password for user 'rails', or alternatively use the OSQLPASSWORD environment variable
|
20
|
+
which allows you to set a default password for the current session.
|
21
|
+
|
22
|
+
Then run "rake create_databases".
|
23
|
+
|
24
|
+
|
25
|
+
== Running with Rake
|
26
|
+
|
27
|
+
The easiest way to run the unit tests is through Rake. Either run "rake test_sqlserver"
|
28
|
+
or "rake test_sqlserver_odbc". For more information, checkout the full array
|
29
|
+
of rake tasks with "rake -T"
|
30
|
+
|
31
|
+
Rake can be found at http://rake.rubyforge.org
|
32
|
+
|
33
|
+
|
34
|
+
== Running by hand
|
35
|
+
|
36
|
+
Unit tests are located in test directory. If you only want to run a single test suite,
|
37
|
+
you can do so with:
|
38
|
+
|
39
|
+
rake test_sqlserver TEST=base_test.rb
|
40
|
+
|
41
|
+
That'll run the base suite using the SQLServer-Ruby adapter.
|
42
|
+
|
43
|
+
|
44
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'rake/packagetask'
|
5
|
+
require 'rake/gempackagetask'
|
6
|
+
require 'rake/contrib/rubyforgepublisher'
|
7
|
+
|
8
|
+
PKG_NAME = 'activerecord-sqlserver-adapter'
|
9
|
+
PKG_BUILD = (".#{ENV['PKG_BUILD']}" if ENV['PKG_BUILD'])
|
10
|
+
PKG_VERSION = "1.0.0#{PKG_BUILD}"
|
11
|
+
|
12
|
+
spec = Gem::Specification.new do |s|
|
13
|
+
s.name = PKG_NAME
|
14
|
+
s.summary = 'SQL Server adapter for Active Record'
|
15
|
+
s.version = PKG_VERSION
|
16
|
+
|
17
|
+
s.add_dependency 'activerecord', '>= 1.15.5.7843'
|
18
|
+
s.require_path = 'lib'
|
19
|
+
|
20
|
+
s.files = %w(lib/active_record/connection_adapters/sqlserver_adapter.rb)
|
21
|
+
|
22
|
+
s.author = 'Tom Ward'
|
23
|
+
s.email = 'tom@popdog.net'
|
24
|
+
s.homepage = 'http://wiki.rubyonrails.org/rails/pages/SQL+Server'
|
25
|
+
s.rubyforge_project = 'activerecord'
|
26
|
+
end
|
27
|
+
|
28
|
+
Rake::GemPackageTask.new(spec) do |p|
|
29
|
+
p.gem_spec = spec
|
30
|
+
p.need_tar = true
|
31
|
+
p.need_zip = true
|
32
|
+
end
|
33
|
+
|
34
|
+
desc "Publish the beta gem"
|
35
|
+
task :pgem => :package do
|
36
|
+
Rake::SshFilePublisher.new("davidhh@wrath.rubyonrails.org", "public_html/gems/gems", "pkg", "#{PKG_NAME}-#{PKG_VERSION}.gem").upload
|
37
|
+
`ssh davidhh@wrath.rubyonrails.org './gemupdate.sh'`
|
38
|
+
end
|
39
|
+
|
40
|
+
desc "Publish the release files to RubyForge."
|
41
|
+
task :release => :package do
|
42
|
+
require 'rubyforge'
|
43
|
+
|
44
|
+
packages = %w(gem tgz zip).collect{ |ext| "pkg/#{PKG_NAME}-#{PKG_VERSION}.#{ext}" }
|
45
|
+
|
46
|
+
rubyforge = RubyForge.new
|
47
|
+
rubyforge.login
|
48
|
+
rubyforge.add_release(PKG_NAME, PKG_NAME, "REL #{PKG_VERSION}", *packages)
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
SCHEMA_PATH = File.join(File.dirname(__FILE__), *%w(test fixtures db_definitions))
|
53
|
+
|
54
|
+
desc 'Create the SQL Server test databases'
|
55
|
+
task :create_databases do
|
56
|
+
# Define a user named 'rails' in SQL Server with all privileges granted
|
57
|
+
# Use an empty password for user 'rails', or alternatively use the OSQLPASSWORD environment variable
|
58
|
+
# which allows you to set a default password for the current session.
|
59
|
+
%x( osql -S localhost -U rails -Q "create database activerecord_unittest" -P )
|
60
|
+
%x( osql -S localhost -U rails -Q "create database activerecord_unittest2" -P )
|
61
|
+
%x( osql -S localhost -U rails -d activerecord_unittest -Q "exec sp_grantdbaccess 'rails'" -P )
|
62
|
+
%x( osql -S localhost -U rails -d activerecord_unittest2 -Q "exec sp_grantdbaccess 'rails'" -P )
|
63
|
+
%x( osql -S localhost -U rails -d activerecord_unittest -Q "grant BACKUP DATABASE, BACKUP LOG, CREATE DEFAULT, CREATE FUNCTION, CREATE PROCEDURE, CREATE RULE, CREATE TABLE, CREATE VIEW to 'rails';" -P )
|
64
|
+
%x( osql -S localhost -U rails -d activerecord_unittest2 -Q "grant BACKUP DATABASE, BACKUP LOG, CREATE DEFAULT, CREATE FUNCTION, CREATE PROCEDURE, CREATE RULE, CREATE TABLE, CREATE VIEW to 'rails';" -P )
|
65
|
+
end
|
66
|
+
|
67
|
+
desc 'Drop the SQL Server test databases'
|
68
|
+
task :drop_databases do
|
69
|
+
%x( osql -S localhost -U rails -Q "drop database activerecord_unittest" -P )
|
70
|
+
%x( osql -S localhost -U rails -Q "drop database activerecord_unittest2" -P )
|
71
|
+
end
|
72
|
+
|
73
|
+
desc 'Recreate the SQL Server test databases'
|
74
|
+
task :recreate_databases => [:drop_databases, :create_databases]
|
75
|
+
|
76
|
+
|
77
|
+
for adapter in %w( sqlserver sqlserver_odbc )
|
78
|
+
Rake::TestTask.new("test_#{adapter}") { |t|
|
79
|
+
t.libs << "test"
|
80
|
+
t.libs << "test/connections/native_#{adapter}"
|
81
|
+
t.libs << "../../../rails/activerecord/test/"
|
82
|
+
t.pattern = ["test/**/*_test_sqlserver.rb", "../../../rails/activerecord/test/**/*_test.rb"]
|
83
|
+
t.verbose = true
|
84
|
+
}
|
85
|
+
|
86
|
+
namespace adapter do
|
87
|
+
task :test => "test_#{adapter}"
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
spec = Gem::Specification.new do |s|
|
2
|
+
s.name = "activerecord-sqlserver-adapter"
|
3
|
+
s.summary = 'SQL Server adapter for Active Record'
|
4
|
+
s.version = "1.1.1"
|
5
|
+
|
6
|
+
s.add_dependency 'activerecord', '>= 1.15.5.7843'
|
7
|
+
s.require_path = 'lib'
|
8
|
+
|
9
|
+
s.files = ["activerecord-sqlserver-adapter.gemspec", "lib", "lib/active_record", "lib/active_record/connection_adapters", "lib/active_record/connection_adapters/sqlserver_adapter.rb", "lib/activerecord-sqlserver-adapter.rb", "lib/dbd", "lib/dbd/ADO.rb", "lib/dbi", "lib/dbi/columninfo.rb", "lib/dbi/row.rb", "lib/dbi/sql.rb", "lib/dbi/trace.rb", "lib/dbi/utils.rb", "lib/dbi/version.rb", "lib/dbi.rb", "lib/rails_fcgi", "lib/rails_fcgi/fixes.rb", "lib/rails_fcgi.rb", "Rakefile", "README", "RUNNING_UNIT_TESTS", "test", "test/aaaa_create_tables_test_sqlserver.rb", "test/affected_rows_test_sqlserver.rb", "test/connections", "test/connections/native_sqlserver", "test/connections/native_sqlserver/connection.rb", "test/connections/native_sqlserver_odbc", "test/connections/native_sqlserver_odbc/connection.rb", "test/fixtures", "test/fixtures/db_definitions", "test/fixtures/db_definitions/sqlserver.drop.sql", "test/fixtures/db_definitions/sqlserver.sql", "test/fixtures/db_definitions/sqlserver2.drop.sql", "test/fixtures/db_definitions/sqlserver2.sql"]
|
10
|
+
|
11
|
+
s.author = 'Tom Ward'
|
12
|
+
s.email = 'tom@popdog.net'
|
13
|
+
s.homepage = 'http://wiki.rubyonrails.org/rails/pages/SQL+Server'
|
14
|
+
s.rubyforge_project = 'activerecord'
|
15
|
+
end
|
data/lib/dbd/ADO.rb
ADDED
@@ -0,0 +1,229 @@
|
|
1
|
+
#
|
2
|
+
# DBD::ADO
|
3
|
+
#
|
4
|
+
# Copyright (c) 2001, 2002 Michael Neumann <neumann@s-direktnet.de>
|
5
|
+
#
|
6
|
+
# All rights reserved.
|
7
|
+
#
|
8
|
+
# Redistribution and use in source and binary forms, with or without
|
9
|
+
# modification, are permitted provided that the following conditions
|
10
|
+
# are met:
|
11
|
+
# 1. Redistributions of source code must retain the above copyright
|
12
|
+
# notice, this list of conditions and the following disclaimer.
|
13
|
+
# 2. Redistributions in binary form must reproduce the above copyright
|
14
|
+
# notice, this list of conditions and the following disclaimer in the
|
15
|
+
# documentation and/or other materials provided with the distribution.
|
16
|
+
# 3. The name of the author may not be used to endorse or promote products
|
17
|
+
# derived from this software without specific prior written permission.
|
18
|
+
#
|
19
|
+
# THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
20
|
+
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
|
21
|
+
# AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
22
|
+
# THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
23
|
+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
24
|
+
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
|
25
|
+
# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
26
|
+
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
27
|
+
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
28
|
+
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
29
|
+
#
|
30
|
+
# $Id$
|
31
|
+
#
|
32
|
+
|
33
|
+
require "win32ole"
|
34
|
+
|
35
|
+
module DBI
|
36
|
+
module DBD
|
37
|
+
module ADO
|
38
|
+
|
39
|
+
VERSION = "0.1"
|
40
|
+
USED_DBD_VERSION = "0.1"
|
41
|
+
|
42
|
+
class Driver < DBI::BaseDriver
|
43
|
+
|
44
|
+
def initialize
|
45
|
+
super(USED_DBD_VERSION)
|
46
|
+
end
|
47
|
+
|
48
|
+
def connect(dbname, user, auth, attr)
|
49
|
+
# connect to database
|
50
|
+
|
51
|
+
handle = WIN32OLE.new('ADODB.Connection')
|
52
|
+
handle.Open(dbname)
|
53
|
+
handle.BeginTrans() # start new Transaction
|
54
|
+
|
55
|
+
return Database.new(handle, attr)
|
56
|
+
rescue RuntimeError => err
|
57
|
+
raise DBI::DatabaseError.new(err.message)
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
class Database < DBI::BaseDatabase
|
63
|
+
|
64
|
+
def disconnect
|
65
|
+
@handle.RollbackTrans()
|
66
|
+
@handle.Close()
|
67
|
+
rescue RuntimeError => err
|
68
|
+
raise DBI::DatabaseError.new(err.message)
|
69
|
+
end
|
70
|
+
|
71
|
+
def prepare(statement)
|
72
|
+
# TODO: create Command instead?
|
73
|
+
Statement.new(@handle, statement, self)
|
74
|
+
end
|
75
|
+
|
76
|
+
def commit
|
77
|
+
# TODO: raise error if AutoCommit on => better in DBI?
|
78
|
+
@handle.CommitTrans()
|
79
|
+
@handle.BeginTrans()
|
80
|
+
rescue RuntimeError => err
|
81
|
+
raise DBI::DatabaseError.new(err.message)
|
82
|
+
end
|
83
|
+
|
84
|
+
def rollback
|
85
|
+
# TODO: raise error if AutoCommit on => better in DBI?
|
86
|
+
@handle.RollbackTrans()
|
87
|
+
@handle.BeginTrans()
|
88
|
+
rescue RuntimeError => err
|
89
|
+
raise DBI::DatabaseError.new(err.message)
|
90
|
+
end
|
91
|
+
|
92
|
+
def []=(attr, value)
|
93
|
+
if attr == 'AutoCommit' then
|
94
|
+
# TODO: commit current transaction?
|
95
|
+
@attr[attr] = value
|
96
|
+
else
|
97
|
+
super
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
end # class Database
|
103
|
+
|
104
|
+
|
105
|
+
class Statement < DBI::BaseStatement
|
106
|
+
include SQL::BasicBind
|
107
|
+
include SQL::BasicQuote
|
108
|
+
|
109
|
+
def initialize(handle, statement, db)
|
110
|
+
@handle = handle
|
111
|
+
@statement = statement
|
112
|
+
@params = []
|
113
|
+
@db = db
|
114
|
+
end
|
115
|
+
|
116
|
+
def bind_param(param, value, attribs)
|
117
|
+
raise InterfaceError, "only ? parameters supported" unless param.is_a? Fixnum
|
118
|
+
|
119
|
+
@params[param-1] = value
|
120
|
+
end
|
121
|
+
|
122
|
+
def execute
|
123
|
+
# TODO: use Command and Parameter
|
124
|
+
# TODO: substitute all ? by the parametes
|
125
|
+
sql = bind(self, @statement, @params)
|
126
|
+
@res_handle = @handle.Execute(sql)
|
127
|
+
|
128
|
+
# TODO: SELECT and AutoCommit finishes the result-set
|
129
|
+
# what to do?
|
130
|
+
if @db['AutoCommit'] == true and not SQL.query?(@statement) then
|
131
|
+
@db.commit
|
132
|
+
end
|
133
|
+
|
134
|
+
rescue RuntimeError => err
|
135
|
+
raise DBI::DatabaseError.new(err.message)
|
136
|
+
end
|
137
|
+
|
138
|
+
def finish
|
139
|
+
# if DCL, DDL or INSERT UPDATE and DELETE, this gives an Error
|
140
|
+
# because no Result-Set is available
|
141
|
+
if @res_handle.Fields.Count() != 0 then
|
142
|
+
@res_handle.Close()
|
143
|
+
end
|
144
|
+
rescue RuntimeError => err
|
145
|
+
raise DBI::DatabaseError.new(err.message)
|
146
|
+
end
|
147
|
+
|
148
|
+
def fetch
|
149
|
+
retval = fetch_currentrow
|
150
|
+
@res_handle.MoveNext() unless retval.nil?
|
151
|
+
retval
|
152
|
+
rescue RuntimeError => err
|
153
|
+
raise DBI::DatabaseError.new(err.message)
|
154
|
+
end
|
155
|
+
|
156
|
+
|
157
|
+
|
158
|
+
def fetch_scroll(direction, offset)
|
159
|
+
case direction
|
160
|
+
when DBI::SQL_FETCH_NEXT
|
161
|
+
return fetch
|
162
|
+
when DBI::SQL_FETCH_PRIOR
|
163
|
+
# TODO: check if already the first?
|
164
|
+
#return nil if @res_handle.AbsolutePosition()
|
165
|
+
@res_handle.MovePrevious()
|
166
|
+
return fetch_currentrow
|
167
|
+
when DBI::SQL_FETCH_FIRST
|
168
|
+
@res_handle.MoveFirst()
|
169
|
+
return fetch_currentrow
|
170
|
+
when DBI::SQL_FETCH_LAST
|
171
|
+
@res_handle.MoveLast()
|
172
|
+
return fetch_currentrow
|
173
|
+
when DBI::SQL_FETCH_RELATIVE
|
174
|
+
@res_handle.Move(offset)
|
175
|
+
return fetch_currentrow
|
176
|
+
when DBI::SQL_FETCH_ABSOLUTE
|
177
|
+
ap = @res_handle.AbsolutePositon()
|
178
|
+
@res_handle.Move(offset-ap)
|
179
|
+
return fetch_currentrow
|
180
|
+
else
|
181
|
+
raise DBI::InterfaceError
|
182
|
+
end
|
183
|
+
rescue RuntimeError => err
|
184
|
+
raise DBI::DatabaseError.new(err.message)
|
185
|
+
end
|
186
|
+
|
187
|
+
def column_info
|
188
|
+
num_cols = @res_handle.Fields().Count()
|
189
|
+
retval = Array.new(num_cols)
|
190
|
+
|
191
|
+
for i in 0...num_cols do
|
192
|
+
retval[i] = {'name' => @res_handle.Fields(i).Name()}
|
193
|
+
end
|
194
|
+
|
195
|
+
retval
|
196
|
+
rescue RuntimeError => err
|
197
|
+
raise DBI::DatabaseError.new(err.message)
|
198
|
+
end
|
199
|
+
|
200
|
+
def rows
|
201
|
+
# TODO: how to get the RPC in ADO?
|
202
|
+
nil
|
203
|
+
end
|
204
|
+
|
205
|
+
|
206
|
+
private
|
207
|
+
|
208
|
+
def fetch_currentrow
|
209
|
+
return nil if @res_handle.EOF() or @res_handle.BOF()
|
210
|
+
|
211
|
+
# TODO: don't create new Array each time
|
212
|
+
num_cols = @res_handle.Fields().Count()
|
213
|
+
retval = Array.new(num_cols)
|
214
|
+
|
215
|
+
for i in 0...num_cols do
|
216
|
+
retval[i] = @res_handle.Fields(i).Value()
|
217
|
+
end
|
218
|
+
|
219
|
+
retval
|
220
|
+
end
|
221
|
+
|
222
|
+
|
223
|
+
end
|
224
|
+
|
225
|
+
|
226
|
+
end # module ADO
|
227
|
+
end # module DBD
|
228
|
+
end # module DBI
|
229
|
+
|
data/lib/dbi.rb
ADDED
@@ -0,0 +1,1043 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
#
|
3
|
+
# Ruby/DBI
|
4
|
+
#
|
5
|
+
# Copyright (c) 2001, 2002, 2003 Michael Neumann <mneumann@ntecs.de>
|
6
|
+
#
|
7
|
+
# All rights reserved.
|
8
|
+
#
|
9
|
+
# Redistribution and use in source and binary forms, with or without
|
10
|
+
# modification, are permitted provided that the following conditions
|
11
|
+
# are met:
|
12
|
+
# 1. Redistributions of source code must retain the above copyright
|
13
|
+
# notice, this list of conditions and the following disclaimer.
|
14
|
+
# 2. Redistributions in binary form must reproduce the above copyright
|
15
|
+
# notice, this list of conditions and the following disclaimer in the
|
16
|
+
# documentation and/or other materials provided with the distribution.
|
17
|
+
# 3. The name of the author may not be used to endorse or promote products
|
18
|
+
# derived from this software without specific prior written permission.
|
19
|
+
#
|
20
|
+
# THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
21
|
+
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
|
22
|
+
# AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
23
|
+
# THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
24
|
+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
25
|
+
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
|
26
|
+
# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
27
|
+
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
28
|
+
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
29
|
+
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
30
|
+
#
|
31
|
+
# $Id: dbi.rb,v 1.8 2006/09/03 04:05:29 pdubois Exp $
|
32
|
+
#
|
33
|
+
|
34
|
+
require "find"
|
35
|
+
require "dbi/row"
|
36
|
+
require "dbi/utils"
|
37
|
+
require "dbi/sql"
|
38
|
+
require "dbi/columninfo"
|
39
|
+
require "date"
|
40
|
+
require "thread"
|
41
|
+
require 'monitor'
|
42
|
+
|
43
|
+
module DBI
|
44
|
+
VERSION = "0.2.2"
|
45
|
+
|
46
|
+
module DBD
|
47
|
+
DIR = "DBD"
|
48
|
+
API_VERSION = "0.3"
|
49
|
+
end
|
50
|
+
|
51
|
+
# Constants
|
52
|
+
|
53
|
+
# Constants for fetch_scroll
|
54
|
+
#
|
55
|
+
SQL_FETCH_NEXT = 1
|
56
|
+
SQL_FETCH_PRIOR = 2
|
57
|
+
SQL_FETCH_FIRST = 3
|
58
|
+
SQL_FETCH_LAST = 4
|
59
|
+
SQL_FETCH_ABSOLUTE = 5
|
60
|
+
SQL_FETCH_RELATIVE = 6
|
61
|
+
|
62
|
+
# SQL type constants
|
63
|
+
#
|
64
|
+
SQL_CHAR = 1
|
65
|
+
SQL_NUMERIC = 2
|
66
|
+
SQL_DECIMAL = 3
|
67
|
+
SQL_INTEGER = 4
|
68
|
+
SQL_SMALLINT = 5
|
69
|
+
SQL_FLOAT = 6
|
70
|
+
SQL_REAL = 7
|
71
|
+
SQL_DOUBLE = 8
|
72
|
+
SQL_DATE = 9 # 91
|
73
|
+
SQL_TIME = 10 # 92
|
74
|
+
SQL_TIMESTAMP = 11 # 93
|
75
|
+
SQL_VARCHAR = 12
|
76
|
+
SQL_BOOLEAN = 13
|
77
|
+
|
78
|
+
SQL_LONGVARCHAR = -1
|
79
|
+
SQL_BINARY = -2
|
80
|
+
SQL_VARBINARY = -3
|
81
|
+
SQL_LONGVARBINARY = -4
|
82
|
+
SQL_BIGINT = -5
|
83
|
+
SQL_TINYINT = -6
|
84
|
+
SQL_BIT = -7
|
85
|
+
|
86
|
+
# TODO
|
87
|
+
# Find types for these (XOPEN?)
|
88
|
+
#SQL_ARRAY =
|
89
|
+
SQL_BLOB = -10 # TODO
|
90
|
+
SQL_CLOB = -11 # TODO
|
91
|
+
#SQL_DISTINCT =
|
92
|
+
#SQL_OBJECT =
|
93
|
+
#SQL_NULL =
|
94
|
+
SQL_OTHER = 100
|
95
|
+
#SQL_REF =
|
96
|
+
#SQL_STRUCT =
|
97
|
+
|
98
|
+
SQL_TYPE_NAMES = {
|
99
|
+
SQL_BIT => 'BIT',
|
100
|
+
SQL_TINYINT => 'TINYINT',
|
101
|
+
SQL_SMALLINT => 'SMALLINT',
|
102
|
+
SQL_INTEGER => 'INTEGER',
|
103
|
+
SQL_BIGINT => 'BIGINT',
|
104
|
+
SQL_FLOAT => 'FLOAT',
|
105
|
+
SQL_REAL => 'REAL',
|
106
|
+
SQL_DOUBLE => 'DOUBLE',
|
107
|
+
SQL_NUMERIC => 'NUMERIC',
|
108
|
+
SQL_DECIMAL => 'DECIMAL',
|
109
|
+
SQL_CHAR => 'CHAR',
|
110
|
+
SQL_VARCHAR => 'VARCHAR',
|
111
|
+
SQL_LONGVARCHAR => 'LONG VARCHAR',
|
112
|
+
SQL_DATE => 'DATE',
|
113
|
+
SQL_TIME => 'TIME',
|
114
|
+
SQL_TIMESTAMP => 'TIMESTAMP',
|
115
|
+
SQL_BINARY => 'BINARY',
|
116
|
+
SQL_VARBINARY => 'VARBINARY',
|
117
|
+
SQL_LONGVARBINARY => 'LONG VARBINARY',
|
118
|
+
SQL_BLOB => 'BLOB',
|
119
|
+
SQL_CLOB => 'CLOB',
|
120
|
+
SQL_OTHER => nil,
|
121
|
+
SQL_BOOLEAN => 'BOOLEAN',
|
122
|
+
|
123
|
+
}
|
124
|
+
|
125
|
+
# Exceptions (borrowed by Python API 2.0)
|
126
|
+
|
127
|
+
# Base class of all other error exceptions. Use this to catch all DBI
|
128
|
+
# errors.
|
129
|
+
class Error < RuntimeError
|
130
|
+
end
|
131
|
+
|
132
|
+
# For important warnings like data truncation, etc.
|
133
|
+
class Warning < RuntimeError
|
134
|
+
end
|
135
|
+
|
136
|
+
# Exception for errors related to the DBI interface rather than the
|
137
|
+
# database itself.
|
138
|
+
class InterfaceError < Error
|
139
|
+
end
|
140
|
+
|
141
|
+
# Exception raised if the DBD driver has not specified a mandatory method.
|
142
|
+
class NotImplementedError < InterfaceError
|
143
|
+
end
|
144
|
+
|
145
|
+
# Exception for errors related to the database.
|
146
|
+
class DatabaseError < Error
|
147
|
+
attr_reader :err, :errstr, :state
|
148
|
+
|
149
|
+
def initialize(errstr="", err=nil, state=nil)
|
150
|
+
super(errstr)
|
151
|
+
@err, @errstr, @state = err, errstr, state
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Exception for errors due to problems with the processed
|
156
|
+
# data such as division by zero, numeric value out of range, etc.
|
157
|
+
class DataError < DatabaseError
|
158
|
+
end
|
159
|
+
|
160
|
+
# Exception for errors related to the database's operation which are not
|
161
|
+
# necessarily under the control of the programmer. This includes such
|
162
|
+
# things as unexpected disconnect, datasource name not found, transaction
|
163
|
+
# could not be processed, a memory allocation error occured during
|
164
|
+
# processing, etc.
|
165
|
+
class OperationalError < DatabaseError
|
166
|
+
end
|
167
|
+
|
168
|
+
# Exception raised when the relational integrity of the database
|
169
|
+
# is affected, e.g. a foreign key check fails.
|
170
|
+
class IntegrityError < DatabaseError
|
171
|
+
end
|
172
|
+
|
173
|
+
# Exception raised when the database encounters an internal error,
|
174
|
+
# e.g. the cursor is not valid anymore, the transaction is out of sync.
|
175
|
+
class InternalError < DatabaseError
|
176
|
+
end
|
177
|
+
|
178
|
+
# Exception raised for programming errors, e.g. table not found
|
179
|
+
# or already exists, syntax error in SQL statement, wrong number
|
180
|
+
# of parameters specified, etc.
|
181
|
+
class ProgrammingError < DatabaseError
|
182
|
+
end
|
183
|
+
|
184
|
+
# Exception raised if e.g. commit() is called for a database which do not
|
185
|
+
# support transactions.
|
186
|
+
class NotSupportedError < DatabaseError
|
187
|
+
end
|
188
|
+
|
189
|
+
# Datatypes
|
190
|
+
|
191
|
+
# TODO: do we need Binary?
|
192
|
+
# perhaps easier to call #bind_param(1, binary_string, 'type' => SQL_BLOB)
|
193
|
+
class Binary
|
194
|
+
attr_accessor :data
|
195
|
+
def initialize(data)
|
196
|
+
@data = data
|
197
|
+
end
|
198
|
+
|
199
|
+
def to_s
|
200
|
+
@data
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# Module functions (of DBI)
|
205
|
+
DEFAULT_TRACE_MODE = 2
|
206
|
+
DEFAULT_TRACE_OUTPUT = STDERR
|
207
|
+
|
208
|
+
# TODO: Is using class variables within a module such a wise idea? - Dan B.
|
209
|
+
@@driver_map = Hash.new
|
210
|
+
@@driver_monitor = ::Monitor.new()
|
211
|
+
@@trace_mode = DEFAULT_TRACE_MODE
|
212
|
+
@@trace_output = DEFAULT_TRACE_OUTPUT
|
213
|
+
|
214
|
+
class << self
|
215
|
+
|
216
|
+
# Establish a database connection. This is mostly a facade for the
|
217
|
+
# DBD's connect method.
|
218
|
+
def connect(driver_url, user=nil, auth=nil, params=nil, &p)
|
219
|
+
dr, db_args = _get_full_driver(driver_url)
|
220
|
+
dh = dr[0] # driver-handle
|
221
|
+
dh.connect(db_args, user, auth, params, &p)
|
222
|
+
end
|
223
|
+
|
224
|
+
# Load a DBD and returns the DriverHandle object
|
225
|
+
def get_driver(driver_url)
|
226
|
+
_get_full_driver(driver_url)[0][0] # return DriverHandle
|
227
|
+
end
|
228
|
+
|
229
|
+
# Extracts the db_args from driver_url and returns the correspondeing
|
230
|
+
# entry of the @@driver_map.
|
231
|
+
def _get_full_driver(driver_url)
|
232
|
+
db_driver, db_args = parse_url(driver_url)
|
233
|
+
db_driver = load_driver(db_driver)
|
234
|
+
dr = @@driver_map[db_driver]
|
235
|
+
[dr, db_args]
|
236
|
+
end
|
237
|
+
|
238
|
+
def trace(mode=nil, output=nil)
|
239
|
+
@@trace_mode = mode || @@trace_mode || DBI::DEFAULT_TRACE_MODE
|
240
|
+
@@trace_output = output || @@trace_output || DBI::DEFAULT_TRACE_OUTPUT
|
241
|
+
end
|
242
|
+
|
243
|
+
# Returns a list of the currently available drivers on your system in
|
244
|
+
# 'dbi:driver:' format.
|
245
|
+
def available_drivers
|
246
|
+
drivers = []
|
247
|
+
#path = File.dirname(File.dirname(__FILE__)) + "/" + DBD::DIR
|
248
|
+
path = File.dirname(__FILE__) + "/" + DBD::DIR
|
249
|
+
Find.find(path){ |f|
|
250
|
+
if File.file?(f)
|
251
|
+
driver = File.basename(f, ".rb")
|
252
|
+
drivers.push("dbi:#{driver}:")
|
253
|
+
end
|
254
|
+
}
|
255
|
+
drivers
|
256
|
+
end
|
257
|
+
|
258
|
+
def data_sources(driver)
|
259
|
+
db_driver, = parse_url(driver)
|
260
|
+
db_driver = load_driver(db_driver)
|
261
|
+
dh = @@driver_map[db_driver][0]
|
262
|
+
dh.data_sources
|
263
|
+
end
|
264
|
+
|
265
|
+
def disconnect_all( driver = nil )
|
266
|
+
if driver.nil?
|
267
|
+
@@driver_map.each {|k,v| v[0].disconnect_all}
|
268
|
+
else
|
269
|
+
db_driver, = parse_url(driver)
|
270
|
+
@@driver_map[db_driver][0].disconnect_all
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
|
275
|
+
private
|
276
|
+
|
277
|
+
##
|
278
|
+
# extended by John Gorman <jgorman@webbysoft.com> for
|
279
|
+
# case insensitive DBD names
|
280
|
+
#
|
281
|
+
def load_driver(driver_name)
|
282
|
+
@@driver_monitor.synchronize do
|
283
|
+
if @@driver_map[driver_name].nil?
|
284
|
+
|
285
|
+
dc = driver_name.downcase
|
286
|
+
|
287
|
+
# caseless look for drivers already loaded
|
288
|
+
found = @@driver_map.keys.find {|key| key.downcase == dc}
|
289
|
+
return found if found
|
290
|
+
|
291
|
+
if $SAFE >= 1
|
292
|
+
# case-sensitive in safe mode
|
293
|
+
require "#{DBD::DIR}/#{driver_name}/#{driver_name}"
|
294
|
+
else
|
295
|
+
# FIXME this whole require scheme is so horribly stupid I
|
296
|
+
# can't even begin to think about how much I'd enjoy what
|
297
|
+
# was smoked when this was thought up.
|
298
|
+
#
|
299
|
+
# Here's a hack to get tests to work.
|
300
|
+
# try a quick load and then a caseless scan
|
301
|
+
begin
|
302
|
+
begin
|
303
|
+
require "dbd/#{driver_name}"
|
304
|
+
rescue LoadError
|
305
|
+
require "#{DBD::DIR}/#{driver_name}/#{driver_name}"
|
306
|
+
end
|
307
|
+
rescue LoadError
|
308
|
+
$LOAD_PATH.each do |dir|
|
309
|
+
path = "#{dir}/dbd"
|
310
|
+
if FileTest.directory?(path)
|
311
|
+
require "#{path}/#{driver_name}"
|
312
|
+
break
|
313
|
+
end
|
314
|
+
|
315
|
+
path = "#{dir}/#{DBD::DIR}"
|
316
|
+
next unless FileTest.directory?(path)
|
317
|
+
found = Dir.entries(path).find {|e| e.downcase == dc}
|
318
|
+
next unless found
|
319
|
+
|
320
|
+
require "#{path}/#{found}/#{found}"
|
321
|
+
break
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
found ||= driver_name
|
327
|
+
|
328
|
+
# On a filesystem that is not case-sensitive (e.g., HFS+ on Mac OS X),
|
329
|
+
# the initial require attempt that loads the driver may succeed even
|
330
|
+
# though the lettercase of driver_name doesn't match the actual
|
331
|
+
# filename. If that happens, const_get will fail and it become
|
332
|
+
# necessary to look though the list of constants and look for a
|
333
|
+
# caseless match. The result of this match provides the constant
|
334
|
+
# with the proper lettercase -- which can be used to generate the
|
335
|
+
# driver handle.
|
336
|
+
|
337
|
+
dr = nil
|
338
|
+
begin
|
339
|
+
dr = DBI::DBD.const_get(found.intern)
|
340
|
+
rescue NameError
|
341
|
+
# caseless look for constants to find actual constant
|
342
|
+
found = found.downcase
|
343
|
+
found = DBI::DBD.constants.find { |e| e.downcase == found }
|
344
|
+
dr = DBI::DBD.const_get(found.intern) unless found.nil?
|
345
|
+
end
|
346
|
+
|
347
|
+
# If dr is nil at this point, it means the underlying driver
|
348
|
+
# failed to load. This usually means it's not installed, but
|
349
|
+
# can fail for other reasons.
|
350
|
+
if dr.nil?
|
351
|
+
err = "Unable to load driver '#{driver_name}'"
|
352
|
+
raise DBI::InterfaceError, err
|
353
|
+
end
|
354
|
+
|
355
|
+
dbd_dr = dr::Driver.new
|
356
|
+
drh = DBI::DriverHandle.new(dbd_dr)
|
357
|
+
drh.trace(@@trace_mode, @@trace_output)
|
358
|
+
@@driver_map[found] = [drh, dbd_dr]
|
359
|
+
return found
|
360
|
+
else
|
361
|
+
return driver_name
|
362
|
+
end
|
363
|
+
end
|
364
|
+
rescue LoadError, NameError
|
365
|
+
if $SAFE >= 1
|
366
|
+
raise InterfaceError, "Could not load driver (#{$!.message}). Note that in SAFE mode >= 1, driver URLs have to be case sensitive!"
|
367
|
+
else
|
368
|
+
raise InterfaceError, "Could not load driver (#{$!.message})"
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
# Splits a DBI URL into two components - the database driver name
|
373
|
+
# and the datasource (along with any options, if any) and returns
|
374
|
+
# a two element array, e.g. 'dbi:foo:bar' would return ['foo','bar'].
|
375
|
+
#
|
376
|
+
# A regular expression is used instead of a simple split to validate
|
377
|
+
# the proper format for the URL. If it isn't correct, an Interface
|
378
|
+
# error is raised.
|
379
|
+
def parse_url(driver_url)
|
380
|
+
if driver_url =~ /^(DBI|dbi):([^:]+)(:(.*))$/
|
381
|
+
[$2, $4]
|
382
|
+
else
|
383
|
+
raise InterfaceError, "Invalid Data Source Name"
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
end # self
|
388
|
+
|
389
|
+
|
390
|
+
|
391
|
+
#----------------------------------------------------
|
392
|
+
# Dispatch classes
|
393
|
+
#----------------------------------------------------
|
394
|
+
|
395
|
+
|
396
|
+
##
|
397
|
+
# Dispatch classes (Handle, DriverHandle, DatabaseHandle and StatementHandle)
|
398
|
+
#
|
399
|
+
|
400
|
+
class Handle
|
401
|
+
attr_reader :trace_mode, :trace_output
|
402
|
+
attr_reader :handle
|
403
|
+
|
404
|
+
def initialize(handle)
|
405
|
+
@handle = handle
|
406
|
+
@trace_mode = @trace_output = nil
|
407
|
+
end
|
408
|
+
|
409
|
+
def trace(mode=nil, output=nil)
|
410
|
+
@trace_mode = mode || @trace_mode || DBI::DEFAULT_TRACE_MODE
|
411
|
+
@trace_output = output || @trace_output || DBI::DEFAULT_TRACE_OUTPUT
|
412
|
+
end
|
413
|
+
|
414
|
+
|
415
|
+
##
|
416
|
+
# call a driver specific function
|
417
|
+
#
|
418
|
+
def func(function, *values)
|
419
|
+
if @handle.respond_to?("__" + function.to_s) then
|
420
|
+
@handle.send("__" + function.to_s, *values)
|
421
|
+
else
|
422
|
+
raise InterfaceError, "Driver specific function <#{function}> not available."
|
423
|
+
end
|
424
|
+
rescue ArgumentError
|
425
|
+
raise InterfaceError, "Wrong # of arguments for driver specific function"
|
426
|
+
end
|
427
|
+
|
428
|
+
# error functions?
|
429
|
+
end
|
430
|
+
|
431
|
+
class DriverHandle < Handle
|
432
|
+
|
433
|
+
def connect(db_args, user, auth, params)
|
434
|
+
|
435
|
+
user = @handle.default_user[0] if user.nil?
|
436
|
+
auth = @handle.default_user[1] if auth.nil?
|
437
|
+
|
438
|
+
# TODO: what if only one of them is nil?
|
439
|
+
#if user.nil? and auth.nil? then
|
440
|
+
# user, auth = @handle.default_user
|
441
|
+
#end
|
442
|
+
|
443
|
+
params ||= {}
|
444
|
+
new_params = @handle.default_attributes
|
445
|
+
params.each {|k,v| new_params[k] = v}
|
446
|
+
|
447
|
+
|
448
|
+
db = @handle.connect(db_args, user, auth, new_params)
|
449
|
+
dbh = DatabaseHandle.new(db)
|
450
|
+
dbh.trace(@trace_mode, @trace_output)
|
451
|
+
|
452
|
+
if block_given?
|
453
|
+
begin
|
454
|
+
yield dbh
|
455
|
+
ensure
|
456
|
+
dbh.disconnect if dbh.connected?
|
457
|
+
end
|
458
|
+
else
|
459
|
+
return dbh
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
def data_sources
|
464
|
+
@handle.data_sources
|
465
|
+
end
|
466
|
+
|
467
|
+
def disconnect_all
|
468
|
+
@handle.disconnect_all
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
class DatabaseHandle < Handle
|
473
|
+
|
474
|
+
include DBI::Utils::ConvParam
|
475
|
+
|
476
|
+
def connected?
|
477
|
+
not @handle.nil?
|
478
|
+
end
|
479
|
+
|
480
|
+
def disconnect
|
481
|
+
raise InterfaceError, "Database connection was already closed!" if @handle.nil?
|
482
|
+
@handle.disconnect
|
483
|
+
@handle = nil
|
484
|
+
end
|
485
|
+
|
486
|
+
def prepare(stmt)
|
487
|
+
raise InterfaceError, "Database connection was already closed!" if @handle.nil?
|
488
|
+
sth = StatementHandle.new(@handle.prepare(stmt), false)
|
489
|
+
sth.trace(@trace_mode, @trace_output)
|
490
|
+
|
491
|
+
if block_given?
|
492
|
+
begin
|
493
|
+
yield sth
|
494
|
+
ensure
|
495
|
+
sth.finish unless sth.finished?
|
496
|
+
end
|
497
|
+
else
|
498
|
+
return sth
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
502
|
+
def execute(stmt, *bindvars)
|
503
|
+
raise InterfaceError, "Database connection was already closed!" if @handle.nil?
|
504
|
+
sth = StatementHandle.new(@handle.execute(stmt, *conv_param(*bindvars)), true, false)
|
505
|
+
sth.trace(@trace_mode, @trace_output)
|
506
|
+
|
507
|
+
if block_given?
|
508
|
+
begin
|
509
|
+
yield sth
|
510
|
+
ensure
|
511
|
+
sth.finish unless sth.finished?
|
512
|
+
end
|
513
|
+
else
|
514
|
+
return sth
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
def do(stmt, *bindvars)
|
519
|
+
raise InterfaceError, "Database connection was already closed!" if @handle.nil?
|
520
|
+
@handle.do(stmt, *conv_param(*bindvars))
|
521
|
+
end
|
522
|
+
|
523
|
+
def select_one(stmt, *bindvars)
|
524
|
+
raise InterfaceError, "Database connection was already closed!" if @handle.nil?
|
525
|
+
row = nil
|
526
|
+
execute(stmt, *bindvars) do |sth|
|
527
|
+
row = sth.fetch
|
528
|
+
end
|
529
|
+
row
|
530
|
+
end
|
531
|
+
|
532
|
+
def select_all(stmt, *bindvars, &p)
|
533
|
+
raise InterfaceError, "Database connection was already closed!" if @handle.nil?
|
534
|
+
rows = nil
|
535
|
+
execute(stmt, *bindvars) do |sth|
|
536
|
+
if block_given?
|
537
|
+
sth.each(&p)
|
538
|
+
else
|
539
|
+
rows = sth.fetch_all
|
540
|
+
end
|
541
|
+
end
|
542
|
+
return rows
|
543
|
+
end
|
544
|
+
|
545
|
+
def tables
|
546
|
+
raise InterfaceError, "Database connection was already closed!" if @handle.nil?
|
547
|
+
@handle.tables
|
548
|
+
end
|
549
|
+
|
550
|
+
def columns( table )
|
551
|
+
raise InterfaceError, "Database connection was already closed!" if @handle.nil?
|
552
|
+
@handle.columns( table ).collect {|col| ColumnInfo.new(col) }
|
553
|
+
end
|
554
|
+
|
555
|
+
def ping
|
556
|
+
raise InterfaceError, "Database connection was already closed!" if @handle.nil?
|
557
|
+
@handle.ping
|
558
|
+
end
|
559
|
+
|
560
|
+
def quote(value)
|
561
|
+
raise InterfaceError, "Database connection was already closed!" if @handle.nil?
|
562
|
+
@handle.quote(value)
|
563
|
+
end
|
564
|
+
|
565
|
+
def commit
|
566
|
+
raise InterfaceError, "Database connection was already closed!" if @handle.nil?
|
567
|
+
@handle.commit
|
568
|
+
end
|
569
|
+
|
570
|
+
def rollback
|
571
|
+
raise InterfaceError, "Database connection was already closed!" if @handle.nil?
|
572
|
+
@handle.rollback
|
573
|
+
end
|
574
|
+
|
575
|
+
def transaction
|
576
|
+
raise InterfaceError, "Database connection was already closed!" if @handle.nil?
|
577
|
+
raise InterfaceError, "No block given" unless block_given?
|
578
|
+
|
579
|
+
commit
|
580
|
+
begin
|
581
|
+
yield self
|
582
|
+
commit
|
583
|
+
rescue Exception
|
584
|
+
rollback
|
585
|
+
raise
|
586
|
+
end
|
587
|
+
end
|
588
|
+
|
589
|
+
|
590
|
+
def [] (attr)
|
591
|
+
raise InterfaceError, "Database connection was already closed!" if @handle.nil?
|
592
|
+
@handle[attr]
|
593
|
+
end
|
594
|
+
|
595
|
+
def []= (attr, val)
|
596
|
+
raise InterfaceError, "Database connection was already closed!" if @handle.nil?
|
597
|
+
@handle[attr] = val
|
598
|
+
end
|
599
|
+
|
600
|
+
end
|
601
|
+
|
602
|
+
class StatementHandle < Handle
|
603
|
+
|
604
|
+
include Enumerable
|
605
|
+
include DBI::Utils::ConvParam
|
606
|
+
|
607
|
+
def initialize(handle, fetchable=false, prepared=true)
|
608
|
+
super(handle)
|
609
|
+
@fetchable = fetchable
|
610
|
+
@prepared = prepared # only false if immediate execute was used
|
611
|
+
@cols = nil
|
612
|
+
|
613
|
+
# TODO: problems with other DB's?
|
614
|
+
#@row = DBI::Row.new(column_names,nil)
|
615
|
+
if @fetchable
|
616
|
+
@row = DBI::Row.new(column_names,nil)
|
617
|
+
else
|
618
|
+
@row = nil
|
619
|
+
end
|
620
|
+
end
|
621
|
+
|
622
|
+
def finished?
|
623
|
+
@handle.nil?
|
624
|
+
end
|
625
|
+
|
626
|
+
def fetchable?
|
627
|
+
@fetchable
|
628
|
+
end
|
629
|
+
|
630
|
+
def bind_param(param, value, attribs=nil)
|
631
|
+
raise InterfaceError, "Statement was already closed!" if @handle.nil?
|
632
|
+
raise InterfaceError, "Statement wasn't prepared before." unless @prepared
|
633
|
+
@handle.bind_param(param, conv_param(value)[0], attribs)
|
634
|
+
end
|
635
|
+
|
636
|
+
def execute(*bindvars)
|
637
|
+
cancel # cancel before
|
638
|
+
raise InterfaceError, "Statement was already closed!" if @handle.nil?
|
639
|
+
raise InterfaceError, "Statement wasn't prepared before." unless @prepared
|
640
|
+
@handle.bind_params(*conv_param(*bindvars))
|
641
|
+
@handle.execute
|
642
|
+
@fetchable = true
|
643
|
+
|
644
|
+
# TODO:?
|
645
|
+
#if @row.nil?
|
646
|
+
@row = DBI::Row.new(column_names,nil)
|
647
|
+
#end
|
648
|
+
return nil
|
649
|
+
end
|
650
|
+
|
651
|
+
def finish
|
652
|
+
raise InterfaceError, "Statement was already closed!" if @handle.nil?
|
653
|
+
@handle.finish
|
654
|
+
@handle = nil
|
655
|
+
end
|
656
|
+
|
657
|
+
def cancel
|
658
|
+
raise InterfaceError, "Statement was already closed!" if @handle.nil?
|
659
|
+
@handle.cancel if @fetchable
|
660
|
+
@fetchable = false
|
661
|
+
end
|
662
|
+
|
663
|
+
def column_names
|
664
|
+
raise InterfaceError, "Statement was already closed!" if @handle.nil?
|
665
|
+
return @cols unless @cols.nil?
|
666
|
+
@cols = @handle.column_info.collect {|col| col['name'] }
|
667
|
+
end
|
668
|
+
|
669
|
+
def column_info
|
670
|
+
raise InterfaceError, "Statement was already closed!" if @handle.nil?
|
671
|
+
@handle.column_info.collect {|col| ColumnInfo.new(col) }
|
672
|
+
end
|
673
|
+
|
674
|
+
def rows
|
675
|
+
raise InterfaceError, "Statement was already closed!" if @handle.nil?
|
676
|
+
@handle.rows
|
677
|
+
end
|
678
|
+
|
679
|
+
|
680
|
+
def fetch(&p)
|
681
|
+
raise InterfaceError, "Statement was already closed!" if @handle.nil?
|
682
|
+
|
683
|
+
if block_given?
|
684
|
+
while (res = @handle.fetch) != nil
|
685
|
+
@row.set_values(res)
|
686
|
+
yield @row
|
687
|
+
end
|
688
|
+
@handle.cancel
|
689
|
+
@fetchable = false
|
690
|
+
return nil
|
691
|
+
else
|
692
|
+
res = @handle.fetch
|
693
|
+
if res.nil?
|
694
|
+
@handle.cancel
|
695
|
+
@fetchable = false
|
696
|
+
else
|
697
|
+
@row.set_values(res)
|
698
|
+
res = @row
|
699
|
+
end
|
700
|
+
return res
|
701
|
+
end
|
702
|
+
end
|
703
|
+
|
704
|
+
def each(&p)
|
705
|
+
raise InterfaceError, "Statement was already closed!" if @handle.nil?
|
706
|
+
raise InterfaceError, "Statement must first be executed" unless @fetchable
|
707
|
+
raise InterfaceError, "No block given" unless block_given?
|
708
|
+
|
709
|
+
fetch(&p)
|
710
|
+
end
|
711
|
+
|
712
|
+
def fetch_array
|
713
|
+
raise InterfaceError, "Statement was already closed!" if @handle.nil?
|
714
|
+
raise InterfaceError, "Statement must first be executed" unless @fetchable
|
715
|
+
|
716
|
+
if block_given?
|
717
|
+
while (res = @handle.fetch) != nil
|
718
|
+
yield res
|
719
|
+
end
|
720
|
+
@handle.cancel
|
721
|
+
@fetchable = false
|
722
|
+
return nil
|
723
|
+
else
|
724
|
+
res = @handle.fetch
|
725
|
+
if res.nil?
|
726
|
+
@handle.cancel
|
727
|
+
@fetchable = false
|
728
|
+
end
|
729
|
+
return res
|
730
|
+
end
|
731
|
+
end
|
732
|
+
|
733
|
+
def fetch_hash
|
734
|
+
raise InterfaceError, "Statement was already closed!" if @handle.nil?
|
735
|
+
raise InterfaceError, "Statement must first be executed" unless @fetchable
|
736
|
+
|
737
|
+
cols = column_names
|
738
|
+
|
739
|
+
if block_given?
|
740
|
+
while (row = @handle.fetch) != nil
|
741
|
+
hash = {}
|
742
|
+
row.each_with_index {|v,i| hash[cols[i]] = v}
|
743
|
+
yield hash
|
744
|
+
end
|
745
|
+
@handle.cancel
|
746
|
+
@fetchable = false
|
747
|
+
return nil
|
748
|
+
else
|
749
|
+
row = @handle.fetch
|
750
|
+
if row.nil?
|
751
|
+
@handle.cancel
|
752
|
+
@fetchable = false
|
753
|
+
return nil
|
754
|
+
else
|
755
|
+
hash = {}
|
756
|
+
row.each_with_index {|v,i| hash[cols[i]] = v}
|
757
|
+
return hash
|
758
|
+
end
|
759
|
+
end
|
760
|
+
end
|
761
|
+
|
762
|
+
def fetch_many(cnt)
|
763
|
+
raise InterfaceError, "Statement was already closed!" if @handle.nil?
|
764
|
+
raise InterfaceError, "Statement must first be executed" unless @fetchable
|
765
|
+
|
766
|
+
cols = column_names
|
767
|
+
rows = @handle.fetch_many(cnt)
|
768
|
+
if rows.nil?
|
769
|
+
@handle.cancel
|
770
|
+
@fetchable = false
|
771
|
+
return []
|
772
|
+
else
|
773
|
+
return rows.collect{|r| Row.new(cols, r)}
|
774
|
+
end
|
775
|
+
end
|
776
|
+
|
777
|
+
def fetch_all
|
778
|
+
raise InterfaceError, "Statement was already closed!" if @handle.nil?
|
779
|
+
raise InterfaceError, "Statement must first be executed" unless @fetchable
|
780
|
+
|
781
|
+
cols = column_names
|
782
|
+
rows = @handle.fetch_all
|
783
|
+
if rows.nil?
|
784
|
+
@handle.cancel
|
785
|
+
@fetchable = false
|
786
|
+
return []
|
787
|
+
else
|
788
|
+
return rows.collect{|r| Row.new(cols, r)}
|
789
|
+
end
|
790
|
+
end
|
791
|
+
|
792
|
+
def fetch_scroll(direction, offset=1)
|
793
|
+
raise InterfaceError, "Statement was already closed!" if @handle.nil?
|
794
|
+
raise InterfaceError, "Statement must first be executed" unless @fetchable
|
795
|
+
|
796
|
+
row = @handle.fetch_scroll(direction, offset)
|
797
|
+
if row.nil?
|
798
|
+
#@handle.cancel
|
799
|
+
#@fetchable = false
|
800
|
+
return nil
|
801
|
+
else
|
802
|
+
@row.set_values(row)
|
803
|
+
return @row
|
804
|
+
end
|
805
|
+
end
|
806
|
+
|
807
|
+
def [] (attr)
|
808
|
+
raise InterfaceError, "Statement was already closed!" if @handle.nil?
|
809
|
+
@handle[attr]
|
810
|
+
end
|
811
|
+
|
812
|
+
def []= (attr, val)
|
813
|
+
raise InterfaceError, "Statement was already closed!" if @handle.nil?
|
814
|
+
@handle[attr] = val
|
815
|
+
end
|
816
|
+
|
817
|
+
end # class StatementHandle
|
818
|
+
|
819
|
+
|
820
|
+
|
821
|
+
|
822
|
+
|
823
|
+
#----------------------------------------------------
|
824
|
+
# Fallback classes
|
825
|
+
#----------------------------------------------------
|
826
|
+
|
827
|
+
|
828
|
+
##
|
829
|
+
# Fallback classes for default behavior of DBD driver
|
830
|
+
# must be inherited by the DBD driver classes
|
831
|
+
#
|
832
|
+
|
833
|
+
class Base
|
834
|
+
end
|
835
|
+
|
836
|
+
|
837
|
+
class BaseDriver < Base
|
838
|
+
|
839
|
+
def initialize(dbd_version)
|
840
|
+
major, minor = dbd_version.split(".")
|
841
|
+
unless major.to_i == DBD::API_VERSION.split(".")[0].to_i
|
842
|
+
raise InterfaceError, "Wrong DBD API version used"
|
843
|
+
end
|
844
|
+
end
|
845
|
+
|
846
|
+
def connect(dbname, user, auth, attr)
|
847
|
+
raise NotImplementedError
|
848
|
+
end
|
849
|
+
|
850
|
+
def default_user
|
851
|
+
['', '']
|
852
|
+
end
|
853
|
+
|
854
|
+
def default_attributes
|
855
|
+
{}
|
856
|
+
end
|
857
|
+
|
858
|
+
def data_sources
|
859
|
+
[]
|
860
|
+
end
|
861
|
+
|
862
|
+
def disconnect_all
|
863
|
+
raise NotImplementedError
|
864
|
+
end
|
865
|
+
|
866
|
+
end # class BaseDriver
|
867
|
+
|
868
|
+
class BaseDatabase < Base
|
869
|
+
|
870
|
+
def initialize(handle, attr)
|
871
|
+
@handle = handle
|
872
|
+
@attr = {}
|
873
|
+
attr.each {|k,v| self[k] = v}
|
874
|
+
end
|
875
|
+
|
876
|
+
def disconnect
|
877
|
+
raise NotImplementedError
|
878
|
+
end
|
879
|
+
|
880
|
+
def ping
|
881
|
+
raise NotImplementedError
|
882
|
+
end
|
883
|
+
|
884
|
+
def prepare(statement)
|
885
|
+
raise NotImplementedError
|
886
|
+
end
|
887
|
+
|
888
|
+
#============================================
|
889
|
+
# OPTIONAL
|
890
|
+
#============================================
|
891
|
+
|
892
|
+
def commit
|
893
|
+
raise NotSupportedError
|
894
|
+
end
|
895
|
+
|
896
|
+
def rollback
|
897
|
+
raise NotSupportedError
|
898
|
+
end
|
899
|
+
|
900
|
+
def tables
|
901
|
+
[]
|
902
|
+
end
|
903
|
+
|
904
|
+
def columns(table)
|
905
|
+
[]
|
906
|
+
end
|
907
|
+
|
908
|
+
|
909
|
+
def execute(statement, *bindvars)
|
910
|
+
stmt = prepare(statement)
|
911
|
+
stmt.bind_params(*bindvars)
|
912
|
+
stmt.execute
|
913
|
+
stmt
|
914
|
+
end
|
915
|
+
|
916
|
+
def do(statement, *bindvars)
|
917
|
+
stmt = execute(statement, *bindvars)
|
918
|
+
res = stmt.rows
|
919
|
+
stmt.finish
|
920
|
+
return res
|
921
|
+
end
|
922
|
+
|
923
|
+
# includes quote
|
924
|
+
include DBI::SQL::BasicQuote
|
925
|
+
|
926
|
+
def [](attr)
|
927
|
+
@attr[attr]
|
928
|
+
end
|
929
|
+
|
930
|
+
def []=(attr, value)
|
931
|
+
raise NotSupportedError
|
932
|
+
end
|
933
|
+
|
934
|
+
end # class BaseDatabase
|
935
|
+
|
936
|
+
|
937
|
+
class BaseStatement < Base
|
938
|
+
|
939
|
+
def initialize(attr=nil)
|
940
|
+
@attr = attr || {}
|
941
|
+
end
|
942
|
+
|
943
|
+
|
944
|
+
|
945
|
+
def bind_param(param, value, attribs)
|
946
|
+
raise NotImplementedError
|
947
|
+
end
|
948
|
+
|
949
|
+
def execute
|
950
|
+
raise NotImplementedError
|
951
|
+
end
|
952
|
+
|
953
|
+
def finish
|
954
|
+
raise NotImplementedError
|
955
|
+
end
|
956
|
+
|
957
|
+
def fetch
|
958
|
+
raise NotImplementedError
|
959
|
+
end
|
960
|
+
|
961
|
+
##
|
962
|
+
# returns result-set column information as array
|
963
|
+
# of hashs, where each hash represents one column
|
964
|
+
def column_info
|
965
|
+
raise NotImplementedError
|
966
|
+
end
|
967
|
+
|
968
|
+
#============================================
|
969
|
+
# OPTIONAL
|
970
|
+
#============================================
|
971
|
+
|
972
|
+
def bind_params(*bindvars)
|
973
|
+
bindvars.each_with_index {|val,i| bind_param(i+1, val, nil) }
|
974
|
+
self
|
975
|
+
end
|
976
|
+
|
977
|
+
def cancel
|
978
|
+
end
|
979
|
+
|
980
|
+
def fetch_scroll(direction, offset)
|
981
|
+
case direction
|
982
|
+
when SQL_FETCH_NEXT
|
983
|
+
return fetch
|
984
|
+
when SQL_FETCH_LAST
|
985
|
+
last_row = nil
|
986
|
+
while (row=fetch) != nil
|
987
|
+
last_row = row
|
988
|
+
end
|
989
|
+
return last_row
|
990
|
+
when SQL_FETCH_RELATIVE
|
991
|
+
raise NotSupportedError if offset <= 0
|
992
|
+
row = nil
|
993
|
+
offset.times { row = fetch; break if row.nil? }
|
994
|
+
return row
|
995
|
+
else
|
996
|
+
raise NotSupportedError
|
997
|
+
end
|
998
|
+
end
|
999
|
+
|
1000
|
+
def fetch_many(cnt)
|
1001
|
+
rows = []
|
1002
|
+
cnt.times do
|
1003
|
+
row = fetch
|
1004
|
+
break if row.nil?
|
1005
|
+
rows << row.dup
|
1006
|
+
end
|
1007
|
+
|
1008
|
+
if rows.empty?
|
1009
|
+
nil
|
1010
|
+
else
|
1011
|
+
rows
|
1012
|
+
end
|
1013
|
+
end
|
1014
|
+
|
1015
|
+
def fetch_all
|
1016
|
+
rows = []
|
1017
|
+
loop do
|
1018
|
+
row = fetch
|
1019
|
+
break if row.nil?
|
1020
|
+
rows << row.dup
|
1021
|
+
end
|
1022
|
+
|
1023
|
+
if rows.empty?
|
1024
|
+
nil
|
1025
|
+
else
|
1026
|
+
rows
|
1027
|
+
end
|
1028
|
+
end
|
1029
|
+
|
1030
|
+
def [](attr)
|
1031
|
+
@attr[attr]
|
1032
|
+
end
|
1033
|
+
|
1034
|
+
def []=(attr, value)
|
1035
|
+
raise NotSupportedError
|
1036
|
+
end
|
1037
|
+
|
1038
|
+
end # class BaseStatement
|
1039
|
+
|
1040
|
+
|
1041
|
+
end # module DBI
|
1042
|
+
|
1043
|
+
|