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 +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
|
[](http://rubygems.org/gems/extralite)
|
4
4
|
[](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:
|