sbf-data_objects 0.10.17
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/ChangeLog.markdown +115 -0
- data/LICENSE +20 -0
- data/README.markdown +18 -0
- data/Rakefile +20 -0
- data/lib/data_objects/byte_array.rb +6 -0
- data/lib/data_objects/command.rb +79 -0
- data/lib/data_objects/connection.rb +95 -0
- data/lib/data_objects/error/connection_error.rb +4 -0
- data/lib/data_objects/error/data_error.rb +4 -0
- data/lib/data_objects/error/integrity_error.rb +4 -0
- data/lib/data_objects/error/sql_error.rb +17 -0
- data/lib/data_objects/error/syntax_error.rb +4 -0
- data/lib/data_objects/error/transaction_error.rb +4 -0
- data/lib/data_objects/error.rb +4 -0
- data/lib/data_objects/extension.rb +9 -0
- data/lib/data_objects/logger.rb +247 -0
- data/lib/data_objects/pooling.rb +243 -0
- data/lib/data_objects/quoting.rb +99 -0
- data/lib/data_objects/reader.rb +45 -0
- data/lib/data_objects/result.rb +21 -0
- data/lib/data_objects/spec/lib/pending_helpers.rb +13 -0
- data/lib/data_objects/spec/lib/ssl.rb +19 -0
- data/lib/data_objects/spec/setup.rb +5 -0
- data/lib/data_objects/spec/shared/command_spec.rb +201 -0
- data/lib/data_objects/spec/shared/connection_spec.rb +148 -0
- data/lib/data_objects/spec/shared/encoding_spec.rb +161 -0
- data/lib/data_objects/spec/shared/error/sql_error_spec.rb +23 -0
- data/lib/data_objects/spec/shared/quoting_spec.rb +0 -0
- data/lib/data_objects/spec/shared/reader_spec.rb +180 -0
- data/lib/data_objects/spec/shared/result_spec.rb +67 -0
- data/lib/data_objects/spec/shared/typecast/array_spec.rb +29 -0
- data/lib/data_objects/spec/shared/typecast/bigdecimal_spec.rb +112 -0
- data/lib/data_objects/spec/shared/typecast/boolean_spec.rb +133 -0
- data/lib/data_objects/spec/shared/typecast/byte_array_spec.rb +76 -0
- data/lib/data_objects/spec/shared/typecast/class_spec.rb +53 -0
- data/lib/data_objects/spec/shared/typecast/date_spec.rb +114 -0
- data/lib/data_objects/spec/shared/typecast/datetime_spec.rb +140 -0
- data/lib/data_objects/spec/shared/typecast/float_spec.rb +115 -0
- data/lib/data_objects/spec/shared/typecast/integer_spec.rb +92 -0
- data/lib/data_objects/spec/shared/typecast/ipaddr_spec.rb +0 -0
- data/lib/data_objects/spec/shared/typecast/nil_spec.rb +107 -0
- data/lib/data_objects/spec/shared/typecast/other_spec.rb +41 -0
- data/lib/data_objects/spec/shared/typecast/range_spec.rb +29 -0
- data/lib/data_objects/spec/shared/typecast/string_spec.rb +130 -0
- data/lib/data_objects/spec/shared/typecast/time_spec.rb +111 -0
- data/lib/data_objects/transaction.rb +111 -0
- data/lib/data_objects/uri.rb +109 -0
- data/lib/data_objects/utilities.rb +18 -0
- data/lib/data_objects/version.rb +3 -0
- data/lib/data_objects.rb +20 -0
- data/spec/command_spec.rb +24 -0
- data/spec/connection_spec.rb +31 -0
- data/spec/do_mock.rb +29 -0
- data/spec/do_mock2.rb +29 -0
- data/spec/pooling_spec.rb +153 -0
- data/spec/reader_spec.rb +19 -0
- data/spec/result_spec.rb +21 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/transaction_spec.rb +37 -0
- data/spec/uri_spec.rb +23 -0
- data/tasks/release.rake +14 -0
- data/tasks/spec.rake +10 -0
- data/tasks/yard.rake +9 -0
- data/tasks/yardstick.rake +19 -0
- metadata +122 -0
@@ -0,0 +1,243 @@
|
|
1
|
+
module DataObjects
|
2
|
+
def self.exiting=(bool)
|
3
|
+
DataObjects::Pooling.scavenger&.wakeup if bool && DataObjects.const_defined?('Pooling') && DataObjects::Pooling.scavenger?
|
4
|
+
@exiting = true
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.exiting
|
8
|
+
return @exiting if defined?(@exiting)
|
9
|
+
|
10
|
+
@exiting = false
|
11
|
+
end
|
12
|
+
|
13
|
+
# ==== Notes
|
14
|
+
# Provides pooling support to class it got included in.
|
15
|
+
#
|
16
|
+
# Pooling of objects is a faster way of acquiring instances
|
17
|
+
# of objects compared to regular allocation and initialization
|
18
|
+
# because instances are kept in memory reused.
|
19
|
+
#
|
20
|
+
# Classes that include Pooling module have re-defined new
|
21
|
+
# method that returns instances acquired from pool.
|
22
|
+
#
|
23
|
+
# Term resource is used for any type of pool-able objects
|
24
|
+
# and should NOT be thought as DataMapper Resource or
|
25
|
+
# ActiveResource resource and such.
|
26
|
+
#
|
27
|
+
# In Data Objects connections are pooled so that it is
|
28
|
+
# unnecessary to allocate and initialize connection object
|
29
|
+
# each time connection is needed, like per request in a
|
30
|
+
# web application.
|
31
|
+
#
|
32
|
+
# Pool obviously has to be thread safe because state of
|
33
|
+
# object is reset when it is released.
|
34
|
+
module Pooling
|
35
|
+
def self.scavenger?
|
36
|
+
defined?(@scavenger) && !@scavenger.nil? && @scavenger.alive?
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.scavenger
|
40
|
+
unless scavenger?
|
41
|
+
@scavenger = Thread.new do
|
42
|
+
running = true
|
43
|
+
while running
|
44
|
+
# Sleep before we actually start doing anything.
|
45
|
+
# Otherwise we might clean up something we just made
|
46
|
+
sleep(scavenger_interval)
|
47
|
+
|
48
|
+
lock.synchronize do
|
49
|
+
pools.each do |pool|
|
50
|
+
# This is a useful check, but non-essential, and right now it breaks lots of stuff.
|
51
|
+
# if pool.expired?
|
52
|
+
pool.lock.synchronize do
|
53
|
+
pool.dispose if pool.expired?
|
54
|
+
end
|
55
|
+
# end
|
56
|
+
end
|
57
|
+
|
58
|
+
# The pool is empty, we stop the scavenger
|
59
|
+
# It wil be restarted if new resources are added again
|
60
|
+
running = false if pools.empty?
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
@scavenger.priority = -10
|
67
|
+
@scavenger
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.pools
|
71
|
+
@pools ||= Set.new
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.append_pool(pool)
|
75
|
+
lock.synchronize do
|
76
|
+
pools << pool
|
77
|
+
end
|
78
|
+
DataObjects::Pooling.scavenger
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.lock
|
82
|
+
@lock ||= Mutex.new
|
83
|
+
end
|
84
|
+
|
85
|
+
class InvalidResourceError < StandardError
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.included(target)
|
89
|
+
lock.synchronize do
|
90
|
+
unless target.respond_to? :__pools
|
91
|
+
target.class_eval do
|
92
|
+
class << self
|
93
|
+
alias_method :__new, :new
|
94
|
+
end
|
95
|
+
|
96
|
+
@__pools = {}
|
97
|
+
@__pool_lock = Mutex.new
|
98
|
+
@__pool_wait = ConditionVariable.new
|
99
|
+
|
100
|
+
def self.__pool_lock
|
101
|
+
@__pool_lock
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.__pool_wait
|
105
|
+
@__pool_wait
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.new(*args)
|
109
|
+
(@__pools[args] ||= __pool_lock.synchronize { Pool.new(pool_size, self, args) }).new
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.__pools
|
113
|
+
@__pools
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.pool_size
|
117
|
+
8
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def release
|
125
|
+
@__pool&.release(self)
|
126
|
+
end
|
127
|
+
|
128
|
+
def detach
|
129
|
+
@__pool&.delete(self)
|
130
|
+
end
|
131
|
+
|
132
|
+
class Pool
|
133
|
+
attr_reader :available, :used
|
134
|
+
|
135
|
+
def initialize(max_size, resource, args)
|
136
|
+
raise ArgumentError, "+max_size+ should be an Integer but was #{max_size.inspect}" unless max_size.is_a?(Integer)
|
137
|
+
raise ArgumentError, "+resource+ should be a Class but was #{resource.inspect}" unless resource.is_a?(Class)
|
138
|
+
|
139
|
+
@max_size = max_size
|
140
|
+
@resource = resource
|
141
|
+
@args = args
|
142
|
+
|
143
|
+
@available = []
|
144
|
+
@used = {}
|
145
|
+
DataObjects::Pooling.append_pool(self)
|
146
|
+
end
|
147
|
+
|
148
|
+
def lock
|
149
|
+
@resource.__pool_lock
|
150
|
+
end
|
151
|
+
|
152
|
+
def wait
|
153
|
+
@resource.__pool_wait
|
154
|
+
end
|
155
|
+
|
156
|
+
def scavenge_interval
|
157
|
+
@resource.scavenge_interval
|
158
|
+
end
|
159
|
+
|
160
|
+
def new
|
161
|
+
instance = nil
|
162
|
+
loop do
|
163
|
+
lock.synchronize do
|
164
|
+
if @available.size.positive?
|
165
|
+
instance = @available.pop
|
166
|
+
@used[instance.object_id] = instance
|
167
|
+
elsif @used.size < @max_size
|
168
|
+
instance = @resource.__new(*@args)
|
169
|
+
raise InvalidResourceError, "#{@resource} constructor created a nil object" if instance.nil?
|
170
|
+
raise InvalidResourceError, "#{instance} is already part of the pool" if @used.include? instance
|
171
|
+
|
172
|
+
instance.instance_variable_set(:@__pool, self)
|
173
|
+
instance.instance_variable_set(:@__allocated_in_pool, Time.now)
|
174
|
+
@used[instance.object_id] = instance
|
175
|
+
else
|
176
|
+
# Wait for another thread to release an instance.
|
177
|
+
# If we exhaust the pool and don't release the active instance,
|
178
|
+
# we'll wait here forever, so it's *very* important to always
|
179
|
+
# release your services and *never* exhaust the pool within
|
180
|
+
# a single thread.
|
181
|
+
wait.wait(lock)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
break if instance
|
185
|
+
end
|
186
|
+
instance
|
187
|
+
end
|
188
|
+
|
189
|
+
def release(instance)
|
190
|
+
lock.synchronize do
|
191
|
+
instance.instance_variable_set(:@__allocated_in_pool, Time.now)
|
192
|
+
@used.delete(instance.object_id)
|
193
|
+
@available.push(instance) unless @available.include?(instance)
|
194
|
+
wait.signal
|
195
|
+
end
|
196
|
+
nil
|
197
|
+
end
|
198
|
+
|
199
|
+
def delete(instance)
|
200
|
+
lock.synchronize do
|
201
|
+
instance.instance_variable_set(:@__pool, nil)
|
202
|
+
@used.delete(instance.object_id)
|
203
|
+
wait.signal
|
204
|
+
end
|
205
|
+
nil
|
206
|
+
end
|
207
|
+
|
208
|
+
def size
|
209
|
+
@used.size + @available.size
|
210
|
+
end
|
211
|
+
alias length size
|
212
|
+
|
213
|
+
def inspect
|
214
|
+
"#<DataObjects::Pooling::Pool<#{@resource.name}> available=#{@available.size} used=#{@used.size} size=#{@max_size}>"
|
215
|
+
end
|
216
|
+
|
217
|
+
def flush!
|
218
|
+
@available.pop.dispose until @available.empty?
|
219
|
+
end
|
220
|
+
|
221
|
+
def dispose
|
222
|
+
flush!
|
223
|
+
@resource.__pools.delete(@args)
|
224
|
+
!DataObjects::Pooling.pools.delete?(self).nil?
|
225
|
+
end
|
226
|
+
|
227
|
+
def expired?
|
228
|
+
@available.each do |instance|
|
229
|
+
next unless DataObjects.exiting ||
|
230
|
+
instance.instance_variable_get(:@__allocated_in_pool) + DataObjects::Pooling.scavenger_interval <= (Time.now + 0.02)
|
231
|
+
|
232
|
+
instance.dispose
|
233
|
+
@available.delete(instance)
|
234
|
+
end
|
235
|
+
size.zero?
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def self.scavenger_interval
|
240
|
+
60
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module DataObjects
|
2
|
+
module Quoting
|
3
|
+
# Quote a value of any of the recognised data types
|
4
|
+
def quote_value(value)
|
5
|
+
return 'NULL' if value.nil?
|
6
|
+
|
7
|
+
case value
|
8
|
+
when Numeric then quote_numeric(value)
|
9
|
+
when ::Extlib::ByteArray then quote_byte_array(value)
|
10
|
+
when String then quote_string(value)
|
11
|
+
when Time then quote_time(value)
|
12
|
+
when DateTime then quote_datetime(value)
|
13
|
+
when Date then quote_date(value)
|
14
|
+
when TrueClass, FalseClass then quote_boolean(value)
|
15
|
+
when Array then quote_array(value)
|
16
|
+
when Range then quote_range(value)
|
17
|
+
when Symbol then quote_symbol(value)
|
18
|
+
when Regexp then quote_regexp(value)
|
19
|
+
when Class then quote_class(value)
|
20
|
+
else
|
21
|
+
raise "Don't know how to quote #{value.class} objects (#{value.inspect})" unless value.respond_to?(:to_sql)
|
22
|
+
|
23
|
+
value.to_sql
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Convert the Symbol to a String and quote that
|
29
|
+
def quote_symbol(value)
|
30
|
+
quote_string(value.to_s)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Convert the Numeric to a String and quote that
|
34
|
+
def quote_numeric(value)
|
35
|
+
value.to_s
|
36
|
+
end
|
37
|
+
|
38
|
+
# Quote a String for SQL by doubling any embedded single-quote characters
|
39
|
+
def quote_string(value)
|
40
|
+
"'#{value.gsub("'", "''")}'"
|
41
|
+
end
|
42
|
+
|
43
|
+
# Quote a class by quoting its name
|
44
|
+
def quote_class(value)
|
45
|
+
quote_string(value.name)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Convert a Time to standard YMDHMS format (with microseconds if necessary)
|
49
|
+
def quote_time(value)
|
50
|
+
offset = value.utc_offset
|
51
|
+
if offset >= 0
|
52
|
+
offset_string = "+#{format('%02d', offset / 3600)}:#{format('%02d', (offset % 3600) / 60)}"
|
53
|
+
elsif offset < 0
|
54
|
+
offset_string = "-#{format('%02d', -offset / 3600)}:#{format('%02d', (-offset % 3600) / 60)}"
|
55
|
+
end
|
56
|
+
"'#{value.strftime('%Y-%m-%dT%H:%M:%S')}" << (if value.usec > 0
|
57
|
+
".#{value.usec.to_s.rjust(6,
|
58
|
+
'0')}"
|
59
|
+
else
|
60
|
+
''
|
61
|
+
end) << offset_string << "'"
|
62
|
+
end
|
63
|
+
|
64
|
+
# Quote a DateTime by relying on it's own to_s conversion
|
65
|
+
def quote_datetime(value)
|
66
|
+
"'#{value.dup}'"
|
67
|
+
end
|
68
|
+
|
69
|
+
# Convert a Date to standard YMD format
|
70
|
+
def quote_date(value)
|
71
|
+
"'#{value.strftime('%Y-%m-%d')}'"
|
72
|
+
end
|
73
|
+
|
74
|
+
# Quote true, false as the strings TRUE, FALSE
|
75
|
+
def quote_boolean(value)
|
76
|
+
value.to_s.upcase
|
77
|
+
end
|
78
|
+
|
79
|
+
# Quote an array as a list of quoted values
|
80
|
+
def quote_array(value)
|
81
|
+
"(#{value.map { |entry| quote_value(entry) }.join(', ')})"
|
82
|
+
end
|
83
|
+
|
84
|
+
# Quote a range by joining the quoted end-point values with AND.
|
85
|
+
# It's not clear whether or when this is a useful or correct thing to do.
|
86
|
+
def quote_range(value)
|
87
|
+
"#{quote_value(value.first)} AND #{quote_value(value.last)}"
|
88
|
+
end
|
89
|
+
|
90
|
+
# Quote a Regex using its string value. Note that there's no attempt to make a valid SQL "LIKE" string.
|
91
|
+
def quote_regexp(value)
|
92
|
+
quote_string(value.source)
|
93
|
+
end
|
94
|
+
|
95
|
+
def quote_byte_array(value)
|
96
|
+
quote_string(value)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module DataObjects
|
2
|
+
# Abstract class to read rows from a query result
|
3
|
+
class Reader
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
# Return the array of field names
|
7
|
+
def fields
|
8
|
+
raise NotImplementedError
|
9
|
+
end
|
10
|
+
|
11
|
+
# Return the array of field values for the current row. Not legal after next! has returned false or before it's been called
|
12
|
+
def values
|
13
|
+
raise NotImplementedError
|
14
|
+
end
|
15
|
+
|
16
|
+
# Close the reader discarding any unread results.
|
17
|
+
def close
|
18
|
+
raise NotImplementedError
|
19
|
+
end
|
20
|
+
|
21
|
+
# Discard the current row (if any) and read the next one (returning true), or return nil if there is no further row.
|
22
|
+
def next!
|
23
|
+
raise NotImplementedError
|
24
|
+
end
|
25
|
+
|
26
|
+
# Return the number of fields in the result set.
|
27
|
+
def field_count
|
28
|
+
raise NotImplementedError
|
29
|
+
end
|
30
|
+
|
31
|
+
# Yield each row to the given block as a Hash
|
32
|
+
def each
|
33
|
+
begin
|
34
|
+
while next!
|
35
|
+
row = {}
|
36
|
+
fields.each_with_index { |field, index| row[field] = values[index] }
|
37
|
+
yield row
|
38
|
+
end
|
39
|
+
ensure
|
40
|
+
close
|
41
|
+
end
|
42
|
+
self
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module DataObjects
|
2
|
+
# The Result class is returned from Connection#execute_non_query.
|
3
|
+
class Result
|
4
|
+
# The ID of a row inserted by the Command
|
5
|
+
attr_accessor :insert_id
|
6
|
+
# The number of rows affected by the Command
|
7
|
+
attr_accessor :affected_rows
|
8
|
+
|
9
|
+
# Create a new Result. Used internally in the adapters.
|
10
|
+
def initialize(command, affected_rows, insert_id = nil)
|
11
|
+
@command = command
|
12
|
+
@affected_rows = affected_rows
|
13
|
+
@insert_id = insert_id
|
14
|
+
end
|
15
|
+
|
16
|
+
# Return the number of affected rows
|
17
|
+
def to_i
|
18
|
+
@affected_rows
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'cgi'
|
3
|
+
|
4
|
+
module SSLHelpers
|
5
|
+
CERTS_DIR = Pathname(__FILE__).dirname.join('ssl_certs').to_s
|
6
|
+
|
7
|
+
CONFIG = OpenStruct.new
|
8
|
+
CONFIG.ca_cert = File.join(CERTS_DIR, 'ca-cert.pem')
|
9
|
+
CONFIG.ca_key = File.join(CERTS_DIR, 'ca-key.pem')
|
10
|
+
CONFIG.server_cert = File.join(CERTS_DIR, 'server-cert.pem')
|
11
|
+
CONFIG.server_key = File.join(CERTS_DIR, 'server-key.pem')
|
12
|
+
CONFIG.client_cert = File.join(CERTS_DIR, 'client-cert.pem')
|
13
|
+
CONFIG.client_key = File.join(CERTS_DIR, 'client-key.pem')
|
14
|
+
CONFIG.cipher = 'AES128-SHA'
|
15
|
+
|
16
|
+
def self.query(*keys)
|
17
|
+
keys.map { |key| "ssl[#{key}]=#{CGI.escape(CONFIG.send(key))}" }.join('&')
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,201 @@
|
|
1
|
+
shared_examples 'a Command' do
|
2
|
+
before :all do
|
3
|
+
setup_test_environment
|
4
|
+
end
|
5
|
+
|
6
|
+
before do
|
7
|
+
@connection = DataObjects::Connection.new(CONFIG.uri)
|
8
|
+
@command = @connection.create_command('INSERT INTO users (name) VALUES (?)')
|
9
|
+
@reader = @connection.create_command('SELECT code, name FROM widgets WHERE ad_description = ?')
|
10
|
+
@arg_command = @connection.create_command('INSERT INTO users (name, fired_at) VALUES (?, ?)')
|
11
|
+
@arg_reader = @connection.create_command('SELECT code, name FROM widgets WHERE ad_description = ? AND whitepaper_text = ?')
|
12
|
+
end
|
13
|
+
|
14
|
+
after do
|
15
|
+
@connection.close
|
16
|
+
end
|
17
|
+
|
18
|
+
it { expect(@command).to be_kind_of(DataObjects::Command) }
|
19
|
+
|
20
|
+
it { expect(@command).to respond_to(:execute_non_query) }
|
21
|
+
|
22
|
+
describe 'execute_non_query' do
|
23
|
+
describe 'with an invalid statement' do
|
24
|
+
before do
|
25
|
+
@invalid_command = @connection.create_command('INSERT INTO non_existent_table (tester) VALUES (1)')
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'raises an error on an invalid query' do
|
29
|
+
expect { @invalid_command.execute_non_query }.to raise_error(DataObjects::SQLError)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'raises an error with too many binding parameters' do
|
33
|
+
expect { @arg_command.execute_non_query('Too', Date.today, 'Many') }.to raise_error(ArgumentError,
|
34
|
+
/Binding mismatch: 3 for 2/)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'raises an error with too few binding parameters' do
|
38
|
+
expect { @arg_command.execute_non_query('Few') }.to raise_error(ArgumentError,
|
39
|
+
/Binding mismatch: 1 for 2/)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe 'with a valid statement' do
|
44
|
+
it 'does not raise an error with an explicit nil as parameter' do
|
45
|
+
expect { @arg_command.execute_non_query(nil, nil) }.not_to raise_error
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe 'with a valid statement and ? inside quotes' do
|
50
|
+
before do
|
51
|
+
@command_with_quotes = @connection.create_command("INSERT INTO users (name) VALUES ('will it work? ')")
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'does not raise an error' do
|
55
|
+
expect { @command_with_quotes.execute_non_query }.not_to raise_error
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
it { expect(@command).to respond_to(:execute_reader) }
|
61
|
+
|
62
|
+
describe 'execute_reader' do
|
63
|
+
describe 'with an invalid reader' do
|
64
|
+
before do
|
65
|
+
@invalid_reader = @connection.create_command('SELECT * FROM non_existent_widgets WHERE ad_description = ? AND white_paper_text = ?')
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'raises an error on an invalid query' do
|
69
|
+
# FIXME: JRuby (and MRI): Should this be an ArgumentError or DataObjects::SQLError?
|
70
|
+
expect { @invalid_reader.execute_reader }.to raise_error(DataObjects::SQLError) # (ArgumentError, DataObjects::SQLError)
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'raises an error with too many few binding parameters' do
|
74
|
+
expect { @arg_reader.execute_reader('Too', 'Many', 'Args') }.to raise_error(ArgumentError,
|
75
|
+
/Binding mismatch: 3 for 2/)
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'raises an error with too few binding parameters' do
|
79
|
+
expect { @arg_reader.execute_reader('Few') }.to raise_error(ArgumentError,
|
80
|
+
/Binding mismatch: 1 for 2/)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe 'with a valid reader' do
|
85
|
+
it 'does not raise an error with an explicit nil as parameter' do
|
86
|
+
expect { @arg_reader.execute_reader(nil, nil) }.not_to raise_error
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'returns an empty reader if the query does not return a result' do
|
90
|
+
runs_command = @connection.create_command("UPDATE widgets SET name = '' WHERE name = ''")
|
91
|
+
res = runs_command.execute_reader
|
92
|
+
expect(res.fields).to eq []
|
93
|
+
expect(res.next!).to be false
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe 'with a valid reader and ? inside column alias' do
|
98
|
+
before do
|
99
|
+
@reader_with_quotes = @connection.create_command('SELECT code AS "code?", name FROM widgets WHERE ad_description = ?')
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'does not raise an error' do
|
103
|
+
expect { @reader_with_quotes.execute_reader(nil) }.not_to raise_error
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
it { expect(@command).to respond_to(:set_types) }
|
109
|
+
|
110
|
+
describe 'set_types' do
|
111
|
+
describe 'is invalid when used with a statement' do
|
112
|
+
before do
|
113
|
+
@command.set_types(String)
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'raises an error when types are set' do
|
117
|
+
expect { @arg_command.execute_non_query('Few') }.to raise_error(ArgumentError)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe 'with an invalid reader' do
|
122
|
+
it 'raises an error with too few types' do
|
123
|
+
@reader.set_types(String)
|
124
|
+
expect { @reader.execute_reader('One parameter') }.to raise_error(ArgumentError,
|
125
|
+
/Field-count mismatch. Expected 1 fields, but the query yielded 2/)
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'raises an error with too many types' do
|
129
|
+
@reader.set_types(String, String, BigDecimal)
|
130
|
+
expect { @reader.execute_reader('One parameter') }.to raise_error(ArgumentError,
|
131
|
+
/Field-count mismatch. Expected 3 fields, but the query yielded 2/)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
describe 'with a valid reader' do
|
136
|
+
it 'does not raise an error with correct number of types' do
|
137
|
+
@reader.set_types(String, String)
|
138
|
+
expect { @result = @reader.execute_reader('Buy this product now!') }.not_to raise_error
|
139
|
+
expect { @result.next! }.not_to raise_error
|
140
|
+
expect { @result.values }.not_to raise_error
|
141
|
+
@result.close
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'also supports old style array argument types' do
|
145
|
+
@reader.set_types([String, String])
|
146
|
+
expect { @result = @reader.execute_reader('Buy this product now!') }.not_to raise_error
|
147
|
+
expect { @result.next! }.not_to raise_error
|
148
|
+
expect { @result.values }.not_to raise_error
|
149
|
+
@result.close
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'allows subtype types' do
|
153
|
+
class MyString < String; end
|
154
|
+
@reader.set_types(MyString, String)
|
155
|
+
expect { @result = @reader.execute_reader('Buy this product now!') }.not_to raise_error
|
156
|
+
expect { @result.next! }.not_to raise_error
|
157
|
+
expect { @result.values }.not_to raise_error
|
158
|
+
@result.close
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
it { expect(@command).to respond_to(:to_s) }
|
164
|
+
|
165
|
+
describe 'to_s' do
|
166
|
+
# Tests not implemented
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
shared_examples 'a Command with async' do
|
171
|
+
before :all do
|
172
|
+
setup_test_environment
|
173
|
+
end
|
174
|
+
|
175
|
+
describe 'running queries in parallel' do
|
176
|
+
before do
|
177
|
+
threads = []
|
178
|
+
|
179
|
+
@start = Time.now
|
180
|
+
4.times do |_i|
|
181
|
+
threads << Thread.new do
|
182
|
+
connection = DataObjects::Connection.new(CONFIG.uri)
|
183
|
+
command = connection.create_command(CONFIG.sleep)
|
184
|
+
if CONFIG.sleep =~ /^SELECT/i
|
185
|
+
reader = command.execute_reader
|
186
|
+
reader.next!
|
187
|
+
reader.close
|
188
|
+
else
|
189
|
+
command.execute_non_query
|
190
|
+
end
|
191
|
+
ensure
|
192
|
+
# Always make sure the connection gets released back into the pool.
|
193
|
+
connection.close
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
threads.each(&:join)
|
198
|
+
@finish = Time.now
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|