You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
v4: Refactor tiny_tds to avoid sharing DBPROCESS (#595)
* Move `insert` to client class
* Move `do` to client class
* Refactor `execute` to fetch an entire result object
* Ensure test database data is loaded before running tests
* Update `CHANGELOG`
* Use `int64_t` instead of custom `LONG_LONG_FORMAT` macro
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.
@@ -153,169 +153,99 @@ Send a SQL string to the database and return a TinyTds::Result object.
153
153
result = client.execute("SELECT * FROM [datatypes]")
154
154
```
155
155
156
+
## Sending queries and receiving results
156
157
157
-
## TinyTds::Result Usage
158
+
The client implements three different methods to send queries to a SQL server.
158
159
159
-
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.
160
-
161
-
Calling #each on the result will lazily load each row from the database.
160
+
`client.insert` will execute the query and return the last identifier.
162
161
163
162
```ruby
164
-
result.each do |row|
165
-
# By default each row is a hash.
166
-
# The keys are the fields, as you'd expect.
167
-
# The values are pre-built Ruby primitives mapped from their corresponding types.
168
-
end
163
+
client.insert("INSERT INTO [datatypes] ([varchar_50]) VALUES ('text')")
164
+
# => 363
169
165
```
170
166
171
-
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.
167
+
`client.do`will execute the query and tell you how many rows were affected.
172
168
173
169
```ruby
174
-
result = client.execute("USE [tinytdstest]")
175
-
result.fields # => []
176
-
result.do
177
-
178
-
result = client.execute("SELECT [id] FROM [datatypes]")
179
-
result.fields # => ["id"]
180
-
result.cancel
181
-
result = client.execute("SELECT [id] FROM [datatypes]")
182
-
result.each(:symbolize_keys => true)
183
-
result.fields # => [:id]
170
+
client.do("DELETE FROM [datatypes] WHERE [varchar_50] = 'text'")
171
+
# 1
184
172
```
185
173
186
-
You can cancel a result object's data from being loading by the server.
174
+
Both `do` and `insert` will not serialize any results sent by the SQL server, making them extremely fast and memory-efficient for large operations.
175
+
176
+
`client.execute` will execute the query and return you a `TinyTds::Result` object.
187
177
188
178
```ruby
189
-
result = client.execute("SELECT * FROM [super_big_table]")
190
-
result.cancel
179
+
client.execute("SELECT [id] FROM [datatypes]")
180
+
# =>
181
+
# #<TinyTds::Result:0x000057d6275ce3b0
182
+
# @fields=["id"],
183
+
# @return_code=nil,
184
+
# @rows=
185
+
# [{"id"=>11},
186
+
# {"id"=>12},
187
+
# {"id"=>21},
188
+
# {"id"=>31},
191
189
```
192
190
193
-
You can use results cancelation in conjunction with results lazy loading, no problem.
191
+
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`.
194
192
195
193
```ruby
196
-
result = client.execute("SELECT * FROM [super_big_table]")
197
-
result.each_with_index do |row, i|
198
-
breakif row >10
199
-
end
200
-
result.cancel
194
+
result = client.execute("USE [tinytdstest]")
195
+
result.fields # => []
196
+
197
+
result = client.execute("SELECT [id] FROM [datatypes]")
198
+
result.fields # => ["id"]
201
199
```
202
200
203
-
If the SQL executed by the client returns affected rows, you can easily find out how many.
201
+
You can retrieve the results by accessing the `rows` property on the result.
204
202
205
203
```ruby
206
-
result.each
207
-
result.affected_rows # => 24
204
+
result.rows
205
+
# =>
206
+
# [{"id"=>11},
207
+
# {"id"=>12},
208
+
# {"id"=>21},
209
+
# ...
208
210
```
209
211
210
-
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`.
212
+
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.
211
213
212
214
```ruby
213
215
result = client.execute("DELETE FROM [datatypes]")
Likewise for `INSERT` statements, the #insert method cancels any need for loading the result data and executes a `SCOPE_IDENTITY()` for the primary key.
218
-
219
-
```ruby
220
-
result = client.execute("INSERT INTO [datatypes] ([xml]) VALUES ('<html><br/></html>')")
221
-
result.insert # => 420
222
-
```
223
+
But as mentioned earlier, best use `do` when you are only interested in the `affected_rows`.
223
224
224
-
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".
225
+
The result object can handle multiple result sets form batched SQL or stored procedures.
225
226
226
227
```ruby
227
228
sql = ["SELECT TOP (1) [id] FROM [datatypes]",
228
229
"SELECT TOP (2) [bigint] FROM [datatypes] WHERE [bigint] IS NOT NULL"].join('')
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.
251
-
252
-
```ruby
253
-
client.sqlsent? # = false
254
-
client.canceled? # = false
255
-
256
-
result = client.execute("SELECT * FROM [super_big_table]")
257
-
258
-
client.sqlsent? # = true
259
-
client.canceled? # = false
260
-
261
-
result.each do |row|
262
-
# Assume we break after 20 rows with 80 still pending.
263
-
breakif row["id"] >20
264
-
end
265
-
266
-
client.sqlsent? # = true
267
-
client.canceled? # = false
268
-
269
-
result.cancel
270
-
271
-
client.sqlsent? # = false
272
-
client.canceled? # = true
273
-
```
274
-
275
-
It is possible to get the return code after executing a stored procedure from either the result or client object.
276
-
277
-
```ruby
278
-
client.return_code # => nil
279
-
280
-
result = client.execute("EXEC tinytds_TestReturnCodes")
281
-
result.do
282
-
result.return_code # => 420
283
-
client.return_code # => 420
284
234
```
285
235
286
-
287
236
## Query Options
288
237
289
-
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:
290
-
291
-
*:as => :hash - Object for each row yielded. Can be set to :array.
*:cache_rows => true - Successive calls to #each returns the cached rows.
294
-
*:timezone => :local - Local to the Ruby client or :utc for UTC.
295
-
*:empty_sets => true - Include empty results set in queries that return multiple result sets.
238
+
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:
296
239
297
-
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
240
+
*`as: :hash` - Object for each row yielded. Can be set to :array.
241
+
*`empty_sets: true` - Include empty results set in queries that return multiple result sets.
242
+
*`timezone: :local` - Local to the Ruby client or :utc for UTC.
298
243
299
244
```ruby
300
-
result.each(:as => :array, :cache_rows => false) do |row|
301
-
# Each row is now an array of values ordered by #fields.
302
-
# Rows are yielded and forgotten about, freeing memory.
303
-
end
245
+
result = client.execute("SELECT [datetime2_2] FROM [datatypes] WHERE [id] = 74", as::array, timezone::utc, empty_sets:true)
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.
307
-
308
-
```ruby
309
-
result = client.execute("SELECT * FROM [super_big_table]")
310
-
result.each(:first => true) # => [{'id' => 24}]
311
-
```
312
-
313
-
314
-
## Row Caching
315
-
316
-
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.
317
-
318
-
319
249
## Encoding Error Handling
320
250
321
251
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