extralite 1.4 → 1.5
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/CHANGELOG.md +5 -0
- data/Gemfile.lock +1 -1
- data/ext/extralite/extralite.c +56 -22
- data/lib/extralite/version.rb +1 -1
- data/test/perf.rb +48 -0
- data/test/test_database.rb +82 -3
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ad8337aebac5ef75132340d8336389fe0c927ed454b5792d57f80efa4f362664
|
4
|
+
data.tar.gz: 602ad3648312e810398fac215353b0fac5aa916ff7a3a7901ba1990c9e3fb145
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a5831721c27d2eb27c8433bcd286ba826861946b7894eb2c1564f449d321d2eaeec7cce13a4166b10db799f9e9e391bc2ba65961edc7b144592d1994767e69b0
|
7
|
+
data.tar.gz: ce77ee584ec89bae2819a2c0384ca7cc08daf00f405d0bf379f322d6182b3bf55bceb0ac0136d52fddd3fe5a43e4e48eb23cbfc00eef74a0439ce25eeeb529f7
|
data/CHANGELOG.md
CHANGED
data/Gemfile.lock
CHANGED
data/ext/extralite/extralite.c
CHANGED
@@ -1,5 +1,6 @@
|
|
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;
|
@@ -109,7 +110,7 @@ inline VALUE get_column_value(sqlite3_stmt *stmt, int col, int type) {
|
|
109
110
|
}
|
110
111
|
|
111
112
|
static inline void bind_parameter_value(sqlite3_stmt *stmt, int pos, VALUE value) {
|
112
|
-
switch (TYPE(value)) {
|
113
|
+
switch (TYPE(value)) {
|
113
114
|
case T_NIL:
|
114
115
|
sqlite3_bind_null(stmt, pos);
|
115
116
|
return;
|
@@ -143,7 +144,7 @@ static inline void bind_all_parameters(sqlite3_stmt *stmt, int argc, VALUE *argv
|
|
143
144
|
|
144
145
|
static inline VALUE get_column_names(sqlite3_stmt *stmt, int column_count) {
|
145
146
|
VALUE arr = rb_ary_new2(column_count);
|
146
|
-
for (int i = 0; i < column_count; i++) {
|
147
|
+
for (int i = 0; i < column_count; i++) {
|
147
148
|
VALUE name = ID2SYM(rb_intern(sqlite3_column_name(stmt, i)));
|
148
149
|
rb_ary_push(arr, name);
|
149
150
|
}
|
@@ -168,29 +169,62 @@ static inline VALUE row_to_ary(sqlite3_stmt *stmt, int column_count) {
|
|
168
169
|
return row;
|
169
170
|
}
|
170
171
|
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
const char *
|
172
|
+
struct multi_stmt_ctx {
|
173
|
+
sqlite3 *db;
|
174
|
+
sqlite3_stmt **stmt;
|
175
|
+
const char *str;
|
176
|
+
int len;
|
177
|
+
int rc;
|
178
|
+
};
|
179
|
+
|
180
|
+
void *prepare_multi_stmt_without_gvl(void *ptr) {
|
181
|
+
struct multi_stmt_ctx *ctx = (struct multi_stmt_ctx *)ptr;
|
182
|
+
const char *rest = NULL;
|
183
|
+
const char *str = ctx->str;
|
184
|
+
const char *end = ctx->str + ctx->len;
|
175
185
|
while (1) {
|
176
|
-
|
177
|
-
if (rc) {
|
178
|
-
sqlite3_finalize(*stmt);
|
179
|
-
|
186
|
+
ctx->rc = sqlite3_prepare_v2(ctx->db, str, end - str, ctx->stmt, &rest);
|
187
|
+
if (ctx->rc) {
|
188
|
+
sqlite3_finalize(*ctx->stmt);
|
189
|
+
return NULL;
|
180
190
|
}
|
181
191
|
|
182
|
-
if (rest == end) return;
|
183
|
-
|
192
|
+
if (rest == end) return NULL;
|
193
|
+
|
184
194
|
// perform current query, but discard its results
|
185
|
-
rc = sqlite3_step(*stmt);
|
186
|
-
sqlite3_finalize(*stmt);
|
187
|
-
switch (rc) {
|
195
|
+
ctx->rc = sqlite3_step(*ctx->stmt);
|
196
|
+
sqlite3_finalize(*ctx->stmt);
|
197
|
+
switch (ctx->rc) {
|
188
198
|
case SQLITE_BUSY:
|
189
|
-
rb_raise(cBusyError, "Database is busy");
|
190
199
|
case SQLITE_ERROR:
|
191
|
-
|
200
|
+
case SQLITE_MISUSE:
|
201
|
+
return NULL;
|
192
202
|
}
|
193
|
-
|
203
|
+
str = rest;
|
204
|
+
}
|
205
|
+
return NULL;
|
206
|
+
}
|
207
|
+
|
208
|
+
/*
|
209
|
+
This function prepares a statement from an SQL string containing one or more SQL
|
210
|
+
statements. It will release the GVL while the statements are being prepared and
|
211
|
+
executed. All statements excluding the last one are executed. The last statement
|
212
|
+
is not executed, but instead handed back to the caller for looping over results.
|
213
|
+
*/
|
214
|
+
inline void prepare_multi_stmt(sqlite3 *db, sqlite3_stmt **stmt, VALUE sql) {
|
215
|
+
struct multi_stmt_ctx ctx = {db, stmt, RSTRING_PTR(sql), RSTRING_LEN(sql), 0};
|
216
|
+
rb_thread_call_without_gvl(prepare_multi_stmt_without_gvl, (void *)&ctx, RUBY_UBF_IO, 0);
|
217
|
+
RB_GC_GUARD(sql);
|
218
|
+
|
219
|
+
switch (ctx.rc) {
|
220
|
+
case 0:
|
221
|
+
return;
|
222
|
+
case SQLITE_BUSY:
|
223
|
+
rb_raise(cBusyError, "Database is busy");
|
224
|
+
case SQLITE_ERROR:
|
225
|
+
rb_raise(cSQLError, "%s", sqlite3_errmsg(db));
|
226
|
+
default:
|
227
|
+
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);
|
194
228
|
}
|
195
229
|
}
|
196
230
|
|
@@ -207,7 +241,7 @@ inline int stmt_iterate(sqlite3_stmt *stmt, sqlite3 *db) {
|
|
207
241
|
case SQLITE_ERROR:
|
208
242
|
rb_raise(cSQLError, "%s", sqlite3_errmsg(db));
|
209
243
|
default:
|
210
|
-
rb_raise(cError, "Invalid return code for sqlite3_step: %d", rc);
|
244
|
+
rb_raise(cError, "Invalid return code for sqlite3_step: %d (please open an issue on https://github.com/digital-fabric/extralite)", rc);
|
211
245
|
}
|
212
246
|
|
213
247
|
return 0;
|
@@ -292,7 +326,7 @@ VALUE safe_query_ary(VALUE arg) {
|
|
292
326
|
row = row_to_ary(ctx->stmt, column_count);
|
293
327
|
if (yield_to_block) rb_yield(row); else rb_ary_push(result, row);
|
294
328
|
}
|
295
|
-
|
329
|
+
|
296
330
|
RB_GC_GUARD(row);
|
297
331
|
RB_GC_GUARD(result);
|
298
332
|
return result;
|
@@ -453,14 +487,14 @@ void Init_Extralite() {
|
|
453
487
|
rb_define_method(cDatabase, "initialize", Database_initialize, 1);
|
454
488
|
rb_define_method(cDatabase, "close", Database_close, 0);
|
455
489
|
rb_define_method(cDatabase, "closed?", Database_closed_p, 0);
|
456
|
-
|
490
|
+
|
457
491
|
rb_define_method(cDatabase, "query", Database_query_hash, -1);
|
458
492
|
rb_define_method(cDatabase, "query_hash", Database_query_hash, -1);
|
459
493
|
rb_define_method(cDatabase, "query_ary", Database_query_ary, -1);
|
460
494
|
rb_define_method(cDatabase, "query_single_row", Database_query_single_row, -1);
|
461
495
|
rb_define_method(cDatabase, "query_single_column", Database_query_single_column, -1);
|
462
496
|
rb_define_method(cDatabase, "query_single_value", Database_query_single_value, -1);
|
463
|
-
|
497
|
+
|
464
498
|
rb_define_method(cDatabase, "last_insert_rowid", Database_last_insert_rowid, 0);
|
465
499
|
rb_define_method(cDatabase, "changes", Database_changes, 0);
|
466
500
|
rb_define_method(cDatabase, "filename", Database_filename, -1);
|
data/lib/extralite/version.rb
CHANGED
data/test/perf.rb
ADDED
@@ -0,0 +1,48 @@
|
|
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
|
+
COUNT = 10000
|
17
|
+
|
18
|
+
def prepare_database
|
19
|
+
FileUtils.rm(DB_PATH) rescue nil
|
20
|
+
db = Extralite::Database.new(DB_PATH)
|
21
|
+
db.query('create table foo ( a integer primary key, b text )')
|
22
|
+
db.query('begin')
|
23
|
+
COUNT.times { db.query('insert into foo (b) values (?)', "hello#{rand(1000)}" )}
|
24
|
+
db.query('commit')
|
25
|
+
end
|
26
|
+
|
27
|
+
def sqlite3_run
|
28
|
+
db = SQLite3::Database.new(DB_PATH, :results_as_hash => true)
|
29
|
+
results = db.execute('select * from foo')
|
30
|
+
raise unless results.size == COUNT
|
31
|
+
end
|
32
|
+
|
33
|
+
def extralite_run
|
34
|
+
db = Extralite::Database.new(DB_PATH)
|
35
|
+
results = db.query('select * from foo')
|
36
|
+
raise unless results.size == COUNT
|
37
|
+
end
|
38
|
+
|
39
|
+
prepare_database
|
40
|
+
|
41
|
+
Benchmark.ips do |x|
|
42
|
+
x.config(:time => 3, :warmup => 1)
|
43
|
+
|
44
|
+
x.report("sqlite3") { sqlite3_run }
|
45
|
+
x.report("extralite") { extralite_run }
|
46
|
+
|
47
|
+
x.compare!
|
48
|
+
end
|
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,75 @@ 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
|
103
114
|
end
|
115
|
+
|
116
|
+
class ScenarioTest < MiniTest::Test
|
117
|
+
def setup
|
118
|
+
@db = Extralite::Database.new('/tmp/extralite.db')
|
119
|
+
@db.query('create table if not exists t (x,y,z)')
|
120
|
+
@db.query('delete from t')
|
121
|
+
@db.query('insert into t values (1, 2, 3)')
|
122
|
+
@db.query('insert into t values (4, 5, 6)')
|
123
|
+
end
|
124
|
+
|
125
|
+
def test_concurrent_transactions
|
126
|
+
done = false
|
127
|
+
t = Thread.new do
|
128
|
+
db = Extralite::Database.new('/tmp/extralite.db')
|
129
|
+
db.query 'begin immediate'
|
130
|
+
sleep 0.01 until done
|
131
|
+
|
132
|
+
while true
|
133
|
+
begin
|
134
|
+
db.query 'commit'
|
135
|
+
break
|
136
|
+
rescue Extralite::BusyError
|
137
|
+
sleep 0.01
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
sleep 0.1
|
143
|
+
@db.query 'begin deferred'
|
144
|
+
result = @db.query_single_column('select x from t')
|
145
|
+
assert_equal [1, 4], result
|
146
|
+
|
147
|
+
assert_raises(Extralite::BusyError) do
|
148
|
+
@db.query('insert into t values (7, 8, 9)')
|
149
|
+
end
|
150
|
+
|
151
|
+
done = true
|
152
|
+
sleep 0.1
|
153
|
+
|
154
|
+
assert_raises(Extralite::BusyError) do
|
155
|
+
@db.query('insert into t values (7, 8, 9)')
|
156
|
+
end
|
157
|
+
|
158
|
+
assert_equal true, @db.transaction_active?
|
159
|
+
|
160
|
+
# the thing to do in this case is to commit the read transaction, allowing
|
161
|
+
# the other thread to commit its write transaction, and then we can
|
162
|
+
# "upgrade" to a write transaction
|
163
|
+
|
164
|
+
@db.query('commit')
|
165
|
+
|
166
|
+
while true
|
167
|
+
begin
|
168
|
+
@db.query('begin immediate')
|
169
|
+
break
|
170
|
+
rescue Extralite::BusyError
|
171
|
+
sleep 0.1
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
@db.query('insert into t values (7, 8, 9)')
|
176
|
+
@db.query('commit')
|
177
|
+
|
178
|
+
result = @db.query_single_column('select x from t')
|
179
|
+
assert_equal [1, 4, 7], result
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
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.5'
|
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
|
@@ -104,6 +104,7 @@ files:
|
|
104
104
|
- lib/extralite.rb
|
105
105
|
- lib/extralite/version.rb
|
106
106
|
- test/helper.rb
|
107
|
+
- test/perf.rb
|
107
108
|
- test/test_database.rb
|
108
109
|
homepage: https://github.com/digital-fabric/extralite
|
109
110
|
licenses:
|