rdbi 0.9.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/.document +5 -0
- data/.gitignore +33 -0
- data/LICENSE +20 -0
- data/README.rdoc +194 -0
- data/Rakefile +109 -0
- data/VERSION +1 -0
- data/docs/external-api.pdf +0 -0
- data/docs/external-api.texi +365 -0
- data/lib/rdbi.rb +189 -0
- data/lib/rdbi/cursor.rb +90 -0
- data/lib/rdbi/database.rb +262 -0
- data/lib/rdbi/driver.rb +35 -0
- data/lib/rdbi/pool.rb +236 -0
- data/lib/rdbi/result.rb +402 -0
- data/lib/rdbi/schema.rb +121 -0
- data/lib/rdbi/statement.rb +204 -0
- data/lib/rdbi/types.rb +200 -0
- data/rdbi.gemspec +94 -0
- data/test/helper.rb +32 -0
- data/test/test_database.rb +246 -0
- data/test/test_pool.rb +181 -0
- data/test/test_rdbi.rb +85 -0
- data/test/test_result.rb +269 -0
- data/test/test_statement.rb +66 -0
- data/test/test_types.rb +114 -0
- data/test/test_util.rb +61 -0
- metadata +201 -0
data/lib/rdbi/driver.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
#
|
2
|
+
# RDBI::Driver is the bootstrap handle to yield database connection
|
3
|
+
# (RDBI::Database) handles. It preserves the connection parameters and the
|
4
|
+
# desired database, and outside of yielding handles, does little else.
|
5
|
+
#
|
6
|
+
# As such, it is normally intended to be used by RDBI internally and (rarely
|
7
|
+
# by) Database drivers.
|
8
|
+
#
|
9
|
+
class RDBI::Driver
|
10
|
+
# connection arguments requested during initialization
|
11
|
+
attr_reader :connect_args
|
12
|
+
# Database driver class requested for initialization
|
13
|
+
attr_reader :dbh_class
|
14
|
+
|
15
|
+
# Initialize a new driver object. This accepts an RDBI::Database subclass as a
|
16
|
+
# class name (shorthand does not work here) and the arguments to pass into
|
17
|
+
# the constructor.
|
18
|
+
def initialize(dbh_class, *args)
|
19
|
+
@dbh_class = dbh_class
|
20
|
+
@connect_args = [RDBI::Util.key_hash_as_symbols(args[0])]
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# This is a proxy method to construct RDBI::Database handles. It constructs
|
25
|
+
# the RDBI::Database object, and sets the driver on the object to this
|
26
|
+
# current object for duplication / multiple creation.
|
27
|
+
#
|
28
|
+
def new_handle
|
29
|
+
dbh = @dbh_class.new(*@connect_args)
|
30
|
+
dbh.driver = self
|
31
|
+
return dbh
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# vim: syntax=ruby ts=2 et sw=2 sts=2
|
data/lib/rdbi/pool.rb
ADDED
@@ -0,0 +1,236 @@
|
|
1
|
+
#
|
2
|
+
# RDBI::Pool - Connection Pooling.
|
3
|
+
#
|
4
|
+
# Pools are named resources that consist of N concurrent connections which all
|
5
|
+
# have the same properties. Many group actions can be performed on them, such
|
6
|
+
# as disconnecting the entire lot.
|
7
|
+
#
|
8
|
+
# RDBI::Pool itself has a global accessor, by way of +RDBI::Pool::[]+, that can
|
9
|
+
# access these pools by name. Alternatively, you may access them through the
|
10
|
+
# RDBI.pool interface.
|
11
|
+
#
|
12
|
+
# Pools are thread-safe and are capable of being resized without disconnecting
|
13
|
+
# the culled database handles.
|
14
|
+
#
|
15
|
+
class RDBI::Pool
|
16
|
+
|
17
|
+
@mutex = Mutex.new
|
18
|
+
|
19
|
+
class << self
|
20
|
+
include Enumerable
|
21
|
+
|
22
|
+
# Iterate each pool and get the name of the pool (as a symbol) and the
|
23
|
+
# value as a Pool object.
|
24
|
+
def each
|
25
|
+
@pools.each do |key, value|
|
26
|
+
yield(key, value)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# obtain the names of each pool.
|
31
|
+
def keys
|
32
|
+
@pools.keys
|
33
|
+
end
|
34
|
+
|
35
|
+
# obtain the pool objects of each pool.
|
36
|
+
def values
|
37
|
+
@pools.values
|
38
|
+
end
|
39
|
+
|
40
|
+
#
|
41
|
+
# Retrieves a pool object for the name, or nothing if it does not exist.
|
42
|
+
#
|
43
|
+
def [](name)
|
44
|
+
mutex.synchronize do
|
45
|
+
@pools ||= { }
|
46
|
+
@pools[name.to_sym]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# Sets the pool for the name. This is not recommended for end-user code.
|
52
|
+
#
|
53
|
+
def []=(name, value)
|
54
|
+
mutex.synchronize do
|
55
|
+
@pools ||= { }
|
56
|
+
@pools[name.to_sym] = value
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def mutex
|
61
|
+
@mutex
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
include Enumerable
|
66
|
+
|
67
|
+
# a list of the pool handles for this object. Do not manipulate this directly.
|
68
|
+
attr_reader :handles
|
69
|
+
# the last index corresponding to the latest allocation request.
|
70
|
+
attr_reader :last_index
|
71
|
+
# the maximum number of items this pool can hold. should only be altered by resize.
|
72
|
+
attr_reader :max
|
73
|
+
# the Mutex for this pool.
|
74
|
+
attr_reader :mutex
|
75
|
+
|
76
|
+
#
|
77
|
+
# Creates a new pool.
|
78
|
+
#
|
79
|
+
# * name: the name of this pool, which will be used to find it in the global accessor.
|
80
|
+
# * connect_args: an array of arguments that would be passed to RDBI.connect, including the driver name.
|
81
|
+
# * max: the maximum number of connections to deal with.
|
82
|
+
#
|
83
|
+
# Usage:
|
84
|
+
#
|
85
|
+
# Pool.new(:fart, [:SQLite3, :database => "/tmp/foo.db"])
|
86
|
+
def initialize(name, connect_args, max=5)
|
87
|
+
@handles = []
|
88
|
+
@connect_args = connect_args
|
89
|
+
@max = max
|
90
|
+
@last_index = 0
|
91
|
+
@mutex = Mutex.new
|
92
|
+
self.class[name] = self
|
93
|
+
end
|
94
|
+
|
95
|
+
# Obtain each database handle in the pool.
|
96
|
+
def each
|
97
|
+
@handles.each { |dbh| yield dbh }
|
98
|
+
end
|
99
|
+
|
100
|
+
#
|
101
|
+
# Ping all database connections and average out the amount.
|
102
|
+
#
|
103
|
+
# Any disconnected handles will be reconnected before this operation
|
104
|
+
# starts.
|
105
|
+
def ping
|
106
|
+
reconnect_if_disconnected
|
107
|
+
mutex.synchronize do
|
108
|
+
@handles.inject(1) { |sum,dbh| sum + (dbh.ping || 1) } / @handles.size
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
#
|
113
|
+
# Unconditionally reconnect all database handles.
|
114
|
+
def reconnect
|
115
|
+
mutex.synchronize do
|
116
|
+
@handles.each { |dbh| dbh.reconnect }
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
#
|
121
|
+
# Only reconnect the database handles that have not been already connected.
|
122
|
+
def reconnect_if_disconnected
|
123
|
+
mutex.synchronize do
|
124
|
+
@handles.each do |dbh|
|
125
|
+
dbh.reconnect unless dbh.connected?
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
#
|
131
|
+
# Disconnect all database handles.
|
132
|
+
def disconnect
|
133
|
+
mutex.synchronize do
|
134
|
+
@handles.each(&:disconnect)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
#
|
139
|
+
# Add a connection, connecting automatically with the connect arguments
|
140
|
+
# supplied to the constructor.
|
141
|
+
def add_connection
|
142
|
+
add(RDBI.connect(*@connect_args))
|
143
|
+
end
|
144
|
+
|
145
|
+
#
|
146
|
+
# Remove a specific connection. If this connection does not exist in the
|
147
|
+
# pool already, nothing will occur.
|
148
|
+
#
|
149
|
+
# This database object is *not* disconnected -- it is your responsibility
|
150
|
+
# to do so.
|
151
|
+
def remove(dbh)
|
152
|
+
mutex.synchronize do
|
153
|
+
@handles.reject! { |x| x.object_id == dbh.object_id }
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
#
|
158
|
+
# Resize the pool. If the new pool size is smaller, connections will be
|
159
|
+
# forcibly removed, preferring disconnected handles over connected ones.
|
160
|
+
#
|
161
|
+
# No database connections are disconnected.
|
162
|
+
#
|
163
|
+
# Returns the handles that were removed, if any.
|
164
|
+
#
|
165
|
+
def resize(max=5)
|
166
|
+
mutex.synchronize do
|
167
|
+
in_pool = @handles.select(&:connected?)
|
168
|
+
|
169
|
+
unless (in_pool.size >= max)
|
170
|
+
disconnected = @handles.select { |x| !x.connected? }
|
171
|
+
if disconnected.size > 0
|
172
|
+
in_pool += disconnected[0..(max - in_pool.size - 1)]
|
173
|
+
end
|
174
|
+
else
|
175
|
+
in_pool = in_pool[0..(max-1)]
|
176
|
+
end
|
177
|
+
|
178
|
+
rejected = @handles - in_pool
|
179
|
+
|
180
|
+
@max = max
|
181
|
+
@handles = in_pool
|
182
|
+
rejected
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
#
|
187
|
+
# Obtain a database handle from the pool. Ordering is round robin.
|
188
|
+
#
|
189
|
+
# A new connection may be created if it fills in the pool where a
|
190
|
+
# previously empty object existed. Additionally, if the current database
|
191
|
+
# handle is disconnected, it will be reconnected.
|
192
|
+
#
|
193
|
+
def get_dbh
|
194
|
+
mutex.synchronize do
|
195
|
+
if @last_index >= @max
|
196
|
+
@last_index = 0
|
197
|
+
end
|
198
|
+
|
199
|
+
# XXX this is longhand for "make sure it's connected before we hand it
|
200
|
+
# off"
|
201
|
+
if @handles[@last_index] and !@handles[@last_index].connected?
|
202
|
+
@handles[@last_index].reconnect
|
203
|
+
elsif !@handles[@last_index]
|
204
|
+
@handles[@last_index] = RDBI.connect(*@connect_args)
|
205
|
+
end
|
206
|
+
|
207
|
+
dbh = @handles[@last_index]
|
208
|
+
@last_index += 1
|
209
|
+
dbh
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
protected
|
214
|
+
|
215
|
+
#
|
216
|
+
# Add any ol' database handle. This is not for global consumption.
|
217
|
+
#
|
218
|
+
def add(dbh)
|
219
|
+
dbh = *MethLab.validate_array_params([RDBI::Database], [dbh])
|
220
|
+
raise dbh if dbh.kind_of?(Exception)
|
221
|
+
|
222
|
+
dbh = dbh[0] if dbh.kind_of?(Array)
|
223
|
+
|
224
|
+
mutex.synchronize do
|
225
|
+
if @handles.size >= @max
|
226
|
+
raise ArgumentError, "too many handles in this pool (max: #{@max})"
|
227
|
+
end
|
228
|
+
|
229
|
+
@handles << dbh
|
230
|
+
end
|
231
|
+
|
232
|
+
return self
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# vim: syntax=ruby ts=2 et sw=2 sts=2
|
data/lib/rdbi/result.rb
ADDED
@@ -0,0 +1,402 @@
|
|
1
|
+
#
|
2
|
+
# RDBI::Result encapsulates results from a statement.
|
3
|
+
#
|
4
|
+
# Results in RDBI::Result are row-oriented and may be transformable by Result
|
5
|
+
# Drivers (RDBI::Result::Driver). They are fetched as a unit or in order.
|
6
|
+
#
|
7
|
+
# The RDBI::Result API is deliberately architected to loosely resemble that of
|
8
|
+
# IO or File.
|
9
|
+
#
|
10
|
+
# == Just give me the data!
|
11
|
+
#
|
12
|
+
# Have a peek at RDBI::Result#fetch.
|
13
|
+
#
|
14
|
+
# == Result Counts
|
15
|
+
#
|
16
|
+
# Multiple kinds of counts are represented in each result:
|
17
|
+
#
|
18
|
+
# * A count of the results provided
|
19
|
+
# * A count of the affected rows.
|
20
|
+
#
|
21
|
+
# To elaborate, the "affected rows" is a count of rows that were altered by the
|
22
|
+
# statement from a DML result such as +INSERT+ or +UPDATE+. In some cases,
|
23
|
+
# statements will both alter rows and yield results, which is why this value is
|
24
|
+
# not switched depending on the kind of statement.
|
25
|
+
#
|
26
|
+
# == Result Drivers
|
27
|
+
#
|
28
|
+
# Result drivers are subclasses of RDBI::Result::Driver that take the result as
|
29
|
+
# input and yield a transformed input: data structures such a hashes, or even
|
30
|
+
# wilder results such as CSV or JSON or YAML. Given the ability to sanely
|
31
|
+
# transform row-oriented input, result drivers effectively have the power to do
|
32
|
+
# anything.
|
33
|
+
#
|
34
|
+
# Accessing result drivers is as easy as using a secondary form of
|
35
|
+
# RDBI::Result#fetch or more explicitly with the RDBI::Result#as call.
|
36
|
+
#
|
37
|
+
class RDBI::Result
|
38
|
+
extend MethLab
|
39
|
+
include Enumerable
|
40
|
+
|
41
|
+
# The RDBI::Schema structure associated with this result.
|
42
|
+
attr_reader :schema
|
43
|
+
|
44
|
+
# The RDBI::Statement that yielded this result.
|
45
|
+
attr_reader :sth
|
46
|
+
|
47
|
+
# The RDBI::Result::Driver currently associated with this Result.
|
48
|
+
attr_reader :driver
|
49
|
+
|
50
|
+
# The count of results (see RDBI::Result main documentation)
|
51
|
+
attr_reader :result_count
|
52
|
+
|
53
|
+
# The count of affected rows by a DML statement (see RDBI::Result main documentation)
|
54
|
+
attr_reader :affected_count
|
55
|
+
|
56
|
+
# The mapping of types for each positional argument in the Result.
|
57
|
+
attr_reader :type_hash
|
58
|
+
|
59
|
+
# The binds used in the statement that yielded this Result.
|
60
|
+
attr_reader :binds
|
61
|
+
|
62
|
+
# FIXME async
|
63
|
+
inline(:complete, :complete?) { true }
|
64
|
+
|
65
|
+
##
|
66
|
+
# :attr_reader: has_data
|
67
|
+
#
|
68
|
+
# Does this result have data?
|
69
|
+
|
70
|
+
##
|
71
|
+
# :attr_reader: has_data?
|
72
|
+
#
|
73
|
+
# Does this result have data?
|
74
|
+
inline(:has_data, :has_data?) { @data.size > 0 }
|
75
|
+
|
76
|
+
#
|
77
|
+
# Creates a new RDBI::Result. Please refer to RDBI::Statement#new_execution
|
78
|
+
# for instructions on how this is typically used and how the contents are
|
79
|
+
# passed to the constructor.
|
80
|
+
#
|
81
|
+
def initialize(sth, binds, data, schema, type_hash)
|
82
|
+
@schema = schema
|
83
|
+
@data = data
|
84
|
+
@result_count = data.size
|
85
|
+
@affected_count = data.affected_count
|
86
|
+
@sth = sth
|
87
|
+
@binds = binds
|
88
|
+
@type_hash = type_hash
|
89
|
+
@mutex = Mutex.new
|
90
|
+
@driver = RDBI::Result::Driver::Array
|
91
|
+
@fetch_handle = nil
|
92
|
+
as(@driver)
|
93
|
+
end
|
94
|
+
|
95
|
+
#
|
96
|
+
# Reload the result. This will:
|
97
|
+
#
|
98
|
+
# * Execute the statement that yielded this result again, with the original binds
|
99
|
+
# * Replace the results and other attributes with the new results.
|
100
|
+
#
|
101
|
+
def reload
|
102
|
+
@data.finish
|
103
|
+
res = @sth.execute(*@binds)
|
104
|
+
@data = res.instance_variable_get(:@data)
|
105
|
+
@type_hash = res.instance_variable_get(:@type_hash)
|
106
|
+
@schema = res.instance_variable_get(:@schema)
|
107
|
+
@result_count = res.instance_variable_get(:@result_count)
|
108
|
+
@affected_count = res.instance_variable_get(:@affected_count)
|
109
|
+
end
|
110
|
+
|
111
|
+
#
|
112
|
+
# Iterator for Enumerable methods. Yields a row at a time.
|
113
|
+
#
|
114
|
+
def each
|
115
|
+
@data.each do |row|
|
116
|
+
yield(row)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
#
|
121
|
+
# Reset the index.
|
122
|
+
#
|
123
|
+
def rewind
|
124
|
+
@data.rewind
|
125
|
+
end
|
126
|
+
|
127
|
+
#
|
128
|
+
# Coerce the underlying result to an array, fetching all values.
|
129
|
+
#
|
130
|
+
def coerce_to_array
|
131
|
+
@data.coerce_to_array
|
132
|
+
end
|
133
|
+
|
134
|
+
#
|
135
|
+
# :call-seq:
|
136
|
+
# as(String)
|
137
|
+
# as(Symbol)
|
138
|
+
# as(Class)
|
139
|
+
# as([Class, String, or Symbol], *driver_arguments)
|
140
|
+
#
|
141
|
+
# Replace the Result Driver. See RDBI::Result's main docs and
|
142
|
+
# RDBI::Result::Driver for more information on Result Drivers.
|
143
|
+
#
|
144
|
+
# You may pass:
|
145
|
+
#
|
146
|
+
# * A Symbol or String which is shorthand for loading from the
|
147
|
+
# RDBI::Result::Driver namespace -- for example: "CSV" will result in the
|
148
|
+
# class RDBI::Result::Driver::CSV.
|
149
|
+
# * A full class name.
|
150
|
+
#
|
151
|
+
# There are no naming requirements; the String/Symbol form is just shorthand
|
152
|
+
# for convention.
|
153
|
+
#
|
154
|
+
# Any additional arguments will be passed to the driver's constructor.
|
155
|
+
#
|
156
|
+
def as(driver_klass, *args)
|
157
|
+
|
158
|
+
driver_klass = RDBI::Util.class_from_class_or_symbol(driver_klass, RDBI::Result::Driver)
|
159
|
+
|
160
|
+
@data.rewind
|
161
|
+
@driver = driver_klass
|
162
|
+
@fetch_handle = driver_klass.new(self, *args)
|
163
|
+
end
|
164
|
+
|
165
|
+
#
|
166
|
+
# :call-seq:
|
167
|
+
# fetch()
|
168
|
+
# fetch(Integer)
|
169
|
+
# fetch(:first)
|
170
|
+
# fetch(:last)
|
171
|
+
# fetch(:all)
|
172
|
+
# fetch(:rest)
|
173
|
+
# fetch(amount, [Class, String, or Symbol], *driver_arguments)
|
174
|
+
#
|
175
|
+
# fetch is the way people will typically interact with this class. It yields
|
176
|
+
# some or all of the results depending on the arguments given. Additionally,
|
177
|
+
# it can be supplemented with the arguments passed to RDBI::Result#as to
|
178
|
+
# one-off select a result driver.
|
179
|
+
#
|
180
|
+
# The initial argument can be none or one of many options:
|
181
|
+
#
|
182
|
+
# * An Integer n requests n rows from the result and increments the index.
|
183
|
+
# * No argument uses an Integer count of 1.
|
184
|
+
# * :first yields the first row of the result, regardless of the index.
|
185
|
+
# * :last yields the last row of the result, regardless of the index.
|
186
|
+
# * :all yields the whole set of rows, regardless of the index.
|
187
|
+
# * :rest yields all the items that have not been fetched, determined by the index.
|
188
|
+
# * :first and :last return nil if there are no results. All others will
|
189
|
+
# return an empty array.
|
190
|
+
#
|
191
|
+
# == The index
|
192
|
+
#
|
193
|
+
# I bet you're wondering what that is now, right? Well, the index is
|
194
|
+
# essentially a running row count that is altered by certain fetch
|
195
|
+
# operations. This makes sequential fetches much simpler.
|
196
|
+
#
|
197
|
+
# The index is largely implemented by RDBI::Cursor (and Database Driver
|
198
|
+
# subclasses)
|
199
|
+
#
|
200
|
+
# Items that do not use the index do not affect it.
|
201
|
+
#
|
202
|
+
# Result Drivers will always rewind the index, as this implicates a "point of
|
203
|
+
# no return" state change. You may always return to the original driver you
|
204
|
+
# were using, but the index position will be lost.
|
205
|
+
#
|
206
|
+
# The default result driver is RDBI::Result::Driver::Array.
|
207
|
+
#
|
208
|
+
def fetch(row_count=1, driver_klass=nil, *args)
|
209
|
+
if driver_klass
|
210
|
+
as(driver_klass, *args)
|
211
|
+
end
|
212
|
+
|
213
|
+
@fetch_handle.fetch(row_count)
|
214
|
+
end
|
215
|
+
|
216
|
+
alias read fetch
|
217
|
+
|
218
|
+
#
|
219
|
+
# raw_fetch is a straight array fetch without driver interaction. If you
|
220
|
+
# think you need this, please still read the fetch documentation as there is
|
221
|
+
# a considerable amount of overlap.
|
222
|
+
#
|
223
|
+
# This is generally used by Result Drivers to transform results.
|
224
|
+
#
|
225
|
+
def raw_fetch(row_count)
|
226
|
+
final_res = case row_count
|
227
|
+
when :all
|
228
|
+
@data.all
|
229
|
+
when :rest
|
230
|
+
@data.rest
|
231
|
+
when :first
|
232
|
+
[@data.first]
|
233
|
+
when :last
|
234
|
+
[@data.last]
|
235
|
+
else
|
236
|
+
@data.fetch(row_count)
|
237
|
+
end
|
238
|
+
RDBI::Util.deep_copy(final_res)
|
239
|
+
end
|
240
|
+
|
241
|
+
#
|
242
|
+
# This call finishes the result and the RDBI::Statement handle, scheduling
|
243
|
+
# any unpreserved data for garbage collection.
|
244
|
+
#
|
245
|
+
def finish
|
246
|
+
@sth.finish
|
247
|
+
@data.finish
|
248
|
+
@data = nil
|
249
|
+
@sth = nil
|
250
|
+
@driver = nil
|
251
|
+
@binds = nil
|
252
|
+
@schema = nil
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
#
|
257
|
+
# A result driver is a transformative element for RDBI::Result. Its design
|
258
|
+
# could be loosely described as a "fancy decorator".
|
259
|
+
#
|
260
|
+
# Usage and purpose is covered in the main RDBI::Result documentation. This
|
261
|
+
# section will largely serve the purpose of helping those who wish to implement
|
262
|
+
# result drivers themselves.
|
263
|
+
#
|
264
|
+
# == Creating a Result Driver
|
265
|
+
#
|
266
|
+
# A result driver typically inherits from RDBI::Result::Driver and implements
|
267
|
+
# at least one method: +fetch+.
|
268
|
+
#
|
269
|
+
# This fetch is not RDBI::Result#fetch, and doesn't have the same call
|
270
|
+
# semantics. Instead, it takes a single argument, the +row_count+, and
|
271
|
+
# typically passes that to RDBI::Result#raw_fetch to get results to process. It
|
272
|
+
# then returns the data transformed.
|
273
|
+
#
|
274
|
+
# RDBI::Result::Driver additionally provides two methods, convert_row and
|
275
|
+
# convert_item, which leverage RDBI's type conversion facility (see RDBI::Type)
|
276
|
+
# to assist in type conversion. For performance reasons, RDBI chooses to
|
277
|
+
# convert on request instead of preemptively, so <b>it is the driver implementor's
|
278
|
+
# job to do any conversion</b>.
|
279
|
+
#
|
280
|
+
# If you wish to implement a constructor in your class, please see
|
281
|
+
# RDBI::Result::Driver.new.
|
282
|
+
#
|
283
|
+
class RDBI::Result::Driver
|
284
|
+
|
285
|
+
#
|
286
|
+
# Result driver constructor. This is the logic that associates the result
|
287
|
+
# driver for decoration over the result; if you wish to override this method,
|
288
|
+
# please call +super+ before performing your own operations.
|
289
|
+
#
|
290
|
+
def initialize(result, *args)
|
291
|
+
@result = result
|
292
|
+
@result.rewind
|
293
|
+
end
|
294
|
+
|
295
|
+
#
|
296
|
+
# Fetch the result with any transformations. The default is to present the
|
297
|
+
# type converted array.
|
298
|
+
#
|
299
|
+
def fetch(row_count)
|
300
|
+
ary = (@result.raw_fetch(row_count) || []).enum_for.with_index.map do |item, i|
|
301
|
+
convert_row(item)
|
302
|
+
end
|
303
|
+
|
304
|
+
RDBI::Util.format_results(row_count, ary)
|
305
|
+
end
|
306
|
+
|
307
|
+
# convert an entire row of data with the specified result map (see
|
308
|
+
# RDBI::Type)
|
309
|
+
def convert_row(row)
|
310
|
+
newrow = []
|
311
|
+
(row || []).each_with_index do |x, i|
|
312
|
+
newrow.push(convert_item(x, @result.schema.columns[i]))
|
313
|
+
end
|
314
|
+
return newrow
|
315
|
+
end
|
316
|
+
|
317
|
+
# convert a single item (row element) with the specified result map.
|
318
|
+
def convert_item(item, column)
|
319
|
+
RDBI::Type::Out.convert(item, column, @result.type_hash)
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
#
|
324
|
+
# This is the standard Array driver. If you are familiar with the typical
|
325
|
+
# results of a database layer similar to RDBI, these results should be very
|
326
|
+
# familiar.
|
327
|
+
#
|
328
|
+
# If you wish for named accessors, please see RDBI::Result::Driver::Struct.
|
329
|
+
#
|
330
|
+
class RDBI::Result::Driver::Array < RDBI::Result::Driver
|
331
|
+
end
|
332
|
+
|
333
|
+
#
|
334
|
+
# This driver yields CSV:
|
335
|
+
#
|
336
|
+
# dbh.execute("select foo, bar from my_table").fetch(:first, :CSV)
|
337
|
+
#
|
338
|
+
# Yields the contents of columns foo and bar in CSV format (a String).
|
339
|
+
#
|
340
|
+
# The +fastercsv+ gem on 1.8 is used, which is the canonical +csv+ library on
|
341
|
+
# 1.9. If you are using Ruby 1.8 and do not have this gem available and try to
|
342
|
+
# use this driver, the code will abort during driver construction.
|
343
|
+
#
|
344
|
+
class RDBI::Result::Driver::CSV < RDBI::Result::Driver
|
345
|
+
def initialize(result, *args)
|
346
|
+
super
|
347
|
+
if RUBY_VERSION =~ /^1.8/
|
348
|
+
RDBI::Util.optional_require('fastercsv')
|
349
|
+
else
|
350
|
+
require 'csv'
|
351
|
+
end
|
352
|
+
# FIXME columns from schema deal maybe?
|
353
|
+
end
|
354
|
+
|
355
|
+
def fetch(row_count)
|
356
|
+
csv_string = ""
|
357
|
+
@result.raw_fetch(row_count).each do |row|
|
358
|
+
csv_string << row.to_csv
|
359
|
+
end
|
360
|
+
return csv_string
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
#
|
365
|
+
# Yields Struct objects instead of arrays for the rows. What this means is that
|
366
|
+
# you will recieve a single array of Structs, each struct representing a row of
|
367
|
+
# the database.
|
368
|
+
#
|
369
|
+
# example:
|
370
|
+
#
|
371
|
+
# results = dbh.execute("select foo, bar from my_table").fetch(:all, :Struct)
|
372
|
+
#
|
373
|
+
# results[0].foo # first row, foo column
|
374
|
+
# results[10].bar # 11th row, bar column
|
375
|
+
#
|
376
|
+
class RDBI::Result::Driver::Struct < RDBI::Result::Driver
|
377
|
+
def initialize(result, *args)
|
378
|
+
super
|
379
|
+
end
|
380
|
+
|
381
|
+
def fetch(row_count)
|
382
|
+
column_names = @result.schema.columns.map(&:name)
|
383
|
+
|
384
|
+
klass = ::Struct.new(*column_names)
|
385
|
+
|
386
|
+
structs = super
|
387
|
+
|
388
|
+
if [:first, :last].include?(row_count)
|
389
|
+
if structs
|
390
|
+
return klass.new(*structs)
|
391
|
+
else
|
392
|
+
return structs
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
structs.collect! { |row| klass.new(*row) }
|
397
|
+
|
398
|
+
return RDBI::Util.format_results(row_count, structs)
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
# vim: syntax=ruby ts=2 et sw=2 sts=2
|