extralite 2.6 → 2.7.1
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/.gitignore +1 -0
- data/.yardopts +1 -1
- data/CHANGELOG.md +32 -17
- data/Gemfile +4 -0
- data/Gemfile-bundle +1 -1
- data/README.md +262 -75
- data/Rakefile +18 -0
- data/TODO.md +0 -9
- data/examples/kv_store.rb +49 -0
- data/examples/multi_fiber.rb +16 -0
- data/examples/on_progress.rb +9 -0
- data/examples/pubsub_store_polyphony.rb +194 -0
- data/examples/pubsub_store_threads.rb +204 -0
- data/ext/extralite/changeset.c +3 -3
- data/ext/extralite/common.c +173 -87
- data/ext/extralite/database.c +650 -315
- data/ext/extralite/extconf.rb +7 -11
- data/ext/extralite/extralite.h +89 -48
- data/ext/extralite/iterator.c +6 -84
- data/ext/extralite/query.c +165 -256
- data/extralite-bundle.gemspec +1 -1
- data/extralite.gemspec +1 -1
- data/gemspec.rb +10 -11
- data/lib/extralite/version.rb +1 -1
- data/lib/extralite.rb +27 -17
- data/lib/sequel/adapters/extralite.rb +1 -1
- data/test/helper.rb +2 -1
- data/test/perf_argv_transform.rb +74 -0
- data/test/perf_hash_transform.rb +66 -0
- data/test/perf_polyphony.rb +74 -0
- data/test/test_changeset.rb +2 -2
- data/test/test_database.rb +531 -115
- data/test/test_extralite.rb +2 -2
- data/test/test_iterator.rb +28 -13
- data/test/test_query.rb +348 -111
- data/test/test_sequel.rb +4 -4
- metadata +20 -14
- data/Gemfile.lock +0 -37
data/lib/extralite.rb
CHANGED
@@ -29,10 +29,9 @@ module Extralite
|
|
29
29
|
class ParameterError < Error
|
30
30
|
end
|
31
31
|
|
32
|
-
# An SQLite database
|
33
32
|
class Database
|
34
33
|
# @!visibility private
|
35
|
-
TABLES_SQL = <<~SQL
|
34
|
+
TABLES_SQL = (<<~SQL).freeze
|
36
35
|
SELECT name FROM %<db>s.sqlite_master
|
37
36
|
WHERE type ='table'
|
38
37
|
AND name NOT LIKE 'sqlite_%%';
|
@@ -46,10 +45,11 @@ module Extralite
|
|
46
45
|
# @param db [String] name of attached database
|
47
46
|
# @return [Array] list of tables
|
48
47
|
def tables(db = 'main')
|
49
|
-
|
48
|
+
query_argv(format(TABLES_SQL, db: db))
|
50
49
|
end
|
51
50
|
|
52
|
-
# Gets or sets one or more pragmas
|
51
|
+
# Gets or sets one or more database pragmas. For a list of available pragmas
|
52
|
+
# see: https://sqlite.org/pragma.html#toc
|
53
53
|
#
|
54
54
|
# db.pragma(:cache_size) # get
|
55
55
|
# db.pragma(cache_size: -2000) # set
|
@@ -74,21 +74,29 @@ module Extralite
|
|
74
74
|
# raise if db.query_single_value('select x from bar') > 42
|
75
75
|
# end
|
76
76
|
#
|
77
|
-
#
|
77
|
+
# For more information on transactions see:
|
78
|
+
# https://sqlite.org/lang_transaction.html
|
79
|
+
#
|
80
|
+
# @param mode [Symbol, String] transaction mode (deferred, immediate or exclusive).
|
78
81
|
# @return [Any] the given block's return value
|
79
82
|
def transaction(mode = :immediate)
|
80
|
-
execute "begin #{mode} transaction"
|
81
|
-
|
82
83
|
abort = false
|
84
|
+
execute "begin #{mode} transaction"
|
83
85
|
yield self
|
84
86
|
rescue => e
|
85
87
|
abort = true
|
86
|
-
|
88
|
+
e.is_a?(Rollback) ? nil : raise
|
87
89
|
ensure
|
88
90
|
execute(abort ? 'rollback' : 'commit')
|
89
91
|
end
|
90
92
|
|
91
|
-
# Creates a savepoint with the given name.
|
93
|
+
# Creates a savepoint with the given name. For more information on
|
94
|
+
# savepoints see: https://sqlite.org/lang_savepoint.html
|
95
|
+
#
|
96
|
+
# db.savepoint(:savepoint1)
|
97
|
+
# db.execute('insert into foo values (42)')
|
98
|
+
# db.rollback_to(:savepoint1)
|
99
|
+
# db.release(:savepoint1)
|
92
100
|
#
|
93
101
|
# @param name [String, Symbol] savepoint name
|
94
102
|
# @return [Extralite::Database] database
|
@@ -97,7 +105,8 @@ module Extralite
|
|
97
105
|
self
|
98
106
|
end
|
99
107
|
|
100
|
-
# Release a savepoint with the given name.
|
108
|
+
# Release a savepoint with the given name. For more information on
|
109
|
+
# savepoints see: https://sqlite.org/lang_savepoint.html
|
101
110
|
#
|
102
111
|
# @param name [String, Symbol] savepoint name
|
103
112
|
# @return [Extralite::Database] database
|
@@ -106,7 +115,8 @@ module Extralite
|
|
106
115
|
self
|
107
116
|
end
|
108
117
|
|
109
|
-
# Rolls back changes to a savepoint with the given name.
|
118
|
+
# Rolls back changes to a savepoint with the given name. For more
|
119
|
+
# information on savepoints see: https://sqlite.org/lang_savepoint.html
|
110
120
|
#
|
111
121
|
# @param name [String, Symbol] savepoint name
|
112
122
|
# @return [Extralite::Database] database
|
@@ -116,14 +126,14 @@ module Extralite
|
|
116
126
|
end
|
117
127
|
|
118
128
|
# Rolls back the currently active transaction. This method should only be
|
119
|
-
# called from within a block passed to Database#transaction
|
129
|
+
# called from within a block passed to `Database#transaction`. This method
|
120
130
|
# raises a Extralite::Rollback exception, which will stop execution of the
|
121
131
|
# transaction block without propagating the exception.
|
122
132
|
#
|
123
|
-
#
|
124
|
-
#
|
125
|
-
#
|
126
|
-
#
|
133
|
+
# db.transaction do
|
134
|
+
# db.execute('insert into foo (42)')
|
135
|
+
# db.rollback!
|
136
|
+
# end
|
127
137
|
#
|
128
138
|
# @param name [String, Symbol] savepoint name
|
129
139
|
# @return [Extralite::Database] database
|
@@ -139,7 +149,7 @@ module Extralite
|
|
139
149
|
end
|
140
150
|
|
141
151
|
def pragma_get(key)
|
142
|
-
|
152
|
+
query_single_argv("pragma #{key}")
|
143
153
|
end
|
144
154
|
end
|
145
155
|
|
data/test/helper.rb
CHANGED
@@ -7,7 +7,8 @@ require 'minitest/autorun'
|
|
7
7
|
puts "sqlite3 version: #{Extralite.sqlite3_version}"
|
8
8
|
|
9
9
|
IS_LINUX = RUBY_PLATFORM =~ /linux/
|
10
|
-
|
10
|
+
# Ractors are kinda flaky, there's no point in testing this
|
11
|
+
SKIP_RACTOR_TESTS = true #!IS_LINUX || (RUBY_VERSION =~ /^3\.[01]/)
|
11
12
|
|
12
13
|
module Minitest::Assertions
|
13
14
|
def assert_in_range exp_range, act
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Run on Ruby 3.3 with YJIT enabled
|
4
|
+
|
5
|
+
require 'bundler/inline'
|
6
|
+
|
7
|
+
gemfile do
|
8
|
+
source 'https://rubygems.org'
|
9
|
+
gem 'extralite', path: '..'
|
10
|
+
gem 'benchmark-ips'
|
11
|
+
end
|
12
|
+
|
13
|
+
require 'benchmark/ips'
|
14
|
+
require 'fileutils'
|
15
|
+
|
16
|
+
DB_PATH = "/tmp/extralite_sqlite3_perf-#{Time.now.to_i}-#{rand(10000)}.db"
|
17
|
+
puts "DB_PATH = #{DB_PATH.inspect}"
|
18
|
+
|
19
|
+
$extralite_db = Extralite::Database.new(DB_PATH, gvl_release_threshold: -1)
|
20
|
+
|
21
|
+
def prepare_database(count)
|
22
|
+
$extralite_db.query('create table if not exists foo (b text)')
|
23
|
+
$extralite_db.query('delete from foo')
|
24
|
+
$extralite_db.query('begin')
|
25
|
+
count.times { $extralite_db.query('insert into foo (b) values (?)', "hello#{rand(1000)}" )}
|
26
|
+
$extralite_db.query('commit')
|
27
|
+
end
|
28
|
+
|
29
|
+
class Model
|
30
|
+
def initialize(h)
|
31
|
+
@h = h
|
32
|
+
end
|
33
|
+
|
34
|
+
def values
|
35
|
+
@h
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
TRANSFORM = ->(b) { { b: b } }
|
40
|
+
|
41
|
+
def extralite_run_ary_map(count)
|
42
|
+
results = []
|
43
|
+
$extralite_db.query_ary('select * from foo') { |(b)| results << { b: b } }
|
44
|
+
raise unless results.size == count
|
45
|
+
end
|
46
|
+
|
47
|
+
def extralite_run_argv_map(count)
|
48
|
+
results = []
|
49
|
+
$extralite_db.query_argv('select * from foo') { |b| results << { b: b } }
|
50
|
+
raise unless results.size == count
|
51
|
+
end
|
52
|
+
|
53
|
+
def extralite_run_transform(count)
|
54
|
+
results = $extralite_db.query_argv(TRANSFORM, 'select * from foo')
|
55
|
+
raise unless results.size == count
|
56
|
+
end
|
57
|
+
|
58
|
+
[10, 1000, 100000].each do |c|
|
59
|
+
puts "Record count: #{c}"
|
60
|
+
prepare_database(c)
|
61
|
+
|
62
|
+
bm = Benchmark.ips do |x|
|
63
|
+
x.config(:time => 5, :warmup => 2)
|
64
|
+
|
65
|
+
x.report("ary_map") { extralite_run_ary_map(c) }
|
66
|
+
x.report("argv_map") { extralite_run_argv_map(c) }
|
67
|
+
x.report("transform") { extralite_run_transform(c) }
|
68
|
+
|
69
|
+
x.compare!
|
70
|
+
end
|
71
|
+
puts;
|
72
|
+
bm.entries.each { |e| puts "#{e.label}: #{(e.ips * c).round.to_i} rows/s" }
|
73
|
+
puts;
|
74
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Run on Ruby 3.3 with YJIT enabled
|
4
|
+
|
5
|
+
require 'bundler/inline'
|
6
|
+
|
7
|
+
gemfile do
|
8
|
+
source 'https://rubygems.org'
|
9
|
+
gem 'extralite', path: '..'
|
10
|
+
gem 'benchmark-ips'
|
11
|
+
end
|
12
|
+
|
13
|
+
require 'benchmark/ips'
|
14
|
+
require 'fileutils'
|
15
|
+
|
16
|
+
DB_PATH = "/tmp/extralite_sqlite3_perf-#{Time.now.to_i}-#{rand(10000)}.db"
|
17
|
+
puts "DB_PATH = #{DB_PATH.inspect}"
|
18
|
+
|
19
|
+
$extralite_db = Extralite::Database.new(DB_PATH, gvl_release_threshold: -1)
|
20
|
+
|
21
|
+
def prepare_database(count)
|
22
|
+
$extralite_db.query('create table if not exists foo ( a integer primary key, b text )')
|
23
|
+
$extralite_db.query('delete from foo')
|
24
|
+
$extralite_db.query('begin')
|
25
|
+
count.times { $extralite_db.query('insert into foo (b) values (?)', "hello#{rand(1000)}" )}
|
26
|
+
$extralite_db.query('commit')
|
27
|
+
end
|
28
|
+
|
29
|
+
class Model
|
30
|
+
def initialize(h)
|
31
|
+
@h = h
|
32
|
+
end
|
33
|
+
|
34
|
+
def values
|
35
|
+
@h
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
TRANSFORM = ->(h) { Model.new(h) }
|
40
|
+
|
41
|
+
def extralite_run_map(count)
|
42
|
+
results = $extralite_db.query('select * from foo').map(&TRANSFORM)
|
43
|
+
raise unless results.size == count
|
44
|
+
end
|
45
|
+
|
46
|
+
def extralite_run_transform(count)
|
47
|
+
results = $extralite_db.query(TRANSFORM, 'select * from foo')
|
48
|
+
raise unless results.size == count
|
49
|
+
end
|
50
|
+
|
51
|
+
[10, 1000, 100000].each do |c|
|
52
|
+
puts "Record count: #{c}"
|
53
|
+
prepare_database(c)
|
54
|
+
|
55
|
+
bm = Benchmark.ips do |x|
|
56
|
+
x.config(:time => 5, :warmup => 2)
|
57
|
+
|
58
|
+
x.report("map") { extralite_run_map(c) }
|
59
|
+
x.report("transform") { extralite_run_transform(c) }
|
60
|
+
|
61
|
+
x.compare!
|
62
|
+
end
|
63
|
+
puts;
|
64
|
+
bm.entries.each { |e| puts "#{e.label}: #{(e.ips * c).round.to_i} rows/s" }
|
65
|
+
puts;
|
66
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Run on Ruby 3.3 with YJIT enabled
|
4
|
+
|
5
|
+
require 'bundler/inline'
|
6
|
+
gemfile do
|
7
|
+
gem 'polyphony'
|
8
|
+
gem 'extralite', path: '.'
|
9
|
+
gem 'benchmark-ips'
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'benchmark/ips'
|
13
|
+
require 'polyphony'
|
14
|
+
|
15
|
+
DB_PATH = "/tmp/extralite_sqlite3_perf-#{Time.now.to_i}-#{rand(10000)}.db"
|
16
|
+
puts "DB_PATH = #{DB_PATH.inspect}"
|
17
|
+
|
18
|
+
$db1 = Extralite::Database.new(DB_PATH, gvl_release_threshold: -1)
|
19
|
+
$db2 = Extralite::Database.new(DB_PATH, gvl_release_threshold: 0)
|
20
|
+
$db3 = Extralite::Database.new(DB_PATH)
|
21
|
+
|
22
|
+
$snooze_count = 0
|
23
|
+
$db3.on_progress(25) { $snooze_count += 1; snooze }
|
24
|
+
|
25
|
+
def prepare_database(count)
|
26
|
+
$db1.execute('create table if not exists foo ( a integer primary key, b text )')
|
27
|
+
$db1.transaction do
|
28
|
+
$db1.execute('delete from foo')
|
29
|
+
rows = count.times.map { "hello#{rand(1000)}" }
|
30
|
+
$db1.batch_execute('insert into foo (b) values (?)', rows)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def extralite_run1(count)
|
35
|
+
results = $db1.query('select * from foo')
|
36
|
+
raise unless results.size == count
|
37
|
+
end
|
38
|
+
|
39
|
+
def extralite_run2(count)
|
40
|
+
results = $db2.query('select * from foo')
|
41
|
+
raise unless results.size == count
|
42
|
+
end
|
43
|
+
|
44
|
+
def extralite_run3(count)
|
45
|
+
results = $db3.query('select * from foo')
|
46
|
+
raise unless results.size == count
|
47
|
+
end
|
48
|
+
|
49
|
+
[10, 1000, 100000].each do |c|
|
50
|
+
puts "Record count: #{c}"
|
51
|
+
prepare_database(c)
|
52
|
+
|
53
|
+
bm = Benchmark.ips do |x|
|
54
|
+
x.config(:time => 3, :warmup => 1)
|
55
|
+
|
56
|
+
x.report('GVL threshold -1') { extralite_run1(c) }
|
57
|
+
x.report('GVL threshold 0') { extralite_run2(c) }
|
58
|
+
$snooze_count = 0
|
59
|
+
x.report('on_progress 1000') { extralite_run3(c) }
|
60
|
+
|
61
|
+
x.compare!
|
62
|
+
end
|
63
|
+
puts;
|
64
|
+
bm.entries.each do |e|
|
65
|
+
score = (e.ips * c).round.to_i
|
66
|
+
if e.label == 'on_progress 1000'
|
67
|
+
snooze_rate = ($snooze_count / e.seconds).to_i
|
68
|
+
puts "#{e.label}: #{score} rows/s snoozes: #{snooze_rate} i/s"
|
69
|
+
else
|
70
|
+
puts "#{e.label}: #{score} rows/s"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
puts;
|
74
|
+
end
|
data/test/test_changeset.rb
CHANGED
@@ -5,7 +5,7 @@ require_relative 'helper'
|
|
5
5
|
require 'date'
|
6
6
|
require 'tempfile'
|
7
7
|
|
8
|
-
class ChangesetTest <
|
8
|
+
class ChangesetTest < Minitest::Test
|
9
9
|
def setup
|
10
10
|
@db = Extralite::Database.new(':memory:')
|
11
11
|
skip if !@db.respond_to?(:track_changes)
|
@@ -130,7 +130,7 @@ class ChangesetTest < MiniTest::Test
|
|
130
130
|
|
131
131
|
def test_blob
|
132
132
|
changeset = Extralite::Changeset.new
|
133
|
-
assert_equal
|
133
|
+
assert_equal '', changeset.to_blob
|
134
134
|
|
135
135
|
changeset.track(@db, [:t]) do
|
136
136
|
@db.execute('insert into t values (1, 2, 3)')
|