sbf-data_objects 0.10.17
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.
- 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
|