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.rb
ADDED
@@ -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
|
data/lib/rdbi/cursor.rb
ADDED
@@ -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
|