extralite 1.5 → 1.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +3 -8
- data/CHANGELOG.md +13 -0
- data/Gemfile.lock +12 -33
- data/README.md +83 -20
- data/Rakefile +17 -8
- data/ext/extralite/extralite.c +184 -5
- 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.rb +16 -13
- data/test/run.rb +5 -0
- data/test/test_database.rb +27 -1
- data/test/test_sequel.rb +24 -0
- metadata +16 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dd26d2937628e408ba8ee2f69e2e170755a43d5e1ffcdcbb82ca184cd5734576
|
|
4
|
+
data.tar.gz: c762b88c6e1671de5daab90f6f6fb574bb4dfb43fb0a866e8154fb8ac02f755f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9ea096a28f15e8415b8e450a45035ffbe0c2186ebc7bba2ddd76c34e2328baaff307cee3c5079dda235992011497ef72b924bf3916628842e100ba8953ee7454
|
|
7
|
+
data.tar.gz: 0efd84d3a7fa1fd1157df43f65bd4fa6999a0943efc8c0b36030a8550bbb2bc1c3a96509d4bf6c7a8a2bb9c4bd071dc0a650cb45e54e75c7ec722ee29c8bb72e
|
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.8.2 2021-12-15
|
|
2
|
+
|
|
3
|
+
- Add documentation
|
|
4
|
+
|
|
5
|
+
## 1.7 2021-12-13
|
|
6
|
+
|
|
7
|
+
- Add extralite Sequel adapter
|
|
8
|
+
- Add support for binding hash parameters
|
|
9
|
+
|
|
10
|
+
## 1.6 2021-12-13
|
|
11
|
+
|
|
12
|
+
- Release GVL while fetching rows
|
|
13
|
+
|
|
1
14
|
## 1.5 2021-12-13
|
|
2
15
|
|
|
3
16
|
- Release GVL while preparing statements
|
data/Gemfile.lock
CHANGED
|
@@ -1,58 +1,37 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
extralite (1.
|
|
4
|
+
extralite (1.8.2)
|
|
5
5
|
|
|
6
6
|
GEM
|
|
7
7
|
remote: https://rubygems.org/
|
|
8
8
|
specs:
|
|
9
|
-
ast (2.4.2)
|
|
10
|
-
coderay (1.1.3)
|
|
11
9
|
docile (1.4.0)
|
|
12
10
|
json (2.5.1)
|
|
13
|
-
|
|
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
|
@@ -6,22 +6,29 @@
|
|
|
6
6
|
|
|
7
7
|
## What is Extralite?
|
|
8
8
|
|
|
9
|
-
Extralite is an extra-lightweight (less than
|
|
10
|
-
Ruby. It provides a single class with a minimal set of methods to
|
|
11
|
-
an SQLite3 database.
|
|
9
|
+
Extralite is an extra-lightweight (less than 430 lines of C-code) SQLite3
|
|
10
|
+
wrapper for Ruby. It provides a single class with a minimal set of methods to
|
|
11
|
+
interact with an SQLite3 database.
|
|
12
12
|
|
|
13
13
|
## Features
|
|
14
14
|
|
|
15
|
-
- A variety of methods for different data access patterns:
|
|
16
|
-
|
|
15
|
+
- A variety of methods for different data access patterns: rows as hashes, rows
|
|
16
|
+
as arrays, single row, single column, single value.
|
|
17
|
+
- Super fast - [up to 12.5x faster](#performance) than the
|
|
18
|
+
[sqlite3](https://github.com/sparklemotion/sqlite3-ruby) gem (see also
|
|
19
|
+
[comparison](#why-not-just-use-the-sqlite3-gem).)
|
|
20
|
+
- Improved [concurrency](#what-about-concurrency) for multithreaded apps: the
|
|
21
|
+
Ruby GVL is released while preparing SQL statements and while iterating over
|
|
22
|
+
results.
|
|
17
23
|
- Iterate over records with a block, or collect records into an array.
|
|
18
24
|
- Parameter binding.
|
|
19
|
-
-
|
|
20
|
-
creating/modifying schemas).
|
|
25
|
+
- Automatically execute SQL strings containing multiple semicolon-separated
|
|
26
|
+
queries (handy for creating/modifying schemas).
|
|
21
27
|
- Get last insert rowid.
|
|
22
28
|
- Get number of rows changed by last query.
|
|
23
29
|
- Load extensions (loading of extensions is autmatically enabled. You can find
|
|
24
30
|
some useful extensions here: https://github.com/nalgeon/sqlean.)
|
|
31
|
+
- Includes a [Sequel adapter](#usage-with-sequel) (an ActiveRecord)
|
|
25
32
|
|
|
26
33
|
## Usage
|
|
27
34
|
|
|
@@ -60,8 +67,13 @@ db.query_single_value("select 'foo'") #=> "foo"
|
|
|
60
67
|
# parameter binding (works for all query_xxx methods)
|
|
61
68
|
db.query_hash('select ? as foo, ? as bar', 1, 2) #=> [{ :foo => 1, :bar => 2 }]
|
|
62
69
|
|
|
70
|
+
# parameter binding of named parameters
|
|
71
|
+
db.query('select * from foo where bar = :bar', bar: 42)
|
|
72
|
+
db.query('select * from foo where bar = :bar', 'bar' => 42)
|
|
73
|
+
db.query('select * from foo where bar = :bar', ':bar' => 42)
|
|
74
|
+
|
|
63
75
|
# get last insert rowid
|
|
64
|
-
rowid = db.
|
|
76
|
+
rowid = db.last_insert_rowid
|
|
65
77
|
|
|
66
78
|
# get number of rows changed in last query
|
|
67
79
|
number_of_rows_affected = db.changes
|
|
@@ -77,23 +89,74 @@ db.close
|
|
|
77
89
|
db.closed? #=> true
|
|
78
90
|
```
|
|
79
91
|
|
|
92
|
+
## Usage with Sequel
|
|
93
|
+
|
|
94
|
+
Extralite includes an adapter for
|
|
95
|
+
[Sequel](https://github.com/jeremyevans/sequel). To use the Extralite adapter,
|
|
96
|
+
just use the `extralite` scheme instead of `sqlite`:
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
DB = Sequel.connect('extralite:blog.db')
|
|
100
|
+
articles = DB[:articles]
|
|
101
|
+
p articles.to_a
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
(Make sure you include `extralite` as a dependency in your `Gemfile`.)
|
|
105
|
+
|
|
80
106
|
## Why not just use the sqlite3 gem?
|
|
81
107
|
|
|
82
|
-
The sqlite3-ruby gem is a
|
|
83
|
-
thousands of developers. I've
|
|
84
|
-
|
|
85
|
-
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.
|
|
118
|
+
|
|
119
|
+
Here's a table summarizing the differences between the two gems:
|
|
120
|
+
|
|
121
|
+
| |sqlite3-ruby|Extralite|
|
|
122
|
+
|-|-|-|
|
|
123
|
+
|API design|multiple classes|single class|
|
|
124
|
+
|Query results|row as hash, row as array, single row, single value|row as hash, row as array, __single column__, single row, single value|
|
|
125
|
+
|execute multiple statements|separate API (#execute_batch)|integrated|
|
|
126
|
+
|custom functions in Ruby|yes|no|
|
|
127
|
+
|custom collations|yes|no|
|
|
128
|
+
|custom aggregate functions|yes|no|
|
|
129
|
+
|Multithread friendly|no|[yes](#what-about-concurrency)|
|
|
130
|
+
|Code size|~2650LoC|~500LoC|
|
|
131
|
+
|Performance|1x|1.5x to 12.5x (see [below](#performance))|
|
|
86
132
|
|
|
87
133
|
## What about concurrency?
|
|
88
134
|
|
|
89
|
-
Extralite
|
|
90
|
-
|
|
91
|
-
|
|
135
|
+
Extralite releases the GVL while making blocking calls to the sqlite3 library,
|
|
136
|
+
that is while preparing SQL statements and fetching rows. Releasing the GVL
|
|
137
|
+
allows other threads to run while the sqlite3 library is busy compiling SQL into
|
|
138
|
+
bytecode, or fetching the next row. This does not seem to hurt Extralite's
|
|
139
|
+
performance:
|
|
140
|
+
|
|
141
|
+
## Performance
|
|
142
|
+
|
|
143
|
+
A benchmark script is
|
|
144
|
+
[included](https://github.com/digital-fabric/extralite/blob/main/test/perf.rb),
|
|
145
|
+
creating a table of various row counts, then fetching the entire table using
|
|
146
|
+
either `sqlite3` or `extralite`. This benchmark shows Extralite to be up to 12.5
|
|
147
|
+
times faster than `sqlite3` when fetching a large number of rows. Here are the
|
|
148
|
+
results (using the `sqlite3` gem performance as baseline):
|
|
149
|
+
|
|
150
|
+
|Row count|sqlite3-ruby (baseline)|Extralite (relative - rounded)|
|
|
151
|
+
|-:|-:|-:|
|
|
152
|
+
|10|1x|1.5x|
|
|
153
|
+
|1K|1x|7x|
|
|
154
|
+
|100K|1x|12.5x|
|
|
92
155
|
|
|
93
|
-
|
|
94
|
-
|
|
156
|
+
(If you're interested in checking this yourself, just run the script and let me
|
|
157
|
+
know if your results are different.)
|
|
95
158
|
|
|
96
|
-
##
|
|
159
|
+
## Contributing
|
|
97
160
|
|
|
98
|
-
|
|
99
|
-
|
|
161
|
+
Contributions in the form of issues, PRs or comments will be greatly
|
|
162
|
+
appreciated!
|
data/Rakefile
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
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);
|
|
@@ -109,6 +124,33 @@ inline VALUE get_column_value(sqlite3_stmt *stmt, int col, int type) {
|
|
|
109
124
|
return Qnil;
|
|
110
125
|
}
|
|
111
126
|
|
|
127
|
+
static void bind_parameter_value(sqlite3_stmt *stmt, int pos, VALUE value);
|
|
128
|
+
|
|
129
|
+
static inline void bind_hash_parameter_values(sqlite3_stmt *stmt, VALUE hash) {
|
|
130
|
+
VALUE keys = rb_funcall(hash, ID_KEYS, 0);
|
|
131
|
+
int len = RARRAY_LEN(keys);
|
|
132
|
+
for (int i = 0; i < len; i++) {
|
|
133
|
+
VALUE k = RARRAY_AREF(keys, i);
|
|
134
|
+
VALUE v = rb_hash_aref(hash, k);
|
|
135
|
+
|
|
136
|
+
switch (TYPE(k)) {
|
|
137
|
+
case T_FIXNUM:
|
|
138
|
+
bind_parameter_value(stmt, NUM2INT(k), v);
|
|
139
|
+
return;
|
|
140
|
+
case T_SYMBOL:
|
|
141
|
+
k = rb_funcall(k, ID_TO_S, 0);
|
|
142
|
+
case T_STRING:
|
|
143
|
+
if(RSTRING_PTR(k)[0] != ':') k = rb_str_plus(rb_str_new2(":"), k);
|
|
144
|
+
int pos = sqlite3_bind_parameter_index(stmt, StringValuePtr(k));
|
|
145
|
+
bind_parameter_value(stmt, pos, v);
|
|
146
|
+
return;
|
|
147
|
+
default:
|
|
148
|
+
rb_raise(cError, "Cannot bind hash key value idx %d", i);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
RB_GC_GUARD(keys);
|
|
152
|
+
}
|
|
153
|
+
|
|
112
154
|
static inline void bind_parameter_value(sqlite3_stmt *stmt, int pos, VALUE value) {
|
|
113
155
|
switch (TYPE(value)) {
|
|
114
156
|
case T_NIL:
|
|
@@ -129,6 +171,9 @@ static inline void bind_parameter_value(sqlite3_stmt *stmt, int pos, VALUE value
|
|
|
129
171
|
case T_STRING:
|
|
130
172
|
sqlite3_bind_text(stmt, pos, RSTRING_PTR(value), RSTRING_LEN(value), SQLITE_TRANSIENT);
|
|
131
173
|
return;
|
|
174
|
+
case T_HASH:
|
|
175
|
+
bind_hash_parameter_values(stmt, value);
|
|
176
|
+
return;
|
|
132
177
|
default:
|
|
133
178
|
rb_raise(cError, "Cannot bind parameter at position %d", pos);
|
|
134
179
|
}
|
|
@@ -228,10 +273,21 @@ inline void prepare_multi_stmt(sqlite3 *db, sqlite3_stmt **stmt, VALUE sql) {
|
|
|
228
273
|
}
|
|
229
274
|
}
|
|
230
275
|
|
|
231
|
-
|
|
276
|
+
struct step_ctx {
|
|
277
|
+
sqlite3_stmt *stmt;
|
|
232
278
|
int rc;
|
|
233
|
-
|
|
234
|
-
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
void *stmt_iterate_without_gvl(void *ptr) {
|
|
282
|
+
struct step_ctx *ctx = (struct step_ctx *)ptr;
|
|
283
|
+
ctx->rc = sqlite3_step(ctx->stmt);
|
|
284
|
+
return NULL;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
inline int stmt_iterate(sqlite3_stmt *stmt, sqlite3 *db) {
|
|
288
|
+
struct step_ctx ctx = {stmt, 0};
|
|
289
|
+
rb_thread_call_without_gvl(stmt_iterate_without_gvl, (void *)&ctx, RUBY_UBF_IO, 0);
|
|
290
|
+
switch (ctx.rc) {
|
|
235
291
|
case SQLITE_ROW:
|
|
236
292
|
return 1;
|
|
237
293
|
case SQLITE_DONE:
|
|
@@ -241,7 +297,7 @@ inline int stmt_iterate(sqlite3_stmt *stmt, sqlite3 *db) {
|
|
|
241
297
|
case SQLITE_ERROR:
|
|
242
298
|
rb_raise(cSQLError, "%s", sqlite3_errmsg(db));
|
|
243
299
|
default:
|
|
244
|
-
rb_raise(cError, "Invalid return code for sqlite3_step: %d (please open an issue on https://github.com/digital-fabric/extralite)", rc);
|
|
300
|
+
rb_raise(cError, "Invalid return code for sqlite3_step: %d (please open an issue on https://github.com/digital-fabric/extralite)", ctx.rc);
|
|
245
301
|
}
|
|
246
302
|
|
|
247
303
|
return 0;
|
|
@@ -298,6 +354,29 @@ VALUE safe_query_hash(VALUE arg) {
|
|
|
298
354
|
return result;
|
|
299
355
|
}
|
|
300
356
|
|
|
357
|
+
/* call-seq:
|
|
358
|
+
* query(sql, *parameters, &block)
|
|
359
|
+
* query_hash(sql, *parameters, &block)
|
|
360
|
+
*
|
|
361
|
+
* Runs a query returning rows as hashes (with symbol keys). If a block is
|
|
362
|
+
* given, it will be called for each row. Otherwise, an array containing all
|
|
363
|
+
* rows is returned.
|
|
364
|
+
*
|
|
365
|
+
* Query parameters to be bound to placeholders in the query can be specified as
|
|
366
|
+
* a list of values or as a hash mapping parameter names to values. When
|
|
367
|
+
* parameters are given as a least, the query should specify parameters using
|
|
368
|
+
* `?`:
|
|
369
|
+
*
|
|
370
|
+
* db.query('select * from foo where x = ?', 42)
|
|
371
|
+
*
|
|
372
|
+
* Named placeholders are specified using `:`. The placeholder values are
|
|
373
|
+
* specified using a hash, where keys are either strings are symbols. String
|
|
374
|
+
* keys can include or omit the `:` prefix. The following are equivalent:
|
|
375
|
+
*
|
|
376
|
+
* db.query('select * from foo where x = :bar', bar: 42)
|
|
377
|
+
* db.query('select * from foo where x = :bar', 'bar' => 42)
|
|
378
|
+
* db.query('select * from foo where x = :bar', ':bar' => 42)
|
|
379
|
+
*/
|
|
301
380
|
VALUE Database_query_hash(int argc, VALUE *argv, VALUE self) {
|
|
302
381
|
query_ctx ctx = { self, argc, argv, 0 };
|
|
303
382
|
return rb_ensure(safe_query_hash, (VALUE)&ctx, cleanup_stmt, (VALUE)&ctx);
|
|
@@ -332,6 +411,26 @@ VALUE safe_query_ary(VALUE arg) {
|
|
|
332
411
|
return result;
|
|
333
412
|
}
|
|
334
413
|
|
|
414
|
+
/* call-seq: query_ary(sql, *parameters, &block)
|
|
415
|
+
*
|
|
416
|
+
* Runs a query returning rows as arrays. If a block is given, it will be called
|
|
417
|
+
* for each row. Otherwise, an array containing all rows is returned.
|
|
418
|
+
*
|
|
419
|
+
* Query parameters to be bound to placeholders in the query can be specified as
|
|
420
|
+
* a list of values or as a hash mapping parameter names to values. When
|
|
421
|
+
* parameters are given as a least, the query should specify parameters using
|
|
422
|
+
* `?`:
|
|
423
|
+
*
|
|
424
|
+
* db.query_ary('select * from foo where x = ?', 42)
|
|
425
|
+
*
|
|
426
|
+
* Named placeholders are specified using `:`. The placeholder values are
|
|
427
|
+
* specified using a hash, where keys are either strings are symbols. String
|
|
428
|
+
* keys can include or omit the `:` prefix. The following are equivalent:
|
|
429
|
+
*
|
|
430
|
+
* db.query_ary('select * from foo where x = :bar', bar: 42)
|
|
431
|
+
* db.query_ary('select * from foo where x = :bar', 'bar' => 42)
|
|
432
|
+
* db.query_ary('select * from foo where x = :bar', ':bar' => 42)
|
|
433
|
+
*/
|
|
335
434
|
VALUE Database_query_ary(int argc, VALUE *argv, VALUE self) {
|
|
336
435
|
query_ctx ctx = { self, argc, argv, 0 };
|
|
337
436
|
return rb_ensure(safe_query_ary, (VALUE)&ctx, cleanup_stmt, (VALUE)&ctx);
|
|
@@ -361,6 +460,25 @@ VALUE safe_query_single_row(VALUE arg) {
|
|
|
361
460
|
return row;
|
|
362
461
|
}
|
|
363
462
|
|
|
463
|
+
/* call-seq: query_single_row(sql, *parameters)
|
|
464
|
+
*
|
|
465
|
+
* Runs a query returning a single row as a hash.
|
|
466
|
+
*
|
|
467
|
+
* Query parameters to be bound to placeholders in the query can be specified as
|
|
468
|
+
* a list of values or as a hash mapping parameter names to values. When
|
|
469
|
+
* parameters are given as a least, the query should specify parameters using
|
|
470
|
+
* `?`:
|
|
471
|
+
*
|
|
472
|
+
* db.query_single_row('select * from foo where x = ?', 42)
|
|
473
|
+
*
|
|
474
|
+
* Named placeholders are specified using `:`. The placeholder values are
|
|
475
|
+
* specified using a hash, where keys are either strings are symbols. String
|
|
476
|
+
* keys can include or omit the `:` prefix. The following are equivalent:
|
|
477
|
+
*
|
|
478
|
+
* db.query_single_row('select * from foo where x = :bar', bar: 42)
|
|
479
|
+
* db.query_single_row('select * from foo where x = :bar', 'bar' => 42)
|
|
480
|
+
* db.query_single_row('select * from foo where x = :bar', ':bar' => 42)
|
|
481
|
+
*/
|
|
364
482
|
VALUE Database_query_single_row(int argc, VALUE *argv, VALUE self) {
|
|
365
483
|
query_ctx ctx = { self, argc, argv, 0 };
|
|
366
484
|
return rb_ensure(safe_query_single_row, (VALUE)&ctx, cleanup_stmt, (VALUE)&ctx);
|
|
@@ -398,6 +516,26 @@ VALUE safe_query_single_column(VALUE arg) {
|
|
|
398
516
|
return result;
|
|
399
517
|
}
|
|
400
518
|
|
|
519
|
+
/* call-seq: query_single_column(sql, *parameters, &block)
|
|
520
|
+
*
|
|
521
|
+
* Runs a query returning single column values. If a block is given, it will be called
|
|
522
|
+
* for each value. Otherwise, an array containing all values is returned.
|
|
523
|
+
*
|
|
524
|
+
* Query parameters to be bound to placeholders in the query can be specified as
|
|
525
|
+
* a list of values or as a hash mapping parameter names to values. When
|
|
526
|
+
* parameters are given as a least, the query should specify parameters using
|
|
527
|
+
* `?`:
|
|
528
|
+
*
|
|
529
|
+
* db.query_single_column('select x from foo where x = ?', 42)
|
|
530
|
+
*
|
|
531
|
+
* Named placeholders are specified using `:`. The placeholder values are
|
|
532
|
+
* specified using a hash, where keys are either strings are symbols. String
|
|
533
|
+
* keys can include or omit the `:` prefix. The following are equivalent:
|
|
534
|
+
*
|
|
535
|
+
* db.query_single_column('select x from foo where x = :bar', bar: 42)
|
|
536
|
+
* db.query_single_column('select x from foo where x = :bar', 'bar' => 42)
|
|
537
|
+
* db.query_single_column('select x from foo where x = :bar', ':bar' => 42)
|
|
538
|
+
*/
|
|
401
539
|
VALUE Database_query_single_column(int argc, VALUE *argv, VALUE self) {
|
|
402
540
|
query_ctx ctx = { self, argc, argv, 0 };
|
|
403
541
|
return rb_ensure(safe_query_single_column, (VALUE)&ctx, cleanup_stmt, (VALUE)&ctx);
|
|
@@ -426,11 +564,34 @@ VALUE safe_query_single_value(VALUE arg) {
|
|
|
426
564
|
return value;
|
|
427
565
|
}
|
|
428
566
|
|
|
567
|
+
/* call-seq: query_single_value(sql, *parameters)
|
|
568
|
+
*
|
|
569
|
+
* Runs a query returning a single value from the first row.
|
|
570
|
+
*
|
|
571
|
+
* Query parameters to be bound to placeholders in the query can be specified as
|
|
572
|
+
* a list of values or as a hash mapping parameter names to values. When
|
|
573
|
+
* parameters are given as a least, the query should specify parameters using
|
|
574
|
+
* `?`:
|
|
575
|
+
*
|
|
576
|
+
* db.query_single_value('select x from foo where x = ?', 42)
|
|
577
|
+
*
|
|
578
|
+
* Named placeholders are specified using `:`. The placeholder values are
|
|
579
|
+
* specified using a hash, where keys are either strings are symbols. String
|
|
580
|
+
* keys can include or omit the `:` prefix. The following are equivalent:
|
|
581
|
+
*
|
|
582
|
+
* db.query_single_value('select x from foo where x = :bar', bar: 42)
|
|
583
|
+
* db.query_single_value('select x from foo where x = :bar', 'bar' => 42)
|
|
584
|
+
* db.query_single_value('select x from foo where x = :bar', ':bar' => 42)
|
|
585
|
+
*/
|
|
429
586
|
VALUE Database_query_single_value(int argc, VALUE *argv, VALUE self) {
|
|
430
587
|
query_ctx ctx = { self, argc, argv, 0 };
|
|
431
588
|
return rb_ensure(safe_query_single_value, (VALUE)&ctx, cleanup_stmt, (VALUE)&ctx);
|
|
432
589
|
}
|
|
433
590
|
|
|
591
|
+
/* call-seq: last_insert_rowid
|
|
592
|
+
*
|
|
593
|
+
* Returns the rowid of the last inserted row.
|
|
594
|
+
*/
|
|
434
595
|
VALUE Database_last_insert_rowid(VALUE self) {
|
|
435
596
|
Database_t *db;
|
|
436
597
|
GetOpenDatabase(self, db);
|
|
@@ -438,6 +599,10 @@ VALUE Database_last_insert_rowid(VALUE self) {
|
|
|
438
599
|
return INT2NUM(sqlite3_last_insert_rowid(db->sqlite3_db));
|
|
439
600
|
}
|
|
440
601
|
|
|
602
|
+
/* call-seq: changes
|
|
603
|
+
*
|
|
604
|
+
* Returns the number of changes made to the database by the last operation.
|
|
605
|
+
*/
|
|
441
606
|
VALUE Database_changes(VALUE self) {
|
|
442
607
|
Database_t *db;
|
|
443
608
|
GetOpenDatabase(self, db);
|
|
@@ -445,6 +610,10 @@ VALUE Database_changes(VALUE self) {
|
|
|
445
610
|
return INT2NUM(sqlite3_changes(db->sqlite3_db));
|
|
446
611
|
}
|
|
447
612
|
|
|
613
|
+
/* call-seq: filename
|
|
614
|
+
*
|
|
615
|
+
* Returns the database filename.
|
|
616
|
+
*/
|
|
448
617
|
VALUE Database_filename(int argc, VALUE *argv, VALUE self) {
|
|
449
618
|
const char *db_name;
|
|
450
619
|
const char *filename;
|
|
@@ -457,6 +626,10 @@ VALUE Database_filename(int argc, VALUE *argv, VALUE self) {
|
|
|
457
626
|
return filename ? rb_str_new_cstr(filename) : Qnil;
|
|
458
627
|
}
|
|
459
628
|
|
|
629
|
+
/* call-seq: transaction_active?
|
|
630
|
+
*
|
|
631
|
+
* Returns true if a transaction is currently in progress.
|
|
632
|
+
*/
|
|
460
633
|
VALUE Database_transaction_active_p(VALUE self) {
|
|
461
634
|
Database_t *db;
|
|
462
635
|
GetOpenDatabase(self, db);
|
|
@@ -464,6 +637,10 @@ VALUE Database_transaction_active_p(VALUE self) {
|
|
|
464
637
|
return sqlite3_get_autocommit(db->sqlite3_db) ? Qfalse : Qtrue;
|
|
465
638
|
}
|
|
466
639
|
|
|
640
|
+
/* call-seq: load_extension(path)
|
|
641
|
+
*
|
|
642
|
+
* Loads an extension with the given path.
|
|
643
|
+
*/
|
|
467
644
|
VALUE Database_load_extension(VALUE self, VALUE path) {
|
|
468
645
|
Database_t *db;
|
|
469
646
|
GetOpenDatabase(self, db);
|
|
@@ -508,5 +685,7 @@ void Init_Extralite() {
|
|
|
508
685
|
rb_gc_register_mark_object(cSQLError);
|
|
509
686
|
rb_gc_register_mark_object(cBusyError);
|
|
510
687
|
|
|
511
|
-
|
|
688
|
+
ID_KEYS = rb_intern("keys");
|
|
689
|
+
ID_STRIP = rb_intern("strip");
|
|
690
|
+
ID_TO_S = rb_intern("to_s");
|
|
512
691
|
}
|
data/extralite.gemspec
CHANGED
|
@@ -11,7 +11,7 @@ Gem::Specification.new do |s|
|
|
|
11
11
|
s.homepage = 'https://github.com/digital-fabric/extralite'
|
|
12
12
|
s.metadata = {
|
|
13
13
|
"source_code_uri" => "https://github.com/digital-fabric/extralite",
|
|
14
|
-
"documentation_uri" => "https://
|
|
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.rb
CHANGED
|
@@ -13,36 +13,39 @@ require 'benchmark/ips'
|
|
|
13
13
|
require 'fileutils'
|
|
14
14
|
|
|
15
15
|
DB_PATH = '/tmp/extralite_sqlite3_perf.db'
|
|
16
|
-
COUNT = 10000
|
|
17
16
|
|
|
18
|
-
def prepare_database
|
|
17
|
+
def prepare_database(count)
|
|
19
18
|
FileUtils.rm(DB_PATH) rescue nil
|
|
20
19
|
db = Extralite::Database.new(DB_PATH)
|
|
21
20
|
db.query('create table foo ( a integer primary key, b text )')
|
|
22
21
|
db.query('begin')
|
|
23
|
-
|
|
22
|
+
count.times { db.query('insert into foo (b) values (?)', "hello#{rand(1000)}" )}
|
|
24
23
|
db.query('commit')
|
|
25
24
|
end
|
|
26
25
|
|
|
27
|
-
def sqlite3_run
|
|
26
|
+
def sqlite3_run(count)
|
|
28
27
|
db = SQLite3::Database.new(DB_PATH, :results_as_hash => true)
|
|
29
28
|
results = db.execute('select * from foo')
|
|
30
|
-
raise unless results.size ==
|
|
29
|
+
raise unless results.size == count
|
|
31
30
|
end
|
|
32
31
|
|
|
33
|
-
def extralite_run
|
|
32
|
+
def extralite_run(count)
|
|
34
33
|
db = Extralite::Database.new(DB_PATH)
|
|
35
34
|
results = db.query('select * from foo')
|
|
36
|
-
raise unless results.size ==
|
|
35
|
+
raise unless results.size == count
|
|
37
36
|
end
|
|
38
37
|
|
|
39
|
-
|
|
38
|
+
[10, 1000, 100000].each do |c|
|
|
39
|
+
puts; puts; puts "Record count: #{c}"
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
x.config(:time => 3, :warmup => 1)
|
|
41
|
+
prepare_database(c)
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
Benchmark.ips do |x|
|
|
44
|
+
x.config(:time => 3, :warmup => 1)
|
|
46
45
|
|
|
47
|
-
|
|
46
|
+
x.report("sqlite3") { sqlite3_run(c) }
|
|
47
|
+
x.report("extralite") { extralite_run(c) }
|
|
48
|
+
|
|
49
|
+
x.compare!
|
|
50
|
+
end
|
|
48
51
|
end
|
data/test/run.rb
ADDED
data/test/test_database.rb
CHANGED
|
@@ -111,6 +111,33 @@ end
|
|
|
111
111
|
|
|
112
112
|
assert_raises(Extralite::Error) { @db.query_single_value('select 42') }
|
|
113
113
|
end
|
|
114
|
+
|
|
115
|
+
def test_parameter_binding_simple
|
|
116
|
+
r = @db.query('select x, y, z from t where x = ?', 1)
|
|
117
|
+
assert_equal [{ x: 1, y: 2, z: 3 }], r
|
|
118
|
+
|
|
119
|
+
r = @db.query('select x, y, z from t where z = ?', 6)
|
|
120
|
+
assert_equal [{ x: 4, y: 5, z: 6 }], r
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def test_parameter_binding_with_index
|
|
124
|
+
r = @db.query('select x, y, z from t where x = ?2', 0, 1)
|
|
125
|
+
assert_equal [{ x: 1, y: 2, z: 3 }], r
|
|
126
|
+
|
|
127
|
+
r = @db.query('select x, y, z from t where z = ?3', 3, 4, 6)
|
|
128
|
+
assert_equal [{ x: 4, y: 5, z: 6 }], r
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def test_parameter_binding_with_name
|
|
132
|
+
r = @db.query('select x, y, z from t where x = :x', x: 1, y: 2)
|
|
133
|
+
assert_equal [{ x: 1, y: 2, z: 3 }], r
|
|
134
|
+
|
|
135
|
+
r = @db.query('select x, y, z from t where z = :zzz', 'zzz' => 6)
|
|
136
|
+
assert_equal [{ x: 4, y: 5, z: 6 }], r
|
|
137
|
+
|
|
138
|
+
r = @db.query('select x, y, z from t where z = :bazzz', ':bazzz' => 6)
|
|
139
|
+
assert_equal [{ x: 4, y: 5, z: 6 }], r
|
|
140
|
+
end
|
|
114
141
|
end
|
|
115
142
|
|
|
116
143
|
class ScenarioTest < MiniTest::Test
|
|
@@ -179,4 +206,3 @@ class ScenarioTest < MiniTest::Test
|
|
|
179
206
|
assert_equal [1, 4, 7], result
|
|
180
207
|
end
|
|
181
208
|
end
|
|
182
|
-
|
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:
|
|
4
|
+
version: 1.8.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sharon Rosner
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2021-12-
|
|
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: []
|
|
@@ -103,15 +103,18 @@ files:
|
|
|
103
103
|
- extralite.gemspec
|
|
104
104
|
- lib/extralite.rb
|
|
105
105
|
- lib/extralite/version.rb
|
|
106
|
+
- lib/sequel/adapters/extralite.rb
|
|
106
107
|
- test/helper.rb
|
|
107
108
|
- test/perf.rb
|
|
109
|
+
- test/run.rb
|
|
108
110
|
- test/test_database.rb
|
|
111
|
+
- test/test_sequel.rb
|
|
109
112
|
homepage: https://github.com/digital-fabric/extralite
|
|
110
113
|
licenses:
|
|
111
114
|
- MIT
|
|
112
115
|
metadata:
|
|
113
116
|
source_code_uri: https://github.com/digital-fabric/extralite
|
|
114
|
-
documentation_uri: https://
|
|
117
|
+
documentation_uri: https://www.rubydoc.info/gems/extralite
|
|
115
118
|
homepage_uri: https://github.com/digital-fabric/extralite
|
|
116
119
|
changelog_uri: https://github.com/digital-fabric/extralite/blob/master/CHANGELOG.md
|
|
117
120
|
post_install_message:
|