Skip to content

Commit 62fd9cd

Browse files
committed
Refactor execute to fetch an entire result object
1 parent 0598fde commit 62fd9cd

File tree

11 files changed

+532
-975
lines changed

11 files changed

+532
-975
lines changed

README.md

+51-121
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ opts[:message_handler] = Proc.new { |m| puts m.message }
116116
client = TinyTds::Client.new opts
117117
# => Changed database context to 'master'.
118118
# => Changed language setting to us_english.
119-
client.execute("print 'hello world!'").do
120-
# => hello world!
119+
client.do("print 'hello world!'")
120+
# => -1 (no affected rows)
121121
```
122122

123123
Use the `#active?` method to determine if a connection is good. The implementation of this method may change but it should always guarantee that a connection is good. Current it checks for either a closed or dead connection.
@@ -147,169 +147,99 @@ Send a SQL string to the database and return a TinyTds::Result object.
147147
result = client.execute("SELECT * FROM [datatypes]")
148148
```
149149

150+
## Sending queries and receiving results
150151

151-
## TinyTds::Result Usage
152+
The client implements three different methods to send queries to a SQL server.
152153

153-
A result object is returned by the client's execute command. It is important that you either return the data from the query, most likely with the #each method, or that you cancel the results before asking the client to execute another SQL batch. Failing to do so will yield an error.
154-
155-
Calling #each on the result will lazily load each row from the database.
154+
`client.insert` will execute the query and return the last identifier.
156155

157156
```ruby
158-
result.each do |row|
159-
# By default each row is a hash.
160-
# The keys are the fields, as you'd expect.
161-
# The values are pre-built Ruby primitives mapped from their corresponding types.
162-
end
157+
client.insert("INSERT INTO [datatypes] ([varchar_50]) VALUES ('text')")
158+
# => 363
163159
```
164160

165-
A result object has a `#fields` accessor. It can be called before the result rows are iterated over. Even if no rows are returned, #fields will still return the column names you expected. Any SQL that does not return columned data will always return an empty array for `#fields`. It is important to remember that if you access the `#fields` before iterating over the results, the columns will always follow the default query option's `:symbolize_keys` setting at the client's level and will ignore the query options passed to each.
161+
`client.do` will execute the query and tell you how many rows were affected.
166162

167163
```ruby
168-
result = client.execute("USE [tinytdstest]")
169-
result.fields # => []
170-
result.do
171-
172-
result = client.execute("SELECT [id] FROM [datatypes]")
173-
result.fields # => ["id"]
174-
result.cancel
175-
result = client.execute("SELECT [id] FROM [datatypes]")
176-
result.each(:symbolize_keys => true)
177-
result.fields # => [:id]
164+
client.do("DELETE FROM [datatypes] WHERE [varchar_50] = 'text'")
165+
# 1
178166
```
179167

180-
You can cancel a result object's data from being loading by the server.
168+
Both `do` and `insert` will not serialize any results sent by the SQL server, making them extremely fast and memory-efficient for large operations.
169+
170+
`client.execute` will execute the query and return you a `TinyTds::Result` object.
181171

182172
```ruby
183-
result = client.execute("SELECT * FROM [super_big_table]")
184-
result.cancel
173+
client.execute("SELECT [id] FROM [datatypes]")
174+
# =>
175+
# #<TinyTds::Result:0x000057d6275ce3b0
176+
# @fields=["id"],
177+
# @return_code=nil,
178+
# @rows=
179+
# [{"id"=>11},
180+
# {"id"=>12},
181+
# {"id"=>21},
182+
# {"id"=>31},
185183
```
186184

187-
You can use results cancelation in conjunction with results lazy loading, no problem.
185+
A result object has a `fields` accessor. Even if no rows are returned, `fields` will still return the column names you expected. Any SQL that does not return columned data will always return an empty array for `fields`.
188186

189187
```ruby
190-
result = client.execute("SELECT * FROM [super_big_table]")
191-
result.each_with_index do |row, i|
192-
break if row > 10
193-
end
194-
result.cancel
188+
result = client.execute("USE [tinytdstest]")
189+
result.fields # => []
190+
191+
result = client.execute("SELECT [id] FROM [datatypes]")
192+
result.fields # => ["id"]
195193
```
196194

197-
If the SQL executed by the client returns affected rows, you can easily find out how many.
195+
You can retrieve the results by accessing the `rows` property on the result.
198196

199197
```ruby
200-
result.each
201-
result.affected_rows # => 24
198+
result.rows
199+
# =>
200+
# [{"id"=>11},
201+
# {"id"=>12},
202+
# {"id"=>21},
203+
# ...
202204
```
203205

204-
This pattern is so common for UPDATE and DELETE statements that the #do method cancels any need for loading the result data and returns the `#affected_rows`.
206+
The result object also has `affected_rows`, which usually also corresponds to the length of items in `rows`. But if you execute a `DELETE` statement with `execute, `rows` is likely empty but `affected_rows` will still list a couple of items.
205207

206208
```ruby
207209
result = client.execute("DELETE FROM [datatypes]")
208-
result.do # => 72
210+
# #<TinyTds::Result:0x00005efc024d9f10 @affected_rows=75, @fields=[], @return_code=nil, @rows=[]>
211+
result.count
212+
# 0
213+
result.affected_rows
214+
# 75
209215
```
210216

211-
Likewise for `INSERT` statements, the #insert method cancels any need for loading the result data and executes a `SCOPE_IDENTITY()` for the primary key.
212-
213-
```ruby
214-
result = client.execute("INSERT INTO [datatypes] ([xml]) VALUES ('<html><br/></html>')")
215-
result.insert # => 420
216-
```
217+
But as mentioned earlier, best use `do` when you are only interested in the `affected_rows`.
217218

218-
The result object can handle multiple result sets form batched SQL or stored procedures. It is critical to remember that when calling each with a block for the first time will return each "row" of each result set. Calling each a second time with a block will yield each "set".
219+
The result object can handle multiple result sets form batched SQL or stored procedures.
219220

220221
```ruby
221222
sql = ["SELECT TOP (1) [id] FROM [datatypes]",
222223
"SELECT TOP (2) [bigint] FROM [datatypes] WHERE [bigint] IS NOT NULL"].join(' ')
223224

224-
set1, set2 = client.execute(sql).each
225+
set1, set2 = client.execute(sql).rows
225226
set1 # => [{"id"=>11}]
226227
set2 # => [{"bigint"=>-9223372036854775807}, {"bigint"=>9223372036854775806}]
227-
228-
result = client.execute(sql)
229-
230-
result.each do |rowset|
231-
# First time data loading, yields each row from each set.
232-
# 1st: {"id"=>11}
233-
# 2nd: {"bigint"=>-9223372036854775807}
234-
# 3rd: {"bigint"=>9223372036854775806}
235-
end
236-
237-
result.each do |rowset|
238-
# Second time over (if columns cached), yields each set.
239-
# 1st: [{"id"=>11}]
240-
# 2nd: [{"bigint"=>-9223372036854775807}, {"bigint"=>9223372036854775806}]
241-
end
242-
```
243-
244-
Use the `#sqlsent?` and `#canceled?` query methods on the client to determine if an active SQL batch still needs to be processed and or if data results were canceled from the last result object. These values reset to true and false respectively for the client at the start of each `#execute` and new result object. Or if all rows are processed normally, `#sqlsent?` will return false. To demonstrate, lets assume we have 100 rows in the result object.
245-
246-
```ruby
247-
client.sqlsent? # = false
248-
client.canceled? # = false
249-
250-
result = client.execute("SELECT * FROM [super_big_table]")
251-
252-
client.sqlsent? # = true
253-
client.canceled? # = false
254-
255-
result.each do |row|
256-
# Assume we break after 20 rows with 80 still pending.
257-
break if row["id"] > 20
258-
end
259-
260-
client.sqlsent? # = true
261-
client.canceled? # = false
262-
263-
result.cancel
264-
265-
client.sqlsent? # = false
266-
client.canceled? # = true
267-
```
268-
269-
It is possible to get the return code after executing a stored procedure from either the result or client object.
270-
271-
```ruby
272-
client.return_code # => nil
273-
274-
result = client.execute("EXEC tinytds_TestReturnCodes")
275-
result.do
276-
result.return_code # => 420
277-
client.return_code # => 420
278228
```
279229

280-
281230
## Query Options
282231

283-
Every `TinyTds::Result` object can pass query options to the #each method. The defaults are defined and configurable by setting options in the `TinyTds::Client.default_query_options` hash. The default values are:
284-
285-
* :as => :hash - Object for each row yielded. Can be set to :array.
286-
* :symbolize_keys => false - Row hash keys. Defaults to shared/frozen string keys.
287-
* :cache_rows => true - Successive calls to #each returns the cached rows.
288-
* :timezone => :local - Local to the Ruby client or :utc for UTC.
289-
* :empty_sets => true - Include empty results set in queries that return multiple result sets.
232+
You can pass query options to `execute`. The defaults are defined and configurable by setting options in the `TinyTds::Client.default_query_options` hash. The default values are:
290233

291-
Each result gets a copy of the default options you specify at the client level and can be overridden by passing an options hash to the #each method. For example
234+
* `as: :hash` - Object for each row yielded. Can be set to :array.
235+
* `empty_sets: true` - Include empty results set in queries that return multiple result sets.
236+
* `timezone: :local` - Local to the Ruby client or :utc for UTC.
292237

293238
```ruby
294-
result.each(:as => :array, :cache_rows => false) do |row|
295-
# Each row is now an array of values ordered by #fields.
296-
# Rows are yielded and forgotten about, freeing memory.
297-
end
239+
result = client.execute("SELECT [datetime2_2] FROM [datatypes] WHERE [id] = 74", as: :array, timezone: :utc, empty_sets: true)
240+
# => #<TinyTds::Result:0x000061e841910600 @affected_rows=1, @fields=["datetime2_2"], @return_code=nil, @rows=[[9999-12-31 23:59:59.12 UTC]]>
298241
```
299242

300-
Besides the standard query options, the result object can take one additional option. Using `:first => true` will only load the first row of data and cancel all remaining results.
301-
302-
```ruby
303-
result = client.execute("SELECT * FROM [super_big_table]")
304-
result.each(:first => true) # => [{'id' => 24}]
305-
```
306-
307-
308-
## Row Caching
309-
310-
By default row caching is turned on because the SQL Server adapter for ActiveRecord would not work without it. I hope to find some time to create some performance patches for ActiveRecord that would allow it to take advantages of lazily created yielded rows from result objects. Currently only TinyTDS and the Mysql2 gem allow such a performance gain.
311-
312-
313243
## Encoding Error Handling
314244

315245
TinyTDS takes an opinionated stance on how we handle encoding errors. First, we treat errors differently on reads vs. writes. Our opinion is that if you are reading bad data due to your client's encoding option, you would rather just find `?` marks in your strings vs being blocked with exceptions. This is how things wold work via ODBC or SMS. On the other hand, writes will raise an exception. In this case we raise the SYBEICONVO/2402 error message which has a description of `Error converting characters into server's character set. Some character(s) could not be converted.`. Even though the severity of this message is only a `4` and TinyTDS will automatically strip/ignore unknown characters, we feel you should know that you are inserting bad encodings. In this way, a transaction can be rolled back, etc. Remember, any database write that has bad characters due to the client encoding will still be written to the database, but it is up to you rollback said write if needed. Most ORMs like ActiveRecord handle this scenario just fine.

0 commit comments

Comments
 (0)