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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b9cad52b4d9e4807384a8e1b3f9b9ab521c539cbc705bcf475fb24f8e3aa462c
4
- data.tar.gz: '04792f9ce5e23c21509a7de369152501972e4a20167988f12211aa6d23b901a3'
3
+ metadata.gz: cf088c359bb74b23020c8cb290cb6c65d06cedd885e4421c3e81f3c21a31ca33
4
+ data.tar.gz: a14199c2f5d07068a1a57eb0b24119964aa7a9960f23e6cb58bac39fe535dbfa
5
5
  SHA512:
6
- metadata.gz: 1e8fac21c746aa4fe8ebd8b09f12bf8c4fb71f2f888af948f90b83ae26fdebbaec67c9e1d5d4c7fda93e22152631579e6aa6cf37d8d6352492dd6ff9e5a1d255
7
- data.tar.gz: 9a9df25229c0939df53f047426bd7df56359e81e187b0ba0ffd98ff1e8c5fd676ac828ebba01aae66524391166968b12b46ebea625775b1414f018ff5f161b08
6
+ metadata.gz: '009ccdd0998a39df02718feeecf981eb03dbf55d0dd6b2d0b8bcfb66754a0ec8fcc2ee9e4e9a2b421a63ed6fa89ae29f7c748ca370d32dae2199a01e50febabc'
7
+ data.tar.gz: e0fc9eb0f8a69dd2017c4d821430a861535fecc2c51e937c58d471098cc0c815867f514a357e2a0572eb848e02e97fac9454116b3cac2ff21abb0e479241552e
@@ -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: actions/setup-ruby@v1
19
+ - uses: ruby/setup-ruby@v1
20
20
  with:
21
21
  ruby-version: ${{matrix.ruby}}
22
- - name: Install dependencies
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.3)
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 400 lines of C-code) SQLite3 wrapper for
10
- Ruby. It provides a single class with a minimal set of methods to interact with
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: row as hash, row as
16
- array, single single row, single column, single value.
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
- - Correctly execute strings with multiple semicolon-separated queries (handy for
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 popular, solid, well-maintained project, used by
83
- thousands of developers. I've been doing a lot of work with SQLite3 lately, and
84
- wanted to have a simpler API that gives me query results in a variety of ways.
85
- Thus extralite was born.
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 currently does not release the GVL. This means that even if queries
90
- are executed on a separate thread, no other Ruby threads will be scheduled while
91
- SQLite3 is busy fetching the next record.
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
- In the future Extralite might be changed to release the GVL each time
94
- `sqlite3_step` is called.
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
- ## Can I use it with an ORM like ActiveRecord or Sequel?
159
+ ## Contributing
97
160
 
98
- Not yet, but you are welcome to contribute adapters for those projects. I will
99
- be releasing my own not-an-ORM tool in the near future.
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/test_database.rb'
15
+ exec 'ruby test/run.rb'
16
16
  end
17
17
 
18
18
  CLEAN.include "**/*.o", "**/*.so", "**/*.so.*", "**/*.a", "**/*.bundle", "**/*.jar", "pkg", "tmp"
@@ -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
- 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);
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
- 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));
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
- rb_raise(cSQLError, "%s", sqlite3_errmsg(db));
233
+ case SQLITE_MISUSE:
234
+ return NULL;
192
235
  }
193
- ptr = rest;
236
+ str = rest;
194
237
  }
238
+ return NULL;
195
239
  }
196
240
 
197
- inline int stmt_iterate(sqlite3_stmt *stmt, sqlite3 *db) {
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
- rc = sqlite3_step(stmt);
200
- switch (rc) {
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
- ID_STRIP = rb_intern("strip");
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
@@ -1,3 +1,3 @@
1
1
  module Extralite
2
- VERSION = '1.3'
2
+ VERSION = '1.7'
3
3
  end
@@ -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
@@ -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,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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir.glob("#{__dir__}/test_*.rb").each do |path|
4
+ require(path)
5
+ 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,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
@@ -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.3'
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-08-17 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
@@ -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: minitest-reporters
42
+ name: simplecov
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - '='
46
46
  - !ruby/object:Gem::Version
47
- version: 1.4.2
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: 1.4.2
54
+ version: 0.17.1
55
55
  - !ruby/object:Gem::Dependency
56
- name: simplecov
56
+ name: rubocop
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - '='
60
60
  - !ruby/object:Gem::Version
61
- version: 0.17.1
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.17.1
68
+ version: 0.85.1
69
69
  - !ruby/object:Gem::Dependency
70
- name: rubocop
70
+ name: pry
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - '='
74
74
  - !ruby/object:Gem::Version
75
- version: 0.85.1
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.85.1
82
+ version: 0.13.1
83
83
  - !ruby/object:Gem::Dependency
84
- name: pry
84
+ name: sequel
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - '='
88
88
  - !ruby/object:Gem::Version
89
- version: 0.13.1
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: 0.13.1
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