extralite 1.3 → 1.7
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 +3 -8
- data/CHANGELOG.md +18 -0
- data/Gemfile.lock +3 -9
- data/README.md +82 -19
- data/Rakefile +1 -1
- data/ext/extralite/extralite.c +107 -27
- data/extralite.gemspec +1 -1
- data/lib/extralite/version.rb +1 -1
- data/lib/sequel/adapters/extralite.rb +376 -0
- data/test/helper.rb +0 -5
- data/test/perf.rb +51 -0
- data/test/run.rb +5 -0
- data/test/test_database.rb +108 -3
- data/test/test_sequel.rb +24 -0
- metadata +18 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cf088c359bb74b23020c8cb290cb6c65d06cedd885e4421c3e81f3c21a31ca33
|
4
|
+
data.tar.gz: a14199c2f5d07068a1a57eb0b24119964aa7a9960f23e6cb58bac39fe535dbfa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '009ccdd0998a39df02718feeecf981eb03dbf55d0dd6b2d0b8bcfb66754a0ec8fcc2ee9e4e9a2b421a63ed6fa89ae29f7c748ca370d32dae2199a01e50febabc'
|
7
|
+
data.tar.gz: e0fc9eb0f8a69dd2017c4d821430a861535fecc2c51e937c58d471098cc0c815867f514a357e2a0572eb848e02e97fac9454116b3cac2ff21abb0e479241552e
|
data/.github/workflows/test.yml
CHANGED
@@ -8,7 +8,7 @@ jobs:
|
|
8
8
|
fail-fast: false
|
9
9
|
matrix:
|
10
10
|
os: [ubuntu-latest]
|
11
|
-
ruby: [2.6, 2.7, 3.0]
|
11
|
+
ruby: [2.6, 2.7, '3.0']
|
12
12
|
|
13
13
|
name: >-
|
14
14
|
${{matrix.os}}, ${{matrix.ruby}}
|
@@ -16,15 +16,10 @@ jobs:
|
|
16
16
|
runs-on: ${{matrix.os}}
|
17
17
|
steps:
|
18
18
|
- uses: actions/checkout@v1
|
19
|
-
- uses:
|
19
|
+
- uses: ruby/setup-ruby@v1
|
20
20
|
with:
|
21
21
|
ruby-version: ${{matrix.ruby}}
|
22
|
-
|
23
|
-
run: |
|
24
|
-
gem install bundler
|
25
|
-
bundle install
|
26
|
-
- name: Show Linux kernel version
|
27
|
-
run: uname -r
|
22
|
+
bundler-cache: true # 'bundle install' and cache
|
28
23
|
- name: Compile C-extension
|
29
24
|
run: bundle exec rake compile
|
30
25
|
- name: Run tests
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,21 @@
|
|
1
|
+
## 1.7 2021-12-13
|
2
|
+
|
3
|
+
- Add extralite Sequel adapter
|
4
|
+
- Add support for binding hash parameters
|
5
|
+
|
6
|
+
## 1.6 2021-12-13
|
7
|
+
|
8
|
+
- Release GVL while fetching rows
|
9
|
+
|
10
|
+
## 1.5 2021-12-13
|
11
|
+
|
12
|
+
- Release GVL while preparing statements
|
13
|
+
- Use `sqlite3_prepare_v2` instead of deprecated `sqlite_prepare`
|
14
|
+
|
15
|
+
## 1.4 2021-08-25
|
16
|
+
|
17
|
+
- Fix possible segfault in cleanup_stmt
|
18
|
+
|
1
19
|
## 1.3 2021-08-17
|
2
20
|
|
3
21
|
- Pin error classes (for better compatibility with `GC.compact`)
|
data/Gemfile.lock
CHANGED
@@ -1,24 +1,17 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
extralite (1.
|
4
|
+
extralite (1.7)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
8
8
|
specs:
|
9
|
-
ansi (1.5.0)
|
10
9
|
ast (2.4.2)
|
11
|
-
builder (3.2.4)
|
12
10
|
coderay (1.1.3)
|
13
11
|
docile (1.4.0)
|
14
12
|
json (2.5.1)
|
15
13
|
method_source (1.0.0)
|
16
14
|
minitest (5.14.4)
|
17
|
-
minitest-reporters (1.4.2)
|
18
|
-
ansi
|
19
|
-
builder
|
20
|
-
minitest (>= 5.0)
|
21
|
-
ruby-progressbar
|
22
15
|
parallel (1.20.1)
|
23
16
|
parser (3.0.1.1)
|
24
17
|
ast (~> 2.4.1)
|
@@ -43,6 +36,7 @@ GEM
|
|
43
36
|
rubocop-ast (1.5.0)
|
44
37
|
parser (>= 3.0.1.1)
|
45
38
|
ruby-progressbar (1.11.0)
|
39
|
+
sequel (5.51.0)
|
46
40
|
simplecov (0.17.1)
|
47
41
|
docile (~> 1.1)
|
48
42
|
json (>= 1.8, < 3)
|
@@ -56,10 +50,10 @@ PLATFORMS
|
|
56
50
|
DEPENDENCIES
|
57
51
|
extralite!
|
58
52
|
minitest (= 5.14.4)
|
59
|
-
minitest-reporters (= 1.4.2)
|
60
53
|
pry (= 0.13.1)
|
61
54
|
rake-compiler (= 1.1.1)
|
62
55
|
rubocop (= 0.85.1)
|
56
|
+
sequel (= 5.51.0)
|
63
57
|
simplecov (= 0.17.1)
|
64
58
|
|
65
59
|
BUNDLED WITH
|
data/README.md
CHANGED
@@ -6,22 +6,29 @@
|
|
6
6
|
|
7
7
|
## What is Extralite?
|
8
8
|
|
9
|
-
Extralite is an extra-lightweight (less than
|
10
|
-
Ruby. It provides a single class with a minimal set of methods to
|
11
|
-
an SQLite3 database.
|
9
|
+
Extralite is an extra-lightweight (less than 430 lines of C-code) SQLite3
|
10
|
+
wrapper for Ruby. It provides a single class with a minimal set of methods to
|
11
|
+
interact with an SQLite3 database.
|
12
12
|
|
13
13
|
## Features
|
14
14
|
|
15
|
-
- A variety of methods for different data access patterns:
|
16
|
-
|
15
|
+
- A variety of methods for different data access patterns: rows as hashes, rows
|
16
|
+
as arrays, single row, single column, single value.
|
17
|
+
- Super fast - [up to 12.5x faster](#performance) than the
|
18
|
+
[sqlite3](https://github.com/sparklemotion/sqlite3-ruby) gem (see also
|
19
|
+
[comparison](#why-not-just-use-the-sqlite3-gem).)
|
20
|
+
- Improved [concurrency](#what-about-concurrency) for multithreaded apps: the
|
21
|
+
Ruby GVL is released while preparing SQL statements and while iterating over
|
22
|
+
results.
|
17
23
|
- Iterate over records with a block, or collect records into an array.
|
18
24
|
- Parameter binding.
|
19
|
-
-
|
20
|
-
creating/modifying schemas).
|
25
|
+
- Automatically execute SQL strings containing multiple semicolon-separated
|
26
|
+
queries (handy for creating/modifying schemas).
|
21
27
|
- Get last insert rowid.
|
22
28
|
- Get number of rows changed by last query.
|
23
29
|
- Load extensions (loading of extensions is autmatically enabled. You can find
|
24
30
|
some useful extensions here: https://github.com/nalgeon/sqlean.)
|
31
|
+
- Includes a [Sequel adapter](#usage-with-sequel) (an ActiveRecord)
|
25
32
|
|
26
33
|
## Usage
|
27
34
|
|
@@ -60,6 +67,11 @@ db.query_single_value("select 'foo'") #=> "foo"
|
|
60
67
|
# parameter binding (works for all query_xxx methods)
|
61
68
|
db.query_hash('select ? as foo, ? as bar', 1, 2) #=> [{ :foo => 1, :bar => 2 }]
|
62
69
|
|
70
|
+
# parameter binding of named parameters
|
71
|
+
db.query('select * from foo where bar = :bar', bar: 42)
|
72
|
+
db.query('select * from foo where bar = :bar', 'bar' => 42)
|
73
|
+
db.query('select * from foo where bar = :bar', ':bar' => 42)
|
74
|
+
|
63
75
|
# get last insert rowid
|
64
76
|
rowid = db.last_insert_id
|
65
77
|
|
@@ -77,23 +89,74 @@ db.close
|
|
77
89
|
db.closed? #=> true
|
78
90
|
```
|
79
91
|
|
92
|
+
## Usage with Sequel
|
93
|
+
|
94
|
+
Extralite includes an adapter for
|
95
|
+
[Sequel](https://github.com/jeremyevans/sequel). To use the Extralite adapter,
|
96
|
+
just use the `extralite` scheme instead of `sqlite`:
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
DB = Sequel.connect('extralite:blog.db')
|
100
|
+
articles = DB[:articles]
|
101
|
+
p articles.to_a
|
102
|
+
```
|
103
|
+
|
104
|
+
(Make sure you include `extralite` as a dependency in your `Gemfile`.)
|
105
|
+
|
80
106
|
## Why not just use the sqlite3 gem?
|
81
107
|
|
82
|
-
The sqlite3-ruby gem is a
|
83
|
-
thousands of developers. I've
|
84
|
-
|
85
|
-
Thus extralite was
|
108
|
+
The [sqlite3-ruby](https://github.com/sparklemotion/sqlite3-ruby) gem is a
|
109
|
+
popular, solid, well-maintained project, used by thousands of developers. I've
|
110
|
+
been doing a lot of work with SQLite3 databases lately, and wanted to have a
|
111
|
+
simpler API that gives me query results in a variety of ways. Thus extralite was
|
112
|
+
born.
|
113
|
+
|
114
|
+
Extralite is quite a bit [faster](#performance) than sqlite3-ruby and is also
|
115
|
+
[thread-friendly](#what-about-concurrency). On the other hand, Extralite does
|
116
|
+
not have support for defining custom functions, aggregates and collations. If
|
117
|
+
you're using those features, you'll need to stick with sqlite3-ruby.
|
118
|
+
|
119
|
+
Here's a table summarizing the differences between the two gems:
|
120
|
+
|
121
|
+
| |sqlite3-ruby|Extralite|
|
122
|
+
|-|-|-|
|
123
|
+
|API design|multiple classes|single class|
|
124
|
+
|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
|
+
|execute multiple statements|separate API (#execute_batch)|integrated|
|
126
|
+
|custom functions in Ruby|yes|no|
|
127
|
+
|custom collations|yes|no|
|
128
|
+
|custom aggregate functions|yes|no|
|
129
|
+
|Multithread friendly|no|[yes](#what-about-concurrency)|
|
130
|
+
|Code size|~2650LoC|~500LoC|
|
131
|
+
|Performance|1x|1.5x to 12.5x (see [below](#performance))|
|
86
132
|
|
87
133
|
## What about concurrency?
|
88
134
|
|
89
|
-
Extralite
|
90
|
-
|
91
|
-
|
135
|
+
Extralite releases the GVL while making blocking calls to the sqlite3 library,
|
136
|
+
that is while preparing SQL statements and fetching rows. Releasing the GVL
|
137
|
+
allows other threads to run while the sqlite3 library is busy compiling SQL into
|
138
|
+
bytecode, or fetching the next row. This does not seem to hurt Extralite's
|
139
|
+
performance:
|
140
|
+
|
141
|
+
## Performance
|
142
|
+
|
143
|
+
A benchmark script is
|
144
|
+
[included](https://github.com/digital-fabric/extralite/blob/main/test/perf.rb),
|
145
|
+
creating a table of various row counts, then fetching the entire table using
|
146
|
+
either `sqlite3` or `extralite`. This benchmark shows Extralite to be up to 12.5
|
147
|
+
times faster than `sqlite3` when fetching a large number of rows. Here are the
|
148
|
+
results (using the `sqlite3` gem performance as baseline):
|
149
|
+
|
150
|
+
|Row count|sqlite3-ruby (baseline)|Extralite (relative - rounded)|
|
151
|
+
|-:|-:|-:|
|
152
|
+
|10|1x|1.5x|
|
153
|
+
|1K|1x|7x|
|
154
|
+
|100K|1x|12.5x|
|
92
155
|
|
93
|
-
|
94
|
-
|
156
|
+
(If you're interested in checking this yourself, just run the script and let me
|
157
|
+
know if your results are different.)
|
95
158
|
|
96
|
-
##
|
159
|
+
## Contributing
|
97
160
|
|
98
|
-
|
99
|
-
|
161
|
+
Contributions in the form of issues, PRs or comments will be greatly
|
162
|
+
appreciated!
|
data/Rakefile
CHANGED
@@ -12,7 +12,7 @@ task :recompile => [:clean, :compile]
|
|
12
12
|
|
13
13
|
task :default => [:compile, :test]
|
14
14
|
task :test do
|
15
|
-
exec 'ruby test/
|
15
|
+
exec 'ruby test/run.rb'
|
16
16
|
end
|
17
17
|
|
18
18
|
CLEAN.include "**/*.o", "**/*.so", "**/*.so.*", "**/*.a", "**/*.bundle", "**/*.jar", "pkg", "tmp"
|
data/ext/extralite/extralite.c
CHANGED
@@ -1,11 +1,15 @@
|
|
1
1
|
#include <stdio.h>
|
2
2
|
#include "ruby.h"
|
3
|
+
#include "ruby/thread.h"
|
3
4
|
#include <sqlite3.h>
|
4
5
|
|
5
6
|
VALUE cError;
|
6
7
|
VALUE cSQLError;
|
7
8
|
VALUE cBusyError;
|
9
|
+
|
10
|
+
ID ID_KEYS;
|
8
11
|
ID ID_STRIP;
|
12
|
+
ID ID_TO_S;
|
9
13
|
|
10
14
|
typedef struct Database_t {
|
11
15
|
sqlite3 *sqlite3_db;
|
@@ -108,8 +112,35 @@ inline VALUE get_column_value(sqlite3_stmt *stmt, int col, int type) {
|
|
108
112
|
return Qnil;
|
109
113
|
}
|
110
114
|
|
115
|
+
static void bind_parameter_value(sqlite3_stmt *stmt, int pos, VALUE value);
|
116
|
+
|
117
|
+
static inline void bind_hash_parameter_values(sqlite3_stmt *stmt, VALUE hash) {
|
118
|
+
VALUE keys = rb_funcall(hash, ID_KEYS, 0);
|
119
|
+
int len = RARRAY_LEN(keys);
|
120
|
+
for (int i = 0; i < len; i++) {
|
121
|
+
VALUE k = RARRAY_AREF(keys, i);
|
122
|
+
VALUE v = rb_hash_aref(hash, k);
|
123
|
+
|
124
|
+
switch (TYPE(k)) {
|
125
|
+
case T_FIXNUM:
|
126
|
+
bind_parameter_value(stmt, NUM2INT(k), v);
|
127
|
+
return;
|
128
|
+
case T_SYMBOL:
|
129
|
+
k = rb_funcall(k, ID_TO_S, 0);
|
130
|
+
case T_STRING:
|
131
|
+
if(RSTRING_PTR(k)[0] != ':') k = rb_str_plus(rb_str_new2(":"), k);
|
132
|
+
int pos = sqlite3_bind_parameter_index(stmt, StringValuePtr(k));
|
133
|
+
bind_parameter_value(stmt, pos, v);
|
134
|
+
return;
|
135
|
+
default:
|
136
|
+
rb_raise(cError, "Cannot bind hash key value idx %d", i);
|
137
|
+
}
|
138
|
+
}
|
139
|
+
RB_GC_GUARD(keys);
|
140
|
+
}
|
141
|
+
|
111
142
|
static inline void bind_parameter_value(sqlite3_stmt *stmt, int pos, VALUE value) {
|
112
|
-
switch (TYPE(value)) {
|
143
|
+
switch (TYPE(value)) {
|
113
144
|
case T_NIL:
|
114
145
|
sqlite3_bind_null(stmt, pos);
|
115
146
|
return;
|
@@ -128,6 +159,9 @@ static inline void bind_parameter_value(sqlite3_stmt *stmt, int pos, VALUE value
|
|
128
159
|
case T_STRING:
|
129
160
|
sqlite3_bind_text(stmt, pos, RSTRING_PTR(value), RSTRING_LEN(value), SQLITE_TRANSIENT);
|
130
161
|
return;
|
162
|
+
case T_HASH:
|
163
|
+
bind_hash_parameter_values(stmt, value);
|
164
|
+
return;
|
131
165
|
default:
|
132
166
|
rb_raise(cError, "Cannot bind parameter at position %d", pos);
|
133
167
|
}
|
@@ -143,7 +177,7 @@ static inline void bind_all_parameters(sqlite3_stmt *stmt, int argc, VALUE *argv
|
|
143
177
|
|
144
178
|
static inline VALUE get_column_names(sqlite3_stmt *stmt, int column_count) {
|
145
179
|
VALUE arr = rb_ary_new2(column_count);
|
146
|
-
for (int i = 0; i < column_count; i++) {
|
180
|
+
for (int i = 0; i < column_count; i++) {
|
147
181
|
VALUE name = ID2SYM(rb_intern(sqlite3_column_name(stmt, i)));
|
148
182
|
rb_ary_push(arr, name);
|
149
183
|
}
|
@@ -168,36 +202,80 @@ static inline VALUE row_to_ary(sqlite3_stmt *stmt, int column_count) {
|
|
168
202
|
return row;
|
169
203
|
}
|
170
204
|
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
const char *
|
205
|
+
struct multi_stmt_ctx {
|
206
|
+
sqlite3 *db;
|
207
|
+
sqlite3_stmt **stmt;
|
208
|
+
const char *str;
|
209
|
+
int len;
|
210
|
+
int rc;
|
211
|
+
};
|
212
|
+
|
213
|
+
void *prepare_multi_stmt_without_gvl(void *ptr) {
|
214
|
+
struct multi_stmt_ctx *ctx = (struct multi_stmt_ctx *)ptr;
|
215
|
+
const char *rest = NULL;
|
216
|
+
const char *str = ctx->str;
|
217
|
+
const char *end = ctx->str + ctx->len;
|
175
218
|
while (1) {
|
176
|
-
|
177
|
-
if (rc) {
|
178
|
-
sqlite3_finalize(*stmt);
|
179
|
-
|
219
|
+
ctx->rc = sqlite3_prepare_v2(ctx->db, str, end - str, ctx->stmt, &rest);
|
220
|
+
if (ctx->rc) {
|
221
|
+
sqlite3_finalize(*ctx->stmt);
|
222
|
+
return NULL;
|
180
223
|
}
|
181
224
|
|
182
|
-
if (rest == end) return;
|
183
|
-
|
225
|
+
if (rest == end) return NULL;
|
226
|
+
|
184
227
|
// perform current query, but discard its results
|
185
|
-
rc = sqlite3_step(*stmt);
|
186
|
-
sqlite3_finalize(*stmt);
|
187
|
-
switch (rc) {
|
228
|
+
ctx->rc = sqlite3_step(*ctx->stmt);
|
229
|
+
sqlite3_finalize(*ctx->stmt);
|
230
|
+
switch (ctx->rc) {
|
188
231
|
case SQLITE_BUSY:
|
189
|
-
rb_raise(cBusyError, "Database is busy");
|
190
232
|
case SQLITE_ERROR:
|
191
|
-
|
233
|
+
case SQLITE_MISUSE:
|
234
|
+
return NULL;
|
192
235
|
}
|
193
|
-
|
236
|
+
str = rest;
|
194
237
|
}
|
238
|
+
return NULL;
|
195
239
|
}
|
196
240
|
|
197
|
-
|
241
|
+
/*
|
242
|
+
This function prepares a statement from an SQL string containing one or more SQL
|
243
|
+
statements. It will release the GVL while the statements are being prepared and
|
244
|
+
executed. All statements excluding the last one are executed. The last statement
|
245
|
+
is not executed, but instead handed back to the caller for looping over results.
|
246
|
+
*/
|
247
|
+
inline void prepare_multi_stmt(sqlite3 *db, sqlite3_stmt **stmt, VALUE sql) {
|
248
|
+
struct multi_stmt_ctx ctx = {db, stmt, RSTRING_PTR(sql), RSTRING_LEN(sql), 0};
|
249
|
+
rb_thread_call_without_gvl(prepare_multi_stmt_without_gvl, (void *)&ctx, RUBY_UBF_IO, 0);
|
250
|
+
RB_GC_GUARD(sql);
|
251
|
+
|
252
|
+
switch (ctx.rc) {
|
253
|
+
case 0:
|
254
|
+
return;
|
255
|
+
case SQLITE_BUSY:
|
256
|
+
rb_raise(cBusyError, "Database is busy");
|
257
|
+
case SQLITE_ERROR:
|
258
|
+
rb_raise(cSQLError, "%s", sqlite3_errmsg(db));
|
259
|
+
default:
|
260
|
+
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);
|
261
|
+
}
|
262
|
+
}
|
263
|
+
|
264
|
+
struct step_ctx {
|
265
|
+
sqlite3_stmt *stmt;
|
198
266
|
int rc;
|
199
|
-
|
200
|
-
|
267
|
+
};
|
268
|
+
|
269
|
+
void *stmt_iterate_without_gvl(void *ptr) {
|
270
|
+
struct step_ctx *ctx = (struct step_ctx *)ptr;
|
271
|
+
ctx->rc = sqlite3_step(ctx->stmt);
|
272
|
+
return NULL;
|
273
|
+
}
|
274
|
+
|
275
|
+
inline int stmt_iterate(sqlite3_stmt *stmt, sqlite3 *db) {
|
276
|
+
struct step_ctx ctx = {stmt, 0};
|
277
|
+
rb_thread_call_without_gvl(stmt_iterate_without_gvl, (void *)&ctx, RUBY_UBF_IO, 0);
|
278
|
+
switch (ctx.rc) {
|
201
279
|
case SQLITE_ROW:
|
202
280
|
return 1;
|
203
281
|
case SQLITE_DONE:
|
@@ -207,7 +285,7 @@ inline int stmt_iterate(sqlite3_stmt *stmt, sqlite3 *db) {
|
|
207
285
|
case SQLITE_ERROR:
|
208
286
|
rb_raise(cSQLError, "%s", sqlite3_errmsg(db));
|
209
287
|
default:
|
210
|
-
rb_raise(cError, "Invalid return code for sqlite3_step: %d", rc);
|
288
|
+
rb_raise(cError, "Invalid return code for sqlite3_step: %d (please open an issue on https://github.com/digital-fabric/extralite)", ctx.rc);
|
211
289
|
}
|
212
290
|
|
213
291
|
return 0;
|
@@ -222,7 +300,7 @@ typedef struct query_ctx {
|
|
222
300
|
|
223
301
|
VALUE cleanup_stmt(VALUE arg) {
|
224
302
|
query_ctx *ctx = (query_ctx *)arg;
|
225
|
-
sqlite3_finalize(ctx->stmt);
|
303
|
+
if (ctx->stmt) sqlite3_finalize(ctx->stmt);
|
226
304
|
return Qnil;
|
227
305
|
}
|
228
306
|
|
@@ -292,7 +370,7 @@ VALUE safe_query_ary(VALUE arg) {
|
|
292
370
|
row = row_to_ary(ctx->stmt, column_count);
|
293
371
|
if (yield_to_block) rb_yield(row); else rb_ary_push(result, row);
|
294
372
|
}
|
295
|
-
|
373
|
+
|
296
374
|
RB_GC_GUARD(row);
|
297
375
|
RB_GC_GUARD(result);
|
298
376
|
return result;
|
@@ -453,14 +531,14 @@ void Init_Extralite() {
|
|
453
531
|
rb_define_method(cDatabase, "initialize", Database_initialize, 1);
|
454
532
|
rb_define_method(cDatabase, "close", Database_close, 0);
|
455
533
|
rb_define_method(cDatabase, "closed?", Database_closed_p, 0);
|
456
|
-
|
534
|
+
|
457
535
|
rb_define_method(cDatabase, "query", Database_query_hash, -1);
|
458
536
|
rb_define_method(cDatabase, "query_hash", Database_query_hash, -1);
|
459
537
|
rb_define_method(cDatabase, "query_ary", Database_query_ary, -1);
|
460
538
|
rb_define_method(cDatabase, "query_single_row", Database_query_single_row, -1);
|
461
539
|
rb_define_method(cDatabase, "query_single_column", Database_query_single_column, -1);
|
462
540
|
rb_define_method(cDatabase, "query_single_value", Database_query_single_value, -1);
|
463
|
-
|
541
|
+
|
464
542
|
rb_define_method(cDatabase, "last_insert_rowid", Database_last_insert_rowid, 0);
|
465
543
|
rb_define_method(cDatabase, "changes", Database_changes, 0);
|
466
544
|
rb_define_method(cDatabase, "filename", Database_filename, -1);
|
@@ -474,5 +552,7 @@ void Init_Extralite() {
|
|
474
552
|
rb_gc_register_mark_object(cSQLError);
|
475
553
|
rb_gc_register_mark_object(cBusyError);
|
476
554
|
|
477
|
-
|
555
|
+
ID_KEYS = rb_intern("keys");
|
556
|
+
ID_STRIP = rb_intern("strip");
|
557
|
+
ID_TO_S = rb_intern("to_s");
|
478
558
|
}
|
data/extralite.gemspec
CHANGED
@@ -23,8 +23,8 @@ Gem::Specification.new do |s|
|
|
23
23
|
|
24
24
|
s.add_development_dependency 'rake-compiler', '1.1.1'
|
25
25
|
s.add_development_dependency 'minitest', '5.14.4'
|
26
|
-
s.add_development_dependency 'minitest-reporters', '1.4.2'
|
27
26
|
s.add_development_dependency 'simplecov', '0.17.1'
|
28
27
|
s.add_development_dependency 'rubocop', '0.85.1'
|
29
28
|
s.add_development_dependency 'pry', '0.13.1'
|
29
|
+
s.add_development_dependency 'sequel', '5.51.0'
|
30
30
|
end
|
data/lib/extralite/version.rb
CHANGED
@@ -0,0 +1,376 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
require 'extralite'
|
4
|
+
require 'sequel/adapters/shared/sqlite'
|
5
|
+
|
6
|
+
module Sequel
|
7
|
+
module Extralite
|
8
|
+
FALSE_VALUES = (%w'0 false f no n'.each(&:freeze) + [0]).freeze
|
9
|
+
|
10
|
+
blob = Object.new
|
11
|
+
def blob.call(s)
|
12
|
+
Sequel::SQL::Blob.new(s.to_s)
|
13
|
+
end
|
14
|
+
|
15
|
+
boolean = Object.new
|
16
|
+
def boolean.call(s)
|
17
|
+
s = s.downcase if s.is_a?(String)
|
18
|
+
!FALSE_VALUES.include?(s)
|
19
|
+
end
|
20
|
+
|
21
|
+
date = Object.new
|
22
|
+
def date.call(s)
|
23
|
+
case s
|
24
|
+
when String
|
25
|
+
Sequel.string_to_date(s)
|
26
|
+
when Integer
|
27
|
+
Date.jd(s)
|
28
|
+
when Float
|
29
|
+
Date.jd(s.to_i)
|
30
|
+
else
|
31
|
+
raise Sequel::Error, "unhandled type when converting to date: #{s.inspect} (#{s.class.inspect})"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
integer = Object.new
|
36
|
+
def integer.call(s)
|
37
|
+
s.to_i
|
38
|
+
end
|
39
|
+
|
40
|
+
float = Object.new
|
41
|
+
def float.call(s)
|
42
|
+
s.to_f
|
43
|
+
end
|
44
|
+
|
45
|
+
numeric = Object.new
|
46
|
+
def numeric.call(s)
|
47
|
+
s = s.to_s unless s.is_a?(String)
|
48
|
+
BigDecimal(s) rescue s
|
49
|
+
end
|
50
|
+
|
51
|
+
time = Object.new
|
52
|
+
def time.call(s)
|
53
|
+
case s
|
54
|
+
when String
|
55
|
+
Sequel.string_to_time(s)
|
56
|
+
when Integer
|
57
|
+
Sequel::SQLTime.create(s/3600, (s % 3600)/60, s % 60)
|
58
|
+
when Float
|
59
|
+
s, f = s.divmod(1)
|
60
|
+
Sequel::SQLTime.create(s/3600, (s % 3600)/60, s % 60, (f*1000000).round)
|
61
|
+
else
|
62
|
+
raise Sequel::Error, "unhandled type when converting to date: #{s.inspect} (#{s.class.inspect})"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Hash with string keys and callable values for converting SQLite types.
|
67
|
+
SQLITE_TYPES = {}
|
68
|
+
{
|
69
|
+
%w'date' => date,
|
70
|
+
%w'time' => time,
|
71
|
+
%w'bit bool boolean' => boolean,
|
72
|
+
%w'integer smallint mediumint int bigint' => integer,
|
73
|
+
%w'numeric decimal money' => numeric,
|
74
|
+
%w'float double real dec fixed' + ['double precision'] => float,
|
75
|
+
%w'blob' => blob
|
76
|
+
}.each do |k,v|
|
77
|
+
k.each{|n| SQLITE_TYPES[n] = v}
|
78
|
+
end
|
79
|
+
SQLITE_TYPES.freeze
|
80
|
+
|
81
|
+
USE_EXTENDED_RESULT_CODES = false
|
82
|
+
|
83
|
+
class Database < Sequel::Database
|
84
|
+
include ::Sequel::SQLite::DatabaseMethods
|
85
|
+
|
86
|
+
set_adapter_scheme :extralite
|
87
|
+
|
88
|
+
# Mimic the file:// uri, by having 2 preceding slashes specify a relative
|
89
|
+
# path, and 3 preceding slashes specify an absolute path.
|
90
|
+
def self.uri_to_options(uri) # :nodoc:
|
91
|
+
{ :database => (uri.host.nil? && uri.path == '/') ? nil : "#{uri.host}#{uri.path}" }
|
92
|
+
end
|
93
|
+
|
94
|
+
private_class_method :uri_to_options
|
95
|
+
|
96
|
+
# The conversion procs to use for this database
|
97
|
+
attr_reader :conversion_procs
|
98
|
+
|
99
|
+
# Connect to the database. Since SQLite is a file based database,
|
100
|
+
# available options are limited:
|
101
|
+
#
|
102
|
+
# :database :: database name (filename or ':memory:' or file: URI)
|
103
|
+
# :readonly :: open database in read-only mode; useful for reading
|
104
|
+
# static data that you do not want to modify
|
105
|
+
# :timeout :: how long to wait for the database to be available if it
|
106
|
+
# is locked, given in milliseconds (default is 5000)
|
107
|
+
def connect(server)
|
108
|
+
opts = server_opts(server)
|
109
|
+
opts[:database] = ':memory:' if blank_object?(opts[:database])
|
110
|
+
# sqlite3_opts = {}
|
111
|
+
# sqlite3_opts[:readonly] = typecast_value_boolean(opts[:readonly]) if opts.has_key?(:readonly)
|
112
|
+
db = ::Extralite::Database.new(opts[:database].to_s)#, sqlite3_opts)
|
113
|
+
# db.busy_timeout(typecast_value_integer(opts.fetch(:timeout, 5000)))
|
114
|
+
|
115
|
+
# if USE_EXTENDED_RESULT_CODES
|
116
|
+
# db.extended_result_codes = true
|
117
|
+
# end
|
118
|
+
|
119
|
+
connection_pragmas.each{|s| log_connection_yield(s, db){db.query(s)}}
|
120
|
+
|
121
|
+
# class << db
|
122
|
+
# attr_reader :prepared_statements
|
123
|
+
# end
|
124
|
+
# db.instance_variable_set(:@prepared_statements, {})
|
125
|
+
|
126
|
+
db
|
127
|
+
end
|
128
|
+
|
129
|
+
# Disconnect given connections from the database.
|
130
|
+
def disconnect_connection(c)
|
131
|
+
# c.prepared_statements.each_value{|v| v.first.close}
|
132
|
+
c.close
|
133
|
+
end
|
134
|
+
|
135
|
+
# Run the given SQL with the given arguments and yield each row.
|
136
|
+
def execute(sql, opts=OPTS, &block)
|
137
|
+
_execute(:select, sql, opts, &block)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Run the given SQL with the given arguments and return the number of changed rows.
|
141
|
+
def execute_dui(sql, opts=OPTS)
|
142
|
+
_execute(:update, sql, opts)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Drop any prepared statements on the connection when executing DDL. This is because
|
146
|
+
# prepared statements lock the table in such a way that you can't drop or alter the
|
147
|
+
# table while a prepared statement that references it still exists.
|
148
|
+
# def execute_ddl(sql, opts=OPTS)
|
149
|
+
# synchronize(opts[:server]) do |conn|
|
150
|
+
# conn.prepared_statements.values.each{|cps, s| cps.close}
|
151
|
+
# conn.prepared_statements.clear
|
152
|
+
# super
|
153
|
+
# end
|
154
|
+
# end
|
155
|
+
|
156
|
+
def execute_insert(sql, opts=OPTS)
|
157
|
+
_execute(:insert, sql, opts)
|
158
|
+
end
|
159
|
+
|
160
|
+
def freeze
|
161
|
+
@conversion_procs.freeze
|
162
|
+
super
|
163
|
+
end
|
164
|
+
|
165
|
+
# Handle Integer and Float arguments, since SQLite can store timestamps as integers and floats.
|
166
|
+
def to_application_timestamp(s)
|
167
|
+
case s
|
168
|
+
when String
|
169
|
+
super
|
170
|
+
when Integer
|
171
|
+
super(Time.at(s).to_s)
|
172
|
+
when Float
|
173
|
+
super(DateTime.jd(s).to_s)
|
174
|
+
else
|
175
|
+
raise Sequel::Error, "unhandled type when converting to : #{s.inspect} (#{s.class.inspect})"
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
private
|
180
|
+
|
181
|
+
def adapter_initialize
|
182
|
+
@conversion_procs = SQLITE_TYPES.dup
|
183
|
+
@conversion_procs['datetime'] = @conversion_procs['timestamp'] = method(:to_application_timestamp)
|
184
|
+
set_integer_booleans
|
185
|
+
end
|
186
|
+
|
187
|
+
# Yield an available connection. Rescue any Extralite::Error and turn
|
188
|
+
# them into DatabaseErrors.
|
189
|
+
def _execute(type, sql, opts, &block)
|
190
|
+
begin
|
191
|
+
synchronize(opts[:server]) do |conn|
|
192
|
+
# return execute_prepared_statement(conn, type, sql, opts, &block) if sql.is_a?(Symbol)
|
193
|
+
log_args = opts[:arguments]
|
194
|
+
args = {}
|
195
|
+
opts.fetch(:arguments, OPTS).each{|k, v| args[k] = prepared_statement_argument(v) }
|
196
|
+
case type
|
197
|
+
when :select
|
198
|
+
log_connection_yield(sql, conn, log_args){conn.query(sql, args, &block)}
|
199
|
+
when :insert
|
200
|
+
log_connection_yield(sql, conn, log_args){conn.query(sql, args)}
|
201
|
+
conn.last_insert_rowid
|
202
|
+
when :update
|
203
|
+
log_connection_yield(sql, conn, log_args){conn.query(sql, args)}
|
204
|
+
conn.changes
|
205
|
+
end
|
206
|
+
end
|
207
|
+
rescue ::Extralite::Error => e
|
208
|
+
raise_error(e)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# The SQLite adapter does not need the pool to convert exceptions.
|
213
|
+
# Also, force the max connections to 1 if a memory database is being
|
214
|
+
# used, as otherwise each connection gets a separate database.
|
215
|
+
def connection_pool_default_options
|
216
|
+
o = super.dup
|
217
|
+
# Default to only a single connection if a memory database is used,
|
218
|
+
# because otherwise each connection will get a separate database
|
219
|
+
o[:max_connections] = 1 if @opts[:database] == ':memory:' || blank_object?(@opts[:database])
|
220
|
+
o
|
221
|
+
end
|
222
|
+
|
223
|
+
def prepared_statement_argument(arg)
|
224
|
+
case arg
|
225
|
+
when Date, DateTime, Time
|
226
|
+
literal(arg)[1...-1]
|
227
|
+
when SQL::Blob
|
228
|
+
arg.to_blob
|
229
|
+
when true, false
|
230
|
+
if integer_booleans
|
231
|
+
arg ? 1 : 0
|
232
|
+
else
|
233
|
+
literal(arg)[1...-1]
|
234
|
+
end
|
235
|
+
else
|
236
|
+
arg
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
# Execute a prepared statement on the database using the given name.
|
241
|
+
def execute_prepared_statement(conn, type, name, opts, &block)
|
242
|
+
ps = prepared_statement(name)
|
243
|
+
sql = ps.prepared_sql
|
244
|
+
args = opts[:arguments]
|
245
|
+
ps_args = {}
|
246
|
+
args.each{|k, v| ps_args[k] = prepared_statement_argument(v)}
|
247
|
+
if cpsa = conn.prepared_statements[name]
|
248
|
+
cps, cps_sql = cpsa
|
249
|
+
if cps_sql != sql
|
250
|
+
cps.close
|
251
|
+
cps = nil
|
252
|
+
end
|
253
|
+
end
|
254
|
+
unless cps
|
255
|
+
cps = log_connection_yield("PREPARE #{name}: #{sql}", conn){conn.prepare(sql)}
|
256
|
+
conn.prepared_statements[name] = [cps, sql]
|
257
|
+
end
|
258
|
+
log_sql = String.new
|
259
|
+
log_sql << "EXECUTE #{name}"
|
260
|
+
if ps.log_sql
|
261
|
+
log_sql << " ("
|
262
|
+
log_sql << sql
|
263
|
+
log_sql << ")"
|
264
|
+
end
|
265
|
+
if block
|
266
|
+
log_connection_yield(log_sql, conn, args){cps.execute(ps_args, &block)}
|
267
|
+
else
|
268
|
+
log_connection_yield(log_sql, conn, args){cps.execute!(ps_args){|r|}}
|
269
|
+
case type
|
270
|
+
when :insert
|
271
|
+
conn.last_insert_rowid
|
272
|
+
when :update
|
273
|
+
conn.changes
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
# # SQLite3 raises ArgumentError in addition to SQLite3::Exception in
|
279
|
+
# # some cases, such as operations on a closed database.
|
280
|
+
def database_error_classes
|
281
|
+
#[Extralite::Error, ArgumentError]
|
282
|
+
[::Extralite::Error]
|
283
|
+
end
|
284
|
+
|
285
|
+
def dataset_class_default
|
286
|
+
Dataset
|
287
|
+
end
|
288
|
+
|
289
|
+
if USE_EXTENDED_RESULT_CODES
|
290
|
+
# Support SQLite exception codes if ruby-sqlite3 supports them.
|
291
|
+
def sqlite_error_code(exception)
|
292
|
+
exception.code if exception.respond_to?(:code)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
class Dataset < Sequel::Dataset
|
298
|
+
include ::Sequel::SQLite::DatasetMethods
|
299
|
+
|
300
|
+
module ArgumentMapper
|
301
|
+
include Sequel::Dataset::ArgumentMapper
|
302
|
+
|
303
|
+
protected
|
304
|
+
|
305
|
+
# Return a hash with the same values as the given hash,
|
306
|
+
# but with the keys converted to strings.
|
307
|
+
def map_to_prepared_args(hash)
|
308
|
+
args = {}
|
309
|
+
hash.each{|k,v| args[k.to_s.gsub('.', '__')] = v}
|
310
|
+
args
|
311
|
+
end
|
312
|
+
|
313
|
+
private
|
314
|
+
|
315
|
+
# SQLite uses a : before the name of the argument for named
|
316
|
+
# arguments.
|
317
|
+
def prepared_arg(k)
|
318
|
+
LiteralString.new("#{prepared_arg_placeholder}#{k.to_s.gsub('.', '__')}")
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
BindArgumentMethods = prepared_statements_module(:bind, ArgumentMapper)
|
323
|
+
PreparedStatementMethods = prepared_statements_module(:prepare, BindArgumentMethods)
|
324
|
+
|
325
|
+
def fetch_rows(sql, &block)
|
326
|
+
execute(sql, &block)
|
327
|
+
# execute(sql) do |result|
|
328
|
+
# cps = db.conversion_procs
|
329
|
+
# type_procs = result.types.map{|t| cps[base_type_name(t)]}
|
330
|
+
# j = -1
|
331
|
+
# cols = result.columns.map{|c| [output_identifier(c), type_procs[(j+=1)]]}
|
332
|
+
# self.columns = cols.map(&:first)
|
333
|
+
# max = cols.length
|
334
|
+
# result.each do |values|
|
335
|
+
# row = {}
|
336
|
+
# i = -1
|
337
|
+
# while (i += 1) < max
|
338
|
+
# name, type_proc = cols[i]
|
339
|
+
# v = values[i]
|
340
|
+
# if type_proc && v
|
341
|
+
# v = type_proc.call(v)
|
342
|
+
# end
|
343
|
+
# row[name] = v
|
344
|
+
# end
|
345
|
+
# yield row
|
346
|
+
# end
|
347
|
+
# end
|
348
|
+
end
|
349
|
+
|
350
|
+
private
|
351
|
+
|
352
|
+
# The base type name for a given type, without any parenthetical part.
|
353
|
+
def base_type_name(t)
|
354
|
+
(t =~ /^(.*?)\(/ ? $1 : t).downcase if t
|
355
|
+
end
|
356
|
+
|
357
|
+
# Quote the string using the adapter class method.
|
358
|
+
def literal_string_append(sql, v)
|
359
|
+
sql << "'" << v.gsub(/'/, "''") << "'"
|
360
|
+
end
|
361
|
+
|
362
|
+
def bound_variable_modules
|
363
|
+
[BindArgumentMethods]
|
364
|
+
end
|
365
|
+
|
366
|
+
def prepared_statement_modules
|
367
|
+
[PreparedStatementMethods]
|
368
|
+
end
|
369
|
+
|
370
|
+
# SQLite uses a : before the name of the argument as a placeholder.
|
371
|
+
def prepared_arg_placeholder
|
372
|
+
':'
|
373
|
+
end
|
374
|
+
end
|
375
|
+
end
|
376
|
+
end
|
data/test/helper.rb
CHANGED
data/test/perf.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/inline'
|
4
|
+
|
5
|
+
gemfile do
|
6
|
+
source 'https://rubygems.org'
|
7
|
+
gem 'sqlite3'
|
8
|
+
gem 'extralite', path: '..'
|
9
|
+
gem 'benchmark-ips'
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'benchmark/ips'
|
13
|
+
require 'fileutils'
|
14
|
+
|
15
|
+
DB_PATH = '/tmp/extralite_sqlite3_perf.db'
|
16
|
+
|
17
|
+
def prepare_database(count)
|
18
|
+
FileUtils.rm(DB_PATH) rescue nil
|
19
|
+
db = Extralite::Database.new(DB_PATH)
|
20
|
+
db.query('create table foo ( a integer primary key, b text )')
|
21
|
+
db.query('begin')
|
22
|
+
count.times { db.query('insert into foo (b) values (?)', "hello#{rand(1000)}" )}
|
23
|
+
db.query('commit')
|
24
|
+
end
|
25
|
+
|
26
|
+
def sqlite3_run(count)
|
27
|
+
db = SQLite3::Database.new(DB_PATH, :results_as_hash => true)
|
28
|
+
results = db.execute('select * from foo')
|
29
|
+
raise unless results.size == count
|
30
|
+
end
|
31
|
+
|
32
|
+
def extralite_run(count)
|
33
|
+
db = Extralite::Database.new(DB_PATH)
|
34
|
+
results = db.query('select * from foo')
|
35
|
+
raise unless results.size == count
|
36
|
+
end
|
37
|
+
|
38
|
+
[10, 1000, 100000].each do |c|
|
39
|
+
puts; puts; puts "Record count: #{c}"
|
40
|
+
|
41
|
+
prepare_database(c)
|
42
|
+
|
43
|
+
Benchmark.ips do |x|
|
44
|
+
x.config(:time => 3, :warmup => 1)
|
45
|
+
|
46
|
+
x.report("sqlite3") { sqlite3_run(c) }
|
47
|
+
x.report("extralite") { extralite_run(c) }
|
48
|
+
|
49
|
+
x.compare!
|
50
|
+
end
|
51
|
+
end
|
data/test/run.rb
ADDED
data/test/test_database.rb
CHANGED
@@ -4,7 +4,7 @@ require_relative 'helper'
|
|
4
4
|
|
5
5
|
class DatabaseTest < MiniTest::Test
|
6
6
|
def setup
|
7
|
-
@db = Extralite::Database.new('
|
7
|
+
@db = Extralite::Database.new(':memory:')
|
8
8
|
@db.query('create table if not exists t (x,y,z)')
|
9
9
|
@db.query('delete from t')
|
10
10
|
@db.query('insert into t values (1, 2, 3)')
|
@@ -55,7 +55,7 @@ class DatabaseTest < MiniTest::Test
|
|
55
55
|
def test_query_single_column
|
56
56
|
r = @db.query_single_column('select y from t')
|
57
57
|
assert_equal [2, 5], r
|
58
|
-
|
58
|
+
|
59
59
|
r = @db.query_single_column('select y from t where x = 2')
|
60
60
|
assert_equal [], r
|
61
61
|
end
|
@@ -82,6 +82,17 @@ end
|
|
82
82
|
assert_equal [1, 4, 'a', 'd'], @db.query_single_column('select x from t order by x')
|
83
83
|
end
|
84
84
|
|
85
|
+
def test_multiple_statements_with_error
|
86
|
+
error = nil
|
87
|
+
begin
|
88
|
+
@db.query("insert into t values foo; insert into t values ('d', 'e', 'f');")
|
89
|
+
rescue => error
|
90
|
+
end
|
91
|
+
|
92
|
+
assert_kind_of Extralite::SQLError, error
|
93
|
+
assert_equal 'near "foo": syntax error', error.message
|
94
|
+
end
|
95
|
+
|
85
96
|
def test_empty_sql
|
86
97
|
r = @db.query(' ')
|
87
98
|
assert_nil r
|
@@ -97,7 +108,101 @@ end
|
|
97
108
|
|
98
109
|
assert_equal @db, @db.close
|
99
110
|
assert_equal true, @db.closed?
|
100
|
-
|
111
|
+
|
101
112
|
assert_raises(Extralite::Error) { @db.query_single_value('select 42') }
|
102
113
|
end
|
114
|
+
|
115
|
+
def test_parameter_binding_simple
|
116
|
+
r = @db.query('select x, y, z from t where x = ?', 1)
|
117
|
+
assert_equal [{ x: 1, y: 2, z: 3 }], r
|
118
|
+
|
119
|
+
r = @db.query('select x, y, z from t where z = ?', 6)
|
120
|
+
assert_equal [{ x: 4, y: 5, z: 6 }], r
|
121
|
+
end
|
122
|
+
|
123
|
+
def test_parameter_binding_with_index
|
124
|
+
r = @db.query('select x, y, z from t where x = ?2', 0, 1)
|
125
|
+
assert_equal [{ x: 1, y: 2, z: 3 }], r
|
126
|
+
|
127
|
+
r = @db.query('select x, y, z from t where z = ?3', 3, 4, 6)
|
128
|
+
assert_equal [{ x: 4, y: 5, z: 6 }], r
|
129
|
+
end
|
130
|
+
|
131
|
+
def test_parameter_binding_with_name
|
132
|
+
r = @db.query('select x, y, z from t where x = :x', x: 1, y: 2)
|
133
|
+
assert_equal [{ x: 1, y: 2, z: 3 }], r
|
134
|
+
|
135
|
+
r = @db.query('select x, y, z from t where z = :zzz', 'zzz' => 6)
|
136
|
+
assert_equal [{ x: 4, y: 5, z: 6 }], r
|
137
|
+
|
138
|
+
r = @db.query('select x, y, z from t where z = :bazzz', ':bazzz' => 6)
|
139
|
+
assert_equal [{ x: 4, y: 5, z: 6 }], r
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
class ScenarioTest < MiniTest::Test
|
144
|
+
def setup
|
145
|
+
@db = Extralite::Database.new('/tmp/extralite.db')
|
146
|
+
@db.query('create table if not exists t (x,y,z)')
|
147
|
+
@db.query('delete from t')
|
148
|
+
@db.query('insert into t values (1, 2, 3)')
|
149
|
+
@db.query('insert into t values (4, 5, 6)')
|
150
|
+
end
|
151
|
+
|
152
|
+
def test_concurrent_transactions
|
153
|
+
done = false
|
154
|
+
t = Thread.new do
|
155
|
+
db = Extralite::Database.new('/tmp/extralite.db')
|
156
|
+
db.query 'begin immediate'
|
157
|
+
sleep 0.01 until done
|
158
|
+
|
159
|
+
while true
|
160
|
+
begin
|
161
|
+
db.query 'commit'
|
162
|
+
break
|
163
|
+
rescue Extralite::BusyError
|
164
|
+
sleep 0.01
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
sleep 0.1
|
170
|
+
@db.query 'begin deferred'
|
171
|
+
result = @db.query_single_column('select x from t')
|
172
|
+
assert_equal [1, 4], result
|
173
|
+
|
174
|
+
assert_raises(Extralite::BusyError) do
|
175
|
+
@db.query('insert into t values (7, 8, 9)')
|
176
|
+
end
|
177
|
+
|
178
|
+
done = true
|
179
|
+
sleep 0.1
|
180
|
+
|
181
|
+
assert_raises(Extralite::BusyError) do
|
182
|
+
@db.query('insert into t values (7, 8, 9)')
|
183
|
+
end
|
184
|
+
|
185
|
+
assert_equal true, @db.transaction_active?
|
186
|
+
|
187
|
+
# the thing to do in this case is to commit the read transaction, allowing
|
188
|
+
# the other thread to commit its write transaction, and then we can
|
189
|
+
# "upgrade" to a write transaction
|
190
|
+
|
191
|
+
@db.query('commit')
|
192
|
+
|
193
|
+
while true
|
194
|
+
begin
|
195
|
+
@db.query('begin immediate')
|
196
|
+
break
|
197
|
+
rescue Extralite::BusyError
|
198
|
+
sleep 0.1
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
@db.query('insert into t values (7, 8, 9)')
|
203
|
+
@db.query('commit')
|
204
|
+
|
205
|
+
result = @db.query_single_column('select x from t')
|
206
|
+
assert_equal [1, 4, 7], result
|
207
|
+
end
|
103
208
|
end
|
data/test/test_sequel.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'helper'
|
4
|
+
require 'sequel'
|
5
|
+
|
6
|
+
class SequelExtraliteTest < MiniTest::Test
|
7
|
+
def test_sequel
|
8
|
+
db = Sequel.connect('extralite::memory:')
|
9
|
+
db.create_table :items do
|
10
|
+
primary_key :id
|
11
|
+
String :name, unique: true, null: false
|
12
|
+
Float :price, null: false
|
13
|
+
end
|
14
|
+
|
15
|
+
items = db[:items]
|
16
|
+
|
17
|
+
items.insert(name: 'abc', price: 123)
|
18
|
+
items.insert(name: 'def', price: 456)
|
19
|
+
items.insert(name: 'ghi', price: 789)
|
20
|
+
|
21
|
+
assert_equal 3, items.count
|
22
|
+
assert_equal (123+456+789) / 3, items.avg(:price)
|
23
|
+
end
|
24
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: extralite
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '1.
|
4
|
+
version: '1.7'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sharon Rosner
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-12-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake-compiler
|
@@ -39,61 +39,61 @@ dependencies:
|
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: 5.14.4
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: simplecov
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - '='
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
47
|
+
version: 0.17.1
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - '='
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
54
|
+
version: 0.17.1
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: rubocop
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - '='
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: 0.
|
61
|
+
version: 0.85.1
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - '='
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: 0.
|
68
|
+
version: 0.85.1
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
70
|
+
name: pry
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - '='
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: 0.
|
75
|
+
version: 0.13.1
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - '='
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: 0.
|
82
|
+
version: 0.13.1
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
84
|
+
name: sequel
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
87
|
- - '='
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version:
|
89
|
+
version: 5.51.0
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
94
|
- - '='
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version:
|
96
|
+
version: 5.51.0
|
97
97
|
description:
|
98
98
|
email: sharon@noteflakes.com
|
99
99
|
executables: []
|
@@ -117,8 +117,12 @@ files:
|
|
117
117
|
- extralite.gemspec
|
118
118
|
- lib/extralite.rb
|
119
119
|
- lib/extralite/version.rb
|
120
|
+
- lib/sequel/adapters/extralite.rb
|
120
121
|
- test/helper.rb
|
122
|
+
- test/perf.rb
|
123
|
+
- test/run.rb
|
121
124
|
- test/test_database.rb
|
125
|
+
- test/test_sequel.rb
|
122
126
|
homepage: https://github.com/digital-fabric/extralite
|
123
127
|
licenses:
|
124
128
|
- MIT
|