extralite 1.27 → 2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -11,24 +11,20 @@ require 'sequel/adapters/shared/sqlite'
11
11
  module Sequel
12
12
  # Extralite Sequel adapter
13
13
  module Extralite
14
- # @!visibility private
15
14
  FALSE_VALUES = (%w'0 false f no n'.each(&:freeze) + [0]).freeze
16
15
 
17
16
  blob = Object.new
18
- # @!visibility private
19
17
  def blob.call(s)
20
18
  Sequel::SQL::Blob.new(s.to_s)
21
19
  end
22
20
 
23
21
  boolean = Object.new
24
- # @!visibility private
25
22
  def boolean.call(s)
26
23
  s = s.downcase if s.is_a?(String)
27
24
  !FALSE_VALUES.include?(s)
28
25
  end
29
26
 
30
27
  date = Object.new
31
- # @!visibility private
32
28
  def date.call(s)
33
29
  case s
34
30
  when String
@@ -43,26 +39,22 @@ module Sequel
43
39
  end
44
40
 
45
41
  integer = Object.new
46
- # @!visibility private
47
42
  def integer.call(s)
48
43
  s.to_i
49
44
  end
50
45
 
51
46
  float = Object.new
52
- # @!visibility private
53
47
  def float.call(s)
54
48
  s.to_f
55
49
  end
56
50
 
57
51
  numeric = Object.new
58
- # @!visibility private
59
52
  def numeric.call(s)
60
53
  s = s.to_s unless s.is_a?(String)
61
54
  BigDecimal(s) rescue s
62
55
  end
63
56
 
64
57
  time = Object.new
65
- # @!visibility private
66
58
  def time.call(s)
67
59
  case s
68
60
  when String
@@ -77,7 +69,7 @@ module Sequel
77
69
  end
78
70
  end
79
71
 
80
- # Hash with string keys and callable values for converting SQLite types.
72
+ # Hash with string keys and callable values for converting SQLITE types.
81
73
  SQLITE_TYPES = {}
82
74
  {
83
75
  %w'date' => date,
@@ -92,27 +84,28 @@ module Sequel
92
84
  end
93
85
  SQLITE_TYPES.freeze
94
86
 
95
- # @!visibility private
96
- USE_EXTENDED_RESULT_CODES = false
97
-
98
- # Database adapter for Sequel
99
87
  class Database < Sequel::Database
100
88
  include ::Sequel::SQLite::DatabaseMethods
101
-
89
+
102
90
  set_adapter_scheme :extralite
103
-
91
+
104
92
  # Mimic the file:// uri, by having 2 preceding slashes specify a relative
105
93
  # path, and 3 preceding slashes specify an absolute path.
106
94
  def self.uri_to_options(uri) # :nodoc:
107
95
  { :database => (uri.host.nil? && uri.path == '/') ? nil : "#{uri.host}#{uri.path}" }
108
96
  end
109
-
97
+
110
98
  private_class_method :uri_to_options
111
99
 
112
100
  # The conversion procs to use for this database
113
101
  attr_reader :conversion_procs
114
102
 
115
- # Connect to the database. Since SQLite is a file based database,
103
+ def initialize(opts = OPTS)
104
+ super
105
+ @allow_regexp = typecast_value_boolean(opts[:setup_regexp_function])
106
+ end
107
+
108
+ # Connect to the database. Since Extralite is a file based database,
116
109
  # available options are limited:
117
110
  #
118
111
  # :database :: database name (filename or ':memory:' or file: URI)
@@ -123,31 +116,39 @@ module Sequel
123
116
  def connect(server)
124
117
  opts = server_opts(server)
125
118
  opts[:database] = ':memory:' if blank_object?(opts[:database])
126
- # sqlite3_opts = {}
127
- # sqlite3_opts[:readonly] = typecast_value_boolean(opts[:readonly]) if opts.has_key?(:readonly)
128
- db = ::Extralite::Database.new(opts[:database].to_s)#, sqlite3_opts)
119
+ # db opts may be used in a future version of Extralite
120
+ db_opts = {}
121
+ db_opts[:readonly] = typecast_value_boolean(opts[:readonly]) if opts.has_key?(:readonly)
122
+ db = ::Extralite::Database.new(opts[:database].to_s)
129
123
  # db.busy_timeout(typecast_value_integer(opts.fetch(:timeout, 5000)))
130
124
 
131
- # if USE_EXTENDED_RESULT_CODES
132
- # db.extended_result_codes = true
133
- # end
134
-
135
125
  connection_pragmas.each{|s| log_connection_yield(s, db){db.query(s)}}
136
126
 
127
+ if typecast_value_boolean(opts[:setup_regexp_function])
128
+ db.create_function("regexp", 2) do |func, regexp_str, string|
129
+ func.result = Regexp.new(regexp_str).match(string) ? 1 : 0
130
+ end
131
+ end
132
+
137
133
  class << db
138
134
  attr_reader :prepared_statements
139
135
  end
140
136
  db.instance_variable_set(:@prepared_statements, {})
141
-
137
+
142
138
  db
143
139
  end
144
140
 
141
+ # Whether this Database instance is setup to allow regexp matching.
142
+ # True if the :setup_regexp_function option was passed when creating the Database.
143
+ def allow_regexp?
144
+ @allow_regexp
145
+ end
146
+
145
147
  # Disconnect given connections from the database.
146
148
  def disconnect_connection(c)
147
- c.prepared_statements.each_value{|v| v.first.close }
148
149
  c.close
149
150
  end
150
-
151
+
151
152
  # Run the given SQL with the given arguments and yield each row.
152
153
  def execute(sql, opts=OPTS, &block)
153
154
  _execute(:select, sql, opts, &block)
@@ -157,30 +158,28 @@ module Sequel
157
158
  def execute_dui(sql, opts=OPTS)
158
159
  _execute(:update, sql, opts)
159
160
  end
160
-
161
+
161
162
  # Drop any prepared statements on the connection when executing DDL. This is because
162
163
  # prepared statements lock the table in such a way that you can't drop or alter the
163
164
  # table while a prepared statement that references it still exists.
164
165
  def execute_ddl(sql, opts=OPTS)
165
166
  synchronize(opts[:server]) do |conn|
166
- conn.prepared_statements.values.each{|cps, s| cps.close}
167
- conn.prepared_statements.clear
167
+ # conn.prepared_statements.values.each{|cps, s| cps.close}
168
+ # conn.prepared_statements.clear
168
169
  super
169
170
  end
170
171
  end
171
-
172
- # @!visibility private
172
+
173
173
  def execute_insert(sql, opts=OPTS)
174
174
  _execute(:insert, sql, opts)
175
175
  end
176
-
177
- # @!visibility private
176
+
178
177
  def freeze
179
178
  @conversion_procs.freeze
180
179
  super
181
180
  end
182
181
 
183
- # Handle Integer and Float arguments, since SQLite can store timestamps as integers and floats.
182
+ # Handle Integer and Float arguments, since Extralite can store timestamps as integers and floats.
184
183
  def to_application_timestamp(s)
185
184
  case s
186
185
  when String
@@ -195,39 +194,38 @@ module Sequel
195
194
  end
196
195
 
197
196
  private
198
-
197
+
199
198
  def adapter_initialize
200
199
  @conversion_procs = SQLITE_TYPES.dup
201
200
  @conversion_procs['datetime'] = @conversion_procs['timestamp'] = method(:to_application_timestamp)
202
201
  set_integer_booleans
203
202
  end
204
-
205
- # Yield an available connection. Rescue any Extralite::Error and turn
206
- # them into DatabaseErrors.
203
+
204
+ # Yield an available connection. Rescue
205
+ # any Extralite::Errors and turn them into DatabaseErrors.
207
206
  def _execute(type, sql, opts, &block)
208
- begin
209
- synchronize(opts[:server]) do |conn|
210
- return execute_prepared_statement(conn, type, sql, opts, &block) if sql.is_a?(Symbol)
211
- log_args = opts[:arguments]
212
- args = {}
213
- opts.fetch(:arguments, OPTS).each{|k, v| args[k] = prepared_statement_argument(v) }
214
- case type
215
- when :select
216
- log_connection_yield(sql, conn, log_args){conn.query(sql, args, &block)}
217
- when :insert
218
- log_connection_yield(sql, conn, log_args){conn.query(sql, args)}
219
- conn.last_insert_rowid
220
- when :update
221
- log_connection_yield(sql, conn, log_args){conn.query(sql, args)}
222
- conn.changes
223
- end
207
+ synchronize(opts[:server]) do |conn|
208
+ return execute_prepared_statement(conn, type, sql, opts, &block) if sql.is_a?(Symbol)
209
+ log_args = opts[:arguments]
210
+ args = {}
211
+ opts.fetch(:arguments, OPTS).each{|k, v| args[k] = prepared_statement_argument(v)}
212
+ case type
213
+ when :select
214
+ query = conn.prepare(sql, args)
215
+ log_connection_yield(sql, conn, log_args){block.call(query.each, query.columns)}
216
+ when :insert
217
+ log_connection_yield(sql, conn, log_args){conn.query(sql, args)}
218
+ conn.last_insert_rowid
219
+ when :update
220
+ log_connection_yield(sql, conn, log_args){conn.query(sql, args)}
221
+ conn.changes
224
222
  end
225
- rescue ::Extralite::Error => e
226
- raise_error(e)
227
223
  end
224
+ rescue ::Extralite::Error => e
225
+ raise_error(e)
228
226
  end
229
-
230
- # The SQLite adapter does not need the pool to convert exceptions.
227
+
228
+ # The Extralite adapter does not need the pool to convert exceptions.
231
229
  # Also, force the max connections to 1 if a memory database is being
232
230
  # used, as otherwise each connection gets a separate database.
233
231
  def connection_pool_default_options
@@ -237,7 +235,7 @@ module Sequel
237
235
  o[:max_connections] = 1 if @opts[:database] == ':memory:' || blank_object?(@opts[:database])
238
236
  o
239
237
  end
240
-
238
+
241
239
  def prepared_statement_argument(arg)
242
240
  case arg
243
241
  when Date, DateTime, Time
@@ -281,9 +279,10 @@ module Sequel
281
279
  log_sql << ")"
282
280
  end
283
281
  if block
284
- log_connection_yield(log_sql, conn, args){cps.query(ps_args, &block)}
282
+ cps.bind(ps_args)
283
+ log_connection_yield(log_sql, conn, args){block.call(cps.each, cps.columns)}
285
284
  else
286
- log_connection_yield(log_sql, conn, args){cps.query(ps_args){|r|}}
285
+ log_connection_yield(log_sql, conn, args){cps.bind(ps_args).to_a}
287
286
  case type
288
287
  when :insert
289
288
  conn.last_insert_rowid
@@ -292,11 +291,8 @@ module Sequel
292
291
  end
293
292
  end
294
293
  end
295
-
296
- # # SQLite3 raises ArgumentError in addition to SQLite3::Exception in
297
- # # some cases, such as operations on a closed database.
294
+
298
295
  def database_error_classes
299
- #[Extralite::Error, ArgumentError]
300
296
  [::Extralite::Error]
301
297
  end
302
298
 
@@ -304,24 +300,19 @@ module Sequel
304
300
  Dataset
305
301
  end
306
302
 
307
- if USE_EXTENDED_RESULT_CODES
308
- # Support SQLite exception codes if ruby-sqlite3 supports them.
309
- def sqlite_error_code(exception)
310
- exception.code if exception.respond_to?(:code)
311
- end
303
+ def extralite_error_code(exception)
304
+ exception.code if exception.respond_to?(:code)
312
305
  end
313
306
  end
314
-
315
- # Dataset adapter for Sequel
307
+
316
308
  class Dataset < Sequel::Dataset
317
309
  include ::Sequel::SQLite::DatasetMethods
318
310
 
319
- # @!visibility private
320
311
  module ArgumentMapper
321
312
  include Sequel::Dataset::ArgumentMapper
322
-
313
+
323
314
  protected
324
-
315
+
325
316
  # Return a hash with the same values as the given hash,
326
317
  # but with the keys converted to strings.
327
318
  def map_to_prepared_args(hash)
@@ -329,49 +320,56 @@ module Sequel
329
320
  hash.each{|k,v| args[k.to_s.gsub('.', '__')] = v}
330
321
  args
331
322
  end
332
-
323
+
333
324
  private
334
-
335
- # SQLite uses a : before the name of the argument for named
325
+
326
+ # Extralite uses a : before the name of the argument for named
336
327
  # arguments.
337
328
  def prepared_arg(k)
338
329
  LiteralString.new("#{prepared_arg_placeholder}#{k.to_s.gsub('.', '__')}")
339
330
  end
340
331
  end
341
-
342
- # @!visibility private
332
+
343
333
  BindArgumentMethods = prepared_statements_module(:bind, ArgumentMapper)
344
- # @!visibility private
345
334
  PreparedStatementMethods = prepared_statements_module(:prepare, BindArgumentMethods)
346
335
 
347
- # @!visibility private
336
+ # Support regexp functions if using :setup_regexp_function Database option.
337
+ def complex_expression_sql_append(sql, op, args)
338
+ case op
339
+ when :~, :'!~', :'~*', :'!~*'
340
+ return super unless supports_regexp?
341
+
342
+ case_insensitive = [:'~*', :'!~*'].include?(op)
343
+ sql << 'NOT ' if [:'!~', :'!~*'].include?(op)
344
+ sql << '('
345
+ sql << 'LOWER(' if case_insensitive
346
+ literal_append(sql, args[0])
347
+ sql << ')' if case_insensitive
348
+ sql << ' REGEXP '
349
+ sql << 'LOWER(' if case_insensitive
350
+ literal_append(sql, args[1])
351
+ sql << ')' if case_insensitive
352
+ sql << ')'
353
+ else
354
+ super
355
+ end
356
+ end
357
+
348
358
  def fetch_rows(sql, &block)
349
- execute(sql, &block)
350
- # execute(sql) do |result|
351
- # cps = db.conversion_procs
352
- # type_procs = result.types.map{|t| cps[base_type_name(t)]}
353
- # j = -1
354
- # cols = result.columns.map{|c| [output_identifier(c), type_procs[(j+=1)]]}
355
- # self.columns = cols.map(&:first)
356
- # max = cols.length
357
- # result.each do |values|
358
- # row = {}
359
- # i = -1
360
- # while (i += 1) < max
361
- # name, type_proc = cols[i]
362
- # v = values[i]
363
- # if type_proc && v
364
- # v = type_proc.call(v)
365
- # end
366
- # row[name] = v
367
- # end
368
- # yield row
369
- # end
370
- # end
359
+ execute(sql) do |result, columns|
360
+ self.columns = columns
361
+ max = columns.size
362
+ result.each(&block)
363
+ end
371
364
  end
372
365
 
366
+ # Support regexp if using :setup_regexp_function Database option.
367
+ def supports_regexp?
368
+ db.allow_regexp?
369
+ end
370
+
373
371
  private
374
-
372
+
375
373
  # The base type name for a given type, without any parenthetical part.
376
374
  def base_type_name(t)
377
375
  (t =~ /^(.*?)\(/ ? $1 : t).downcase if t
@@ -39,9 +39,9 @@ def extralite_prepare
39
39
  db.prepare('select * from foo')
40
40
  end
41
41
 
42
- def extralite_run(stmt, count)
42
+ def extralite_run(query, count)
43
43
  # db = Extralite::Database.new(DB_PATH)
44
- results = stmt.query
44
+ results = query.to_a
45
45
  raise unless results.size == count
46
46
  end
47
47
 
@@ -176,7 +176,7 @@ end
176
176
  when /darwin/
177
177
  @db.load_extension(File.join(__dir__, 'extensions/text.dylib'))
178
178
  end
179
-
179
+
180
180
  r = @db.query_single_value("select reverse('abcd')")
181
181
  assert_equal 'dcba', r
182
182
  end
@@ -285,7 +285,7 @@ end
285
285
  fn = "/tmp/extralite-#{rand(10000)}.db"
286
286
  db1 = Extralite::Database.new(fn)
287
287
  db2 = Extralite::Database.new(fn)
288
-
288
+
289
289
  db1.query('begin exclusive')
290
290
  assert_raises(Extralite::BusyError) { db2.query('begin exclusive') }
291
291
 
@@ -349,7 +349,7 @@ end
349
349
  end
350
350
 
351
351
  @db.query('create table t2 (v not null)')
352
-
352
+
353
353
  assert_raises(Extralite::Error) { @db.query('insert into t2 values (null)') }
354
354
  assert_equal Extralite::SQLITE_CONSTRAINT_NOTNULL, @db.errcode
355
355
  assert_equal 'NOT NULL constraint failed: t2.v', @db.errmsg
@@ -357,8 +357,8 @@ end
357
357
 
358
358
 
359
359
  def test_close_with_open_prepared_statement
360
- stmt = @db.prepare('select * from t')
361
- stmt.query
360
+ query = @db.prepare('select * from t')
361
+ query.next
362
362
  @db.close
363
363
  end
364
364
  end
@@ -439,15 +439,15 @@ class ScenarioTest < MiniTest::Test
439
439
  @db.query('select 2')
440
440
  assert_equal ['select 1', 'select 2'], sqls
441
441
 
442
- stmt = @db.prepare('select 3')
443
-
444
- stmt.query
442
+ query = @db.prepare('select 3')
443
+
444
+ query.to_a
445
445
  assert_equal ['select 1', 'select 2', 'select 3'], sqls
446
446
 
447
447
  # turn off
448
448
  @db.trace
449
449
 
450
- stmt.query
450
+ query.to_a
451
451
 
452
452
  @db.query('select 4')
453
453
  assert_equal ['select 1', 'select 2', 'select 3'], sqls
@@ -20,7 +20,7 @@ class ExtraliteTest < MiniTest::Test
20
20
 
21
21
  assert_operator 0, :<, a[0]
22
22
  assert_operator a[0], :<=, a[1]
23
-
23
+
24
24
  assert_equal a, b
25
25
  assert_equal a, c
26
26
 
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ class IteratorTest < MiniTest::Test
6
+ def setup
7
+ @db = Extralite::Database.new(':memory:')
8
+ @db.query('create table if not exists t (x,y,z)')
9
+ @db.query('delete from t')
10
+ @db.query('insert into t values (1, 2, 3)')
11
+ @db.query('insert into t values (4, 5, 6)')
12
+ @db.query('insert into t values (7, 8, 9)')
13
+
14
+ @query = @db.prepare('select * from t')
15
+ end
16
+
17
+ def test_iterator_each_idempotency
18
+ iter = @query.each
19
+ assert_equal iter, iter.each
20
+ assert_equal iter, iter.each.each
21
+ end
22
+
23
+ def test_iterator_hash
24
+ iter = @query.each
25
+ assert_kind_of Extralite::Iterator, iter
26
+
27
+ buf = []
28
+ v = iter.each { |r| buf << r }
29
+ assert_equal iter, v
30
+ assert_equal [{x: 1, y: 2, z: 3},{ x: 4, y: 5, z: 6 }, { x: 7, y: 8, z: 9 }], buf
31
+ end
32
+
33
+ def test_iterator_ary
34
+ iter = @query.each_ary
35
+ assert_kind_of Extralite::Iterator, iter
36
+
37
+ buf = []
38
+ v = iter.each { |r| buf << r }
39
+ assert_equal iter, v
40
+ assert_equal [[1, 2, 3], [4, 5, 6], [7, 8, 9]], buf
41
+ end
42
+
43
+ def test_iterator_single_column
44
+ query = @db.prepare('select x from t')
45
+ iter = query.each_single_column
46
+ assert_kind_of Extralite::Iterator, iter
47
+
48
+ buf = []
49
+ v = iter.each { |r| buf << r }
50
+ assert_equal iter, v
51
+ assert_equal [1, 4, 7], buf
52
+ end
53
+
54
+ def test_iterator_next
55
+ iter = @query.each
56
+ assert_equal({x: 1, y: 2, z: 3}, iter.next)
57
+ assert_equal({x: 4, y: 5, z: 6}, iter.next)
58
+ assert_equal({x: 7, y: 8, z: 9}, iter.next)
59
+ assert_nil iter.next
60
+ assert_nil iter.next
61
+
62
+ iter = @query.reset.each_ary
63
+ assert_equal([1, 2, 3], iter.next)
64
+ assert_equal([4, 5, 6], iter.next)
65
+ assert_equal([7, 8, 9], iter.next)
66
+ assert_nil iter.next
67
+ assert_nil iter.next
68
+
69
+ iter = @db.prepare('select y from t').each_single_column
70
+ assert_equal(2, iter.next)
71
+ assert_equal(5, iter.next)
72
+ assert_equal(8, iter.next)
73
+ assert_nil iter.next
74
+ assert_nil iter.next
75
+ end
76
+
77
+ def test_iterator_to_a
78
+ iter = @query.each
79
+ assert_equal [{x: 1, y: 2, z: 3},{ x: 4, y: 5, z: 6 }, { x: 7, y: 8, z: 9 }], iter.to_a
80
+
81
+ iter = @query.each_ary
82
+ assert_equal [[1, 2, 3], [4, 5, 6], [7, 8, 9]], iter.to_a
83
+
84
+ iter = @db.prepare('select x from t').each_single_column
85
+ assert_equal [1, 4, 7], iter.to_a
86
+ end
87
+
88
+ def test_iterator_enumerable_methods
89
+ mapped = @query.each.map { |row| row[:x] * 10 }
90
+ assert_equal [10, 40, 70], mapped
91
+
92
+ mapped = @query.each_ary.map { |row| row[1] * 10 }
93
+ assert_equal [20, 50, 80], mapped
94
+
95
+ query = @db.prepare('select z from t')
96
+ mapped = query.each_single_column.map { |v| v * 10 }
97
+ assert_equal [30, 60, 90], mapped
98
+ end
99
+ end