extralite-bundle 1.14
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 +7 -0
- data/.github/FUNDING.yml +1 -0
- data/.github/workflows/test.yml +26 -0
- data/.gitignore +57 -0
- data/CHANGELOG.md +96 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +37 -0
- data/LICENSE +21 -0
- data/README.md +248 -0
- data/Rakefile +46 -0
- data/TODO.md +0 -0
- data/bin/update_sqlite_source +26 -0
- data/ext/extralite/common.c +347 -0
- data/ext/extralite/database.c +385 -0
- data/ext/extralite/extconf-bundle.rb +9 -0
- data/ext/extralite/extconf.rb +115 -0
- data/ext/extralite/extralite.h +64 -0
- data/ext/extralite/extralite_ext.c +7 -0
- data/ext/extralite/extralite_sqlite3.c +3 -0
- data/ext/extralite/prepared_statement.c +238 -0
- data/ext/sqlite3/sqlite3.c +239246 -0
- data/ext/sqlite3/sqlite3.h +12802 -0
- data/extralite-bundle.gemspec +8 -0
- data/extralite.gemspec +8 -0
- data/gemspec.rb +26 -0
- data/lib/extralite/version.rb +3 -0
- data/lib/extralite.rb +21 -0
- data/lib/sequel/adapters/extralite.rb +380 -0
- data/test/extensions/text.dylib +0 -0
- data/test/extensions/text.so +0 -0
- data/test/helper.rb +7 -0
- data/test/perf_ary.rb +51 -0
- data/test/perf_hash.rb +51 -0
- data/test/perf_prepared.rb +64 -0
- data/test/run.rb +5 -0
- data/test/test_database.rb +247 -0
- data/test/test_extralite.rb +9 -0
- data/test/test_prepared_statement.rb +165 -0
- data/test/test_sequel.rb +24 -0
- metadata +160 -0
data/extralite.gemspec
ADDED
data/gemspec.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require_relative './lib/extralite/version'
|
2
|
+
|
3
|
+
def common_spec(s)
|
4
|
+
s.version = Extralite::VERSION
|
5
|
+
s.licenses = ['MIT']
|
6
|
+
s.author = 'Sharon Rosner'
|
7
|
+
s.email = 'sharon@noteflakes.com'
|
8
|
+
s.files = `git ls-files`.split
|
9
|
+
s.homepage = 'https://github.com/digital-fabric/extralite'
|
10
|
+
s.metadata = {
|
11
|
+
"source_code_uri" => "https://github.com/digital-fabric/extralite",
|
12
|
+
"documentation_uri" => "https://www.rubydoc.info/gems/extralite",
|
13
|
+
"homepage_uri" => "https://github.com/digital-fabric/extralite",
|
14
|
+
"changelog_uri" => "https://github.com/digital-fabric/extralite/blob/master/CHANGELOG.md"
|
15
|
+
}
|
16
|
+
s.rdoc_options = ["--title", "extralite", "--main", "README.md"]
|
17
|
+
s.extra_rdoc_files = ["README.md"]
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
s.required_ruby_version = '>= 2.7'
|
20
|
+
|
21
|
+
s.add_development_dependency 'rake-compiler', '1.1.6'
|
22
|
+
s.add_development_dependency 'minitest', '5.15.0'
|
23
|
+
s.add_development_dependency 'simplecov', '0.17.1'
|
24
|
+
s.add_development_dependency 'yard', '0.9.27'
|
25
|
+
s.add_development_dependency 'sequel', '5.51.0'
|
26
|
+
end
|
data/lib/extralite.rb
ADDED
@@ -0,0 +1,21 @@
|
|
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
|
Binary file
|
Binary file
|
data/test/helper.rb
ADDED
data/test/perf_ary.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/inline'
|
4
|
+
|
5
|
+
gemfile do
|
6
|
+
source 'https://rubygems.org'
|
7
|
+
gem 'sqlite3'
|
8
|
+
gem 'extralite', path: '..'
|
9
|
+
gem 'benchmark-ips'
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'benchmark/ips'
|
13
|
+
require 'fileutils'
|
14
|
+
|
15
|
+
DB_PATH = '/tmp/extralite_sqlite3_perf.db'
|
16
|
+
|
17
|
+
def prepare_database(count)
|
18
|
+
FileUtils.rm(DB_PATH) rescue nil
|
19
|
+
db = Extralite::Database.new(DB_PATH)
|
20
|
+
db.query('create table foo ( a integer primary key, b text )')
|
21
|
+
db.query('begin')
|
22
|
+
count.times { db.query('insert into foo (b) values (?)', "hello#{rand(1000)}" )}
|
23
|
+
db.query('commit')
|
24
|
+
end
|
25
|
+
|
26
|
+
def sqlite3_run(count)
|
27
|
+
db = SQLite3::Database.new(DB_PATH)
|
28
|
+
results = db.execute('select * from foo')
|
29
|
+
raise unless results.size == count
|
30
|
+
end
|
31
|
+
|
32
|
+
def extralite_run(count)
|
33
|
+
db = Extralite::Database.new(DB_PATH)
|
34
|
+
results = db.query_ary('select * from foo')
|
35
|
+
raise unless results.size == count
|
36
|
+
end
|
37
|
+
|
38
|
+
[10, 1000, 100000].each do |c|
|
39
|
+
puts; puts; puts "Record count: #{c}"
|
40
|
+
|
41
|
+
prepare_database(c)
|
42
|
+
|
43
|
+
Benchmark.ips do |x|
|
44
|
+
x.config(:time => 3, :warmup => 1)
|
45
|
+
|
46
|
+
x.report("sqlite3") { sqlite3_run(c) }
|
47
|
+
x.report("extralite") { extralite_run(c) }
|
48
|
+
|
49
|
+
x.compare!
|
50
|
+
end
|
51
|
+
end
|
data/test/perf_hash.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/inline'
|
4
|
+
|
5
|
+
gemfile do
|
6
|
+
source 'https://rubygems.org'
|
7
|
+
gem 'extralite', path: '..'
|
8
|
+
gem 'sqlite3'
|
9
|
+
gem 'benchmark-ips'
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'benchmark/ips'
|
13
|
+
require 'fileutils'
|
14
|
+
|
15
|
+
DB_PATH = '/tmp/extralite_sqlite3_perf.db'
|
16
|
+
|
17
|
+
def prepare_database(count)
|
18
|
+
FileUtils.rm(DB_PATH) rescue nil
|
19
|
+
db = Extralite::Database.new(DB_PATH)
|
20
|
+
db.query('create table foo ( a integer primary key, b text )')
|
21
|
+
db.query('begin')
|
22
|
+
count.times { db.query('insert into foo (b) values (?)', "hello#{rand(1000)}" )}
|
23
|
+
db.query('commit')
|
24
|
+
end
|
25
|
+
|
26
|
+
def sqlite3_run(count)
|
27
|
+
db = SQLite3::Database.new(DB_PATH, :results_as_hash => true)
|
28
|
+
results = db.execute('select * from foo')
|
29
|
+
raise unless results.size == count
|
30
|
+
end
|
31
|
+
|
32
|
+
def extralite_run(count)
|
33
|
+
db = Extralite::Database.new(DB_PATH)
|
34
|
+
results = db.query('select * from foo')
|
35
|
+
raise unless results.size == count
|
36
|
+
end
|
37
|
+
|
38
|
+
[10, 1000, 100000].each do |c|
|
39
|
+
puts; puts; puts "Record count: #{c}"
|
40
|
+
|
41
|
+
prepare_database(c)
|
42
|
+
|
43
|
+
Benchmark.ips do |x|
|
44
|
+
x.config(:time => 3, :warmup => 1)
|
45
|
+
|
46
|
+
x.report("sqlite3") { sqlite3_run(c) }
|
47
|
+
x.report("extralite") { extralite_run(c) }
|
48
|
+
|
49
|
+
x.compare!
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/inline'
|
4
|
+
|
5
|
+
gemfile do
|
6
|
+
source 'https://rubygems.org'
|
7
|
+
gem 'extralite', path: '..'
|
8
|
+
gem 'sqlite3'
|
9
|
+
gem 'benchmark-ips'
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'benchmark/ips'
|
13
|
+
require 'fileutils'
|
14
|
+
|
15
|
+
DB_PATH = '/tmp/extralite_sqlite3_perf.db'
|
16
|
+
|
17
|
+
def prepare_database(count)
|
18
|
+
FileUtils.rm(DB_PATH) rescue nil
|
19
|
+
db = Extralite::Database.new(DB_PATH)
|
20
|
+
db.query('create table foo ( a integer primary key, b text )')
|
21
|
+
db.query('begin')
|
22
|
+
count.times { db.query('insert into foo (b) values (?)', "hello#{rand(1000)}" )}
|
23
|
+
db.query('commit')
|
24
|
+
end
|
25
|
+
|
26
|
+
def sqlite3_prepare
|
27
|
+
db = SQLite3::Database.new(DB_PATH, :results_as_hash => true)
|
28
|
+
db.prepare('select * from foo')
|
29
|
+
end
|
30
|
+
|
31
|
+
def sqlite3_run(stmt, count)
|
32
|
+
# db = SQLite3::Database.new(DB_PATH, :results_as_hash => true)
|
33
|
+
results = stmt.execute.to_a
|
34
|
+
raise unless results.size == count
|
35
|
+
end
|
36
|
+
|
37
|
+
def extralite_prepare
|
38
|
+
db = Extralite::Database.new(DB_PATH)
|
39
|
+
db.prepare('select * from foo')
|
40
|
+
end
|
41
|
+
|
42
|
+
def extralite_run(stmt, count)
|
43
|
+
# db = Extralite::Database.new(DB_PATH)
|
44
|
+
results = stmt.query
|
45
|
+
raise unless results.size == count
|
46
|
+
end
|
47
|
+
|
48
|
+
[10, 1000, 100000].each do |c|
|
49
|
+
puts; puts; puts "Record count: #{c}"
|
50
|
+
|
51
|
+
prepare_database(c)
|
52
|
+
|
53
|
+
sqlite3_stmt = sqlite3_prepare
|
54
|
+
extralite_stmt = extralite_prepare
|
55
|
+
|
56
|
+
Benchmark.ips do |x|
|
57
|
+
x.config(:time => 3, :warmup => 1)
|
58
|
+
|
59
|
+
x.report("sqlite3") { sqlite3_run(sqlite3_stmt, c) }
|
60
|
+
x.report("extralite") { extralite_run(extralite_stmt, c) }
|
61
|
+
|
62
|
+
x.compare!
|
63
|
+
end
|
64
|
+
end
|