extralite-bundle 2.4 → 2.6

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,235 +1,888 @@
1
- # Extralite - a Super Fast Ruby Gem for Working with SQLite3 Databases
2
-
3
- * Source code: https://github.com/digital-fabric/extralite
4
- * Documentation: http://www.rubydoc.info/gems/extralite
5
-
6
- [![Ruby gem](https://badge.fury.io/rb/extralite.svg)](https://rubygems.org/gems/extralite) [![Tests](https://github.com/digital-fabric/extralite/workflows/Tests/badge.svg)](https://github.com/digital-fabric/extralite/actions?query=workflow%3ATests) [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/digital-fabric/extralite/blob/master/LICENSE)
1
+ <h1 align="center">
2
+ <br>
3
+ Extralite
4
+ </h1>
5
+
6
+ <h4 align="center">SQLite for Ruby</h4>
7
+
8
+ <p align="center">
9
+ <a href="http://rubygems.org/gems/extralite">
10
+ <img src="https://badge.fury.io/rb/extralite.svg" alt="Ruby gem">
11
+ </a>
12
+ <a href="https://github.com/digital-fabric/extralite/actions">
13
+ <img src="https://github.com/digital-fabric/extralite/actions/workflows/test.yml/badge.svg" alt="Tests">
14
+ </a>
15
+ <a href="https://github.com/digital-fabric/extralite/blob/master/LICENSE">
16
+ <img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License">
17
+ </a>
18
+ </p>
19
+
20
+ <p align="center">
21
+ <a href="https://www.rubydoc.info/gems/extralite">API reference</a>
22
+ </p>
7
23
 
8
24
  ## What is Extralite?
9
25
 
10
- Extralite is a super fast, extra-lightweight (about 1300 lines of C-code)
11
- SQLite3 wrapper for Ruby. It provides a minimal set of methods for interacting
12
- with an SQLite3 database, as well as prepared queries (prepared statements).
26
+ Extralite is a fast and innovative SQLite wrapper for Ruby with a rich set of
27
+ features. It provides multiple ways of retrieving data from SQLite databases,
28
+ makes it possible to use SQLite databases in multi-threaded and multi-fibered
29
+ Ruby apps, and provides a comprehensive set of tools for managing SQLite
30
+ databases.
13
31
 
14
32
  Extralite comes in two flavors: the `extralite` gem which uses the
15
33
  system-installed sqlite3 library, and the `extralite-bundle` gem which bundles
16
34
  the latest version of SQLite
17
- ([3.44.2](https://sqlite.org/releaselog/3_44_2.html)), offering access to the
35
+ ([3.45.0](https://sqlite.org/releaselog/3_45_0.html)), offering access to the
18
36
  latest features and enhancements.
19
37
 
20
38
  ## Features
21
39
 
22
- - Super fast - [up to 11x faster](#performance) than the
23
- [sqlite3](https://github.com/sparklemotion/sqlite3-ruby) gem.
24
- - A variety of methods for different data access patterns: rows as hashes, rows
25
- as arrays, single row, single column, single value.
26
- - Prepared statements.
40
+ - Best-in-class performance (up to 14X the performance of the
41
+ [sqlite3](https://github.com/sparklemotion/sqlite3-ruby) gem).
42
+ - Support for [concurrency](#concurrency) out of the box for multi-threaded
43
+ apps.
44
+ - A variety of methods for retrieving data - hashes, array, single rows, single
45
+ values.
46
+ - Support for external iteration, allowing iterating through single records or
47
+ batches of records.
48
+ - Prepared queries.
27
49
  - Parameter binding.
28
- - External iteration - get single records or batches of records.
29
- - Use system-installed sqlite3, or the [bundled latest version of
30
- SQLite3](#installing-the-extralite-sqlite3-bundle).
31
- - Improved [concurrency](#concurrency) for multithreaded apps: the Ruby GVL is
32
- released peridically while preparing SQL statements and while iterating over
33
- results.
34
- - Automatically execute SQL strings containing multiple semicolon-separated
35
- queries (handy for creating/modifying schemas).
36
- - Execute the same query with multiple parameter lists (useful for inserting
37
- records).
38
- - Load extensions (loading of extensions is autmatically enabled. You can find
39
- some useful extensions here: https://github.com/nalgeon/sqlean.)
40
- - Includes a [Sequel adapter](#usage-with-sequel).
41
-
42
- ## Installation
43
-
44
- To use Extralite in your Ruby app, add the following to your `Gemfile`:
50
+ - Batch execution of queries.
51
+ - Support for transactions and savepoints.
52
+ - Support for loading [SQLite extensions](https://github.com/nalgeon/sqlean).
53
+ - APIs for doing database backups.
54
+ - Sequel adapter.
55
+
56
+ ## Table of Content
57
+
58
+ - [Installing Extralite](#installing-extralite)
59
+ - [Basic Usage](#basic-usage)
60
+ - [Parameter binding](#parameter-binding)
61
+ - [Data Types](#data-types)
62
+ - [Prepared Queries](#prepared-queries)
63
+ - [Batch Execution of Queries](#batch-execution-of-queries)
64
+ - [Transactions and Savepoints](#transactions-and-savepoints)
65
+ - [Database Information](#database-information)
66
+ - [Error Handling](#error-handling)
67
+ - [Concurrency](#concurrency)
68
+ - [Advanced Usage](#advanced-usage)
69
+ - [Usage with Sequel](#usage-with-sequel)
70
+ - [Performance](#performance)
71
+ - [License](#license)
72
+ - [Contributing](#contributing)
73
+
74
+ ## Installing Extralite
75
+
76
+ Using bundler:
45
77
 
46
78
  ```ruby
47
79
  gem 'extralite'
48
80
  ```
49
81
 
50
- You can also run `gem install extralite` if you just want to check it out.
82
+ Or manually:
83
+
84
+ ```bash
85
+ $ gem install extralite
86
+ ```
51
87
 
52
- ### Installing the Extralite-SQLite3 Bundle
88
+ __Note__: Extralite supports Ruby 3.0 and newer.
53
89
 
54
- If you don't have sqlite3 installed on your system, do not want to use the
55
- system-installed version of SQLite3, or would like to use the latest version of
56
- SQLite3, you can install the `extralite-bundle` gem, which integrates the
57
- SQLite3 source code.
90
+ ### Installing the Extralite-SQLite Bundle
58
91
 
59
- > **Important note**: The `extralite-bundle` gem will take a while to install
60
- > (on my modest machine it takes about a minute), due to the size of the sqlite3
61
- > code.
92
+ If you don't have the sqlite3 lib installed on your system, do not want to use
93
+ the system-installed version of SQLite, or would like to use the latest version
94
+ of SQLite, you can install the `extralite-bundle` gem, which integrates the
95
+ SQLite source code.
62
96
 
63
97
  Usage of the `extralite-bundle` gem is identical to the usage of the normal
64
98
  `extralite` gem, using `require 'extralite'` to load the gem.
65
99
 
66
- ## Synopsis
100
+ ## Basic Usage
101
+
102
+ Here's as an example showing how to open an SQLite database and run some
103
+ queries:
67
104
 
68
105
  ```ruby
69
- require 'extralite'
106
+ db = Extralite::Database.new('mydb.sqlite')
107
+ db.execute('create table foo (x, y, z)')
108
+ db.execute('insert into foo values (?, ?, ?)', 1, 2, 3)
109
+ db.execute('insert into foo values (?, ?, ?)', 4, 5, 6)
110
+ db.query('select * from foo') #=> [{x: 1, y: 2, z: 3}, {x: 4, y: 5, z: 6}]
111
+ ```
70
112
 
71
- # get sqlite3 version
72
- Extralite.sqlite3_version #=> "3.35.2"
113
+ The `#execute` method is used to make changes to the database, such as creating
114
+ tables, or inserting records. It returns the number of records changed by the
115
+ query.
73
116
 
74
- # open a database
75
- db = Extralite::Database.new('/tmp/my.db')
117
+ The `#query` method is used to read data from the database. It returns an array
118
+ containing the resulting records, represented as hashes mapping column names (as
119
+ symbols) to individual values.
76
120
 
77
- # get query results as array of hashes
78
- db.query('select 1 as foo') #=> [{ :foo => 1 }]
79
- # or:
80
- db.query_hash('select 1 as foo') #=> [{ :foo => 1 }]
81
- # or iterate over results
82
- db.query('select 1 as foo') { |r| p r }
83
- # { :foo => 1 }
121
+ You can also iterate on records by providing a block to `#query`:
84
122
 
85
- # get query results as array of arrays
86
- db.query_ary('select 1, 2, 3') #=> [[1, 2, 3]]
87
- # or iterate over results
88
- db.query_ary('select 1, 2, 3') { |r| p r }
89
- # [1, 2, 3]
123
+ ```ruby
124
+ db.query 'select * from foo' do |r|
125
+ p record: r
126
+ end
127
+ ```
90
128
 
91
- # get a single row as a hash
92
- db.query_single_row("select 1 as foo") #=> { :foo => 1 }
129
+ Extralite also provides other ways of retrieving data:
93
130
 
94
- # get single column query results as array of values
95
- db.query_single_column('select 42') #=> [42]
96
- # or iterate over results
97
- db.query_single_column('select 42') { |v| p v }
98
- # 42
131
+ ```ruby
132
+ # get rows as arrays
133
+ db.query_ary 'select * from foo'
134
+ #=> [[1, 2, 3], [4, 5, 6]]
99
135
 
100
- # get single value from first row of results
101
- db.query_single_value("select 'foo'") #=> "foo"
136
+ # get a single column
137
+ db.query_single_column 'select x from foo'
138
+ #=> [1, 4]
102
139
 
103
- # parameter binding (works for all query_xxx methods)
104
- db.query_hash('select ? as foo, ? as bar', 1, 2) #=> [{ :foo => 1, :bar => 2 }]
105
- db.query_hash('select ?2 as foo, ?1 as bar, ?1 * ?2 as baz', 6, 7) #=> [{ :foo => 7, :bar => 6, :baz => 42 }]
140
+ # get a single row
141
+ db.query_single_row 'select * from foo order by x desc limit 1'
142
+ #=> { x: 4, y: 5, z: 6 }
106
143
 
107
- # parameter binding of named parameters
108
- db.query('select * from foo where bar = :bar', bar: 42)
109
- db.query('select * from foo where bar = :bar', 'bar' => 42)
110
- db.query('select * from foo where bar = :bar', ':bar' => 42)
144
+ # get a single value (a single column from a single row)
145
+ db.query_single_value 'select z from foo order by x desc limit 1'
146
+ #=> 6
147
+ ```
111
148
 
112
- # parameter binding of named parameters from Struct and Data
113
- SomeStruct = Struct.new(:foo, :bar)
114
- db.query_single_column('select :bar', SomeStruct.new(41, 42)) #=> [42]
115
- SomeData = Data.define(:foo, :bar)
116
- db.query_single_column('select :bar', SomeData.new(foo: 41, bar: 42)) #=> [42]
149
+ ## Parameter binding
117
150
 
118
- # parameter binding for binary data (BLOBs)
119
- db.execute('insert into foo values (?)', File.binread('/path/to/file'))
120
- db.execute('insert into foo values (?)', Extralite::Blob.new('Hello, 世界!'))
121
- db.execute('insert into foo values (?)', 'Hello, 世界!'.force_encoding(Encoding::ASCII_8BIT))
151
+ As shown in the above example, the `#execute` and `#query_xxx` methods accept
152
+ parameters that can be bound to the query, which means that their values will be
153
+ used for each corresponding place-holder (expressed using `?`) in the SQL
154
+ statement:
122
155
 
123
- # insert multiple rows
124
- db.execute_multi('insert into foo values (?)', ['bar', 'baz'])
125
- db.execute_multi('insert into foo values (?, ?)', [[1, 2], [3, 4]])
156
+ ```ruby
157
+ db.query('select x from my_table where y = ? and z = ?', 'foo', 'bar')
158
+ ```
126
159
 
127
- # prepared queries
128
- query = db.prepare('select ? as foo, ? as bar') #=> Extralite::Query
129
- query.bind(1, 2) #=> [{ :foo => 1, :bar => 2 }]
160
+ You can also express place holders by specifying their index (starting from 1) using `?IDX`:
130
161
 
131
- query.next #=> next row in result_set (as hash)
132
- query.next_hash #=> next row in result_set (as hash)
133
- query.next_ary #=> next row in result_set (as array)
134
- query.next_single_column #=> next row in result_set (as single value)
162
+ ```ruby
163
+ # use the same value for both place holders:
164
+ db.query('select x from my_table where y = ?1 and z = ?1', 42)
165
+ ```
135
166
 
136
- query.next(10) #=> next 10 rows in result_set (as hash)
137
- query.next_hash(10) #=> next 10 rows in result_set (as hash)
138
- query.next_ary(10) #=> next 10 rows in result_set (as array)
139
- query.next_single_column(10) #=> next 10 rows in result_set (as single value)
167
+ Another possibility is to use named parameters, which can be done by expressing place holders as `:KEY`, `@KEY` or `$KEY`:
140
168
 
141
- query.to_a #=> all rows as array of hashes
142
- query.to_a_hash #=> all rows as array of hashes
143
- query.to_a_ary #=> all rows as array of arrays
144
- query.to_a_single_column #=> all rows as array of single values
169
+ ```ruby
170
+ db.query('select x from my_table where y = $y and z = $z', y: 'foo', z: 'bar')
171
+ ```
145
172
 
146
- query.each { |r| ... } #=> iterate over all rows as hashes
147
- query.each_hash { |r| ... } #=> iterate over all rows as hashes
148
- query.each_ary { |r| ... } #=> iterate over all rows as arrays
149
- query.each_single_column { |r| ... } #=> iterate over all rows as single columns
173
+ Extralite supports specifying named parameters using `Struct` or `Data` objects:
150
174
 
151
- iterator = query.each #=> create enumerable iterator
152
- iterator.next #=> next row
153
- iterator.each { |r| ... } #=> iterate over all rows
154
- values = iterator.map { |r| r[:foo] * 10 } #=> map all rows
175
+ ```ruby
176
+ MyStruct = Struct.new(:x, :z)
177
+ params = MyStruct.new(42, 6)
178
+ db.execute('update foo set x = $x where z = $z', params)
155
179
 
156
- iterator = query.each_ary #=> create enumerable iterator with rows as arrays
157
- iterator = query.each_single_column #=> create enumerable iterator with single values
180
+ MyData = Data.define(:x, :z)
181
+ params = MyData.new(43, 3)
182
+ db.execute('update foo set x = $x where z = $z', params)
183
+ ```
158
184
 
159
- # get last insert rowid
160
- rowid = db.last_insert_rowid
185
+ Parameter binding is especially useful for preventing [SQL-injection
186
+ attacks](https://en.wikipedia.org/wiki/SQL_injection), but is also useful when
187
+ combined with [prepared queries](#prepared-queries) when repeatedly running the
188
+ same query over and over.
161
189
 
162
- # get number of rows changed in last query
163
- number_of_rows_affected = db.changes
190
+ ## Data Types
164
191
 
165
- # get column names for the given sql
166
- db.columns('select a, b, c from foo') => [:a, :b, :c]
192
+ Extralite supports the following data types for either bound parameters or row
193
+ values:
167
194
 
168
- # get db filename
169
- db.filename #=> "/tmp/my.db"
170
-
171
- # get list of tables
172
- db.tables #=> ['foo', 'bar']
173
-
174
- # get and set pragmas
175
- db.pragma(:journal_mode) #=> 'delete'
176
- db.pragma(journal_mode: 'wal')
177
- db.pragma(:journal_mode) #=> 'wal'
195
+ - `Integer`
196
+ - `Float`
197
+ - `Boolean` (see below)
198
+ - `String` (see below)
199
+ - nil
178
200
 
179
- # load an extension
180
- db.load_extension('/path/to/extension.so')
201
+ ### Boolean values
202
+
203
+ SQLite does not have a boolean data type. Extralite will automatically translate
204
+ bound parameter values of `true` or `false` to the integer values `1` and `0`,
205
+ respectively. Note that boolean values stored in the database will be fetched as
206
+ integers.
207
+
208
+ ### String values
209
+
210
+ String parameter values are translated by Extralite to either `TEXT` or `BLOB`
211
+ values according to the string encoding used. Strings with an `ASCII-8BIT` are
212
+ treated as blobs. Otherwise they are treated as text values.
213
+
214
+ Likewise, when fetching records, Extralite will convert a `BLOB` column value to
215
+ a string with `ASCII-8BIT` encoding, and a `TEXT` column value to a string with
216
+ `UTF-8` encoding.
217
+
218
+ ```ruby
219
+ # The following calls will insert blob values into the database
220
+ sql = 'insert into foo values (?)'
221
+ db.execute(sql, File.binread('/path/to/file'))
222
+ db.execute(sql, Extralite::Blob.new('Hello, 世界!'))
223
+ db.execute(sql, 'Hello, 世界!'.force_encoding(Encoding::ASCII_8BIT))
224
+ ```
225
+
226
+ ## Prepared Queries
227
+
228
+ Prepared queries (also known as prepared statements) allow you to maximize
229
+ performance and reduce memory usage when running the same query repeatedly. They
230
+ also allow you to create parameterized queries that can be repeatedly executed
231
+ with different parameters:
232
+
233
+ ```ruby
234
+ query = db.prepare('select * from foo where x = ?')
235
+
236
+ # bind parameters and get results as an array of hashes
237
+ query.bind(1).to_a
238
+ #=> [{ x: 1, y: 2, z: 3 }]
239
+ ```
240
+
241
+ ### Binding Values to Prepared Queries
242
+
243
+ To bind parameter values to the query, use the `#bind` method. The parameters will remain bound to the query until `#bind` is called again.
244
+
245
+ ```ruby
246
+ query.bind(1)
247
+
248
+ # run the query any number of times
249
+ 3.times { query.to_a }
250
+
251
+ # bind other parameter values
252
+ query.bind(4)
253
+ ```
254
+
255
+ You can also bind parameters when creating the prepared query by passing
256
+ additional parameters to the `Database#prepare` method:
257
+
258
+ ```ruby
259
+ query = db.prepare('select * from foo where x = ?', 1)
260
+ ```
261
+
262
+ ### Fetching Records from a Prepared Query
263
+
264
+ Just like the `Database` interface, prepared queries offer various ways of
265
+ retrieving records: as hashes, as arrays, as single column values, as a single
266
+ value:
267
+
268
+ ```ruby
269
+ # get records as arrays
270
+ query.to_a_ary
271
+ #=> [[1, 2, 3]]
272
+
273
+ # get records as single values
274
+ query2 = db.prepare('select z from foo where x = ?', 1)
275
+ query2.to_a_single_column
276
+ #=> [3]
277
+
278
+ # get a single value
279
+ query2.next_single_column
280
+ #=> 3
281
+ ```
282
+
283
+ ### Fetching Single Records or Batches of Records
284
+
285
+ Prepared queries let you iterate over records one by one, or by batches. For
286
+ this, use the `#next` method:
287
+
288
+ ```ruby
289
+ query = db.prepare('select * from foo')
290
+
291
+ query.next
292
+ #=> { x: 1, y: 2, z: 3 }
293
+ query.next
294
+ #=> { x: 4, y: 5, z: 6 }
295
+
296
+ # Fetch the next 10 records
297
+ query.reset # go back tpo the beginning
298
+ query.next(10)
299
+ #=> [{ x: 1, y: 2, z: 3 }, { x: 4, y: 5, z: 6 }]
300
+
301
+ # Fetch the next row as an array
302
+ query.reset
303
+ query.next_ary
304
+ #=> [1, 2, 3]
305
+
306
+ # Fetch the next row as a single column
307
+ db.prepare('select z from foo').next_single_column
308
+ #=> 3
309
+ ```
310
+
311
+ To detect the end of the result, you can use `#eof?`. To go back to the
312
+ beginning of the result set, use `#reset`. The following example shows how to
313
+ read the query results in batches of 10:
314
+
315
+ ```ruby
316
+ query.reset
317
+ while !query.eof?
318
+ records = query.next(10)
319
+ process_records(records)
320
+ end
321
+ ```
322
+
323
+ ### Iterating over Records in a Prepared Query
324
+
325
+ In addition to the `#next` method, you can also iterate over query results by
326
+ using the familiar `#each` method:
327
+
328
+ ```ruby
329
+ # iterate over records as hashes
330
+ query.each { |r| ... }
331
+
332
+ # iterate over records as arrays
333
+ query.each_ary { |r| ... }
334
+
335
+ # iterate over records as single values
336
+ query.each_single_column { |v| }
337
+ ```
338
+
339
+ ### Prepared Query as an Enumerable
340
+
341
+ You can also use a prepared query as an enumerable, allowing you to chain
342
+ enumerable method calls while iterating over the query result set. This is done
343
+ by calling `#each` without a block:
344
+
345
+ ```ruby
346
+ iterator = query.each
347
+ #=> Returns an Extralite::Iterator instance
348
+
349
+ iterator.map { |r| r[:x] *100 + r[:y] * 10 + r[:z] }
350
+ #=> [123, 345]
351
+ ```
352
+
353
+ The Iterator class includes the
354
+ [`Enumerable`](https://rubyapi.org/3.3/o/enumerable) module, with all its
355
+ methods, such as `#map`, `#select`, `#inject`, `#lazy` etc. You can also
356
+ instantiate an iterator explicitly:
357
+
358
+ ```ruby
359
+ # You need to pass the query to iterate over and the access mode (hash, ary, or
360
+ # single_column):
361
+ iterator = Extralite::Iterator(query, :hash)
362
+ ```
363
+
364
+ ## Batch Execution of Queries
365
+
366
+ Extralite provides methods for batch execution of queries, with multiple sets of
367
+ parameters. The `#batch_execute` method lets you insert or update a large number
368
+ of records with a single call:
369
+
370
+ ```ruby
371
+ values = [
372
+ [1, 11, 111],
373
+ [2, 22, 222],
374
+ [3, 33, 333]
375
+ ]
376
+ # insert the above records in one fell swoop, and returns the total number of
377
+ # changes:
378
+ db.batch_execute('insert into foo values (?, ?, ?)', values)
379
+ #=> 3
380
+ ```
381
+
382
+ Parameters to the query can also be provided by any object that is an
383
+ `Enumerable` or has an `#each` method, or any *callable* object that responds to
384
+ `#call`:
385
+
386
+ ```ruby
387
+ # Take parameter values from a Range
388
+ db.batch_execute('insert into foo values (?)', 1..10)
389
+ #=> 10
390
+
391
+ # Insert (chomped) lines from a file
392
+ File.open('foo.txt') do |f|
393
+ source = f.each_line.map(&:chomp)
394
+ db.batch_execute('insert into foo values (?)', source)
395
+ end
396
+
397
+ # Insert items from a queue
398
+ parameters = proc do
399
+ item = queue.shift
400
+ # when we're done, we return nil
401
+ (item == :done) ? nil : item
402
+ end
403
+ db.batch_execute('insert into foo values (?)', parameters)
404
+ #=> number of rows inserted
405
+ ```
406
+
407
+ Like its cousin `#execute`, the `#batch_execute` returns the total number of
408
+ changes to the database (rows inserted, deleted or udpated).
409
+
410
+ ### Batch Execution of Queries that Return Rows
411
+
412
+ Extralite also provides a `#batch_query` method that like `#batch_execute` takes
413
+ a parameter source and returns an array containing the result sets for all query
414
+ invocations. If a block is given, the result sets are passed to the block
415
+ instead.
416
+
417
+ The `#batch_query` method is especially useful for batch queries with a
418
+ `RETURNING` clause:
419
+
420
+ ```ruby
421
+ updates = [
422
+ { id: 3, price: 42 },
423
+ { id: 5, price: 43 }
424
+ ]
425
+ sql = 'update foo set price = $price where id = $id returning id, quantity'
426
+ db.batch_query(sql, updates)
427
+ #=> [[{ id: 3, quantity: 4 }], [{ id: 5, quantity: 5 }]]
428
+
429
+ # The same with a block (returns the total number of changes)
430
+ db.batch_query(sql, updates) do |rows|
431
+ p rows
432
+ #=> [{ id: 3, quantity: 4 }]
433
+ #=> [{ id: 5, quantity: 5 }]
434
+ end
435
+ #=> 2
436
+ ```
437
+
438
+ And of course, for your convenience there are also `#batch_query_ary` and
439
+ `#batch_query_single_column` methods that retrieve records as arrays or as
440
+ single values.
441
+
442
+ ### Batch Execution of Prepared Queries
443
+
444
+ Batch execution can also be done using prepared queries, using the same methods
445
+ `#batch_execute` and `#batch_query`:
446
+
447
+ ```ruby
448
+ query = db.prepare 'update foo set x = ? where z = ? returning *'
449
+
450
+ query.batch_execute([[42, 3], [43, 6]])
451
+ #=> 2
452
+
453
+ query.batch_query([[42, 3], [43, 6]])
454
+ #=> [{ x: 42, y: 2, z: 3 }, { x: 43, y: 5, z: 6 }]
455
+ ```
456
+
457
+ ## Transactions and Savepoints
458
+
459
+ All reads and writes to SQLite databases occur within a
460
+ [transaction](https://www.sqlite.org/lang_transaction.html). If no explicit
461
+ transaction has started, the submitted SQL statements passed to `#execute` or
462
+ `#query` will all run within an implicit transaction:
463
+
464
+ ```ruby
465
+ # The following two SQL statements will run in a single implicit transaction:
466
+ db.execute('insert into foo values (42); insert into bar values (43)')
467
+
468
+ # Otherwise, each call to #execute runs in a separate transaction:
469
+ db.execute('insert into foo values (42)')
470
+ db.execute('insert into bar values (43)')
471
+ ```
472
+
473
+ ### Explicit Transactions
474
+
475
+ While you can issue `BEGIN` and `COMMIT` SQL statements yourself to start and
476
+ commit explicit transactions, Extralite provides a convenient `#transaction`
477
+ method that manages starting, commiting and rolling back of transactions
478
+ automatically:
479
+
480
+ ```ruby
481
+ db.transaction do
482
+ db.execute('insert into foo values (42)')
483
+ raise 'Something bad happened' if something_bad_happened
484
+ db.execute('insert into bar values (43)')
485
+ end
486
+ ```
181
487
 
182
- # run queries in a transaction
488
+ If no exception is raised in the transaction block, the changes are commited. If
489
+ an exception is raised, the changes are rolled back and the exception is
490
+ propagated to the application code. You can prevent the exception from being
491
+ propagated by calling `#rollback!`:
492
+
493
+ ```ruby
183
494
  db.transaction do
184
- db.execute('insert into foo values (?)', 1)
185
- db.execute('insert into foo values (?)', 2)
186
- db.execute('insert into foo values (?)', 3)
495
+ db.execute('insert into foo values (42)')
496
+ rollback! if something_bad_happened
497
+ db.execute('insert into bar values (43)')
498
+ end
499
+ ```
500
+
501
+ ### Transaction Modes
502
+
503
+ By default, `#transaction` starts an `IMMEDIATE` transaction. To start a
504
+ `DEFERRED` or `EXCLUSIVE` transaction, pass the desired mode to `#transaction`:
505
+
506
+ ```ruby
507
+ # Start a DEFERRED transaction
508
+ db.transaction(:deferred) do
509
+ ...
187
510
  end
188
511
 
189
- # close database
190
- db.close
191
- db.closed? #=> true
512
+ # Start a EXCLUSIVE transaction
513
+ db.transaction(:exclusive) do
514
+ ...
515
+ end
192
516
  ```
193
517
 
194
- ## More Features
518
+ Note that running an `IMMEDIATE` or `EXCLUSIVE` transaction blocks the database
519
+ for writing (and also reading in certain cases) for the duration of the
520
+ transaction. This can cause queries to the same database on a different
521
+ connection to fail with a `BusyError` exception. This can be mitigated by
522
+ setting a [busy timeout](#dealing-with-a-busy-database).
195
523
 
196
- ### Interrupting Long-running Queries
524
+ ### Savepoints
197
525
 
198
- When running long-running queries, you can use `Database#interrupt` to interrupt
199
- the query:
526
+ In addition to transactions, SQLite also supports the use of
527
+ [savepoints](https://www.sqlite.org/lang_savepoint.html), which can be used for
528
+ more fine-grained control of changes within a transaction, and to be able to
529
+ rollback specific changes without abandoning the entire transaction:
200
530
 
201
531
  ```ruby
202
- timeout_thread = Thread.new do
203
- sleep 10
204
- db.interrupt
532
+ db.transaction do
533
+ db.execute 'insert into foo values (1)'
534
+
535
+ db.savepoint :my_savepoint
536
+ db.execute 'insert into foo values (2)'
537
+
538
+ # the following cancels the last insert
539
+ db.rollback_to :my_savepoint
540
+ db.execute 'insert into foo values (3)'
541
+
542
+ db.release :my_savepoint
205
543
  end
544
+ ```
206
545
 
207
- result = begin
208
- db.query(super_slow_sql)
209
- rescue Extralite::InterruptError
210
- nil
211
- ensure
546
+ ## Database Information
547
+
548
+ ### Getting the list of tables
549
+
550
+ To get the list of tables in a database, use the `#tables` method:
551
+
552
+ ```ruby
553
+ db.tables
554
+ #=> [...]
555
+ ```
556
+
557
+ To get the list of tables in an attached database, you can pass the database name to `#tables`:
558
+
559
+ ```ruby
560
+ db.execute "attach database 'foo.db' as foo"
561
+ db.tables('foo')
562
+ #=> [...]
563
+ ```
564
+
565
+ ### Getting the last insert row id
566
+
567
+ ```ruby
568
+ db.execute 'insert into foo values (?)', 42
569
+ db.last_insert_rowid
570
+ #=> 1
571
+ ```
572
+
573
+ ### Getting the columns names for a given query
574
+
575
+ ```ruby
576
+ db.columns('select a, b, c from foo')
577
+ #=> [:a, :b, :c]
578
+
579
+ # Columns a prepared query:
580
+ query = db.prepare('select x, y from foo')
581
+ query.columns
582
+ #=> [:x, :y]
583
+ ```
584
+
585
+ ### Pragmas
586
+
587
+ You can get or set pragma values using `#pragma`:
588
+
589
+ ```ruby
590
+ # get a pragma value:
591
+ db.pragma(:journal_mode)
592
+ #=> 'delete'
593
+
594
+ # set a pragma value:
595
+ db.pragma(journal_mode: 'wal')
596
+ db.pragma(:journal_mode)
597
+ #=> 'wal'
598
+ ```
599
+
600
+ ## Error Handling
601
+
602
+ Extralite defines various exception classes that are raised when an error is
603
+ encountered while interacting with the underlying SQLite library:
604
+
605
+ - `Extralite::SQLError` - raised when SQLite encounters an invalid SQL query.
606
+ - `Extralite::BusyError` - raised when the underlying database is locked for use
607
+ by another database connection.
608
+ - `Extralite::InterruptError` - raised when a query has been interrupted.
609
+ - `Extralite::ParameterError` - raised when an invalid parameter value has been
610
+ specified.
611
+ - `Extralite::Error` - raised on all other errors.
612
+
613
+ In addition to the above exceptions, further information about the last error
614
+ that occurred is provided by the following methods:
615
+
616
+ - `#errcode` - the [error code](https://www.sqlite.org/rescode.html) returned by
617
+ the underlying SQLite library.
618
+ - `#errmsg` - the error message for the last error. For most errors, the error
619
+ message is copied into the exception message.
620
+ - `#error_offset` - for SQL errors, the offset into the SQL string where the
621
+ error was encountered.
622
+
623
+ ## Concurrency
624
+
625
+ Extralite provides a comprehensive set of tools for dealing with concurrency
626
+ issues, and for making sure that running queries on SQLite databases does not
627
+ cause the app to freeze.
628
+
629
+ ### The Ruby GVL
630
+
631
+ In order to support multi-threading, Extralite releases the [Ruby
632
+ GVL](https://www.speedshop.co/2020/05/11/the-ruby-gvl-and-scaling.html)
633
+ periodically while running queries. This allows other threads to run while the
634
+ underlying SQLite library is busy preparing queries, fetching records and
635
+ backing up databases. By default, the GVL is when preparing the query, and once
636
+ for every 1000 iterated records. The GVL release threshold can be set separately
637
+ for each database:
638
+
639
+ ```ruby
640
+ # release the GVL on preparing the query, and every 10 records
641
+ db.gvl_release_threshold = 10
642
+
643
+ # release the GVL only when preparing the query
644
+ db.gvl_release_threshold = 0
645
+
646
+ # never release the GVL (for single-threaded apps only)
647
+ db.gvl_release_threshold = -1
648
+
649
+ db.gvl_release_threshold = nil # use default value (currently 1000)
650
+ ```
651
+
652
+ For most applications, there's no need to tune the GVL threshold value, as it
653
+ provides [excellent](#performance) performance characteristics for both
654
+ single-threaded and multi-threaded applications.
655
+
656
+ In a heavily multi-threaded application, releasing the GVL more often (lower
657
+ threshold value) will lead to less latency (for threads not running a query),
658
+ but will also hurt the throughput (for the thread running the query). Releasing
659
+ the GVL less often (higher threshold value) will lead to better throughput for
660
+ queries, while increasing latency for threads not running a query. The following
661
+ diagram demonstrates the relationship between the GVL release threshold value,
662
+ latency and throughput:
663
+
664
+ ```
665
+ less latency & throughput <<< GVL release threshold >>> more latency & throughput
666
+ ```
667
+
668
+ ### Dealing with a Busy Database
669
+
670
+ When multiple threads or processes access the same database, the database may be
671
+ locked for writing by one process, which will block other processes wishing to
672
+ write to the database. When attempting to write to a locked database, a
673
+ `Extralite::BusyError` will be raised:
674
+
675
+ ```ruby
676
+ ready = nil
677
+ locker = Thread.new do
678
+ db1 = Extralite::Database.new('my.db')
679
+ # Lock the database for 3 seconds
680
+ db1.transaction do
681
+ ready = true
682
+ sleep(3)
683
+ end
684
+ end
685
+
686
+ db2 = Extralite::Database.new('my.db')
687
+ # wait for writer1 to enter a transaction
688
+ sleep(0) while !ready
689
+ # This will raise a Extralite::BusyError
690
+ db2.transaction { }
691
+ # Extralite::BusyError!
692
+ ```
693
+
694
+ You can mitigate this by setting a busy timeout. This will cause SQLite to wait
695
+ for the database to become unlocked up to the specified timeout period:
696
+
697
+ ```ruby
698
+ # Wait for up to 5 seconds before giving up
699
+ db2.busy_timeout = 5
700
+ # Now it'll work!
701
+ db2.transaction { }
702
+ ```
703
+
704
+ For most use cases, setting the busy timeout solves the problem of failing to
705
+ run queries because of a busy database, as normally transactions are
706
+ short-lived.
707
+
708
+ However, in some cases, such as when running a multi-fibered app or when
709
+ implementing your own timeout mechanisms, you'll want to set a [progress
710
+ handler](#the-progress-handler).
711
+
712
+ ### Interrupting a Query
713
+
714
+ To interrupt an ongoing query, use the `#interrupt` method. Normally this is done from a separate thread. Here's a way to implement a timeout using `#interrupt`:
715
+
716
+ ```ruby
717
+ def run_query_with_timeout(sql, timeout)
718
+ timeout_thread = Thread.new do
719
+ t0 = Time.now
720
+ sleep(timeout)
721
+ @db.interrupt
722
+ end
723
+ result = @db.query(sql)
212
724
  timeout_thread.kill
213
- timeout_thread.join
725
+ result
214
726
  end
727
+
728
+ run_query_with_timeout('select 1 as foo', 5)
729
+ #=> [{ foo: 1 }]
730
+
731
+ # A timeout will cause a Extralite::InterruptError to be raised
732
+ run_query_with_timeout(slow_sql, 5)
733
+ #=> Extralite::InterruptError!
215
734
  ```
216
735
 
217
- ### Running transactions
736
+ You can also call `#interrupt` from within the [progress
737
+ handler](#the-progress-handler).
738
+
739
+ ### The Progress Handler
740
+
741
+ Extralite also supports setting up a progress handler, which is a piece of code
742
+ that will be called while a query is in progress, or while the database is busy.
743
+ This is useful especially when you want to implement a general purpose timeout
744
+ mechanism that deals with both a busy database and with slow queries.
745
+
746
+ The progress handler can also be used for performing any kind of operation while
747
+ a query is in progress. Here are some use cases:
218
748
 
219
- In order to run multiple queries in a single transaction, use
220
- `Database#transaction`, passing a block that runs the queries. You can specify
221
- the [transaction
222
- mode](https://www.sqlite.org/lang_transaction.html#deferred_immediate_and_exclusive_transactions).
223
- The default mode is `:immediate`:
749
+ - Interrupting queries that take too long to run.
750
+ - Interrupting queries on an exceptional condition, such as a received signal.
751
+ - Updating the UI while a query is running.
752
+ - Switching between fibers in multi-fibered apps.
753
+ - Switching between threads in multi-threaded apps.
754
+ - Instrumenting the performance of queries.
755
+
756
+ Setting the progress handler requires that Extralite hold the GVL while running
757
+ all queries. Therefore, it should be used with care. In a multi-threaded app,
758
+ you'll need to call `Thread.pass` from the progress handler in order for other
759
+ threads to be able to run while the query is in progress. The progress handler
760
+ is set per-database using `#on_progress`. This method takes a single parameter
761
+ that specifies the approximate number of SQLite VM instructions between
762
+ successive calls to the progress handler:
224
763
 
225
764
  ```ruby
226
- db.transaction { ... } # Run an immediate transaction
227
- db.transaction(:deferred) { ... } # Run a deferred transaction
228
- db.transaction(:exclusive) { ... } # Run an exclusive transaction
765
+ # Run progress handler every 100 SQLite VM instructions
766
+ db.on_progress(100) do
767
+ check_for_timeout
768
+ # Allow other threads to run
769
+ Thread.pass
770
+ end
229
771
  ```
230
772
 
231
- If an exception is raised in the given block, the transaction will be rolled
232
- back. Otherwise, it is committed.
773
+ The progress handler can be used to interrupt queries in progress. This can be
774
+ done by either calling `#interrupt`, or by raising an exception. As discussed
775
+ above, calling `#interrupt` causes the query to raise a
776
+ `Extralite::InterruptError` exception:
777
+
778
+ ```ruby
779
+ db.on_progress(100) { db.interrupt }
780
+ db.query('select 1')
781
+ #=> Extralite::InterruptError!
782
+ ```
783
+
784
+ You can also interrupt queries in progress by raising an exception. The query
785
+ will be stopped, and the exception will propagate to the call site:
786
+
787
+ ```ruby
788
+ db.on_progress(100) do
789
+ raise 'BOOM!'
790
+ end
791
+
792
+ db.query('select 1')
793
+ #=> BOOM!
794
+ ```
795
+
796
+ Here's how a timeout might be implemented using the progress handler:
797
+
798
+ ```ruby
799
+ def setup_progress_handler
800
+ @db.on_progress(100) do
801
+ raise TimeoutError if Time.now - @t0 >= @timeout
802
+ Thread.pass
803
+ end
804
+ end
805
+
806
+ # In this example, we just return nil on timeout
807
+ def run_query_with_timeout(sql, timeout)
808
+ @t0 = Time.now
809
+ @db.query(sql)
810
+ rescue TimeoutError
811
+ nil
812
+ end
813
+
814
+ run_query_with_timeout('select 1 as foo', 5)
815
+ #=> [{ foo: 1 }]
816
+
817
+ run_query_with_timeout(slow_sql, 5)
818
+ #=> nil
819
+ ```
820
+
821
+ ### Extralite and Fibers
822
+
823
+ The progress handler can also be used to switch between fibers in a
824
+ multi-fibered Ruby app, based on libraries such as
825
+ [Async](https://github.com/socketry/async) or
826
+ [Polyphony](https://github.com/digital-fabric/polyphony). A general solution
827
+ (that also works for multi-threaded apps) is to call `sleep(0)` in the progress
828
+ handler. This will work for switching between fibers using either Polyphony or
829
+ any fiber scheduler gem, such as Async et al:
830
+
831
+ ```ruby
832
+ db.on_progress(100) { sleep(0) }
833
+ ```
834
+
835
+ For Polyphony-based apps, you can also call `snooze` to allow other fibers to
836
+ run while a query is progressing. If your Polyphony app is multi-threaded,
837
+ you'll also need to call `Thread.pass` in order to allow other threads to run:
838
+
839
+ ```ruby
840
+ db.on_progress(100) do
841
+ snooze
842
+ Thread.pass
843
+ end
844
+ ```
845
+
846
+ Note that with Polyphony, once you install the progress handler, you can just
847
+ use the regular `#move_on_after` and `#cancel_after` methods to implement
848
+ timeouts for queries:
849
+
850
+ ```ruby
851
+ db.on_progress(100) { snooze }
852
+
853
+ cancel_after(3) do
854
+ db.query(long_running_query)
855
+ end
856
+ ```
857
+
858
+ ### Thread Safety
859
+
860
+ A single database instance can be safely used in multiple threads simultaneously
861
+ as long as the following conditions are met:
862
+
863
+ - No explicit transactions are used.
864
+ - Each thread issues queries by calling `Database#query_xxx`, or uses a separate
865
+ `Query` instance.
866
+ - The GVL release threshold is not `0` (i.e. the GVL is released periodically
867
+ while running queries.)
868
+
869
+ ### Use with Ractors
870
+
871
+ Extralite databases can safely be used inside ractors. Note that ractors are still an experimental feature of Ruby. A ractor has the benefit of using a separate GVL from the maine one, which allows true parallelism for Ruby apps. So when you use Extralite to access SQLite databases from within a ractor, you can do so without any considerations for what's happening outside the ractor when it runs queries.
872
+
873
+ ## Advanced Usage
874
+
875
+ ### Loading Extensions
876
+
877
+ Extensions can be loaded by calling `#load_extension`:
878
+
879
+ ```ruby
880
+ db.load_extension('/path/to/extension.so')
881
+ ```
882
+
883
+ A pretty comprehensive set of extensions can be found here:
884
+
885
+ https://github.com/nalgeon/sqlean
233
886
 
234
887
  ### Creating Backups
235
888
 
@@ -254,6 +907,51 @@ db.backup('backup.db') do |remaining, total|
254
907
  end
255
908
  ```
256
909
 
910
+ ### Working with Changesets
911
+
912
+ __Note__: as the session extension is by default disabled in SQLite
913
+ distributions, support for changesets is currently only available withthe
914
+ bundled version of Extralite, `extralite-bundle`.
915
+
916
+ Changesets can be used to track and persist changes to data in a database. They
917
+ can also be used to apply the same changes to another database, or to undo them.
918
+ To track changes to a database, use the `#track_changes` method:
919
+
920
+ ```ruby
921
+ # track changes to the foo and bar tables:
922
+ changeset = db.track_changes(:foo, :bar) do
923
+ insert_a_bunch_of_records(db)
924
+ end
925
+
926
+ # to track changes to all tables, pass nil:
927
+ changeset = db.track_changes(nil) do
928
+ insert_a_bunch_of_records(db)
929
+ end
930
+ ```
931
+
932
+ You can then apply the same changes to another database:
933
+
934
+ ```ruby
935
+ changeset.apply(some_other_db)
936
+ ```
937
+
938
+ To undo the changes, obtain an inverted changeset and apply it to the database:
939
+
940
+ ```ruby
941
+ changeset.invert.apply(db)
942
+ ```
943
+
944
+ You can also save and load the changeset:
945
+
946
+ ```ruby
947
+ # save the changeset
948
+ IO.write('my.changes', changeset.to_blob)
949
+
950
+ # load the changeset
951
+ changeset = Extralite::Changeset.new
952
+ changeset.load(IO.read('my.changes'))
953
+ ```
954
+
257
955
  ### Retrieving Status Information
258
956
 
259
957
  Extralite provides methods for retrieving status information about the sqlite
@@ -293,16 +991,6 @@ value = db.limit(Extralite::SQLITE_LIMIT_ATTACHED)
293
991
  db.limit(Extralite::SQLITE_LIMIT_ATTACHED, new_value)
294
992
  ```
295
993
 
296
- ### Setting the Busy Timeout
297
-
298
- When accessing a database concurrently it can be handy to set a busy timeout, in
299
- order to not have to deal with rescuing `Extralite::BusyError` exceptions. The
300
- timeout is given in seconds:
301
-
302
- ```ruby
303
- db.busy_timeout = 5
304
- ```
305
-
306
994
  ### Tracing SQL Statements
307
995
 
308
996
  To trace all SQL statements executed on the database, pass a block to
@@ -330,36 +1018,6 @@ p articles.to_a
330
1018
 
331
1019
  (Make sure you include `extralite` as a dependency in your `Gemfile`.)
332
1020
 
333
- ## Concurrency
334
-
335
- Extralite releases the GVL while making calls to the sqlite3 library that might
336
- block, such as when backing up a database, or when preparing a query. Extralite
337
- also releases the GVL periodically when iterating over records. By default, the
338
- GVL is released every 1000 records iterated. The GVL release threshold can be
339
- set separately for each database:
340
-
341
- ```ruby
342
- db.gvl_release_threshold = 10 # release GVL every 10 records
343
-
344
- db.gvl_release_threshold = nil # use default value (currently 1000)
345
- ```
346
-
347
- For most applications, there's no need to tune the GVL threshold value, as it
348
- provides [excellent](#performance) performance characteristics for both single-threaded and
349
- multi-threaded applications.
350
-
351
- In a heavily multi-threaded application, releasing the GVL more often (lower
352
- threshold value) will lead to less latency (for threads not running a query),
353
- but will also hurt the throughput (for the thread running the query). Releasing
354
- the GVL less often (higher threshold value) will lead to better throughput for
355
- queries, while increasing latency for threads not running a query. The following
356
- diagram demonstrates the relationship between the GVL release threshold value,
357
- latency and throughput:
358
-
359
- ```
360
- less latency & throughput <<< GVL release threshold >>> more latency & throughput
361
- ```
362
-
363
1021
  ## Performance
364
1022
 
365
1023
  A benchmark script is included, creating a table of various row counts, then
@@ -371,35 +1029,36 @@ large number of rows.
371
1029
 
372
1030
  [Benchmark source code](https://github.com/digital-fabric/extralite/blob/main/test/perf_hash.rb)
373
1031
 
374
- |Row count|sqlite3 1.6.0|Extralite 1.21|Advantage|
1032
+ |Row count|sqlite3 1.7.0|Extralite 2.5|Advantage|
375
1033
  |-:|-:|-:|-:|
376
- |10|63.7K rows/s|94.0K rows/s|__1.48x__|
377
- |1K|299.2K rows/s|1.983M rows/s|__6.63x__|
378
- |100K|185.4K rows/s|2.033M rows/s|__10.97x__|
1034
+ |10|184.9K rows/s|473.2K rows/s|__2.56x__|
1035
+ |1K|290.5K rows/s|2320.7K rows/s|__7.98x__|
1036
+ |100K|143.0K rows/s|2061.3K rows/s|__14.41x__|
379
1037
 
380
1038
  ### Rows as Arrays
381
1039
 
382
1040
  [Benchmark source code](https://github.com/digital-fabric/extralite/blob/main/test/perf_ary.rb)
383
1041
 
384
- |Row count|sqlite3 1.6.0|Extralite 1.21|Advantage|
1042
+ |Row count|sqlite3 1.7.0|Extralite 2.5|Advantage|
385
1043
  |-:|-:|-:|-:|
386
- |10|71.2K rows/s|92.1K rows/s|__1.29x__|
387
- |1K|502.1K rows/s|2.065M rows/s|__4.11x__|
388
- |100K|455.7K rows/s|2.511M rows/s|__5.51x__|
1044
+ |10|276.9K rows/s|472.3K rows/s|__1.71x__|
1045
+ |1K|615.6K rows/s|2324.3K rows/s|__3.78x__|
1046
+ |100K|477.4K rows/s|1982.7K rows/s|__4.15x__|
389
1047
 
390
1048
  ### Prepared Queries (Prepared Statements)
391
1049
 
392
- [Benchmark source code](https://github.com/digital-fabric/extralite/blob/main/test/perf_prepared.rb)
1050
+ [Benchmark source code](https://github.com/digital-fabric/extralite/blob/main/test/perf_hash_prepared.rb)
393
1051
 
394
- |Row count|sqlite3 1.6.0|Extralite 1.21|Advantage|
1052
+ |Row count|sqlite3 1.7.0|Extralite 2.5|Advantage|
395
1053
  |-:|-:|-:|-:|
396
- |10|232.2K rows/s|741.6K rows/s|__3.19x__|
397
- |1K|299.8K rows/s|2386.0M rows/s|__7.96x__|
398
- |100K|183.1K rows/s|1.893M rows/s|__10.34x__|
1054
+ |10|228.5K rows/s|707.9K rows/s|__3.10x__|
1055
+ |1K|296.5K rows/s|2396.2K rows/s|__8.08x__|
1056
+ |100K|145.9K rows/s|2107.3K rows/s|__14.45x__|
1057
+
1058
+ As those benchmarks show, Extralite is capabale of reading up to 2.4M rows per
1059
+ second, and can be more than 14 times faster than the `sqlite3` gem.
399
1060
 
400
- As those benchmarks show, Extralite is capabale of reading up to 2.5M
401
- rows/second when fetching rows as arrays, and up to 2M rows/second when fetching
402
- rows as hashes.
1061
+ Note that the benchmarks above were performed on synthetic data, in a single-threaded environment, with the GVL release threshold set to -1, which means that both Extralite and the `sqlite3` gem hold the GVL for the duration of the query.
403
1062
 
404
1063
  ## License
405
1064