extralite 1.6 → 1.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +1 -0
- data/.github/workflows/test.yml +3 -8
- data/CHANGELOG.md +13 -0
- data/Gemfile.lock +12 -33
- data/README.md +63 -30
- data/Rakefile +17 -8
- data/ext/extralite/extralite.c +170 -2
- data/extralite.gemspec +6 -5
- data/lib/extralite/version.rb +1 -1
- data/lib/extralite.rb +20 -0
- data/lib/sequel/adapters/extralite.rb +380 -0
- data/test/perf_ary.rb +51 -0
- data/test/{perf.rb → perf_hash.rb} +0 -0
- data/test/run.rb +5 -0
- data/test/test_database.rb +44 -1
- data/test/test_sequel.rb +24 -0
- metadata +19 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 874f2ebc573b23e4336b69409252b2cbb8c71c1275b1ca34d1d1924999d8ed31
|
4
|
+
data.tar.gz: 5b304d82a818c3e0da2dd72bca6fa590ff8be84241c6dc54fdb9f3bee49e498a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dd039c09306a04eb1e1cd42eb968c58cbf7693fd09c31587e9b7853a061c2a607af9d594859510f6cabc2ef5f166426a5a0ae4459b40168b10ea3fc4c6e3063f
|
7
|
+
data.tar.gz: 2b65161b0ec69a8072e8e2f343386d8178194d1dae3bfc2ac16e414f5682d2ca1da501057f1da38cf875238e0314aa35032dcdbbf3d610affe14fb572713435a
|
data/.github/FUNDING.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
github: ciconia
|
data/.github/workflows/test.yml
CHANGED
@@ -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:
|
19
|
+
- uses: ruby/setup-ruby@v1
|
20
20
|
with:
|
21
21
|
ruby-version: ${{matrix.ruby}}
|
22
|
-
|
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.
|
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
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
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
|
-
|
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.
|
52
|
-
|
53
|
-
|
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
|
10
|
-
wrapper for Ruby. It provides a single class with a minimal set of methods
|
11
|
-
|
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
|
21
|
-
released while preparing SQL statements and while iterating over
|
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.
|
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
|
88
|
-
thousands of developers. I've
|
89
|
-
|
90
|
-
variety of ways. Thus extralite was
|
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,
|
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|~
|
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
|
-
|
118
|
-
|
119
|
-
|
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
|
124
|
-
|
125
|
-
|10|
|
126
|
-
|1K|
|
127
|
-
|100K|
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
4
|
-
require
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'rake/clean'
|
5
5
|
|
6
|
-
require
|
7
|
-
Rake::ExtensionTask.new(
|
8
|
-
ext.ext_dir =
|
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/
|
16
|
+
exec 'ruby test/run.rb'
|
16
17
|
end
|
17
18
|
|
18
|
-
CLEAN.include
|
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
|
data/ext/extralite/extralite.c
CHANGED
@@ -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
|
-
|
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
|
-
|
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://
|
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.
|
25
|
-
s.add_development_dependency 'minitest', '5.
|
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 '
|
28
|
-
|
27
|
+
s.add_development_dependency 'yard', '0.9.27'
|
28
|
+
|
29
|
+
s.add_development_dependency 'sequel', '5.51.0'
|
29
30
|
end
|
data/lib/extralite/version.rb
CHANGED
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
data/test/test_database.rb
CHANGED
@@ -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
|
-
|
data/test/test_sequel.rb
ADDED
@@ -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.
|
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-
|
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.
|
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.
|
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.
|
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.
|
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:
|
56
|
+
name: yard
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - '='
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: 0.
|
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.
|
68
|
+
version: 0.9.27
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
70
|
+
name: sequel
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - '='
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version:
|
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:
|
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/
|
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://
|
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:
|