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.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/ChangeLog.markdown +115 -0
  3. data/LICENSE +20 -0
  4. data/README.markdown +18 -0
  5. data/Rakefile +20 -0
  6. data/lib/data_objects/byte_array.rb +6 -0
  7. data/lib/data_objects/command.rb +79 -0
  8. data/lib/data_objects/connection.rb +95 -0
  9. data/lib/data_objects/error/connection_error.rb +4 -0
  10. data/lib/data_objects/error/data_error.rb +4 -0
  11. data/lib/data_objects/error/integrity_error.rb +4 -0
  12. data/lib/data_objects/error/sql_error.rb +17 -0
  13. data/lib/data_objects/error/syntax_error.rb +4 -0
  14. data/lib/data_objects/error/transaction_error.rb +4 -0
  15. data/lib/data_objects/error.rb +4 -0
  16. data/lib/data_objects/extension.rb +9 -0
  17. data/lib/data_objects/logger.rb +247 -0
  18. data/lib/data_objects/pooling.rb +243 -0
  19. data/lib/data_objects/quoting.rb +99 -0
  20. data/lib/data_objects/reader.rb +45 -0
  21. data/lib/data_objects/result.rb +21 -0
  22. data/lib/data_objects/spec/lib/pending_helpers.rb +13 -0
  23. data/lib/data_objects/spec/lib/ssl.rb +19 -0
  24. data/lib/data_objects/spec/setup.rb +5 -0
  25. data/lib/data_objects/spec/shared/command_spec.rb +201 -0
  26. data/lib/data_objects/spec/shared/connection_spec.rb +148 -0
  27. data/lib/data_objects/spec/shared/encoding_spec.rb +161 -0
  28. data/lib/data_objects/spec/shared/error/sql_error_spec.rb +23 -0
  29. data/lib/data_objects/spec/shared/quoting_spec.rb +0 -0
  30. data/lib/data_objects/spec/shared/reader_spec.rb +180 -0
  31. data/lib/data_objects/spec/shared/result_spec.rb +67 -0
  32. data/lib/data_objects/spec/shared/typecast/array_spec.rb +29 -0
  33. data/lib/data_objects/spec/shared/typecast/bigdecimal_spec.rb +112 -0
  34. data/lib/data_objects/spec/shared/typecast/boolean_spec.rb +133 -0
  35. data/lib/data_objects/spec/shared/typecast/byte_array_spec.rb +76 -0
  36. data/lib/data_objects/spec/shared/typecast/class_spec.rb +53 -0
  37. data/lib/data_objects/spec/shared/typecast/date_spec.rb +114 -0
  38. data/lib/data_objects/spec/shared/typecast/datetime_spec.rb +140 -0
  39. data/lib/data_objects/spec/shared/typecast/float_spec.rb +115 -0
  40. data/lib/data_objects/spec/shared/typecast/integer_spec.rb +92 -0
  41. data/lib/data_objects/spec/shared/typecast/ipaddr_spec.rb +0 -0
  42. data/lib/data_objects/spec/shared/typecast/nil_spec.rb +107 -0
  43. data/lib/data_objects/spec/shared/typecast/other_spec.rb +41 -0
  44. data/lib/data_objects/spec/shared/typecast/range_spec.rb +29 -0
  45. data/lib/data_objects/spec/shared/typecast/string_spec.rb +130 -0
  46. data/lib/data_objects/spec/shared/typecast/time_spec.rb +111 -0
  47. data/lib/data_objects/transaction.rb +111 -0
  48. data/lib/data_objects/uri.rb +109 -0
  49. data/lib/data_objects/utilities.rb +18 -0
  50. data/lib/data_objects/version.rb +3 -0
  51. data/lib/data_objects.rb +20 -0
  52. data/spec/command_spec.rb +24 -0
  53. data/spec/connection_spec.rb +31 -0
  54. data/spec/do_mock.rb +29 -0
  55. data/spec/do_mock2.rb +29 -0
  56. data/spec/pooling_spec.rb +153 -0
  57. data/spec/reader_spec.rb +19 -0
  58. data/spec/result_spec.rb +21 -0
  59. data/spec/spec_helper.rb +18 -0
  60. data/spec/transaction_spec.rb +37 -0
  61. data/spec/uri_spec.rb +23 -0
  62. data/tasks/release.rake +14 -0
  63. data/tasks/spec.rake +10 -0
  64. data/tasks/yard.rake +9 -0
  65. data/tasks/yardstick.rake +19 -0
  66. 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,13 @@
1
+ module DataObjects
2
+ module Spec
3
+ module PendingHelpers
4
+ def pending_if(message, boolean, &block)
5
+ if boolean
6
+ pending(message, &block)
7
+ else
8
+ yield
9
+ end
10
+ end
11
+ end
12
+ end
13
+ 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,5 @@
1
+ RSpec::Matchers.define :be_array_case_insensitively_equal_to do |attribute|
2
+ match do |model|
3
+ model.map(&:downcase) == attribute
4
+ end
5
+ 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