extralite 1.10 → 1.13
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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +2 -2
- data/CHANGELOG.md +9 -0
- data/Gemfile.lock +3 -3
- data/README.md +91 -32
- data/Rakefile +1 -1
- data/bin/update_sqlite_source +26 -0
- data/ext/extralite/common.c +347 -0
- data/ext/extralite/database.c +385 -0
- data/ext/extralite/extconf.rb +3 -109
- data/ext/extralite/extralite.h +59 -0
- data/ext/extralite/extralite_ext.c +4 -2
- data/ext/extralite/prepared_statement.c +238 -0
- data/ext/extralite/sqlite3.c +239246 -0
- data/ext/extralite/sqlite3.h +12802 -0
- data/extralite.gemspec +1 -1
- data/lib/extralite/version.rb +1 -1
- data/test/extensions/text.dylib +0 -0
- data/test/extensions/text.so +0 -0
- data/test/helper.rb +2 -0
- data/test/perf_hash.rb +1 -1
- data/test/perf_prepared.rb +64 -0
- data/test/test_database.rb +14 -0
- data/test/test_extralite.rb +14 -0
- data/test/test_prepared_statement.rb +165 -0
- metadata +20 -9
- data/ext/extralite/extralite.c +0 -691
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5d4d7fe119c7197de4a8cf2a7c54a98845a6bf3cbb5e5140c6b272e9b5d1e458
|
|
4
|
+
data.tar.gz: bfd3010a039dd6445cb0a0fe0c5dca12a1a7aa19d7c44ec3f89c5b8768f464f7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bc80083972aafcca792665edaeaed05dedcc8d22f3f1c277d7211d99a807344ba6ec203098aa6b4038f77318fc40aa44511d46c6d586e7ed8c75862596c7a092
|
|
7
|
+
data.tar.gz: 0a4f2d8779ba19732e1ec2f5a2367b39d0889de83fc6cf03a90cd49fae2f378ffcf7238842ca17aff4e8826c66d98158a497e200361cff6caac0ff70cde59ad9
|
data/.github/workflows/test.yml
CHANGED
data/CHANGELOG.md
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
extralite (1.
|
|
4
|
+
extralite (1.13)
|
|
5
5
|
|
|
6
6
|
GEM
|
|
7
7
|
remote: https://rubygems.org/
|
|
8
8
|
specs:
|
|
9
9
|
docile (1.4.0)
|
|
10
|
-
json (2.
|
|
10
|
+
json (2.6.1)
|
|
11
11
|
minitest (5.15.0)
|
|
12
12
|
rake (13.0.6)
|
|
13
13
|
rake-compiler (1.1.6)
|
|
@@ -34,4 +34,4 @@ DEPENDENCIES
|
|
|
34
34
|
yard (= 0.9.27)
|
|
35
35
|
|
|
36
36
|
BUNDLED WITH
|
|
37
|
-
2.
|
|
37
|
+
2.3.3
|
data/README.md
CHANGED
|
@@ -1,25 +1,46 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
<h1 align="center">
|
|
2
|
+
Extralite
|
|
3
|
+
</h1>
|
|
4
|
+
|
|
5
|
+
<h4 align="center">A fast Ruby gem for working with SQLite3 databases</h4>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="http://rubygems.org/gems/extralite">
|
|
9
|
+
<img src="https://badge.fury.io/rb/extralite.svg" alt="Ruby gem">
|
|
10
|
+
</a>
|
|
11
|
+
<a href="https://github.com/digital-fabric/extralite/actions?query=workflow%3ATests">
|
|
12
|
+
<img src="https://github.com/digital-fabric/extralite/workflows/Tests/badge.svg" alt="Tests">
|
|
13
|
+
</a>
|
|
14
|
+
<a href="https://github.com/digital-fabric/extralite/blob/master/LICENSE">
|
|
15
|
+
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License">
|
|
16
|
+
</a>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<a href="https://www.rubydoc.info/gems/extralite">DOCS</a> |
|
|
21
|
+
<a href="https://noteflakes.com/articles/2021-12-15-extralite">BLOG POST</a>
|
|
22
|
+
</p>
|
|
6
23
|
|
|
7
24
|
## What is Extralite?
|
|
8
25
|
|
|
9
|
-
Extralite is a fast, extra-lightweight (
|
|
10
|
-
wrapper for Ruby. It provides a
|
|
11
|
-
|
|
26
|
+
Extralite is a fast, extra-lightweight (about 600 lines of C-code) SQLite3
|
|
27
|
+
wrapper for Ruby. It provides a minimal set of methods for interacting with an
|
|
28
|
+
SQLite3 database, as well as prepared statements. Extralite bundles the latest
|
|
29
|
+
version of SQLite, offering access to the latest features and enhancements.
|
|
12
30
|
|
|
13
31
|
## Features
|
|
14
32
|
|
|
33
|
+
- Zero dependencies: Extralite bundles SQLite3 version
|
|
34
|
+
[3.38.0](https://sqlite.org/releaselog/3_38_0.html) - no need to install any
|
|
35
|
+
`libsqlite3` packages.
|
|
15
36
|
- A variety of methods for different data access patterns: rows as hashes, rows
|
|
16
37
|
as arrays, single row, single column, single value.
|
|
38
|
+
- Prepared statements.
|
|
17
39
|
- Super fast - [up to 12.5x faster](#performance) than the
|
|
18
40
|
[sqlite3](https://github.com/sparklemotion/sqlite3-ruby) gem (see also
|
|
19
41
|
[comparison](#why-not-just-use-the-sqlite3-gem).)
|
|
20
|
-
- Improved [concurrency](#
|
|
21
|
-
|
|
22
|
-
results.
|
|
42
|
+
- Improved [concurrency](#concurrency) for multithreaded apps: the Ruby GVL is
|
|
43
|
+
released while preparing SQL statements and while iterating over results.
|
|
23
44
|
- Iterate over records with a block, or collect records into an array.
|
|
24
45
|
- Parameter binding.
|
|
25
46
|
- Automatically execute SQL strings containing multiple semicolon-separated
|
|
@@ -28,13 +49,30 @@ interacting with an SQLite3 database.
|
|
|
28
49
|
- Get number of rows changed by last query.
|
|
29
50
|
- Load extensions (loading of extensions is autmatically enabled. You can find
|
|
30
51
|
some useful extensions here: https://github.com/nalgeon/sqlean.)
|
|
31
|
-
- Includes a [Sequel adapter](#usage-with-sequel)
|
|
52
|
+
- Includes a [Sequel adapter](#usage-with-sequel).
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
To use Extralite in your Ruby app, add the following to your `Gemfile`:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
gem 'extralite'
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
You can also run `gem install extralite` if you just want to check it out.
|
|
63
|
+
|
|
64
|
+
> **Important note**: Extralite will take a while to install (on my modest
|
|
65
|
+
> machine it takes about a minute). This is owing to the fact that Extralite
|
|
66
|
+
> bundles the sqlite3 code, which is compiled upon installation.
|
|
32
67
|
|
|
33
68
|
## Usage
|
|
34
69
|
|
|
35
70
|
```ruby
|
|
36
71
|
require 'extralite'
|
|
37
72
|
|
|
73
|
+
# get sqlite3 version
|
|
74
|
+
Extralite.sqlite3_version #=> "3.38.0"
|
|
75
|
+
|
|
38
76
|
# open a database
|
|
39
77
|
db = Extralite::Database.new('/tmp/my.db')
|
|
40
78
|
|
|
@@ -72,6 +110,12 @@ db.query('select * from foo where bar = :bar', bar: 42)
|
|
|
72
110
|
db.query('select * from foo where bar = :bar', 'bar' => 42)
|
|
73
111
|
db.query('select * from foo where bar = :bar', ':bar' => 42)
|
|
74
112
|
|
|
113
|
+
# prepared statements
|
|
114
|
+
stmt = db.prepare('select ? as foo, ? as bar') #=> Extralite::PreparedStatement
|
|
115
|
+
stmt.query(1, 2) #=> [{ :foo => 1, :bar => 2 }]
|
|
116
|
+
# PreparedStatement offers the same data access methods as the Database class,
|
|
117
|
+
# but without the sql parameter.
|
|
118
|
+
|
|
75
119
|
# get last insert rowid
|
|
76
120
|
rowid = db.last_insert_rowid
|
|
77
121
|
|
|
@@ -96,7 +140,7 @@ Extralite includes an adapter for
|
|
|
96
140
|
just use the `extralite` scheme instead of `sqlite`:
|
|
97
141
|
|
|
98
142
|
```ruby
|
|
99
|
-
DB = Sequel.connect('extralite
|
|
143
|
+
DB = Sequel.connect('extralite://blog.db')
|
|
100
144
|
articles = DB[:articles]
|
|
101
145
|
p articles.to_a
|
|
102
146
|
```
|
|
@@ -112,25 +156,27 @@ simpler API that gives me query results in a variety of ways. Thus extralite was
|
|
|
112
156
|
born.
|
|
113
157
|
|
|
114
158
|
Extralite is quite a bit [faster](#performance) than sqlite3-ruby and is also
|
|
115
|
-
[thread-friendly](#
|
|
116
|
-
|
|
117
|
-
|
|
159
|
+
[thread-friendly](#concurrency). On the other hand, Extralite does not have
|
|
160
|
+
support for defining custom functions, aggregates and collations. If you're
|
|
161
|
+
using any of those features, you'll have to stick to sqlite3-ruby.
|
|
118
162
|
|
|
119
163
|
Here's a table summarizing the differences between the two gems:
|
|
120
164
|
|
|
121
165
|
| |sqlite3-ruby|Extralite|
|
|
122
166
|
|-|-|-|
|
|
167
|
+
|SQLite3 dependency|depends on OS-installed libsqlite3|bundles latest version of SQLite3|
|
|
123
168
|
|API design|multiple classes|single class|
|
|
124
169
|
|Query results|row as hash, row as array, single row, single value|row as hash, row as array, __single column__, single row, single value|
|
|
125
|
-
|
|
|
170
|
+
|Execute multiple statements|separate API (#execute_batch)|integrated|
|
|
171
|
+
|Prepared statements|yes|yes|
|
|
126
172
|
|custom functions in Ruby|yes|no|
|
|
127
173
|
|custom collations|yes|no|
|
|
128
174
|
|custom aggregate functions|yes|no|
|
|
129
|
-
|Multithread friendly|no|[yes](#
|
|
130
|
-
|Code size|~2650LoC|~
|
|
175
|
+
|Multithread friendly|no|[yes](#concurrency)|
|
|
176
|
+
|Code size|~2650LoC|~600LoC|
|
|
131
177
|
|Performance|1x|1.5x to 12.5x (see [below](#performance))|
|
|
132
178
|
|
|
133
|
-
##
|
|
179
|
+
## Concurrency
|
|
134
180
|
|
|
135
181
|
Extralite releases the GVL while making blocking calls to the sqlite3 library,
|
|
136
182
|
that is while preparing SQL statements and fetching rows. Releasing the GVL
|
|
@@ -142,29 +188,42 @@ performance:
|
|
|
142
188
|
|
|
143
189
|
A benchmark script is included, creating a table of various row counts, then
|
|
144
190
|
fetching the entire table using either `sqlite3` or `extralite`. This benchmark
|
|
145
|
-
shows Extralite to be up to 12
|
|
146
|
-
large number of rows.
|
|
191
|
+
shows Extralite to be up to ~12 times faster than `sqlite3` when fetching a
|
|
192
|
+
large number of rows.
|
|
193
|
+
|
|
194
|
+
### Rows as hashes
|
|
195
|
+
|
|
196
|
+
[Benchmark source code](https://github.com/digital-fabric/extralite/blob/main/test/perf_hash.rb)
|
|
147
197
|
|
|
148
198
|
|Row count|sqlite3-ruby|Extralite|Advantage|
|
|
149
199
|
|-:|-:|-:|-:|
|
|
150
|
-
|10|
|
|
151
|
-
|1K|286.8K rows/s|2106.
|
|
152
|
-
|100K|
|
|
200
|
+
|10|75.3K rows/s|134.2K rows/s|__1.78x__|
|
|
201
|
+
|1K|286.8K rows/s|2106.4K rows/s|__7.35x__|
|
|
202
|
+
|100K|181.0K rows/s|2275.3K rows/s|__12.53x__|
|
|
203
|
+
|
|
204
|
+
### Rows as arrays
|
|
153
205
|
|
|
154
|
-
|
|
206
|
+
[Benchmark source code](https://github.com/digital-fabric/extralite/blob/main/test/perf_ary.rb)
|
|
155
207
|
|
|
156
208
|
|Row count|sqlite3-ruby|Extralite|Advantage|
|
|
157
209
|
|-:|-:|-:|-:|
|
|
158
|
-
|10|
|
|
210
|
+
|10|64.3K rows/s|94.0K rows/s|__1.46x__|
|
|
159
211
|
|1K|498.9K rows/s|2478.2K rows/s|__4.97x__|
|
|
160
212
|
|100K|441.1K rows/s|3023.4K rows/s|__6.85x__|
|
|
161
213
|
|
|
162
|
-
|
|
163
|
-
|
|
214
|
+
### Prepared statements
|
|
215
|
+
|
|
216
|
+
[Benchmark source code](https://github.com/digital-fabric/extralite/blob/main/test/perf_prepared.rb)
|
|
217
|
+
|
|
218
|
+
|Row count|sqlite3-ruby|Extralite|Advantage|
|
|
219
|
+
|-:|-:|-:|-:|
|
|
220
|
+
|10|241.8K rows/s|888K rows/s|__3.67x__|
|
|
221
|
+
|1K|298.6K rows/s|2606K rows/s|__8.73x__|
|
|
222
|
+
|100K|201.6K rows/s|1934K rows/s|__9.6x__|
|
|
164
223
|
|
|
165
|
-
As those benchmarks show, Extralite is capabale of reading
|
|
166
|
-
|
|
167
|
-
|
|
224
|
+
As those benchmarks show, Extralite is capabale of reading up to 3M rows/second
|
|
225
|
+
when fetching rows as arrays, and up to 2.2M rows/second when fetching
|
|
226
|
+
rows as hashes.
|
|
168
227
|
|
|
169
228
|
## Contributing
|
|
170
229
|
|
data/Rakefile
CHANGED
|
@@ -16,7 +16,7 @@ task :test do
|
|
|
16
16
|
exec 'ruby test/run.rb'
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
CLEAN.include '
|
|
19
|
+
CLEAN.include 'lib/*.o', 'lib/*.so', 'lib/*.so.*', 'lib/*.a', 'lib/*.bundle', 'lib/*.jar', 'pkg', 'tmp'
|
|
20
20
|
|
|
21
21
|
require 'yard'
|
|
22
22
|
YARD_FILES = FileList['ext/extralite/extralite.c', 'lib/extralite.rb', 'lib/sequel/adapters/extralite.rb']
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
version = ARGV[0]
|
|
5
|
+
raise "Please specify version" unless version
|
|
6
|
+
|
|
7
|
+
require 'fileutils'
|
|
8
|
+
require 'date'
|
|
9
|
+
|
|
10
|
+
FileUtils.cd '/tmp'
|
|
11
|
+
|
|
12
|
+
version_id = version.gsub('.', '')
|
|
13
|
+
version_id += '0' * (7 - version_id.length)
|
|
14
|
+
url = "https://sqlite.org/#{Date.today.year}/sqlite-amalgamation-#{version_id}.zip"
|
|
15
|
+
dest = File.expand_path('../ext/extralite', __dir__)
|
|
16
|
+
|
|
17
|
+
puts "Downloading from #{url}..."
|
|
18
|
+
`curl #{url} > #{version_id}.zip`
|
|
19
|
+
|
|
20
|
+
puts "Unzipping zip file..."
|
|
21
|
+
`unzip -o #{version_id}.zip`
|
|
22
|
+
|
|
23
|
+
puts "Copying source files"
|
|
24
|
+
`cp sqlite-amalgamation-#{version_id}/sqlite3.* #{dest}/`
|
|
25
|
+
|
|
26
|
+
puts 'Done updating source files'
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
#include <stdio.h>
|
|
2
|
+
#include "extralite.h"
|
|
3
|
+
|
|
4
|
+
static inline VALUE get_column_value(sqlite3_stmt *stmt, int col, int type) {
|
|
5
|
+
switch (type) {
|
|
6
|
+
case SQLITE_NULL:
|
|
7
|
+
return Qnil;
|
|
8
|
+
case SQLITE_INTEGER:
|
|
9
|
+
return LL2NUM(sqlite3_column_int64(stmt, col));
|
|
10
|
+
case SQLITE_FLOAT:
|
|
11
|
+
return DBL2NUM(sqlite3_column_double(stmt, col));
|
|
12
|
+
case SQLITE_TEXT:
|
|
13
|
+
return rb_str_new_cstr((char *)sqlite3_column_text(stmt, col));
|
|
14
|
+
case SQLITE_BLOB:
|
|
15
|
+
return rb_str_new((const char *)sqlite3_column_blob(stmt, col), (long)sqlite3_column_bytes(stmt, col));
|
|
16
|
+
default:
|
|
17
|
+
rb_raise(cError, "Unknown column type: %d", type);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return Qnil;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
void bind_parameter_value(sqlite3_stmt *stmt, int pos, VALUE value);
|
|
24
|
+
|
|
25
|
+
void bind_hash_parameter_values(sqlite3_stmt *stmt, VALUE hash) {
|
|
26
|
+
VALUE keys = rb_funcall(hash, ID_KEYS, 0);
|
|
27
|
+
long len = RARRAY_LEN(keys);
|
|
28
|
+
for (long i = 0; i < len; i++) {
|
|
29
|
+
VALUE k = RARRAY_AREF(keys, i);
|
|
30
|
+
VALUE v = rb_hash_aref(hash, k);
|
|
31
|
+
|
|
32
|
+
switch (TYPE(k)) {
|
|
33
|
+
case T_FIXNUM:
|
|
34
|
+
bind_parameter_value(stmt, NUM2INT(k), v);
|
|
35
|
+
break;
|
|
36
|
+
case T_SYMBOL:
|
|
37
|
+
k = rb_funcall(k, ID_TO_S, 0);
|
|
38
|
+
case T_STRING:
|
|
39
|
+
if(RSTRING_PTR(k)[0] != ':') k = rb_str_plus(rb_str_new2(":"), k);
|
|
40
|
+
int pos = sqlite3_bind_parameter_index(stmt, StringValuePtr(k));
|
|
41
|
+
bind_parameter_value(stmt, pos, v);
|
|
42
|
+
break;
|
|
43
|
+
default:
|
|
44
|
+
rb_raise(cError, "Cannot bind hash key value idx %ld", i);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
RB_GC_GUARD(keys);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
inline void bind_parameter_value(sqlite3_stmt *stmt, int pos, VALUE value) {
|
|
51
|
+
switch (TYPE(value)) {
|
|
52
|
+
case T_NIL:
|
|
53
|
+
sqlite3_bind_null(stmt, pos);
|
|
54
|
+
return;
|
|
55
|
+
case T_FIXNUM:
|
|
56
|
+
sqlite3_bind_int64(stmt, pos, NUM2LL(value));
|
|
57
|
+
return;
|
|
58
|
+
case T_FLOAT:
|
|
59
|
+
sqlite3_bind_double(stmt, pos, NUM2DBL(value));
|
|
60
|
+
return;
|
|
61
|
+
case T_TRUE:
|
|
62
|
+
sqlite3_bind_int(stmt, pos, 1);
|
|
63
|
+
return;
|
|
64
|
+
case T_FALSE:
|
|
65
|
+
sqlite3_bind_int(stmt, pos, 0);
|
|
66
|
+
return;
|
|
67
|
+
case T_STRING:
|
|
68
|
+
sqlite3_bind_text(stmt, pos, RSTRING_PTR(value), RSTRING_LEN(value), SQLITE_TRANSIENT);
|
|
69
|
+
return;
|
|
70
|
+
case T_HASH:
|
|
71
|
+
bind_hash_parameter_values(stmt, value);
|
|
72
|
+
return;
|
|
73
|
+
default:
|
|
74
|
+
rb_raise(cError, "Cannot bind parameter at position %d", pos);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
void bind_all_parameters(sqlite3_stmt *stmt, int argc, VALUE *argv) {
|
|
79
|
+
for (int i = 0; i < argc; i++) {
|
|
80
|
+
bind_parameter_value(stmt, i + 1, argv[i]);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
static inline VALUE get_column_names(sqlite3_stmt *stmt, int column_count) {
|
|
85
|
+
VALUE arr = rb_ary_new2(column_count);
|
|
86
|
+
for (int i = 0; i < column_count; i++) {
|
|
87
|
+
VALUE name = ID2SYM(rb_intern(sqlite3_column_name(stmt, i)));
|
|
88
|
+
rb_ary_push(arr, name);
|
|
89
|
+
}
|
|
90
|
+
return arr;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
static inline VALUE row_to_hash(sqlite3_stmt *stmt, int column_count, VALUE column_names) {
|
|
94
|
+
VALUE row = rb_hash_new();
|
|
95
|
+
for (int i = 0; i < column_count; i++) {
|
|
96
|
+
VALUE value = get_column_value(stmt, i, sqlite3_column_type(stmt, i));
|
|
97
|
+
rb_hash_aset(row, RARRAY_AREF(column_names, i), value);
|
|
98
|
+
}
|
|
99
|
+
return row;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
static inline VALUE row_to_ary(sqlite3_stmt *stmt, int column_count) {
|
|
103
|
+
VALUE row = rb_ary_new2(column_count);
|
|
104
|
+
for (int i = 0; i < column_count; i++) {
|
|
105
|
+
VALUE value = get_column_value(stmt, i, sqlite3_column_type(stmt, i));
|
|
106
|
+
rb_ary_push(row, value);
|
|
107
|
+
}
|
|
108
|
+
return row;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
typedef struct {
|
|
112
|
+
sqlite3 *db;
|
|
113
|
+
sqlite3_stmt **stmt;
|
|
114
|
+
const char *str;
|
|
115
|
+
long len;
|
|
116
|
+
int rc;
|
|
117
|
+
} prepare_stmt_ctx;
|
|
118
|
+
|
|
119
|
+
void *prepare_multi_stmt_without_gvl(void *ptr) {
|
|
120
|
+
prepare_stmt_ctx *ctx = (prepare_stmt_ctx *)ptr;
|
|
121
|
+
const char *rest = NULL;
|
|
122
|
+
const char *str = ctx->str;
|
|
123
|
+
const char *end = ctx->str + ctx->len;
|
|
124
|
+
while (1) {
|
|
125
|
+
ctx->rc = sqlite3_prepare_v2(ctx->db, str, end - str, ctx->stmt, &rest);
|
|
126
|
+
if (ctx->rc) {
|
|
127
|
+
// error
|
|
128
|
+
sqlite3_finalize(*ctx->stmt);
|
|
129
|
+
return NULL;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (rest == end) return NULL;
|
|
133
|
+
|
|
134
|
+
// perform current query, but discard its results
|
|
135
|
+
ctx->rc = sqlite3_step(*ctx->stmt);
|
|
136
|
+
sqlite3_finalize(*ctx->stmt);
|
|
137
|
+
switch (ctx->rc) {
|
|
138
|
+
case SQLITE_BUSY:
|
|
139
|
+
case SQLITE_ERROR:
|
|
140
|
+
case SQLITE_MISUSE:
|
|
141
|
+
return NULL;
|
|
142
|
+
}
|
|
143
|
+
str = rest;
|
|
144
|
+
}
|
|
145
|
+
return NULL;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/*
|
|
149
|
+
This function prepares a statement from an SQL string containing one or more SQL
|
|
150
|
+
statements. It will release the GVL while the statements are being prepared and
|
|
151
|
+
executed. All statements excluding the last one are executed. The last statement
|
|
152
|
+
is not executed, but instead handed back to the caller for looping over results.
|
|
153
|
+
*/
|
|
154
|
+
void prepare_multi_stmt(sqlite3 *db, sqlite3_stmt **stmt, VALUE sql) {
|
|
155
|
+
prepare_stmt_ctx ctx = {db, stmt, RSTRING_PTR(sql), RSTRING_LEN(sql), 0};
|
|
156
|
+
rb_thread_call_without_gvl(prepare_multi_stmt_without_gvl, (void *)&ctx, RUBY_UBF_IO, 0);
|
|
157
|
+
RB_GC_GUARD(sql);
|
|
158
|
+
|
|
159
|
+
switch (ctx.rc) {
|
|
160
|
+
case 0:
|
|
161
|
+
return;
|
|
162
|
+
case SQLITE_BUSY:
|
|
163
|
+
rb_raise(cBusyError, "Database is busy");
|
|
164
|
+
case SQLITE_ERROR:
|
|
165
|
+
rb_raise(cSQLError, "%s", sqlite3_errmsg(db));
|
|
166
|
+
default:
|
|
167
|
+
rb_raise(cError, "Invalid return code for prepare_multi_stmt_without_gvl: %d (please open an issue on https://github.com/digital-fabric/extralite)", ctx.rc);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
#define SQLITE_MULTI_STMT -1
|
|
172
|
+
|
|
173
|
+
void *prepare_single_stmt_without_gvl(void *ptr) {
|
|
174
|
+
prepare_stmt_ctx *ctx = (prepare_stmt_ctx *)ptr;
|
|
175
|
+
const char *rest = NULL;
|
|
176
|
+
const char *str = ctx->str;
|
|
177
|
+
const char *end = ctx->str + ctx->len;
|
|
178
|
+
|
|
179
|
+
ctx->rc = sqlite3_prepare_v2(ctx->db, str, end - str, ctx->stmt, &rest);
|
|
180
|
+
if (ctx->rc)
|
|
181
|
+
goto discard_stmt;
|
|
182
|
+
else if (rest != end) {
|
|
183
|
+
ctx->rc = SQLITE_MULTI_STMT;
|
|
184
|
+
goto discard_stmt;
|
|
185
|
+
}
|
|
186
|
+
goto end;
|
|
187
|
+
discard_stmt:
|
|
188
|
+
if (*ctx->stmt) {
|
|
189
|
+
sqlite3_finalize(*ctx->stmt);
|
|
190
|
+
*ctx->stmt = NULL;
|
|
191
|
+
}
|
|
192
|
+
end:
|
|
193
|
+
return NULL;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
void prepare_single_stmt(sqlite3 *db, sqlite3_stmt **stmt, VALUE sql) {
|
|
197
|
+
prepare_stmt_ctx ctx = {db, stmt, RSTRING_PTR(sql), RSTRING_LEN(sql), 0};
|
|
198
|
+
rb_thread_call_without_gvl(prepare_single_stmt_without_gvl, (void *)&ctx, RUBY_UBF_IO, 0);
|
|
199
|
+
RB_GC_GUARD(sql);
|
|
200
|
+
|
|
201
|
+
switch (ctx.rc) {
|
|
202
|
+
case 0:
|
|
203
|
+
return;
|
|
204
|
+
case SQLITE_BUSY:
|
|
205
|
+
rb_raise(cBusyError, "Database is busy");
|
|
206
|
+
case SQLITE_ERROR:
|
|
207
|
+
rb_raise(cSQLError, "%s", sqlite3_errmsg(db));
|
|
208
|
+
case SQLITE_MULTI_STMT:
|
|
209
|
+
rb_raise(cSQLError, "A prepared statement does not accept SQL strings with multiple queries");
|
|
210
|
+
default:
|
|
211
|
+
rb_raise(cError, "Invalid return code for prepare_multi_stmt_without_gvl: %d (please open an issue on https://github.com/digital-fabric/extralite)", ctx.rc);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
struct step_ctx {
|
|
216
|
+
sqlite3_stmt *stmt;
|
|
217
|
+
int rc;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
void *stmt_iterate_without_gvl(void *ptr) {
|
|
221
|
+
struct step_ctx *ctx = (struct step_ctx *)ptr;
|
|
222
|
+
ctx->rc = sqlite3_step(ctx->stmt);
|
|
223
|
+
return NULL;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
inline int stmt_iterate(sqlite3_stmt *stmt, sqlite3 *db) {
|
|
227
|
+
struct step_ctx ctx = {stmt, 0};
|
|
228
|
+
rb_thread_call_without_gvl(stmt_iterate_without_gvl, (void *)&ctx, RUBY_UBF_IO, 0);
|
|
229
|
+
switch (ctx.rc) {
|
|
230
|
+
case SQLITE_ROW:
|
|
231
|
+
return 1;
|
|
232
|
+
case SQLITE_DONE:
|
|
233
|
+
return 0;
|
|
234
|
+
case SQLITE_BUSY:
|
|
235
|
+
rb_raise(cBusyError, "Database is busy");
|
|
236
|
+
case SQLITE_ERROR:
|
|
237
|
+
rb_raise(cSQLError, "%s", sqlite3_errmsg(db));
|
|
238
|
+
default:
|
|
239
|
+
rb_raise(cError, "Invalid return code for sqlite3_step: %d (please open an issue on https://github.com/digital-fabric/extralite)", ctx.rc);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
VALUE cleanup_stmt(query_ctx *ctx) {
|
|
246
|
+
if (ctx->stmt) sqlite3_finalize(ctx->stmt);
|
|
247
|
+
return Qnil;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
VALUE safe_query_hash(query_ctx *ctx) {
|
|
251
|
+
VALUE result = ctx->self;
|
|
252
|
+
int yield_to_block = rb_block_given_p();
|
|
253
|
+
VALUE row;
|
|
254
|
+
int column_count;
|
|
255
|
+
VALUE column_names;
|
|
256
|
+
|
|
257
|
+
column_count = sqlite3_column_count(ctx->stmt);
|
|
258
|
+
column_names = get_column_names(ctx->stmt, column_count);
|
|
259
|
+
|
|
260
|
+
// block not given, so prepare the array of records to be returned
|
|
261
|
+
if (!yield_to_block) result = rb_ary_new();
|
|
262
|
+
|
|
263
|
+
while (stmt_iterate(ctx->stmt, ctx->sqlite3_db)) {
|
|
264
|
+
row = row_to_hash(ctx->stmt, column_count, column_names);
|
|
265
|
+
if (yield_to_block) rb_yield(row); else rb_ary_push(result, row);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
RB_GC_GUARD(column_names);
|
|
269
|
+
RB_GC_GUARD(row);
|
|
270
|
+
RB_GC_GUARD(result);
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
VALUE safe_query_ary(query_ctx *ctx) {
|
|
275
|
+
int column_count;
|
|
276
|
+
VALUE result = ctx->self;
|
|
277
|
+
int yield_to_block = rb_block_given_p();
|
|
278
|
+
VALUE row;
|
|
279
|
+
|
|
280
|
+
column_count = sqlite3_column_count(ctx->stmt);
|
|
281
|
+
|
|
282
|
+
// block not given, so prepare the array of records to be returned
|
|
283
|
+
if (!yield_to_block) result = rb_ary_new();
|
|
284
|
+
|
|
285
|
+
while (stmt_iterate(ctx->stmt, ctx->sqlite3_db)) {
|
|
286
|
+
row = row_to_ary(ctx->stmt, column_count);
|
|
287
|
+
if (yield_to_block) rb_yield(row); else rb_ary_push(result, row);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
RB_GC_GUARD(row);
|
|
291
|
+
RB_GC_GUARD(result);
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
VALUE safe_query_single_row(query_ctx *ctx) {
|
|
296
|
+
int column_count;
|
|
297
|
+
VALUE row = Qnil;
|
|
298
|
+
VALUE column_names;
|
|
299
|
+
|
|
300
|
+
column_count = sqlite3_column_count(ctx->stmt);
|
|
301
|
+
column_names = get_column_names(ctx->stmt, column_count);
|
|
302
|
+
|
|
303
|
+
if (stmt_iterate(ctx->stmt, ctx->sqlite3_db))
|
|
304
|
+
row = row_to_hash(ctx->stmt, column_count, column_names);
|
|
305
|
+
|
|
306
|
+
RB_GC_GUARD(row);
|
|
307
|
+
RB_GC_GUARD(column_names);
|
|
308
|
+
return row;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
VALUE safe_query_single_column(query_ctx *ctx) {
|
|
312
|
+
int column_count;
|
|
313
|
+
VALUE result = ctx->self;
|
|
314
|
+
int yield_to_block = rb_block_given_p();
|
|
315
|
+
VALUE value;
|
|
316
|
+
|
|
317
|
+
column_count = sqlite3_column_count(ctx->stmt);
|
|
318
|
+
if (column_count != 1)
|
|
319
|
+
rb_raise(cError, "Expected query result to have 1 column");
|
|
320
|
+
|
|
321
|
+
// block not given, so prepare the array of records to be returned
|
|
322
|
+
if (!yield_to_block) result = rb_ary_new();
|
|
323
|
+
|
|
324
|
+
while (stmt_iterate(ctx->stmt, ctx->sqlite3_db)) {
|
|
325
|
+
value = get_column_value(ctx->stmt, 0, sqlite3_column_type(ctx->stmt, 0));
|
|
326
|
+
if (yield_to_block) rb_yield(value); else rb_ary_push(result, value);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
RB_GC_GUARD(value);
|
|
330
|
+
RB_GC_GUARD(result);
|
|
331
|
+
return result;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
VALUE safe_query_single_value(query_ctx *ctx) {
|
|
335
|
+
int column_count;
|
|
336
|
+
VALUE value = Qnil;
|
|
337
|
+
|
|
338
|
+
column_count = sqlite3_column_count(ctx->stmt);
|
|
339
|
+
if (column_count != 1)
|
|
340
|
+
rb_raise(cError, "Expected query result to have 1 column");
|
|
341
|
+
|
|
342
|
+
if (stmt_iterate(ctx->stmt, ctx->sqlite3_db))
|
|
343
|
+
value = get_column_value(ctx->stmt, 0, sqlite3_column_type(ctx->stmt, 0));
|
|
344
|
+
|
|
345
|
+
RB_GC_GUARD(value);
|
|
346
|
+
return value;
|
|
347
|
+
}
|