extralite 1.3 → 1.7

Sign up to get free protection for your applications and to get access to all the features.
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