extralite 2.4 → 2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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