extralite 1.6 → 1.9

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: e2924788672c7c3960a5baeaeacf14d297cfe461cfb1d20db828d7a67b5536c8
4
- data.tar.gz: fb4da5551c3b1ddca95ef05520eee477bfb7a29383b9c7e5f5c0638392eba3e9
3
+ metadata.gz: 874f2ebc573b23e4336b69409252b2cbb8c71c1275b1ca34d1d1924999d8ed31
4
+ data.tar.gz: 5b304d82a818c3e0da2dd72bca6fa590ff8be84241c6dc54fdb9f3bee49e498a
5
5
  SHA512:
6
- metadata.gz: 63b73c0d05cb294b75d79bef5c1362e2c422f16b0e7ae76c3d2b577af20f612007a4fe272942958b38d545efabe4d975c1a0475ee8dec4a68e957d61800ffca7
7
- data.tar.gz: b79c50cb5c696cf1eb505bbd126b197f1160f56af70e669cfc432e2e16d5423848c139ff9e942fb26c06c3d15f94e1a43f22ab4c317dae4fb0c2290f8d431aa6
6
+ metadata.gz: dd039c09306a04eb1e1cd42eb968c58cbf7693fd09c31587e9b7853a061c2a607af9d594859510f6cabc2ef5f166426a5a0ae4459b40168b10ea3fc4c6e3063f
7
+ data.tar.gz: 2b65161b0ec69a8072e8e2f343386d8178194d1dae3bfc2ac16e414f5682d2ca1da501057f1da38cf875238e0314aa35032dcdbbf3d610affe14fb572713435a
@@ -0,0 +1 @@
1
+ github: ciconia
@@ -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.9 2021-12-15
2
+
3
+ - Add support for reading BLOBs
4
+
5
+ ## 1.8.2 2021-12-15
6
+
7
+ - Add documentation
8
+
9
+ ## 1.7 2021-12-13
10
+
11
+ - Add extralite Sequel adapter
12
+ - Add support for binding hash parameters
13
+
1
14
  ## 1.6 2021-12-13
2
15
 
3
16
  - Release GVL while fetching rows
data/Gemfile.lock CHANGED
@@ -1,58 +1,37 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- extralite (1.6)
4
+ extralite (1.9)
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
@@ -1,4 +1,4 @@
1
- # Extralite - a Ruby gem for working with SQLite3 databases
1
+ # Extralite - a fast Ruby gem for working with SQLite3 databases
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/extralite.svg)](http://rubygems.org/gems/extralite)
4
4
  [![Modulation Test](https://github.com/digital-fabric/extralite/workflows/Tests/badge.svg)](https://github.com/digital-fabric/extralite/actions?query=workflow%3ATests)
@@ -6,9 +6,9 @@
6
6
 
7
7
  ## What is Extralite?
8
8
 
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.
9
+ Extralite is a fast, extra-lightweight (less than 460 lines of C-code) SQLite3
10
+ wrapper for Ruby. It provides a single class with a minimal set of methods for
11
+ interacting with an SQLite3 database.
12
12
 
13
13
  ## Features
14
14
 
@@ -17,8 +17,9 @@ interact with an SQLite3 database.
17
17
  - Super fast - [up to 12.5x faster](#performance) than the
18
18
  [sqlite3](https://github.com/sparklemotion/sqlite3-ruby) gem (see also
19
19
  [comparison](#why-not-just-use-the-sqlite3-gem).)
20
- - Improved [concurrency](#concurrency) for multithreaded apps: the Ruby GVL is
21
- released while preparing SQL statements and while iterating over results.
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.
22
23
  - Iterate over records with a block, or collect records into an array.
23
24
  - Parameter binding.
24
25
  - Automatically execute SQL strings containing multiple semicolon-separated
@@ -27,6 +28,7 @@ interact with an SQLite3 database.
27
28
  - Get number of rows changed by last query.
28
29
  - Load extensions (loading of extensions is autmatically enabled. You can find
29
30
  some useful extensions here: https://github.com/nalgeon/sqlean.)
31
+ - Includes a [Sequel adapter](#usage-with-sequel) (an ActiveRecord)
30
32
 
31
33
  ## Usage
32
34
 
@@ -65,8 +67,13 @@ db.query_single_value("select 'foo'") #=> "foo"
65
67
  # parameter binding (works for all query_xxx methods)
66
68
  db.query_hash('select ? as foo, ? as bar', 1, 2) #=> [{ :foo => 1, :bar => 2 }]
67
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
+
68
75
  # get last insert rowid
69
- rowid = db.last_insert_id
76
+ rowid = db.last_insert_rowid
70
77
 
71
78
  # get number of rows changed in last query
72
79
  number_of_rows_affected = db.changes
@@ -82,25 +89,45 @@ db.close
82
89
  db.closed? #=> true
83
90
  ```
84
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
+
85
106
  ## Why not just use the sqlite3 gem?
86
107
 
87
- The sqlite3-ruby gem is a popular, solid, well-maintained project, used by
88
- thousands of developers. I've been doing a lot of work with SQLite3 databases
89
- lately, and wanted to have a simpler API that gives me query results in a
90
- variety of ways. 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.
91
118
 
92
119
  Here's a table summarizing the differences between the two gems:
93
120
 
94
121
  | |sqlite3-ruby|Extralite|
95
122
  |-|-|-|
96
123
  |API design|multiple classes|single class|
97
- |Query results|row as hash, row as array, single row, single value|row as hash, row as array, single column, single row, single value|
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|
98
125
  |execute multiple statements|separate API (#execute_batch)|integrated|
99
126
  |custom functions in Ruby|yes|no|
100
127
  |custom collations|yes|no|
101
128
  |custom aggregate functions|yes|no|
102
- |Multithread friendly|no|[yes](#concurrency)|
103
- |Code size|~2650LoC|~500LoC|
129
+ |Multithread friendly|no|[yes](#what-about-concurrency)|
130
+ |Code size|~2650LoC|~530LoC|
104
131
  |Performance|1x|1.5x to 12.5x (see [below](#performance))|
105
132
 
106
133
  ## What about concurrency?
@@ -113,27 +140,33 @@ performance:
113
140
 
114
141
  ## Performance
115
142
 
116
- A benchmark script is
117
- [included](https://github.com/digital-fabric/extralite/blob/main/test/perf.rb),
118
- creating a table of various row counts, then fetching the entire table using
119
- either `sqlite3` or `extralite`. This benchmark shows Extralite to be up to 12.5
120
- times faster than `sqlite3` when fetching a large number of rows. Here are the
121
- results (using the `sqlite3` gem performance as baseline):
143
+ A benchmark script is included, creating a table of various row counts, then
144
+ fetching the entire table using either `sqlite3` or `extralite`. This benchmark
145
+ shows Extralite to be up to 12.5 times faster than `sqlite3` when fetching a
146
+ large number of rows. Here are the [results for fetching rows as hashes](https://github.com/digital-fabric/extralite/blob/main/test/perf_hash.rb):
122
147
 
123
- |Row count|sqlite3-ruby (baseline)|Extralite (relative - rounded)|
124
- |-:|-:|-:|
125
- |10|1x|1.5x|
126
- |1K|1x|7x|
127
- |100K|1x|12.5x|
148
+ |Row count|sqlite3-ruby|Extralite|Advantage|
149
+ |-:|-:|-:|-:|
150
+ |10|57620 rows/s|95340 rows/s|__1.65x__|
151
+ |1K|286.8K rows/s|2106.4 rows/s|__7.35x__|
152
+ |100K|181K rows/s|2275.3K rows/s|__12.53x__|
128
153
 
129
- (If you're interested in checking this yourself, just run the script and let me
130
- know if your results are different.)
154
+ When [fetching rows as arrays](https://github.com/digital-fabric/extralite/blob/main/test/perf_ary.rb) Extralite also significantly outperforms sqlite3-ruby:
131
155
 
132
- ## Can I use it with an ORM like ActiveRecord or Sequel?
156
+ |Row count|sqlite3-ruby|Extralite|Advantage|
157
+ |-:|-:|-:|-:|
158
+ |10|64365 rows/s|94031 rows/s|__1.46x__|
159
+ |1K|498.9K rows/s|2478.2K rows/s|__4.97x__|
160
+ |100K|441.1K rows/s|3023.4K rows/s|__6.85x__|
161
+
162
+ (If you're interested in checking this yourself, just run the script and let me
163
+ know if your results are better/worse.)
133
164
 
134
- Not yet, but you are welcome to contribute adapters for those projects.
165
+ As those benchmarks show, Extralite is capabale of reading more than 3M
166
+ rows/second (when fetching rows as arrays), and more than 2.2M rows/second (when
167
+ fetching rows as hashes.)
135
168
 
136
169
  ## Contributing
137
170
 
138
171
  Contributions in the form of issues, PRs or comments will be greatly
139
- appreciated!
172
+ 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);
@@ -101,7 +116,7 @@ inline VALUE get_column_value(sqlite3_stmt *stmt, int col, int type) {
101
116
  case SQLITE_TEXT:
102
117
  return rb_str_new_cstr((char *)sqlite3_column_text(stmt, col));
103
118
  case SQLITE_BLOB:
104
- rb_raise(cError, "BLOB reading not yet implemented");
119
+ return rb_str_new((const char *)sqlite3_column_blob(stmt, col), (long)sqlite3_column_bytes(stmt, col));
105
120
  default:
106
121
  rb_raise(cError, "Unknown column type: %d", type);
107
122
  }
@@ -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
  }
@@ -309,6 +354,29 @@ VALUE safe_query_hash(VALUE arg) {
309
354
  return result;
310
355
  }
311
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
+ */
312
380
  VALUE Database_query_hash(int argc, VALUE *argv, VALUE self) {
313
381
  query_ctx ctx = { self, argc, argv, 0 };
314
382
  return rb_ensure(safe_query_hash, (VALUE)&ctx, cleanup_stmt, (VALUE)&ctx);
@@ -343,6 +411,26 @@ VALUE safe_query_ary(VALUE arg) {
343
411
  return result;
344
412
  }
345
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
+ */
346
434
  VALUE Database_query_ary(int argc, VALUE *argv, VALUE self) {
347
435
  query_ctx ctx = { self, argc, argv, 0 };
348
436
  return rb_ensure(safe_query_ary, (VALUE)&ctx, cleanup_stmt, (VALUE)&ctx);
@@ -372,6 +460,25 @@ VALUE safe_query_single_row(VALUE arg) {
372
460
  return row;
373
461
  }
374
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
+ */
375
482
  VALUE Database_query_single_row(int argc, VALUE *argv, VALUE self) {
376
483
  query_ctx ctx = { self, argc, argv, 0 };
377
484
  return rb_ensure(safe_query_single_row, (VALUE)&ctx, cleanup_stmt, (VALUE)&ctx);
@@ -409,6 +516,26 @@ VALUE safe_query_single_column(VALUE arg) {
409
516
  return result;
410
517
  }
411
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
+ */
412
539
  VALUE Database_query_single_column(int argc, VALUE *argv, VALUE self) {
413
540
  query_ctx ctx = { self, argc, argv, 0 };
414
541
  return rb_ensure(safe_query_single_column, (VALUE)&ctx, cleanup_stmt, (VALUE)&ctx);
@@ -437,11 +564,34 @@ VALUE safe_query_single_value(VALUE arg) {
437
564
  return value;
438
565
  }
439
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
+ */
440
586
  VALUE Database_query_single_value(int argc, VALUE *argv, VALUE self) {
441
587
  query_ctx ctx = { self, argc, argv, 0 };
442
588
  return rb_ensure(safe_query_single_value, (VALUE)&ctx, cleanup_stmt, (VALUE)&ctx);
443
589
  }
444
590
 
591
+ /* call-seq: last_insert_rowid
592
+ *
593
+ * Returns the rowid of the last inserted row.
594
+ */
445
595
  VALUE Database_last_insert_rowid(VALUE self) {
446
596
  Database_t *db;
447
597
  GetOpenDatabase(self, db);
@@ -449,6 +599,10 @@ VALUE Database_last_insert_rowid(VALUE self) {
449
599
  return INT2NUM(sqlite3_last_insert_rowid(db->sqlite3_db));
450
600
  }
451
601
 
602
+ /* call-seq: changes
603
+ *
604
+ * Returns the number of changes made to the database by the last operation.
605
+ */
452
606
  VALUE Database_changes(VALUE self) {
453
607
  Database_t *db;
454
608
  GetOpenDatabase(self, db);
@@ -456,6 +610,10 @@ VALUE Database_changes(VALUE self) {
456
610
  return INT2NUM(sqlite3_changes(db->sqlite3_db));
457
611
  }
458
612
 
613
+ /* call-seq: filename
614
+ *
615
+ * Returns the database filename.
616
+ */
459
617
  VALUE Database_filename(int argc, VALUE *argv, VALUE self) {
460
618
  const char *db_name;
461
619
  const char *filename;
@@ -468,6 +626,10 @@ VALUE Database_filename(int argc, VALUE *argv, VALUE self) {
468
626
  return filename ? rb_str_new_cstr(filename) : Qnil;
469
627
  }
470
628
 
629
+ /* call-seq: transaction_active?
630
+ *
631
+ * Returns true if a transaction is currently in progress.
632
+ */
471
633
  VALUE Database_transaction_active_p(VALUE self) {
472
634
  Database_t *db;
473
635
  GetOpenDatabase(self, db);
@@ -475,6 +637,10 @@ VALUE Database_transaction_active_p(VALUE self) {
475
637
  return sqlite3_get_autocommit(db->sqlite3_db) ? Qfalse : Qtrue;
476
638
  }
477
639
 
640
+ /* call-seq: load_extension(path)
641
+ *
642
+ * Loads an extension with the given path.
643
+ */
478
644
  VALUE Database_load_extension(VALUE self, VALUE path) {
479
645
  Database_t *db;
480
646
  GetOpenDatabase(self, db);
@@ -519,5 +685,7 @@ void Init_Extralite() {
519
685
  rb_gc_register_mark_object(cSQLError);
520
686
  rb_gc_register_mark_object(cBusyError);
521
687
 
522
- 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");
523
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.6'
2
+ VERSION = '1.9'
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_ary.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)
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_ary('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
File without changes
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,50 @@ 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
141
+
142
+ def test_value_casting
143
+ r = @db.query_single_value("select 'abc'")
144
+ assert_equal 'abc', r
145
+
146
+ r = @db.query_single_value('select 123')
147
+ assert_equal 123, r
148
+
149
+ r = @db.query_single_value('select 12.34')
150
+ assert_equal 12.34, r
151
+
152
+ r = @db.query_single_value('select zeroblob(4)')
153
+ assert_equal "\x00\x00\x00\x00", r
154
+
155
+ r = @db.query_single_value('select null')
156
+ assert_nil r
157
+ end
114
158
  end
115
159
 
116
160
  class ScenarioTest < MiniTest::Test
@@ -179,4 +223,3 @@ class ScenarioTest < MiniTest::Test
179
223
  assert_equal [1, 4, 7], result
180
224
  end
181
225
  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.6'
4
+ version: '1.9'
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: []
@@ -88,6 +88,7 @@ extensions:
88
88
  extra_rdoc_files:
89
89
  - README.md
90
90
  files:
91
+ - ".github/FUNDING.yml"
91
92
  - ".github/workflows/test.yml"
92
93
  - ".gitignore"
93
94
  - CHANGELOG.md
@@ -103,15 +104,19 @@ files:
103
104
  - extralite.gemspec
104
105
  - lib/extralite.rb
105
106
  - lib/extralite/version.rb
107
+ - lib/sequel/adapters/extralite.rb
106
108
  - test/helper.rb
107
- - test/perf.rb
109
+ - test/perf_ary.rb
110
+ - test/perf_hash.rb
111
+ - test/run.rb
108
112
  - test/test_database.rb
113
+ - test/test_sequel.rb
109
114
  homepage: https://github.com/digital-fabric/extralite
110
115
  licenses:
111
116
  - MIT
112
117
  metadata:
113
118
  source_code_uri: https://github.com/digital-fabric/extralite
114
- documentation_uri: https://github.com/digital-fabric/extralite
119
+ documentation_uri: https://www.rubydoc.info/gems/extralite
115
120
  homepage_uri: https://github.com/digital-fabric/extralite
116
121
  changelog_uri: https://github.com/digital-fabric/extralite/blob/master/CHANGELOG.md
117
122
  post_install_message: