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.
@@ -0,0 +1,189 @@
1
+ require 'epoxy'
2
+ require 'methlab'
3
+ require 'thread'
4
+
5
+ module RDBI
6
+ class << self
7
+ extend MethLab
8
+
9
+ # Every database handle allocated throughout the lifetime of the
10
+ # program. This functionality is subject to change and may be pruned
11
+ # during disconnection.
12
+ attr_reader :all_connections
13
+
14
+ #
15
+ # The last database handle allocated. This may come from pooled connections or regular ones.
16
+ #
17
+ attr_threaded_accessor :last_dbh
18
+ end
19
+
20
+ #
21
+ # connect() takes a class name, which may be represented as:
22
+ #
23
+ # * The full class name, such as RDBI::Driver::Mock
24
+ # * A symbol representing the significant portion, such as :Mock, which corresponds to RDBI::Driver::Mock
25
+ # * A string representing the same data as the symbol.
26
+ #
27
+ # Additionally, arguments that are passed on to the driver for consumption
28
+ # may be passed. Please refer to the driver documentation for more
29
+ # information.
30
+ #
31
+ # connect() returns an instance of RDBI::Database. In the instance a block
32
+ # is provided, it will be called upon connection success, with the
33
+ # RDBI::Database object provided in as the first argument.
34
+ def self.connect(klass, *args)
35
+
36
+ klass = RDBI::Util.class_from_class_or_symbol(klass, self::Driver)
37
+
38
+ driver = klass.new(*args)
39
+ dbh = self.last_dbh = driver.new_handle
40
+
41
+ @all_connections ||= []
42
+ @all_connections.push(dbh)
43
+
44
+ yield dbh if block_given?
45
+ return dbh
46
+ end
47
+
48
+ #
49
+ # connect_cached() works similarly to connect, but yields a database handle
50
+ # copied from a RDBI::Pool. The 'default' pool is the ... default, but this
51
+ # may be manipulated by providing :pool_name to the connection arguments.
52
+ #
53
+ # If a pool does not exist already, it will be created and a database
54
+ # handle instanced from your connection arguments.
55
+ #
56
+ # If a pool *already* exists, your connection arguments will be ignored and
57
+ # it will instance from the Pool's connection arguments.
58
+ def self.connect_cached(klass, *args)
59
+ args = args[0]
60
+ pool_name = args[:pool_name] || :default
61
+
62
+ dbh = nil
63
+
64
+ if RDBI::Pool[pool_name]
65
+ dbh = RDBI::Pool[pool_name].get_dbh
66
+ else
67
+ dbh = RDBI::Pool.new(pool_name, [klass, args]).get_dbh
68
+ end
69
+
70
+ self.last_dbh = dbh
71
+
72
+ yield dbh if block_given?
73
+ return dbh
74
+ end
75
+
76
+ #
77
+ # Retrieves a RDBI::Pool. See RDBI::Pool.[].
78
+ def self.pool(pool_name=:default)
79
+ RDBI::Pool[pool_name]
80
+ end
81
+
82
+ #
83
+ # Connects to and pings the database. Arguments are the same as for RDBI.connect.
84
+ def self.ping(klass, *args)
85
+ connect(klass, *args).ping
86
+ end
87
+
88
+ #
89
+ # Reconnects all known connections. See RDBI.all_connections.
90
+ def self.reconnect_all
91
+ @all_connections.each(&:reconnect)
92
+ end
93
+
94
+ #
95
+ # Disconnects all known connections. See RDBI.all_connections.
96
+ def self.disconnect_all
97
+ @all_connections.each(&:disconnect)
98
+ end
99
+
100
+ #
101
+ # Base Error class for RDBI. Rescue this to catch all RDBI-specific errors.
102
+ #
103
+ class Error < StandardError
104
+ end
105
+
106
+ #
107
+ # This error will be thrown if an operation is attempted while the database
108
+ # is disconnected.
109
+ #
110
+ class DisconnectedError < Error
111
+ end
112
+
113
+ #
114
+ # This error will be thrown if an operation is attempted that violated
115
+ # transaction semantics.
116
+ #
117
+ class TransactionError < Error
118
+ end
119
+ end
120
+
121
+ #
122
+ # RDBI::Util is a set of utility methods used internally. It is not geared for
123
+ # public consumption.
124
+ #
125
+ module RDBI::Util
126
+ #
127
+ # Requires with a LoadError check and emits a friendly "please install me"
128
+ # message.
129
+ #
130
+ def self.optional_require(lib)
131
+ require lib
132
+ rescue LoadError => e
133
+ raise LoadError, "The '#{lib}' gem is required to use this driver. Please install it."
134
+ end
135
+
136
+ #
137
+ # This is the loading logic we use to import drivers of various natures.
138
+ #
139
+ def self.class_from_class_or_symbol(klass, namespace)
140
+ klass.kind_of?(Class) ? klass : namespace.const_get(klass.to_s)
141
+ rescue
142
+ raise ArgumentError, "Invalid argument for driver name; must be Class, or a Symbol or String identifying the Class, and the driver Class must have been loaded"
143
+ end
144
+
145
+ #
146
+ # Rekey a string-keyed hash with equivalent symbols.
147
+ #
148
+ def self.key_hash_as_symbols(hash)
149
+ return nil unless hash
150
+
151
+ Hash[hash.map { |k,v| [k.to_sym, v] }]
152
+ end
153
+
154
+ #
155
+ # Copy an object and all of its descendants to form a new tree
156
+ #
157
+ def self.deep_copy(obj)
158
+ Marshal.load(Marshal.dump(obj))
159
+ end
160
+
161
+ #
162
+ # Takes an array and appropriate boxes/deboxes it based on what was
163
+ # requested.
164
+ #
165
+ #--
166
+ # FIXME this is a really poorly performing way of doing this.
167
+ #++
168
+ def self.format_results(row_count, ary)
169
+ case row_count
170
+ when :first, :last
171
+ ary = ary[0]
172
+ return nil if ary.empty?
173
+ return ary
174
+ else
175
+ return ary
176
+ end
177
+ end
178
+ end
179
+
180
+ require 'rdbi/pool'
181
+ require 'rdbi/driver'
182
+ require 'rdbi/database'
183
+ require 'rdbi/statement'
184
+ require 'rdbi/schema'
185
+ require 'rdbi/result'
186
+ require 'rdbi/cursor'
187
+ require 'rdbi/types'
188
+
189
+ # vim: syntax=ruby ts=2 et sw=2 sts=2
@@ -0,0 +1,90 @@
1
+ #
2
+ # RDBI::Cursor is a method of abstractly encapsulating result handles that we
3
+ # get back from databases. It has a consistent interface and therefore can be
4
+ # used by RDBI::Result and its drivers.
5
+ #
6
+ # Drivers should make a whole-hearted attempt to do iterative fetching instead
7
+ # of array fetching.. this will perform much better for larger results.
8
+ #
9
+ # RDBI::Cursor is largely an abstract class and will error if methods are not
10
+ # implemented in an inheriting class. Please read the individual method
11
+ # documentation for what each call should yield.
12
+ #
13
+ class RDBI::Cursor
14
+ #
15
+ # Exception which indicates that the inheriting class has not implemented
16
+ # these interface calls.
17
+ #
18
+ class NotImplementedError < Exception; end
19
+
20
+ extend MethLab
21
+ include Enumerable
22
+
23
+ # underlying handle.
24
+
25
+ attr_reader :handle
26
+
27
+ # Default constructor. Feel free to override this.
28
+ #
29
+ def initialize(handle)
30
+ @handle = handle
31
+ end
32
+
33
+
34
+ # Returns the next row in the result.
35
+ def next_row; raise NotImplementedError, 'Subclasses must implement this method'; end
36
+
37
+ # Returns the count of rows that exist in this result.
38
+ def result_count; raise NotImplementedError, 'Subclasses must implement this method'; end
39
+
40
+ # Returns the number of affected rows (DML) in this result.
41
+ def affected_count; raise NotImplementedError, 'Subclasses must implement this method'; end
42
+
43
+ # Returns the first tuple in the result.
44
+ def first; raise NotImplementedError, 'Subclasses must implement this method'; end
45
+
46
+ # Returns the last tuple in the result.
47
+ def last; raise NotImplementedError, 'Subclasses must implement this method'; end
48
+
49
+ # Returns the items that have not been fetched yet in this result. Equivalent
50
+ # to all() if the fetched count is zero.
51
+ def rest; raise NotImplementedError, 'Subclasses must implement this method'; end
52
+
53
+ # Returns all the tuples.
54
+ def all; raise NotImplementedError, 'Subclasses must implement this method'; end
55
+
56
+ # Fetches +count+ tuples from the result and returns them.
57
+ def fetch(count=1); raise NotImplementedError, 'Subclasses must implement this method'; end
58
+
59
+ # Fetches the tuple at position +index+.
60
+ def [](index); raise NotImplementedError, 'Subclasses must implement this method'; end
61
+
62
+ # Are we on the last row?
63
+ def last_row?; raise NotImplementedError, 'Subclasses must implement this method'; end
64
+
65
+ # Is this result empty?
66
+ def empty?; raise NotImplementedError, 'Subclasses must implement this method'; end
67
+
68
+ # rewind the result to start again from the top.
69
+ def rewind; raise NotImplementedError, 'Subclasses must implement this method'; end
70
+
71
+ # See result_count().
72
+ def size
73
+ result_count
74
+ end
75
+
76
+ # Finish this cursor and schedule it for termination.
77
+ def finish
78
+ end
79
+
80
+ # If your result handles cannot support operation disconnected from the
81
+ # statement, you will want to implement this method to fetch all values in
82
+ # certain situations.
83
+ def coerce_to_array
84
+ end
85
+
86
+ # Enumerable helper. Iterate over each item and yield it to a block.
87
+ def each
88
+ yield next_row until last_row?
89
+ end
90
+ end
@@ -0,0 +1,262 @@
1
+ #
2
+ # RDBI::Database is the base class for database handles. This is the primary
3
+ # method in which most users will access their database system.
4
+ #
5
+ # To execute statements, look at +prepare+ and +execute+.
6
+ #
7
+ # To retrieve schema information, look at +schema+ and +table_schema+.
8
+ #
9
+ # To deal with transactions, +transaction+, +commit+, and +rollback+.
10
+ class RDBI::Database
11
+ extend MethLab
12
+
13
+ # the driver class that is responsible for creating this database handle.
14
+ attr_accessor :driver
15
+
16
+ # the name of the database we're connected to, if any.
17
+ attr_accessor :database_name
18
+
19
+ # the arguments used to create the connection.
20
+ attr_reader :connect_args
21
+
22
+ ##
23
+ # :attr_reader: last_statement
24
+ #
25
+ # the last statement handle allocated. affected by +prepare+ and +execute+.
26
+ attr_threaded_accessor :last_statement
27
+
28
+ ##
29
+ # :attr: last_query
30
+ # the last query sent, as a string.
31
+ attr_threaded_accessor :last_query
32
+
33
+ ##
34
+ # :attr: open_statements
35
+ # all the open statement handles.
36
+ attr_threaded_accessor :open_statements
37
+
38
+ ##
39
+ # :attr: in_transaction
40
+ # are we currently in a transaction?
41
+
42
+ ##
43
+ # :attr: in_transaction?
44
+ # are we currently in a transaction?
45
+ inline(:in_transaction, :in_transaction?) { @in_transaction > 0 }
46
+
47
+ # the mutex for this database handle.
48
+ attr_reader :mutex
49
+
50
+ ##
51
+ # :attr: connected
52
+ # are we connected to the database?
53
+
54
+ ##
55
+ # :attr_accessor: connected?
56
+ # are we connected to the database?
57
+ inline(:connected, :connected?) { @connected }
58
+
59
+ ##
60
+ # :method: ping
61
+ # ping the database. yield an integer result on success.
62
+ inline(:ping) { raise NoMethodError, "this method is not implemented in this driver" }
63
+
64
+ ##
65
+ # :method: table_schema
66
+ # query the schema for a specific table. Returns a RDBI::Schema object.
67
+ inline(:table_schema) { |*args| raise NoMethodError, "this method is not implemented in this driver" }
68
+
69
+ ##
70
+ # :method: schema
71
+ # query the schema for the entire database. Returns an array of RDBI::Schema objects.
72
+ inline(:schema) { |*args| raise NoMethodError, "this method is not implemented in this driver" }
73
+
74
+ ##
75
+ # :method: rollback
76
+ # ends the outstanding transaction and rolls the affected rows back.
77
+ inline(:rollback) { @in_transaction -= 1 unless @in_transaction == 0 }
78
+
79
+ ##
80
+ # :method: commit
81
+ # ends the outstanding transaction and commits the result.
82
+ inline(:commit) { @in_transaction -= 1 unless @in_transaction == 0 }
83
+
84
+ #
85
+ # Create a new database handle. This is typically done by a driver and
86
+ # likely shouldn't be done directly.
87
+ #
88
+ # args is the connection arguments the user initially supplied to
89
+ # RDBI.connect.
90
+ def initialize(*args)
91
+ @connect_args = RDBI::Util.key_hash_as_symbols(args[0])
92
+ @connected = true
93
+ @mutex = Mutex.new
94
+ @in_transaction = 0
95
+ self.open_statements = []
96
+ end
97
+
98
+ # reconnect to the database. Any outstanding connection will be terminated.
99
+ def reconnect
100
+ disconnect rescue nil
101
+ @connected = true
102
+ end
103
+
104
+ #
105
+ # disconnects from the database: will close (and complain, loudly) any
106
+ # statement handles left open.
107
+ #
108
+ def disconnect
109
+ unless self.open_statements.empty?
110
+ warn "[RDBI] Open statements during disconnection -- automatically finishing. You should fix this."
111
+ self.open_statements.each(&:finish)
112
+ end
113
+ self.open_statements = []
114
+ @connected = false
115
+ end
116
+
117
+ #
118
+ # Open a new transaction for processing. Accepts a block which will execute
119
+ # the portions during the transaction.
120
+ #
121
+ # Example:
122
+ #
123
+ # dbh.transaction do |dbh|
124
+ # dbh.execute("some query")
125
+ # dbh.execute("some other query")
126
+ # raise "oh crap!" # would rollback
127
+ # dbh.commit # commits
128
+ # dbh.rollback # rolls back
129
+ # end
130
+ #
131
+ # # at this point, if no raise or commit/rollback was triggered, it would
132
+ # # commit.
133
+ #
134
+ # Any exception that isn't caught within this block will trigger a
135
+ # rollback. Additionally, you may use +commit+ and +rollback+ directly
136
+ # within the block to terminate the transaction early -- at which point
137
+ # *the transaction is over with and you may be in autocommit*. The
138
+ # RDBI::Database accessor +in_transaction+ exists to tell you if RDBI
139
+ # thinks its in a transaction or not.
140
+ #
141
+ # If you do not +commit+ or +rollback+ within the block and no exception is
142
+ # raised, RDBI presumes you wish this transaction to succeed and commits
143
+ # for you.
144
+ #
145
+ def transaction(&block)
146
+ @in_transaction += 1
147
+ begin
148
+ yield self
149
+ self.commit if @in_transaction > 0
150
+ rescue => e
151
+ self.rollback
152
+ raise e
153
+ ensure
154
+ @in_transaction -= 1 unless @in_transaction == 0
155
+ end
156
+ end
157
+
158
+ #
159
+ # Prepares a statement for execution. Takes a query as its only argument,
160
+ # returns a RDBI::Statement.
161
+ #
162
+ # ex:
163
+ # sth = dbh.prepare("select * from foo where item = ?")
164
+ # res = sth.execute("an item")
165
+ # ary = res.to_a
166
+ # sth.finish
167
+ #
168
+ # You can also use a block form which will auto-finish:
169
+ # dbh.prepare("select * from foo where item = ?") do |sth|
170
+ # sth.execute("an item")
171
+ # end
172
+ #
173
+ def prepare(query)
174
+ sth = nil
175
+ mutex.synchronize do
176
+ self.last_query = query
177
+ sth = new_statement(query)
178
+ yield sth if block_given?
179
+ sth.finish if block_given?
180
+ end
181
+
182
+ return self.last_statement = sth
183
+ end
184
+
185
+ #
186
+ # Prepares and executes a statement. Takes a string query and an optional
187
+ # number of variable type binds.
188
+ #
189
+ # ex:
190
+ # res = dbh.execute("select * from foo where item = ?", "an item")
191
+ # ary = res.to_a
192
+ #
193
+ # You can also use a block form which will finish the statement and yield the
194
+ # result handle:
195
+ # dbh.execute("select * from foo where item = ?", "an item") do |res|
196
+ # res.as(:Struct).fetch(:all).each do |struct|
197
+ # p struct.item
198
+ # end
199
+ # end
200
+ #
201
+ # Which will be considerably more performant under some database drivers.
202
+ #
203
+ def execute(query, *binds)
204
+ res = nil
205
+
206
+ mutex.synchronize do
207
+ self.last_query = query
208
+ self.last_statement = sth = new_statement(query)
209
+ res = sth.execute(*binds)
210
+
211
+ if block_given?
212
+ yield res
213
+ else
214
+ res.coerce_to_array
215
+ end
216
+
217
+ sth.finish
218
+ end
219
+
220
+ return res
221
+ end
222
+
223
+ #
224
+ # Process the query as your driver would normally, and return the result.
225
+ # Depending on the driver implementation and potentially connection
226
+ # settings, this may include interpolated data or client binding
227
+ # placeholders.
228
+ #
229
+ # <b>Driver Authors</b>: if the instance variable @preprocess_quoter is set
230
+ # to a proc that accepts an index/key, a map of named binds and an array of
231
+ # indexed binds, it will be called instead of the default quoter and there is
232
+ # no need to override this method. For example:
233
+ #
234
+ # def initialize(...)
235
+ # @preprocess_quoter = proc do |x, named, indexed|
236
+ # @some_handle.quote((named[x] || indexed[x]).to_s)
237
+ # end
238
+ # end
239
+ #
240
+ # This will use RDBI's code to manage the binds before quoting, but use your
241
+ # quoter during bind processing.
242
+ #
243
+ def preprocess_query(query, *binds)
244
+ mutex.synchronize do
245
+ self.last_query = query
246
+ end
247
+
248
+ ep = Epoxy.new(query)
249
+
250
+ hashes = binds.select { |x| x.kind_of?(Hash) }
251
+ binds.collect! { |x| x.kind_of?(Hash) ? nil : x }
252
+ total_hash = hashes.inject({}) { |x, y| x.merge(y) }
253
+
254
+ if @preprocess_quoter.respond_to?(:call)
255
+ ep.quote(total_hash) { |x| @preprocess_quoter.call(x, total_hash, binds) }
256
+ else
257
+ ep.quote(total_hash) { |x| %Q{'#{(total_hash[x] || binds[x]).to_s.gsub(/'/, "''")}'} }
258
+ end
259
+ end
260
+ end
261
+
262
+ # vim: syntax=ruby ts=2 et sw=2 sts=2