extralite 1.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bcad0681ac1ee598acef1989d7320f4215242dcfb3dad8857e791e313b2e673b
4
- data.tar.gz: c26b4ed323da3ccd965a026b7d1c738723293412d226bda3c7c6795881b73239
3
+ metadata.gz: ad8337aebac5ef75132340d8336389fe0c927ed454b5792d57f80efa4f362664
4
+ data.tar.gz: 602ad3648312e810398fac215353b0fac5aa916ff7a3a7901ba1990c9e3fb145
5
5
  SHA512:
6
- metadata.gz: f1d5ab7ba821785f24a75cfe2e0d9d9fac0dbf6f2504321e24a89a89fc783fc3a30bc10f9ee4a97340202db04a8351e730c701d11e53643b065dee5fc009014b
7
- data.tar.gz: 31a6ad671bb88f74ae2ba02d0da1e8c23275816ce41f630e82bcb7003b7e3f06a5cd9a53abeeb215bbedee54aeac763445809d4add0906cfbce087ce2acbc5b9
6
+ metadata.gz: a5831721c27d2eb27c8433bcd286ba826861946b7894eb2c1564f449d321d2eaeec7cce13a4166b10db799f9e9e391bc2ba65961edc7b144592d1994767e69b0
7
+ data.tar.gz: ce77ee584ec89bae2819a2c0384ca7cc08daf00f405d0bf379f322d6182b3bf55bceb0ac0136d52fddd3fe5a43e4e48eb23cbfc00eef74a0439ce25eeeb529f7
data/CHANGELOG.md CHANGED
@@ -1,3 +1,20 @@
1
+ ## 1.5 2021-12-13
2
+
3
+ - Release GVL while preparing statements
4
+ - Use `sqlite3_prepare_v2` instead of deprecated `sqlite_prepare`
5
+
6
+ ## 1.4 2021-08-25
7
+
8
+ - Fix possible segfault in cleanup_stmt
9
+
10
+ ## 1.3 2021-08-17
11
+
12
+ - Pin error classes (for better compatibility with `GC.compact`)
13
+
14
+ ## 1.2 2021-06-06
15
+
16
+ - Add support for big integers
17
+
1
18
  ## 1.1 2021-06-02
2
19
 
3
20
  - Add `#close`, `#closed?` methods
data/Gemfile.lock CHANGED
@@ -1,24 +1,17 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- extralite (1.1)
4
+ extralite (1.5)
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)
@@ -56,7 +49,6 @@ PLATFORMS
56
49
  DEPENDENCIES
57
50
  extralite!
58
51
  minitest (= 5.14.4)
59
- minitest-reporters (= 1.4.2)
60
52
  pry (= 0.13.1)
61
53
  rake-compiler (= 1.1.1)
62
54
  rubocop (= 0.85.1)
data/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## What is Extralite?
8
8
 
9
- Extralite is an extra-lightweight (~365 lines of C-code) SQLite3 wrapper for
9
+ Extralite is an extra-lightweight (less than 400 lines of C-code) SQLite3 wrapper for
10
10
  Ruby. It provides a single class with a minimal set of methods to interact with
11
11
  an SQLite3 database.
12
12
 
@@ -71,6 +71,10 @@ db.filename #=> "/tmp/my.db"
71
71
 
72
72
  # load an extension
73
73
  db.load_extension('/path/to/extension.so')
74
+
75
+ # close database
76
+ db.close
77
+ db.closed? #=> true
74
78
  ```
75
79
 
76
80
  ## Why not just use the sqlite3 gem?
@@ -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;
@@ -94,7 +95,7 @@ inline VALUE get_column_value(sqlite3_stmt *stmt, int col, int type) {
94
95
  case SQLITE_NULL:
95
96
  return Qnil;
96
97
  case SQLITE_INTEGER:
97
- return INT2NUM(sqlite3_column_int(stmt, col));
98
+ return LL2NUM(sqlite3_column_int64(stmt, col));
98
99
  case SQLITE_FLOAT:
99
100
  return DBL2NUM(sqlite3_column_double(stmt, col));
100
101
  case SQLITE_TEXT:
@@ -109,12 +110,12 @@ 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;
116
117
  case T_FIXNUM:
117
- sqlite3_bind_int(stmt, pos, NUM2INT(value));
118
+ sqlite3_bind_int64(stmt, pos, NUM2LL(value));
118
119
  return;
119
120
  case T_FLOAT:
120
121
  sqlite3_bind_double(stmt, pos, NUM2DBL(value));
@@ -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
- inline void prepare_multi_stmt(sqlite3 *db, sqlite3_stmt **stmt, VALUE sql) {
172
- const char *rest = 0;
173
- const char *ptr = RSTRING_PTR(sql);
174
- const char *end = ptr + RSTRING_LEN(sql);
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
- int rc = sqlite3_prepare(db, ptr, end - ptr, stmt, &rest);
177
- if (rc) {
178
- sqlite3_finalize(*stmt);
179
- rb_raise(cSQLError, "%s", sqlite3_errmsg(db));
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
- rb_raise(cSQLError, "%s", sqlite3_errmsg(db));
200
+ case SQLITE_MISUSE:
201
+ return NULL;
192
202
  }
193
- ptr = rest;
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;
@@ -222,7 +256,7 @@ typedef struct query_ctx {
222
256
 
223
257
  VALUE cleanup_stmt(VALUE arg) {
224
258
  query_ctx *ctx = (query_ctx *)arg;
225
- sqlite3_finalize(ctx->stmt);
259
+ if (ctx->stmt) sqlite3_finalize(ctx->stmt);
226
260
  return Qnil;
227
261
  }
228
262
 
@@ -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);
@@ -470,6 +504,9 @@ void Init_Extralite() {
470
504
  cError = rb_define_class_under(mExtralite, "Error", rb_eRuntimeError);
471
505
  cSQLError = rb_define_class_under(mExtralite, "SQLError", cError);
472
506
  cBusyError = rb_define_class_under(mExtralite, "BusyError", cError);
507
+ rb_gc_register_mark_object(cError);
508
+ rb_gc_register_mark_object(cSQLError);
509
+ rb_gc_register_mark_object(cBusyError);
473
510
 
474
511
  ID_STRIP = rb_intern("strip");
475
512
  }
data/extralite.gemspec CHANGED
@@ -23,7 +23,6 @@ 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'
@@ -1,3 +1,3 @@
1
1
  module Extralite
2
- VERSION = '1.1'
2
+ VERSION = '1.5'
3
3
  end
data/test/helper.rb CHANGED
@@ -3,8 +3,3 @@
3
3
  require 'bundler/setup'
4
4
  require 'extralite'
5
5
  require 'minitest/autorun'
6
- require 'minitest/reporters'
7
-
8
- Minitest::Reporters.use! [
9
- Minitest::Reporters::SpecReporter.new
10
- ]
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
@@ -4,7 +4,7 @@ require_relative 'helper'
4
4
 
5
5
  class DatabaseTest < MiniTest::Test
6
6
  def setup
7
- @db = Extralite::Database.new('/tmp/extralite.db')
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.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-06-02 00:00:00.000000000 Z
11
+ date: 2021-12-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake-compiler
@@ -38,20 +38,6 @@ dependencies:
38
38
  - - '='
39
39
  - !ruby/object:Gem::Version
40
40
  version: 5.14.4
41
- - !ruby/object:Gem::Dependency
42
- name: minitest-reporters
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - '='
46
- - !ruby/object:Gem::Version
47
- version: 1.4.2
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - '='
53
- - !ruby/object:Gem::Version
54
- version: 1.4.2
55
41
  - !ruby/object:Gem::Dependency
56
42
  name: simplecov
57
43
  requirement: !ruby/object:Gem::Requirement
@@ -118,6 +104,7 @@ files:
118
104
  - lib/extralite.rb
119
105
  - lib/extralite/version.rb
120
106
  - test/helper.rb
107
+ - test/perf.rb
121
108
  - test/test_database.rb
122
109
  homepage: https://github.com/digital-fabric/extralite
123
110
  licenses:
@@ -146,7 +133,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
146
133
  - !ruby/object:Gem::Version
147
134
  version: '0'
148
135
  requirements: []
149
- rubygems_version: 3.1.4
136
+ rubygems_version: 3.1.6
150
137
  signing_key:
151
138
  specification_version: 4
152
139
  summary: Extra-lightweight SQLite3 wrapper for Ruby