extralite 1.6 → 1.7
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 +5 -0
- data/Gemfile.lock +3 -1
- data/README.md +35 -12
- data/Rakefile +1 -1
- data/ext/extralite/extralite.c +36 -1
- data/extralite.gemspec +1 -0
- data/lib/extralite/version.rb +1 -1
- data/lib/sequel/adapters/extralite.rb +376 -0
- data/test/run.rb +5 -0
- data/test/test_database.rb +27 -1
- data/test/test_sequel.rb +24 -0
- metadata +18 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cf088c359bb74b23020c8cb290cb6c65d06cedd885e4421c3e81f3c21a31ca33
|
4
|
+
data.tar.gz: a14199c2f5d07068a1a57eb0b24119964aa7a9960f23e6cb58bac39fe535dbfa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '009ccdd0998a39df02718feeecf981eb03dbf55d0dd6b2d0b8bcfb66754a0ec8fcc2ee9e4e9a2b421a63ed6fa89ae29f7c748ca370d32dae2199a01e50febabc'
|
7
|
+
data.tar.gz: e0fc9eb0f8a69dd2017c4d821430a861535fecc2c51e937c58d471098cc0c815867f514a357e2a0572eb848e02e97fac9454116b3cac2ff21abb0e479241552e
|
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
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
extralite (1.
|
4
|
+
extralite (1.7)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
@@ -36,6 +36,7 @@ GEM
|
|
36
36
|
rubocop-ast (1.5.0)
|
37
37
|
parser (>= 3.0.1.1)
|
38
38
|
ruby-progressbar (1.11.0)
|
39
|
+
sequel (5.51.0)
|
39
40
|
simplecov (0.17.1)
|
40
41
|
docile (~> 1.1)
|
41
42
|
json (>= 1.8, < 3)
|
@@ -52,6 +53,7 @@ DEPENDENCIES
|
|
52
53
|
pry (= 0.13.1)
|
53
54
|
rake-compiler (= 1.1.1)
|
54
55
|
rubocop (= 0.85.1)
|
56
|
+
sequel (= 5.51.0)
|
55
57
|
simplecov (= 0.17.1)
|
56
58
|
|
57
59
|
BUNDLED WITH
|
data/README.md
CHANGED
@@ -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,6 +67,11 @@ 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
76
|
rowid = db.last_insert_id
|
70
77
|
|
@@ -82,24 +89,44 @@ 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)|
|
129
|
+
|Multithread friendly|no|[yes](#what-about-concurrency)|
|
103
130
|
|Code size|~2650LoC|~500LoC|
|
104
131
|
|Performance|1x|1.5x to 12.5x (see [below](#performance))|
|
105
132
|
|
@@ -129,10 +156,6 @@ results (using the `sqlite3` gem performance as baseline):
|
|
129
156
|
(If you're interested in checking this yourself, just run the script and let me
|
130
157
|
know if your results are different.)
|
131
158
|
|
132
|
-
## Can I use it with an ORM like ActiveRecord or Sequel?
|
133
|
-
|
134
|
-
Not yet, but you are welcome to contribute adapters for those projects.
|
135
|
-
|
136
159
|
## Contributing
|
137
160
|
|
138
161
|
Contributions in the form of issues, PRs or comments will be greatly
|
data/Rakefile
CHANGED
@@ -12,7 +12,7 @@ task :recompile => [:clean, :compile]
|
|
12
12
|
|
13
13
|
task :default => [:compile, :test]
|
14
14
|
task :test do
|
15
|
-
exec 'ruby test/
|
15
|
+
exec 'ruby test/run.rb'
|
16
16
|
end
|
17
17
|
|
18
18
|
CLEAN.include "**/*.o", "**/*.so", "**/*.so.*", "**/*.a", "**/*.bundle", "**/*.jar", "pkg", "tmp"
|
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;
|
@@ -109,6 +112,33 @@ inline VALUE get_column_value(sqlite3_stmt *stmt, int col, int type) {
|
|
109
112
|
return Qnil;
|
110
113
|
}
|
111
114
|
|
115
|
+
static void bind_parameter_value(sqlite3_stmt *stmt, int pos, VALUE value);
|
116
|
+
|
117
|
+
static inline void bind_hash_parameter_values(sqlite3_stmt *stmt, VALUE hash) {
|
118
|
+
VALUE keys = rb_funcall(hash, ID_KEYS, 0);
|
119
|
+
int len = RARRAY_LEN(keys);
|
120
|
+
for (int i = 0; i < len; i++) {
|
121
|
+
VALUE k = RARRAY_AREF(keys, i);
|
122
|
+
VALUE v = rb_hash_aref(hash, k);
|
123
|
+
|
124
|
+
switch (TYPE(k)) {
|
125
|
+
case T_FIXNUM:
|
126
|
+
bind_parameter_value(stmt, NUM2INT(k), v);
|
127
|
+
return;
|
128
|
+
case T_SYMBOL:
|
129
|
+
k = rb_funcall(k, ID_TO_S, 0);
|
130
|
+
case T_STRING:
|
131
|
+
if(RSTRING_PTR(k)[0] != ':') k = rb_str_plus(rb_str_new2(":"), k);
|
132
|
+
int pos = sqlite3_bind_parameter_index(stmt, StringValuePtr(k));
|
133
|
+
bind_parameter_value(stmt, pos, v);
|
134
|
+
return;
|
135
|
+
default:
|
136
|
+
rb_raise(cError, "Cannot bind hash key value idx %d", i);
|
137
|
+
}
|
138
|
+
}
|
139
|
+
RB_GC_GUARD(keys);
|
140
|
+
}
|
141
|
+
|
112
142
|
static inline void bind_parameter_value(sqlite3_stmt *stmt, int pos, VALUE value) {
|
113
143
|
switch (TYPE(value)) {
|
114
144
|
case T_NIL:
|
@@ -129,6 +159,9 @@ static inline void bind_parameter_value(sqlite3_stmt *stmt, int pos, VALUE value
|
|
129
159
|
case T_STRING:
|
130
160
|
sqlite3_bind_text(stmt, pos, RSTRING_PTR(value), RSTRING_LEN(value), SQLITE_TRANSIENT);
|
131
161
|
return;
|
162
|
+
case T_HASH:
|
163
|
+
bind_hash_parameter_values(stmt, value);
|
164
|
+
return;
|
132
165
|
default:
|
133
166
|
rb_raise(cError, "Cannot bind parameter at position %d", pos);
|
134
167
|
}
|
@@ -519,5 +552,7 @@ void Init_Extralite() {
|
|
519
552
|
rb_gc_register_mark_object(cSQLError);
|
520
553
|
rb_gc_register_mark_object(cBusyError);
|
521
554
|
|
522
|
-
|
555
|
+
ID_KEYS = rb_intern("keys");
|
556
|
+
ID_STRIP = rb_intern("strip");
|
557
|
+
ID_TO_S = rb_intern("to_s");
|
523
558
|
}
|
data/extralite.gemspec
CHANGED
data/lib/extralite/version.rb
CHANGED
@@ -0,0 +1,376 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
require 'extralite'
|
4
|
+
require 'sequel/adapters/shared/sqlite'
|
5
|
+
|
6
|
+
module Sequel
|
7
|
+
module Extralite
|
8
|
+
FALSE_VALUES = (%w'0 false f no n'.each(&:freeze) + [0]).freeze
|
9
|
+
|
10
|
+
blob = Object.new
|
11
|
+
def blob.call(s)
|
12
|
+
Sequel::SQL::Blob.new(s.to_s)
|
13
|
+
end
|
14
|
+
|
15
|
+
boolean = Object.new
|
16
|
+
def boolean.call(s)
|
17
|
+
s = s.downcase if s.is_a?(String)
|
18
|
+
!FALSE_VALUES.include?(s)
|
19
|
+
end
|
20
|
+
|
21
|
+
date = Object.new
|
22
|
+
def date.call(s)
|
23
|
+
case s
|
24
|
+
when String
|
25
|
+
Sequel.string_to_date(s)
|
26
|
+
when Integer
|
27
|
+
Date.jd(s)
|
28
|
+
when Float
|
29
|
+
Date.jd(s.to_i)
|
30
|
+
else
|
31
|
+
raise Sequel::Error, "unhandled type when converting to date: #{s.inspect} (#{s.class.inspect})"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
integer = Object.new
|
36
|
+
def integer.call(s)
|
37
|
+
s.to_i
|
38
|
+
end
|
39
|
+
|
40
|
+
float = Object.new
|
41
|
+
def float.call(s)
|
42
|
+
s.to_f
|
43
|
+
end
|
44
|
+
|
45
|
+
numeric = Object.new
|
46
|
+
def numeric.call(s)
|
47
|
+
s = s.to_s unless s.is_a?(String)
|
48
|
+
BigDecimal(s) rescue s
|
49
|
+
end
|
50
|
+
|
51
|
+
time = Object.new
|
52
|
+
def time.call(s)
|
53
|
+
case s
|
54
|
+
when String
|
55
|
+
Sequel.string_to_time(s)
|
56
|
+
when Integer
|
57
|
+
Sequel::SQLTime.create(s/3600, (s % 3600)/60, s % 60)
|
58
|
+
when Float
|
59
|
+
s, f = s.divmod(1)
|
60
|
+
Sequel::SQLTime.create(s/3600, (s % 3600)/60, s % 60, (f*1000000).round)
|
61
|
+
else
|
62
|
+
raise Sequel::Error, "unhandled type when converting to date: #{s.inspect} (#{s.class.inspect})"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Hash with string keys and callable values for converting SQLite types.
|
67
|
+
SQLITE_TYPES = {}
|
68
|
+
{
|
69
|
+
%w'date' => date,
|
70
|
+
%w'time' => time,
|
71
|
+
%w'bit bool boolean' => boolean,
|
72
|
+
%w'integer smallint mediumint int bigint' => integer,
|
73
|
+
%w'numeric decimal money' => numeric,
|
74
|
+
%w'float double real dec fixed' + ['double precision'] => float,
|
75
|
+
%w'blob' => blob
|
76
|
+
}.each do |k,v|
|
77
|
+
k.each{|n| SQLITE_TYPES[n] = v}
|
78
|
+
end
|
79
|
+
SQLITE_TYPES.freeze
|
80
|
+
|
81
|
+
USE_EXTENDED_RESULT_CODES = false
|
82
|
+
|
83
|
+
class Database < Sequel::Database
|
84
|
+
include ::Sequel::SQLite::DatabaseMethods
|
85
|
+
|
86
|
+
set_adapter_scheme :extralite
|
87
|
+
|
88
|
+
# Mimic the file:// uri, by having 2 preceding slashes specify a relative
|
89
|
+
# path, and 3 preceding slashes specify an absolute path.
|
90
|
+
def self.uri_to_options(uri) # :nodoc:
|
91
|
+
{ :database => (uri.host.nil? && uri.path == '/') ? nil : "#{uri.host}#{uri.path}" }
|
92
|
+
end
|
93
|
+
|
94
|
+
private_class_method :uri_to_options
|
95
|
+
|
96
|
+
# The conversion procs to use for this database
|
97
|
+
attr_reader :conversion_procs
|
98
|
+
|
99
|
+
# Connect to the database. Since SQLite is a file based database,
|
100
|
+
# available options are limited:
|
101
|
+
#
|
102
|
+
# :database :: database name (filename or ':memory:' or file: URI)
|
103
|
+
# :readonly :: open database in read-only mode; useful for reading
|
104
|
+
# static data that you do not want to modify
|
105
|
+
# :timeout :: how long to wait for the database to be available if it
|
106
|
+
# is locked, given in milliseconds (default is 5000)
|
107
|
+
def connect(server)
|
108
|
+
opts = server_opts(server)
|
109
|
+
opts[:database] = ':memory:' if blank_object?(opts[:database])
|
110
|
+
# sqlite3_opts = {}
|
111
|
+
# sqlite3_opts[:readonly] = typecast_value_boolean(opts[:readonly]) if opts.has_key?(:readonly)
|
112
|
+
db = ::Extralite::Database.new(opts[:database].to_s)#, sqlite3_opts)
|
113
|
+
# db.busy_timeout(typecast_value_integer(opts.fetch(:timeout, 5000)))
|
114
|
+
|
115
|
+
# if USE_EXTENDED_RESULT_CODES
|
116
|
+
# db.extended_result_codes = true
|
117
|
+
# end
|
118
|
+
|
119
|
+
connection_pragmas.each{|s| log_connection_yield(s, db){db.query(s)}}
|
120
|
+
|
121
|
+
# class << db
|
122
|
+
# attr_reader :prepared_statements
|
123
|
+
# end
|
124
|
+
# db.instance_variable_set(:@prepared_statements, {})
|
125
|
+
|
126
|
+
db
|
127
|
+
end
|
128
|
+
|
129
|
+
# Disconnect given connections from the database.
|
130
|
+
def disconnect_connection(c)
|
131
|
+
# c.prepared_statements.each_value{|v| v.first.close}
|
132
|
+
c.close
|
133
|
+
end
|
134
|
+
|
135
|
+
# Run the given SQL with the given arguments and yield each row.
|
136
|
+
def execute(sql, opts=OPTS, &block)
|
137
|
+
_execute(:select, sql, opts, &block)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Run the given SQL with the given arguments and return the number of changed rows.
|
141
|
+
def execute_dui(sql, opts=OPTS)
|
142
|
+
_execute(:update, sql, opts)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Drop any prepared statements on the connection when executing DDL. This is because
|
146
|
+
# prepared statements lock the table in such a way that you can't drop or alter the
|
147
|
+
# table while a prepared statement that references it still exists.
|
148
|
+
# def execute_ddl(sql, opts=OPTS)
|
149
|
+
# synchronize(opts[:server]) do |conn|
|
150
|
+
# conn.prepared_statements.values.each{|cps, s| cps.close}
|
151
|
+
# conn.prepared_statements.clear
|
152
|
+
# super
|
153
|
+
# end
|
154
|
+
# end
|
155
|
+
|
156
|
+
def execute_insert(sql, opts=OPTS)
|
157
|
+
_execute(:insert, sql, opts)
|
158
|
+
end
|
159
|
+
|
160
|
+
def freeze
|
161
|
+
@conversion_procs.freeze
|
162
|
+
super
|
163
|
+
end
|
164
|
+
|
165
|
+
# Handle Integer and Float arguments, since SQLite can store timestamps as integers and floats.
|
166
|
+
def to_application_timestamp(s)
|
167
|
+
case s
|
168
|
+
when String
|
169
|
+
super
|
170
|
+
when Integer
|
171
|
+
super(Time.at(s).to_s)
|
172
|
+
when Float
|
173
|
+
super(DateTime.jd(s).to_s)
|
174
|
+
else
|
175
|
+
raise Sequel::Error, "unhandled type when converting to : #{s.inspect} (#{s.class.inspect})"
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
private
|
180
|
+
|
181
|
+
def adapter_initialize
|
182
|
+
@conversion_procs = SQLITE_TYPES.dup
|
183
|
+
@conversion_procs['datetime'] = @conversion_procs['timestamp'] = method(:to_application_timestamp)
|
184
|
+
set_integer_booleans
|
185
|
+
end
|
186
|
+
|
187
|
+
# Yield an available connection. Rescue any Extralite::Error and turn
|
188
|
+
# them into DatabaseErrors.
|
189
|
+
def _execute(type, sql, opts, &block)
|
190
|
+
begin
|
191
|
+
synchronize(opts[:server]) do |conn|
|
192
|
+
# return execute_prepared_statement(conn, type, sql, opts, &block) if sql.is_a?(Symbol)
|
193
|
+
log_args = opts[:arguments]
|
194
|
+
args = {}
|
195
|
+
opts.fetch(:arguments, OPTS).each{|k, v| args[k] = prepared_statement_argument(v) }
|
196
|
+
case type
|
197
|
+
when :select
|
198
|
+
log_connection_yield(sql, conn, log_args){conn.query(sql, args, &block)}
|
199
|
+
when :insert
|
200
|
+
log_connection_yield(sql, conn, log_args){conn.query(sql, args)}
|
201
|
+
conn.last_insert_rowid
|
202
|
+
when :update
|
203
|
+
log_connection_yield(sql, conn, log_args){conn.query(sql, args)}
|
204
|
+
conn.changes
|
205
|
+
end
|
206
|
+
end
|
207
|
+
rescue ::Extralite::Error => e
|
208
|
+
raise_error(e)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# The SQLite adapter does not need the pool to convert exceptions.
|
213
|
+
# Also, force the max connections to 1 if a memory database is being
|
214
|
+
# used, as otherwise each connection gets a separate database.
|
215
|
+
def connection_pool_default_options
|
216
|
+
o = super.dup
|
217
|
+
# Default to only a single connection if a memory database is used,
|
218
|
+
# because otherwise each connection will get a separate database
|
219
|
+
o[:max_connections] = 1 if @opts[:database] == ':memory:' || blank_object?(@opts[:database])
|
220
|
+
o
|
221
|
+
end
|
222
|
+
|
223
|
+
def prepared_statement_argument(arg)
|
224
|
+
case arg
|
225
|
+
when Date, DateTime, Time
|
226
|
+
literal(arg)[1...-1]
|
227
|
+
when SQL::Blob
|
228
|
+
arg.to_blob
|
229
|
+
when true, false
|
230
|
+
if integer_booleans
|
231
|
+
arg ? 1 : 0
|
232
|
+
else
|
233
|
+
literal(arg)[1...-1]
|
234
|
+
end
|
235
|
+
else
|
236
|
+
arg
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
# Execute a prepared statement on the database using the given name.
|
241
|
+
def execute_prepared_statement(conn, type, name, opts, &block)
|
242
|
+
ps = prepared_statement(name)
|
243
|
+
sql = ps.prepared_sql
|
244
|
+
args = opts[:arguments]
|
245
|
+
ps_args = {}
|
246
|
+
args.each{|k, v| ps_args[k] = prepared_statement_argument(v)}
|
247
|
+
if cpsa = conn.prepared_statements[name]
|
248
|
+
cps, cps_sql = cpsa
|
249
|
+
if cps_sql != sql
|
250
|
+
cps.close
|
251
|
+
cps = nil
|
252
|
+
end
|
253
|
+
end
|
254
|
+
unless cps
|
255
|
+
cps = log_connection_yield("PREPARE #{name}: #{sql}", conn){conn.prepare(sql)}
|
256
|
+
conn.prepared_statements[name] = [cps, sql]
|
257
|
+
end
|
258
|
+
log_sql = String.new
|
259
|
+
log_sql << "EXECUTE #{name}"
|
260
|
+
if ps.log_sql
|
261
|
+
log_sql << " ("
|
262
|
+
log_sql << sql
|
263
|
+
log_sql << ")"
|
264
|
+
end
|
265
|
+
if block
|
266
|
+
log_connection_yield(log_sql, conn, args){cps.execute(ps_args, &block)}
|
267
|
+
else
|
268
|
+
log_connection_yield(log_sql, conn, args){cps.execute!(ps_args){|r|}}
|
269
|
+
case type
|
270
|
+
when :insert
|
271
|
+
conn.last_insert_rowid
|
272
|
+
when :update
|
273
|
+
conn.changes
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
# # SQLite3 raises ArgumentError in addition to SQLite3::Exception in
|
279
|
+
# # some cases, such as operations on a closed database.
|
280
|
+
def database_error_classes
|
281
|
+
#[Extralite::Error, ArgumentError]
|
282
|
+
[::Extralite::Error]
|
283
|
+
end
|
284
|
+
|
285
|
+
def dataset_class_default
|
286
|
+
Dataset
|
287
|
+
end
|
288
|
+
|
289
|
+
if USE_EXTENDED_RESULT_CODES
|
290
|
+
# Support SQLite exception codes if ruby-sqlite3 supports them.
|
291
|
+
def sqlite_error_code(exception)
|
292
|
+
exception.code if exception.respond_to?(:code)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
class Dataset < Sequel::Dataset
|
298
|
+
include ::Sequel::SQLite::DatasetMethods
|
299
|
+
|
300
|
+
module ArgumentMapper
|
301
|
+
include Sequel::Dataset::ArgumentMapper
|
302
|
+
|
303
|
+
protected
|
304
|
+
|
305
|
+
# Return a hash with the same values as the given hash,
|
306
|
+
# but with the keys converted to strings.
|
307
|
+
def map_to_prepared_args(hash)
|
308
|
+
args = {}
|
309
|
+
hash.each{|k,v| args[k.to_s.gsub('.', '__')] = v}
|
310
|
+
args
|
311
|
+
end
|
312
|
+
|
313
|
+
private
|
314
|
+
|
315
|
+
# SQLite uses a : before the name of the argument for named
|
316
|
+
# arguments.
|
317
|
+
def prepared_arg(k)
|
318
|
+
LiteralString.new("#{prepared_arg_placeholder}#{k.to_s.gsub('.', '__')}")
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
BindArgumentMethods = prepared_statements_module(:bind, ArgumentMapper)
|
323
|
+
PreparedStatementMethods = prepared_statements_module(:prepare, BindArgumentMethods)
|
324
|
+
|
325
|
+
def fetch_rows(sql, &block)
|
326
|
+
execute(sql, &block)
|
327
|
+
# execute(sql) do |result|
|
328
|
+
# cps = db.conversion_procs
|
329
|
+
# type_procs = result.types.map{|t| cps[base_type_name(t)]}
|
330
|
+
# j = -1
|
331
|
+
# cols = result.columns.map{|c| [output_identifier(c), type_procs[(j+=1)]]}
|
332
|
+
# self.columns = cols.map(&:first)
|
333
|
+
# max = cols.length
|
334
|
+
# result.each do |values|
|
335
|
+
# row = {}
|
336
|
+
# i = -1
|
337
|
+
# while (i += 1) < max
|
338
|
+
# name, type_proc = cols[i]
|
339
|
+
# v = values[i]
|
340
|
+
# if type_proc && v
|
341
|
+
# v = type_proc.call(v)
|
342
|
+
# end
|
343
|
+
# row[name] = v
|
344
|
+
# end
|
345
|
+
# yield row
|
346
|
+
# end
|
347
|
+
# end
|
348
|
+
end
|
349
|
+
|
350
|
+
private
|
351
|
+
|
352
|
+
# The base type name for a given type, without any parenthetical part.
|
353
|
+
def base_type_name(t)
|
354
|
+
(t =~ /^(.*?)\(/ ? $1 : t).downcase if t
|
355
|
+
end
|
356
|
+
|
357
|
+
# Quote the string using the adapter class method.
|
358
|
+
def literal_string_append(sql, v)
|
359
|
+
sql << "'" << v.gsub(/'/, "''") << "'"
|
360
|
+
end
|
361
|
+
|
362
|
+
def bound_variable_modules
|
363
|
+
[BindArgumentMethods]
|
364
|
+
end
|
365
|
+
|
366
|
+
def prepared_statement_modules
|
367
|
+
[PreparedStatementMethods]
|
368
|
+
end
|
369
|
+
|
370
|
+
# SQLite uses a : before the name of the argument as a placeholder.
|
371
|
+
def prepared_arg_placeholder
|
372
|
+
':'
|
373
|
+
end
|
374
|
+
end
|
375
|
+
end
|
376
|
+
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,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: extralite
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '1.
|
4
|
+
version: '1.7'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sharon Rosner
|
@@ -80,6 +80,20 @@ dependencies:
|
|
80
80
|
- - '='
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: 0.13.1
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: sequel
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 5.51.0
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 5.51.0
|
83
97
|
description:
|
84
98
|
email: sharon@noteflakes.com
|
85
99
|
executables: []
|
@@ -103,9 +117,12 @@ files:
|
|
103
117
|
- extralite.gemspec
|
104
118
|
- lib/extralite.rb
|
105
119
|
- lib/extralite/version.rb
|
120
|
+
- lib/sequel/adapters/extralite.rb
|
106
121
|
- test/helper.rb
|
107
122
|
- test/perf.rb
|
123
|
+
- test/run.rb
|
108
124
|
- test/test_database.rb
|
125
|
+
- test/test_sequel.rb
|
109
126
|
homepage: https://github.com/digital-fabric/extralite
|
110
127
|
licenses:
|
111
128
|
- MIT
|