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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e2924788672c7c3960a5baeaeacf14d297cfe461cfb1d20db828d7a67b5536c8
4
- data.tar.gz: fb4da5551c3b1ddca95ef05520eee477bfb7a29383b9c7e5f5c0638392eba3e9
3
+ metadata.gz: cf088c359bb74b23020c8cb290cb6c65d06cedd885e4421c3e81f3c21a31ca33
4
+ data.tar.gz: a14199c2f5d07068a1a57eb0b24119964aa7a9960f23e6cb58bac39fe535dbfa
5
5
  SHA512:
6
- metadata.gz: 63b73c0d05cb294b75d79bef5c1362e2c422f16b0e7ae76c3d2b577af20f612007a4fe272942958b38d545efabe4d975c1a0475ee8dec4a68e957d61800ffca7
7
- data.tar.gz: b79c50cb5c696cf1eb505bbd126b197f1160f56af70e669cfc432e2e16d5423848c139ff9e942fb26c06c3d15f94e1a43f22ab4c317dae4fb0c2290f8d431aa6
6
+ metadata.gz: '009ccdd0998a39df02718feeecf981eb03dbf55d0dd6b2d0b8bcfb66754a0ec8fcc2ee9e4e9a2b421a63ed6fa89ae29f7c748ca370d32dae2199a01e50febabc'
7
+ data.tar.gz: e0fc9eb0f8a69dd2017c4d821430a861535fecc2c51e937c58d471098cc0c815867f514a357e2a0572eb848e02e97fac9454116b3cac2ff21abb0e479241552e
@@ -8,7 +8,7 @@ jobs:
8
8
  fail-fast: false
9
9
  matrix:
10
10
  os: [ubuntu-latest]
11
- ruby: [2.6, 2.7, 3.0]
11
+ ruby: [2.6, 2.7, '3.0']
12
12
 
13
13
  name: >-
14
14
  ${{matrix.os}}, ${{matrix.ruby}}
@@ -16,15 +16,10 @@ jobs:
16
16
  runs-on: ${{matrix.os}}
17
17
  steps:
18
18
  - uses: actions/checkout@v1
19
- - uses: actions/setup-ruby@v1
19
+ - uses: ruby/setup-ruby@v1
20
20
  with:
21
21
  ruby-version: ${{matrix.ruby}}
22
- - name: Install dependencies
23
- run: |
24
- gem install bundler
25
- bundle install
26
- - name: Show Linux kernel version
27
- run: uname -r
22
+ bundler-cache: true # 'bundle install' and cache
28
23
  - name: Compile C-extension
29
24
  run: bundle exec rake compile
30
25
  - name: Run tests
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 1.7 2021-12-13
2
+
3
+ - Add extralite Sequel adapter
4
+ - Add support for binding hash parameters
5
+
1
6
  ## 1.6 2021-12-13
2
7
 
3
8
  - Release GVL while fetching rows
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- extralite (1.6)
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 Ruby GVL is
21
- released while preparing SQL statements and while iterating over results.
20
+ - Improved [concurrency](#what-about-concurrency) for multithreaded apps: the
21
+ Ruby GVL is released while preparing SQL statements and while iterating over
22
+ results.
22
23
  - Iterate over records with a block, or collect records into an array.
23
24
  - Parameter binding.
24
25
  - Automatically execute SQL strings containing multiple semicolon-separated
@@ -27,6 +28,7 @@ interact with an SQLite3 database.
27
28
  - Get number of rows changed by last query.
28
29
  - Load extensions (loading of extensions is autmatically enabled. You can find
29
30
  some useful extensions here: https://github.com/nalgeon/sqlean.)
31
+ - Includes a [Sequel adapter](#usage-with-sequel) (an ActiveRecord)
30
32
 
31
33
  ## Usage
32
34
 
@@ -65,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 popular, solid, well-maintained project, used by
88
- thousands of developers. I've been doing a lot of work with SQLite3 databases
89
- lately, and wanted to have a simpler API that gives me query results in a
90
- variety of ways. Thus extralite was born.
108
+ The [sqlite3-ruby](https://github.com/sparklemotion/sqlite3-ruby) gem is a
109
+ popular, solid, well-maintained project, used by thousands of developers. I've
110
+ been doing a lot of work with SQLite3 databases lately, and wanted to have a
111
+ simpler API that gives me query results in a variety of ways. Thus extralite was
112
+ born.
113
+
114
+ Extralite is quite a bit [faster](#performance) than sqlite3-ruby and is also
115
+ [thread-friendly](#what-about-concurrency). On the other hand, Extralite does
116
+ not have support for defining custom functions, aggregates and collations. If
117
+ you're using those features, you'll need to stick with sqlite3-ruby.
91
118
 
92
119
  Here's a table summarizing the differences between the two gems:
93
120
 
94
121
  | |sqlite3-ruby|Extralite|
95
122
  |-|-|-|
96
123
  |API design|multiple classes|single class|
97
- |Query results|row as hash, row as array, single row, single value|row as hash, row as array, single column, single row, single value|
124
+ |Query results|row as hash, row as array, single row, single value|row as hash, row as array, __single column__, single row, single value|
98
125
  |execute multiple statements|separate API (#execute_batch)|integrated|
99
126
  |custom functions in Ruby|yes|no|
100
127
  |custom collations|yes|no|
101
128
  |custom aggregate functions|yes|no|
102
- |Multithread friendly|no|[yes](#concurrency)|
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/test_database.rb'
15
+ exec 'ruby test/run.rb'
16
16
  end
17
17
 
18
18
  CLEAN.include "**/*.o", "**/*.so", "**/*.so.*", "**/*.a", "**/*.bundle", "**/*.jar", "pkg", "tmp"
@@ -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
- ID_STRIP = rb_intern("strip");
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
@@ -26,4 +26,5 @@ Gem::Specification.new do |s|
26
26
  s.add_development_dependency 'simplecov', '0.17.1'
27
27
  s.add_development_dependency 'rubocop', '0.85.1'
28
28
  s.add_development_dependency 'pry', '0.13.1'
29
+ s.add_development_dependency 'sequel', '5.51.0'
29
30
  end
@@ -1,3 +1,3 @@
1
1
  module Extralite
2
- VERSION = '1.6'
2
+ VERSION = '1.7'
3
3
  end
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir.glob("#{__dir__}/test_*.rb").each do |path|
4
+ require(path)
5
+ end
@@ -111,6 +111,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
-
@@ -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.6'
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