extralite 1.5 → 1.8.2

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: ad8337aebac5ef75132340d8336389fe0c927ed454b5792d57f80efa4f362664
4
- data.tar.gz: 602ad3648312e810398fac215353b0fac5aa916ff7a3a7901ba1990c9e3fb145
3
+ metadata.gz: dd26d2937628e408ba8ee2f69e2e170755a43d5e1ffcdcbb82ca184cd5734576
4
+ data.tar.gz: c762b88c6e1671de5daab90f6f6fb574bb4dfb43fb0a866e8154fb8ac02f755f
5
5
  SHA512:
6
- metadata.gz: a5831721c27d2eb27c8433bcd286ba826861946b7894eb2c1564f449d321d2eaeec7cce13a4166b10db799f9e9e391bc2ba65961edc7b144592d1994767e69b0
7
- data.tar.gz: ce77ee584ec89bae2819a2c0384ca7cc08daf00f405d0bf379f322d6182b3bf55bceb0ac0136d52fddd3fe5a43e4e48eb23cbfc00eef74a0439ce25eeeb529f7
6
+ metadata.gz: 9ea096a28f15e8415b8e450a45035ffbe0c2186ebc7bba2ddd76c34e2328baaff307cee3c5079dda235992011497ef72b924bf3916628842e100ba8953ee7454
7
+ data.tar.gz: 0efd84d3a7fa1fd1157df43f65bd4fa6999a0943efc8c0b36030a8550bbb2bc1c3a96509d4bf6c7a8a2bb9c4bd071dc0a650cb45e54e75c7ec722ee29c8bb72e
@@ -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,16 @@
1
+ ## 1.8.2 2021-12-15
2
+
3
+ - Add documentation
4
+
5
+ ## 1.7 2021-12-13
6
+
7
+ - Add extralite Sequel adapter
8
+ - Add support for binding hash parameters
9
+
10
+ ## 1.6 2021-12-13
11
+
12
+ - Release GVL while fetching rows
13
+
1
14
  ## 1.5 2021-12-13
2
15
 
3
16
  - Release GVL while preparing statements
data/Gemfile.lock CHANGED
@@ -1,58 +1,37 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- extralite (1.5)
4
+ extralite (1.8.2)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
- ast (2.4.2)
10
- coderay (1.1.3)
11
9
  docile (1.4.0)
12
10
  json (2.5.1)
13
- method_source (1.0.0)
14
- minitest (5.14.4)
15
- parallel (1.20.1)
16
- parser (3.0.1.1)
17
- ast (~> 2.4.1)
18
- pry (0.13.1)
19
- coderay (~> 1.1)
20
- method_source (~> 1.0)
21
- rainbow (3.0.0)
22
- rake (13.0.3)
23
- rake-compiler (1.1.1)
11
+ minitest (5.15.0)
12
+ rake (13.0.6)
13
+ rake-compiler (1.1.6)
24
14
  rake
25
- regexp_parser (2.1.1)
26
- rexml (3.2.5)
27
- rubocop (0.85.1)
28
- parallel (~> 1.10)
29
- parser (>= 2.7.0.1)
30
- rainbow (>= 2.2.2, < 4.0)
31
- regexp_parser (>= 1.7)
32
- rexml
33
- rubocop-ast (>= 0.0.3)
34
- ruby-progressbar (~> 1.7)
35
- unicode-display_width (>= 1.4.0, < 2.0)
36
- rubocop-ast (1.5.0)
37
- parser (>= 3.0.1.1)
38
- ruby-progressbar (1.11.0)
15
+ sequel (5.51.0)
39
16
  simplecov (0.17.1)
40
17
  docile (~> 1.1)
41
18
  json (>= 1.8, < 3)
42
19
  simplecov-html (~> 0.10.0)
43
20
  simplecov-html (0.10.2)
44
- unicode-display_width (1.7.0)
21
+ webrick (1.7.0)
22
+ yard (0.9.27)
23
+ webrick (~> 1.7.0)
45
24
 
46
25
  PLATFORMS
47
26
  ruby
48
27
 
49
28
  DEPENDENCIES
50
29
  extralite!
51
- minitest (= 5.14.4)
52
- pry (= 0.13.1)
53
- rake-compiler (= 1.1.1)
54
- rubocop (= 0.85.1)
30
+ minitest (= 5.15.0)
31
+ rake-compiler (= 1.1.6)
32
+ sequel (= 5.51.0)
55
33
  simplecov (= 0.17.1)
34
+ yard (= 0.9.27)
56
35
 
57
36
  BUNDLED WITH
58
37
  2.1.4
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,8 +67,13 @@ 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
- rowid = db.last_insert_id
76
+ rowid = db.last_insert_rowid
65
77
 
66
78
  # get number of rows changed in last query
67
79
  number_of_rows_affected = db.changes
@@ -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
@@ -1,18 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bundler/gem_tasks"
4
- require "rake/clean"
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/clean'
5
5
 
6
- require "rake/extensiontask"
7
- Rake::ExtensionTask.new("extralite_ext") do |ext|
8
- ext.ext_dir = "ext/extralite"
6
+ require 'rake/extensiontask'
7
+ Rake::ExtensionTask.new('extralite_ext') do |ext|
8
+ ext.ext_dir = 'ext/extralite'
9
9
  end
10
10
 
11
11
  task :recompile => [:clean, :compile]
12
12
 
13
- task :default => [:compile, :test]
13
+ task :default => [:compile, :doc, :test]
14
+ task :doc => :yard
14
15
  task :test do
15
- exec 'ruby test/test_database.rb'
16
+ exec 'ruby test/run.rb'
16
17
  end
17
18
 
18
- CLEAN.include "**/*.o", "**/*.so", "**/*.so.*", "**/*.a", "**/*.bundle", "**/*.jar", "pkg", "tmp"
19
+ CLEAN.include '**/*.o', '**/*.so', '**/*.so.*', '**/*.a', '**/*.bundle', '**/*.jar', 'pkg', 'tmp'
20
+
21
+ require 'yard'
22
+ YARD_FILES = FileList['ext/extralite/extralite.c', 'lib/extralite.rb', 'lib/sequel/adapters/extralite.rb']
23
+
24
+ YARD::Rake::YardocTask.new do |t|
25
+ t.files = YARD_FILES
26
+ t.options = %w(-o doc --readme README.md)
27
+ end
@@ -6,7 +6,10 @@
6
6
  VALUE cError;
7
7
  VALUE cSQLError;
8
8
  VALUE cBusyError;
9
+
10
+ ID ID_KEYS;
9
11
  ID ID_STRIP;
12
+ ID ID_TO_S;
10
13
 
11
14
  typedef struct Database_t {
12
15
  sqlite3 *sqlite3_db;
@@ -48,6 +51,10 @@ static VALUE Database_allocate(VALUE klass) {
48
51
  } \
49
52
  }
50
53
 
54
+ /* call-seq: initialize(path)
55
+ *
56
+ * Initializes a new SQLite database with the given path.
57
+ */
51
58
 
52
59
  VALUE Database_initialize(VALUE self, VALUE path) {
53
60
  int rc;
@@ -69,6 +76,10 @@ VALUE Database_initialize(VALUE self, VALUE path) {
69
76
  return Qnil;
70
77
  }
71
78
 
79
+ /* call-seq: close
80
+ *
81
+ * Closes the database.
82
+ */
72
83
  VALUE Database_close(VALUE self) {
73
84
  int rc;
74
85
  Database_t *db;
@@ -83,6 +94,10 @@ VALUE Database_close(VALUE self) {
83
94
  return self;
84
95
  }
85
96
 
97
+ /* call-seq: closed?
98
+ *
99
+ * Returns true if the database is closed.
100
+ */
86
101
  VALUE Database_closed_p(VALUE self) {
87
102
  Database_t *db;
88
103
  GetDatabase(self, db);
@@ -109,6 +124,33 @@ inline VALUE get_column_value(sqlite3_stmt *stmt, int col, int type) {
109
124
  return Qnil;
110
125
  }
111
126
 
127
+ static void bind_parameter_value(sqlite3_stmt *stmt, int pos, VALUE value);
128
+
129
+ static inline void bind_hash_parameter_values(sqlite3_stmt *stmt, VALUE hash) {
130
+ VALUE keys = rb_funcall(hash, ID_KEYS, 0);
131
+ int len = RARRAY_LEN(keys);
132
+ for (int i = 0; i < len; i++) {
133
+ VALUE k = RARRAY_AREF(keys, i);
134
+ VALUE v = rb_hash_aref(hash, k);
135
+
136
+ switch (TYPE(k)) {
137
+ case T_FIXNUM:
138
+ bind_parameter_value(stmt, NUM2INT(k), v);
139
+ return;
140
+ case T_SYMBOL:
141
+ k = rb_funcall(k, ID_TO_S, 0);
142
+ case T_STRING:
143
+ if(RSTRING_PTR(k)[0] != ':') k = rb_str_plus(rb_str_new2(":"), k);
144
+ int pos = sqlite3_bind_parameter_index(stmt, StringValuePtr(k));
145
+ bind_parameter_value(stmt, pos, v);
146
+ return;
147
+ default:
148
+ rb_raise(cError, "Cannot bind hash key value idx %d", i);
149
+ }
150
+ }
151
+ RB_GC_GUARD(keys);
152
+ }
153
+
112
154
  static inline void bind_parameter_value(sqlite3_stmt *stmt, int pos, VALUE value) {
113
155
  switch (TYPE(value)) {
114
156
  case T_NIL:
@@ -129,6 +171,9 @@ static inline void bind_parameter_value(sqlite3_stmt *stmt, int pos, VALUE value
129
171
  case T_STRING:
130
172
  sqlite3_bind_text(stmt, pos, RSTRING_PTR(value), RSTRING_LEN(value), SQLITE_TRANSIENT);
131
173
  return;
174
+ case T_HASH:
175
+ bind_hash_parameter_values(stmt, value);
176
+ return;
132
177
  default:
133
178
  rb_raise(cError, "Cannot bind parameter at position %d", pos);
134
179
  }
@@ -228,10 +273,21 @@ inline void prepare_multi_stmt(sqlite3 *db, sqlite3_stmt **stmt, VALUE sql) {
228
273
  }
229
274
  }
230
275
 
231
- inline int stmt_iterate(sqlite3_stmt *stmt, sqlite3 *db) {
276
+ struct step_ctx {
277
+ sqlite3_stmt *stmt;
232
278
  int rc;
233
- rc = sqlite3_step(stmt);
234
- switch (rc) {
279
+ };
280
+
281
+ void *stmt_iterate_without_gvl(void *ptr) {
282
+ struct step_ctx *ctx = (struct step_ctx *)ptr;
283
+ ctx->rc = sqlite3_step(ctx->stmt);
284
+ return NULL;
285
+ }
286
+
287
+ inline int stmt_iterate(sqlite3_stmt *stmt, sqlite3 *db) {
288
+ struct step_ctx ctx = {stmt, 0};
289
+ rb_thread_call_without_gvl(stmt_iterate_without_gvl, (void *)&ctx, RUBY_UBF_IO, 0);
290
+ switch (ctx.rc) {
235
291
  case SQLITE_ROW:
236
292
  return 1;
237
293
  case SQLITE_DONE:
@@ -241,7 +297,7 @@ inline int stmt_iterate(sqlite3_stmt *stmt, sqlite3 *db) {
241
297
  case SQLITE_ERROR:
242
298
  rb_raise(cSQLError, "%s", sqlite3_errmsg(db));
243
299
  default:
244
- rb_raise(cError, "Invalid return code for sqlite3_step: %d (please open an issue on https://github.com/digital-fabric/extralite)", rc);
300
+ rb_raise(cError, "Invalid return code for sqlite3_step: %d (please open an issue on https://github.com/digital-fabric/extralite)", ctx.rc);
245
301
  }
246
302
 
247
303
  return 0;
@@ -298,6 +354,29 @@ VALUE safe_query_hash(VALUE arg) {
298
354
  return result;
299
355
  }
300
356
 
357
+ /* call-seq:
358
+ * query(sql, *parameters, &block)
359
+ * query_hash(sql, *parameters, &block)
360
+ *
361
+ * Runs a query returning rows as hashes (with symbol keys). If a block is
362
+ * given, it will be called for each row. Otherwise, an array containing all
363
+ * rows is returned.
364
+ *
365
+ * Query parameters to be bound to placeholders in the query can be specified as
366
+ * a list of values or as a hash mapping parameter names to values. When
367
+ * parameters are given as a least, the query should specify parameters using
368
+ * `?`:
369
+ *
370
+ * db.query('select * from foo where x = ?', 42)
371
+ *
372
+ * Named placeholders are specified using `:`. The placeholder values are
373
+ * specified using a hash, where keys are either strings are symbols. String
374
+ * keys can include or omit the `:` prefix. The following are equivalent:
375
+ *
376
+ * db.query('select * from foo where x = :bar', bar: 42)
377
+ * db.query('select * from foo where x = :bar', 'bar' => 42)
378
+ * db.query('select * from foo where x = :bar', ':bar' => 42)
379
+ */
301
380
  VALUE Database_query_hash(int argc, VALUE *argv, VALUE self) {
302
381
  query_ctx ctx = { self, argc, argv, 0 };
303
382
  return rb_ensure(safe_query_hash, (VALUE)&ctx, cleanup_stmt, (VALUE)&ctx);
@@ -332,6 +411,26 @@ VALUE safe_query_ary(VALUE arg) {
332
411
  return result;
333
412
  }
334
413
 
414
+ /* call-seq: query_ary(sql, *parameters, &block)
415
+ *
416
+ * Runs a query returning rows as arrays. If a block is given, it will be called
417
+ * for each row. Otherwise, an array containing all rows is returned.
418
+ *
419
+ * Query parameters to be bound to placeholders in the query can be specified as
420
+ * a list of values or as a hash mapping parameter names to values. When
421
+ * parameters are given as a least, the query should specify parameters using
422
+ * `?`:
423
+ *
424
+ * db.query_ary('select * from foo where x = ?', 42)
425
+ *
426
+ * Named placeholders are specified using `:`. The placeholder values are
427
+ * specified using a hash, where keys are either strings are symbols. String
428
+ * keys can include or omit the `:` prefix. The following are equivalent:
429
+ *
430
+ * db.query_ary('select * from foo where x = :bar', bar: 42)
431
+ * db.query_ary('select * from foo where x = :bar', 'bar' => 42)
432
+ * db.query_ary('select * from foo where x = :bar', ':bar' => 42)
433
+ */
335
434
  VALUE Database_query_ary(int argc, VALUE *argv, VALUE self) {
336
435
  query_ctx ctx = { self, argc, argv, 0 };
337
436
  return rb_ensure(safe_query_ary, (VALUE)&ctx, cleanup_stmt, (VALUE)&ctx);
@@ -361,6 +460,25 @@ VALUE safe_query_single_row(VALUE arg) {
361
460
  return row;
362
461
  }
363
462
 
463
+ /* call-seq: query_single_row(sql, *parameters)
464
+ *
465
+ * Runs a query returning a single row as a hash.
466
+ *
467
+ * Query parameters to be bound to placeholders in the query can be specified as
468
+ * a list of values or as a hash mapping parameter names to values. When
469
+ * parameters are given as a least, the query should specify parameters using
470
+ * `?`:
471
+ *
472
+ * db.query_single_row('select * from foo where x = ?', 42)
473
+ *
474
+ * Named placeholders are specified using `:`. The placeholder values are
475
+ * specified using a hash, where keys are either strings are symbols. String
476
+ * keys can include or omit the `:` prefix. The following are equivalent:
477
+ *
478
+ * db.query_single_row('select * from foo where x = :bar', bar: 42)
479
+ * db.query_single_row('select * from foo where x = :bar', 'bar' => 42)
480
+ * db.query_single_row('select * from foo where x = :bar', ':bar' => 42)
481
+ */
364
482
  VALUE Database_query_single_row(int argc, VALUE *argv, VALUE self) {
365
483
  query_ctx ctx = { self, argc, argv, 0 };
366
484
  return rb_ensure(safe_query_single_row, (VALUE)&ctx, cleanup_stmt, (VALUE)&ctx);
@@ -398,6 +516,26 @@ VALUE safe_query_single_column(VALUE arg) {
398
516
  return result;
399
517
  }
400
518
 
519
+ /* call-seq: query_single_column(sql, *parameters, &block)
520
+ *
521
+ * Runs a query returning single column values. If a block is given, it will be called
522
+ * for each value. Otherwise, an array containing all values is returned.
523
+ *
524
+ * Query parameters to be bound to placeholders in the query can be specified as
525
+ * a list of values or as a hash mapping parameter names to values. When
526
+ * parameters are given as a least, the query should specify parameters using
527
+ * `?`:
528
+ *
529
+ * db.query_single_column('select x from foo where x = ?', 42)
530
+ *
531
+ * Named placeholders are specified using `:`. The placeholder values are
532
+ * specified using a hash, where keys are either strings are symbols. String
533
+ * keys can include or omit the `:` prefix. The following are equivalent:
534
+ *
535
+ * db.query_single_column('select x from foo where x = :bar', bar: 42)
536
+ * db.query_single_column('select x from foo where x = :bar', 'bar' => 42)
537
+ * db.query_single_column('select x from foo where x = :bar', ':bar' => 42)
538
+ */
401
539
  VALUE Database_query_single_column(int argc, VALUE *argv, VALUE self) {
402
540
  query_ctx ctx = { self, argc, argv, 0 };
403
541
  return rb_ensure(safe_query_single_column, (VALUE)&ctx, cleanup_stmt, (VALUE)&ctx);
@@ -426,11 +564,34 @@ VALUE safe_query_single_value(VALUE arg) {
426
564
  return value;
427
565
  }
428
566
 
567
+ /* call-seq: query_single_value(sql, *parameters)
568
+ *
569
+ * Runs a query returning a single value from the first row.
570
+ *
571
+ * Query parameters to be bound to placeholders in the query can be specified as
572
+ * a list of values or as a hash mapping parameter names to values. When
573
+ * parameters are given as a least, the query should specify parameters using
574
+ * `?`:
575
+ *
576
+ * db.query_single_value('select x from foo where x = ?', 42)
577
+ *
578
+ * Named placeholders are specified using `:`. The placeholder values are
579
+ * specified using a hash, where keys are either strings are symbols. String
580
+ * keys can include or omit the `:` prefix. The following are equivalent:
581
+ *
582
+ * db.query_single_value('select x from foo where x = :bar', bar: 42)
583
+ * db.query_single_value('select x from foo where x = :bar', 'bar' => 42)
584
+ * db.query_single_value('select x from foo where x = :bar', ':bar' => 42)
585
+ */
429
586
  VALUE Database_query_single_value(int argc, VALUE *argv, VALUE self) {
430
587
  query_ctx ctx = { self, argc, argv, 0 };
431
588
  return rb_ensure(safe_query_single_value, (VALUE)&ctx, cleanup_stmt, (VALUE)&ctx);
432
589
  }
433
590
 
591
+ /* call-seq: last_insert_rowid
592
+ *
593
+ * Returns the rowid of the last inserted row.
594
+ */
434
595
  VALUE Database_last_insert_rowid(VALUE self) {
435
596
  Database_t *db;
436
597
  GetOpenDatabase(self, db);
@@ -438,6 +599,10 @@ VALUE Database_last_insert_rowid(VALUE self) {
438
599
  return INT2NUM(sqlite3_last_insert_rowid(db->sqlite3_db));
439
600
  }
440
601
 
602
+ /* call-seq: changes
603
+ *
604
+ * Returns the number of changes made to the database by the last operation.
605
+ */
441
606
  VALUE Database_changes(VALUE self) {
442
607
  Database_t *db;
443
608
  GetOpenDatabase(self, db);
@@ -445,6 +610,10 @@ VALUE Database_changes(VALUE self) {
445
610
  return INT2NUM(sqlite3_changes(db->sqlite3_db));
446
611
  }
447
612
 
613
+ /* call-seq: filename
614
+ *
615
+ * Returns the database filename.
616
+ */
448
617
  VALUE Database_filename(int argc, VALUE *argv, VALUE self) {
449
618
  const char *db_name;
450
619
  const char *filename;
@@ -457,6 +626,10 @@ VALUE Database_filename(int argc, VALUE *argv, VALUE self) {
457
626
  return filename ? rb_str_new_cstr(filename) : Qnil;
458
627
  }
459
628
 
629
+ /* call-seq: transaction_active?
630
+ *
631
+ * Returns true if a transaction is currently in progress.
632
+ */
460
633
  VALUE Database_transaction_active_p(VALUE self) {
461
634
  Database_t *db;
462
635
  GetOpenDatabase(self, db);
@@ -464,6 +637,10 @@ VALUE Database_transaction_active_p(VALUE self) {
464
637
  return sqlite3_get_autocommit(db->sqlite3_db) ? Qfalse : Qtrue;
465
638
  }
466
639
 
640
+ /* call-seq: load_extension(path)
641
+ *
642
+ * Loads an extension with the given path.
643
+ */
467
644
  VALUE Database_load_extension(VALUE self, VALUE path) {
468
645
  Database_t *db;
469
646
  GetOpenDatabase(self, db);
@@ -508,5 +685,7 @@ void Init_Extralite() {
508
685
  rb_gc_register_mark_object(cSQLError);
509
686
  rb_gc_register_mark_object(cBusyError);
510
687
 
511
- ID_STRIP = rb_intern("strip");
688
+ ID_KEYS = rb_intern("keys");
689
+ ID_STRIP = rb_intern("strip");
690
+ ID_TO_S = rb_intern("to_s");
512
691
  }
data/extralite.gemspec CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |s|
11
11
  s.homepage = 'https://github.com/digital-fabric/extralite'
12
12
  s.metadata = {
13
13
  "source_code_uri" => "https://github.com/digital-fabric/extralite",
14
- "documentation_uri" => "https://github.com/digital-fabric/extralite",
14
+ "documentation_uri" => "https://www.rubydoc.info/gems/extralite",
15
15
  "homepage_uri" => "https://github.com/digital-fabric/extralite",
16
16
  "changelog_uri" => "https://github.com/digital-fabric/extralite/blob/master/CHANGELOG.md"
17
17
  }
@@ -21,9 +21,10 @@ Gem::Specification.new do |s|
21
21
  s.require_paths = ["lib"]
22
22
  s.required_ruby_version = '>= 2.6'
23
23
 
24
- s.add_development_dependency 'rake-compiler', '1.1.1'
25
- s.add_development_dependency 'minitest', '5.14.4'
24
+ s.add_development_dependency 'rake-compiler', '1.1.6'
25
+ s.add_development_dependency 'minitest', '5.15.0'
26
26
  s.add_development_dependency 'simplecov', '0.17.1'
27
- s.add_development_dependency 'rubocop', '0.85.1'
28
- s.add_development_dependency 'pry', '0.13.1'
27
+ s.add_development_dependency 'yard', '0.9.27'
28
+
29
+ s.add_development_dependency 'sequel', '5.51.0'
29
30
  end
@@ -1,3 +1,3 @@
1
1
  module Extralite
2
- VERSION = '1.5'
2
+ VERSION = '1.8.2'
3
3
  end
data/lib/extralite.rb CHANGED
@@ -1 +1,21 @@
1
1
  require_relative './extralite_ext'
2
+
3
+ # Extralite is a Ruby gem for working with SQLite databases
4
+ module Extralite
5
+ # A base class for Extralite exceptions
6
+ class Error < RuntimeError
7
+ end
8
+
9
+ # An exception representing an SQL error emitted by SQLite
10
+ class SQLError < Error
11
+ end
12
+
13
+ # An exception raised when an SQLite database is busy (locked by another
14
+ # thread or process)
15
+ class BusyError < Error
16
+ end
17
+
18
+ # An SQLite database
19
+ class Database
20
+ end
21
+ end
@@ -0,0 +1,380 @@
1
+ # frozen-string-literal: true
2
+
3
+ # This file was adapted from the SQLite adapter included in Sequel:
4
+ # https://github.com/jeremyevans/sequel
5
+ # (distributed under the MIT license)
6
+
7
+ require 'extralite'
8
+ require 'sequel/adapters/shared/sqlite'
9
+
10
+ module Sequel
11
+ module Extralite
12
+ FALSE_VALUES = (%w'0 false f no n'.each(&:freeze) + [0]).freeze
13
+
14
+ blob = Object.new
15
+ def blob.call(s)
16
+ Sequel::SQL::Blob.new(s.to_s)
17
+ end
18
+
19
+ boolean = Object.new
20
+ def boolean.call(s)
21
+ s = s.downcase if s.is_a?(String)
22
+ !FALSE_VALUES.include?(s)
23
+ end
24
+
25
+ date = Object.new
26
+ def date.call(s)
27
+ case s
28
+ when String
29
+ Sequel.string_to_date(s)
30
+ when Integer
31
+ Date.jd(s)
32
+ when Float
33
+ Date.jd(s.to_i)
34
+ else
35
+ raise Sequel::Error, "unhandled type when converting to date: #{s.inspect} (#{s.class.inspect})"
36
+ end
37
+ end
38
+
39
+ integer = Object.new
40
+ def integer.call(s)
41
+ s.to_i
42
+ end
43
+
44
+ float = Object.new
45
+ def float.call(s)
46
+ s.to_f
47
+ end
48
+
49
+ numeric = Object.new
50
+ def numeric.call(s)
51
+ s = s.to_s unless s.is_a?(String)
52
+ BigDecimal(s) rescue s
53
+ end
54
+
55
+ time = Object.new
56
+ def time.call(s)
57
+ case s
58
+ when String
59
+ Sequel.string_to_time(s)
60
+ when Integer
61
+ Sequel::SQLTime.create(s/3600, (s % 3600)/60, s % 60)
62
+ when Float
63
+ s, f = s.divmod(1)
64
+ Sequel::SQLTime.create(s/3600, (s % 3600)/60, s % 60, (f*1000000).round)
65
+ else
66
+ raise Sequel::Error, "unhandled type when converting to date: #{s.inspect} (#{s.class.inspect})"
67
+ end
68
+ end
69
+
70
+ # Hash with string keys and callable values for converting SQLite types.
71
+ SQLITE_TYPES = {}
72
+ {
73
+ %w'date' => date,
74
+ %w'time' => time,
75
+ %w'bit bool boolean' => boolean,
76
+ %w'integer smallint mediumint int bigint' => integer,
77
+ %w'numeric decimal money' => numeric,
78
+ %w'float double real dec fixed' + ['double precision'] => float,
79
+ %w'blob' => blob
80
+ }.each do |k,v|
81
+ k.each{|n| SQLITE_TYPES[n] = v}
82
+ end
83
+ SQLITE_TYPES.freeze
84
+
85
+ USE_EXTENDED_RESULT_CODES = false
86
+
87
+ class Database < Sequel::Database
88
+ include ::Sequel::SQLite::DatabaseMethods
89
+
90
+ set_adapter_scheme :extralite
91
+
92
+ # Mimic the file:// uri, by having 2 preceding slashes specify a relative
93
+ # path, and 3 preceding slashes specify an absolute path.
94
+ def self.uri_to_options(uri) # :nodoc:
95
+ { :database => (uri.host.nil? && uri.path == '/') ? nil : "#{uri.host}#{uri.path}" }
96
+ end
97
+
98
+ private_class_method :uri_to_options
99
+
100
+ # The conversion procs to use for this database
101
+ attr_reader :conversion_procs
102
+
103
+ # Connect to the database. Since SQLite is a file based database,
104
+ # available options are limited:
105
+ #
106
+ # :database :: database name (filename or ':memory:' or file: URI)
107
+ # :readonly :: open database in read-only mode; useful for reading
108
+ # static data that you do not want to modify
109
+ # :timeout :: how long to wait for the database to be available if it
110
+ # is locked, given in milliseconds (default is 5000)
111
+ def connect(server)
112
+ opts = server_opts(server)
113
+ opts[:database] = ':memory:' if blank_object?(opts[:database])
114
+ # sqlite3_opts = {}
115
+ # sqlite3_opts[:readonly] = typecast_value_boolean(opts[:readonly]) if opts.has_key?(:readonly)
116
+ db = ::Extralite::Database.new(opts[:database].to_s)#, sqlite3_opts)
117
+ # db.busy_timeout(typecast_value_integer(opts.fetch(:timeout, 5000)))
118
+
119
+ # if USE_EXTENDED_RESULT_CODES
120
+ # db.extended_result_codes = true
121
+ # end
122
+
123
+ connection_pragmas.each{|s| log_connection_yield(s, db){db.query(s)}}
124
+
125
+ # class << db
126
+ # attr_reader :prepared_statements
127
+ # end
128
+ # db.instance_variable_set(:@prepared_statements, {})
129
+
130
+ db
131
+ end
132
+
133
+ # Disconnect given connections from the database.
134
+ def disconnect_connection(c)
135
+ # c.prepared_statements.each_value{|v| v.first.close}
136
+ c.close
137
+ end
138
+
139
+ # Run the given SQL with the given arguments and yield each row.
140
+ def execute(sql, opts=OPTS, &block)
141
+ _execute(:select, sql, opts, &block)
142
+ end
143
+
144
+ # Run the given SQL with the given arguments and return the number of changed rows.
145
+ def execute_dui(sql, opts=OPTS)
146
+ _execute(:update, sql, opts)
147
+ end
148
+
149
+ # Drop any prepared statements on the connection when executing DDL. This is because
150
+ # prepared statements lock the table in such a way that you can't drop or alter the
151
+ # table while a prepared statement that references it still exists.
152
+ # def execute_ddl(sql, opts=OPTS)
153
+ # synchronize(opts[:server]) do |conn|
154
+ # conn.prepared_statements.values.each{|cps, s| cps.close}
155
+ # conn.prepared_statements.clear
156
+ # super
157
+ # end
158
+ # end
159
+
160
+ def execute_insert(sql, opts=OPTS)
161
+ _execute(:insert, sql, opts)
162
+ end
163
+
164
+ def freeze
165
+ @conversion_procs.freeze
166
+ super
167
+ end
168
+
169
+ # Handle Integer and Float arguments, since SQLite can store timestamps as integers and floats.
170
+ def to_application_timestamp(s)
171
+ case s
172
+ when String
173
+ super
174
+ when Integer
175
+ super(Time.at(s).to_s)
176
+ when Float
177
+ super(DateTime.jd(s).to_s)
178
+ else
179
+ raise Sequel::Error, "unhandled type when converting to : #{s.inspect} (#{s.class.inspect})"
180
+ end
181
+ end
182
+
183
+ private
184
+
185
+ def adapter_initialize
186
+ @conversion_procs = SQLITE_TYPES.dup
187
+ @conversion_procs['datetime'] = @conversion_procs['timestamp'] = method(:to_application_timestamp)
188
+ set_integer_booleans
189
+ end
190
+
191
+ # Yield an available connection. Rescue any Extralite::Error and turn
192
+ # them into DatabaseErrors.
193
+ def _execute(type, sql, opts, &block)
194
+ begin
195
+ synchronize(opts[:server]) do |conn|
196
+ # return execute_prepared_statement(conn, type, sql, opts, &block) if sql.is_a?(Symbol)
197
+ log_args = opts[:arguments]
198
+ args = {}
199
+ opts.fetch(:arguments, OPTS).each{|k, v| args[k] = prepared_statement_argument(v) }
200
+ case type
201
+ when :select
202
+ log_connection_yield(sql, conn, log_args){conn.query(sql, args, &block)}
203
+ when :insert
204
+ log_connection_yield(sql, conn, log_args){conn.query(sql, args)}
205
+ conn.last_insert_rowid
206
+ when :update
207
+ log_connection_yield(sql, conn, log_args){conn.query(sql, args)}
208
+ conn.changes
209
+ end
210
+ end
211
+ rescue ::Extralite::Error => e
212
+ raise_error(e)
213
+ end
214
+ end
215
+
216
+ # The SQLite adapter does not need the pool to convert exceptions.
217
+ # Also, force the max connections to 1 if a memory database is being
218
+ # used, as otherwise each connection gets a separate database.
219
+ def connection_pool_default_options
220
+ o = super.dup
221
+ # Default to only a single connection if a memory database is used,
222
+ # because otherwise each connection will get a separate database
223
+ o[:max_connections] = 1 if @opts[:database] == ':memory:' || blank_object?(@opts[:database])
224
+ o
225
+ end
226
+
227
+ def prepared_statement_argument(arg)
228
+ case arg
229
+ when Date, DateTime, Time
230
+ literal(arg)[1...-1]
231
+ when SQL::Blob
232
+ arg.to_blob
233
+ when true, false
234
+ if integer_booleans
235
+ arg ? 1 : 0
236
+ else
237
+ literal(arg)[1...-1]
238
+ end
239
+ else
240
+ arg
241
+ end
242
+ end
243
+
244
+ # Execute a prepared statement on the database using the given name.
245
+ def execute_prepared_statement(conn, type, name, opts, &block)
246
+ ps = prepared_statement(name)
247
+ sql = ps.prepared_sql
248
+ args = opts[:arguments]
249
+ ps_args = {}
250
+ args.each{|k, v| ps_args[k] = prepared_statement_argument(v)}
251
+ if cpsa = conn.prepared_statements[name]
252
+ cps, cps_sql = cpsa
253
+ if cps_sql != sql
254
+ cps.close
255
+ cps = nil
256
+ end
257
+ end
258
+ unless cps
259
+ cps = log_connection_yield("PREPARE #{name}: #{sql}", conn){conn.prepare(sql)}
260
+ conn.prepared_statements[name] = [cps, sql]
261
+ end
262
+ log_sql = String.new
263
+ log_sql << "EXECUTE #{name}"
264
+ if ps.log_sql
265
+ log_sql << " ("
266
+ log_sql << sql
267
+ log_sql << ")"
268
+ end
269
+ if block
270
+ log_connection_yield(log_sql, conn, args){cps.execute(ps_args, &block)}
271
+ else
272
+ log_connection_yield(log_sql, conn, args){cps.execute!(ps_args){|r|}}
273
+ case type
274
+ when :insert
275
+ conn.last_insert_rowid
276
+ when :update
277
+ conn.changes
278
+ end
279
+ end
280
+ end
281
+
282
+ # # SQLite3 raises ArgumentError in addition to SQLite3::Exception in
283
+ # # some cases, such as operations on a closed database.
284
+ def database_error_classes
285
+ #[Extralite::Error, ArgumentError]
286
+ [::Extralite::Error]
287
+ end
288
+
289
+ def dataset_class_default
290
+ Dataset
291
+ end
292
+
293
+ if USE_EXTENDED_RESULT_CODES
294
+ # Support SQLite exception codes if ruby-sqlite3 supports them.
295
+ def sqlite_error_code(exception)
296
+ exception.code if exception.respond_to?(:code)
297
+ end
298
+ end
299
+ end
300
+
301
+ class Dataset < Sequel::Dataset
302
+ include ::Sequel::SQLite::DatasetMethods
303
+
304
+ module ArgumentMapper
305
+ include Sequel::Dataset::ArgumentMapper
306
+
307
+ protected
308
+
309
+ # Return a hash with the same values as the given hash,
310
+ # but with the keys converted to strings.
311
+ def map_to_prepared_args(hash)
312
+ args = {}
313
+ hash.each{|k,v| args[k.to_s.gsub('.', '__')] = v}
314
+ args
315
+ end
316
+
317
+ private
318
+
319
+ # SQLite uses a : before the name of the argument for named
320
+ # arguments.
321
+ def prepared_arg(k)
322
+ LiteralString.new("#{prepared_arg_placeholder}#{k.to_s.gsub('.', '__')}")
323
+ end
324
+ end
325
+
326
+ BindArgumentMethods = prepared_statements_module(:bind, ArgumentMapper)
327
+ PreparedStatementMethods = prepared_statements_module(:prepare, BindArgumentMethods)
328
+
329
+ def fetch_rows(sql, &block)
330
+ execute(sql, &block)
331
+ # execute(sql) do |result|
332
+ # cps = db.conversion_procs
333
+ # type_procs = result.types.map{|t| cps[base_type_name(t)]}
334
+ # j = -1
335
+ # cols = result.columns.map{|c| [output_identifier(c), type_procs[(j+=1)]]}
336
+ # self.columns = cols.map(&:first)
337
+ # max = cols.length
338
+ # result.each do |values|
339
+ # row = {}
340
+ # i = -1
341
+ # while (i += 1) < max
342
+ # name, type_proc = cols[i]
343
+ # v = values[i]
344
+ # if type_proc && v
345
+ # v = type_proc.call(v)
346
+ # end
347
+ # row[name] = v
348
+ # end
349
+ # yield row
350
+ # end
351
+ # end
352
+ end
353
+
354
+ private
355
+
356
+ # The base type name for a given type, without any parenthetical part.
357
+ def base_type_name(t)
358
+ (t =~ /^(.*?)\(/ ? $1 : t).downcase if t
359
+ end
360
+
361
+ # Quote the string using the adapter class method.
362
+ def literal_string_append(sql, v)
363
+ sql << "'" << v.gsub(/'/, "''") << "'"
364
+ end
365
+
366
+ def bound_variable_modules
367
+ [BindArgumentMethods]
368
+ end
369
+
370
+ def prepared_statement_modules
371
+ [PreparedStatementMethods]
372
+ end
373
+
374
+ # SQLite uses a : before the name of the argument as a placeholder.
375
+ def prepared_arg_placeholder
376
+ ':'
377
+ end
378
+ end
379
+ end
380
+ end
data/test/perf.rb CHANGED
@@ -13,36 +13,39 @@ require 'benchmark/ips'
13
13
  require 'fileutils'
14
14
 
15
15
  DB_PATH = '/tmp/extralite_sqlite3_perf.db'
16
- COUNT = 10000
17
16
 
18
- def prepare_database
17
+ def prepare_database(count)
19
18
  FileUtils.rm(DB_PATH) rescue nil
20
19
  db = Extralite::Database.new(DB_PATH)
21
20
  db.query('create table foo ( a integer primary key, b text )')
22
21
  db.query('begin')
23
- COUNT.times { db.query('insert into foo (b) values (?)', "hello#{rand(1000)}" )}
22
+ count.times { db.query('insert into foo (b) values (?)', "hello#{rand(1000)}" )}
24
23
  db.query('commit')
25
24
  end
26
25
 
27
- def sqlite3_run
26
+ def sqlite3_run(count)
28
27
  db = SQLite3::Database.new(DB_PATH, :results_as_hash => true)
29
28
  results = db.execute('select * from foo')
30
- raise unless results.size == COUNT
29
+ raise unless results.size == count
31
30
  end
32
31
 
33
- def extralite_run
32
+ def extralite_run(count)
34
33
  db = Extralite::Database.new(DB_PATH)
35
34
  results = db.query('select * from foo')
36
- raise unless results.size == COUNT
35
+ raise unless results.size == count
37
36
  end
38
37
 
39
- prepare_database
38
+ [10, 1000, 100000].each do |c|
39
+ puts; puts; puts "Record count: #{c}"
40
40
 
41
- Benchmark.ips do |x|
42
- x.config(:time => 3, :warmup => 1)
41
+ prepare_database(c)
43
42
 
44
- x.report("sqlite3") { sqlite3_run }
45
- x.report("extralite") { extralite_run }
43
+ Benchmark.ips do |x|
44
+ x.config(:time => 3, :warmup => 1)
46
45
 
47
- x.compare!
46
+ x.report("sqlite3") { sqlite3_run(c) }
47
+ x.report("extralite") { extralite_run(c) }
48
+
49
+ x.compare!
50
+ end
48
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
@@ -111,6 +111,33 @@ end
111
111
 
112
112
  assert_raises(Extralite::Error) { @db.query_single_value('select 42') }
113
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
114
141
  end
115
142
 
116
143
  class ScenarioTest < MiniTest::Test
@@ -179,4 +206,3 @@ class ScenarioTest < MiniTest::Test
179
206
  assert_equal [1, 4, 7], result
180
207
  end
181
208
  end
182
-
@@ -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.5'
4
+ version: 1.8.2
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-12-13 00:00:00.000000000 Z
11
+ date: 2021-12-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake-compiler
@@ -16,28 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 1.1.1
19
+ version: 1.1.6
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 1.1.1
26
+ version: 1.1.6
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: minitest
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - '='
32
32
  - !ruby/object:Gem::Version
33
- version: 5.14.4
33
+ version: 5.15.0
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - '='
39
39
  - !ruby/object:Gem::Version
40
- version: 5.14.4
40
+ version: 5.15.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: simplecov
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -53,33 +53,33 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: 0.17.1
55
55
  - !ruby/object:Gem::Dependency
56
- name: rubocop
56
+ name: yard
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - '='
60
60
  - !ruby/object:Gem::Version
61
- version: 0.85.1
61
+ version: 0.9.27
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.85.1
68
+ version: 0.9.27
69
69
  - !ruby/object:Gem::Dependency
70
- name: pry
70
+ name: sequel
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - '='
74
74
  - !ruby/object:Gem::Version
75
- version: 0.13.1
75
+ version: 5.51.0
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.13.1
82
+ version: 5.51.0
83
83
  description:
84
84
  email: sharon@noteflakes.com
85
85
  executables: []
@@ -103,15 +103,18 @@ files:
103
103
  - extralite.gemspec
104
104
  - lib/extralite.rb
105
105
  - lib/extralite/version.rb
106
+ - lib/sequel/adapters/extralite.rb
106
107
  - test/helper.rb
107
108
  - test/perf.rb
109
+ - test/run.rb
108
110
  - test/test_database.rb
111
+ - test/test_sequel.rb
109
112
  homepage: https://github.com/digital-fabric/extralite
110
113
  licenses:
111
114
  - MIT
112
115
  metadata:
113
116
  source_code_uri: https://github.com/digital-fabric/extralite
114
- documentation_uri: https://github.com/digital-fabric/extralite
117
+ documentation_uri: https://www.rubydoc.info/gems/extralite
115
118
  homepage_uri: https://github.com/digital-fabric/extralite
116
119
  changelog_uri: https://github.com/digital-fabric/extralite/blob/master/CHANGELOG.md
117
120
  post_install_message: