sbf-data_objects 0.10.17

Sign up to get free protection for your applications and to get access to all the features.
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