extralite 2.6 → 2.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/.gitignore +1 -0
- data/CHANGELOG.md +28 -17
- data/Gemfile +4 -0
- data/Gemfile-bundle +1 -1
- data/README.md +259 -72
- 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 +638 -316
- data/ext/extralite/extconf.rb +7 -11
- data/ext/extralite/extralite.h +89 -48
- data/ext/extralite/iterator.c +6 -83
- data/ext/extralite/query.c +164 -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 +28 -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,10 @@ module Extralite
|
|
29
29
|
class ParameterError < Error
|
30
30
|
end
|
31
31
|
|
32
|
-
#
|
32
|
+
# This class encapsulates an SQLite database connection.
|
33
33
|
class Database
|
34
34
|
# @!visibility private
|
35
|
-
TABLES_SQL = <<~SQL
|
35
|
+
TABLES_SQL = (<<~SQL).freeze
|
36
36
|
SELECT name FROM %<db>s.sqlite_master
|
37
37
|
WHERE type ='table'
|
38
38
|
AND name NOT LIKE 'sqlite_%%';
|
@@ -46,10 +46,11 @@ module Extralite
|
|
46
46
|
# @param db [String] name of attached database
|
47
47
|
# @return [Array] list of tables
|
48
48
|
def tables(db = 'main')
|
49
|
-
|
49
|
+
query_argv(format(TABLES_SQL, db: db))
|
50
50
|
end
|
51
51
|
|
52
|
-
# Gets or sets one or more pragmas
|
52
|
+
# Gets or sets one or more database pragmas. For a list of available pragmas
|
53
|
+
# see: https://sqlite.org/pragma.html#toc
|
53
54
|
#
|
54
55
|
# db.pragma(:cache_size) # get
|
55
56
|
# db.pragma(cache_size: -2000) # set
|
@@ -74,21 +75,29 @@ module Extralite
|
|
74
75
|
# raise if db.query_single_value('select x from bar') > 42
|
75
76
|
# end
|
76
77
|
#
|
77
|
-
#
|
78
|
+
# For more information on transactions see:
|
79
|
+
# https://sqlite.org/lang_transaction.html
|
80
|
+
#
|
81
|
+
# @param mode [Symbol, String] transaction mode (deferred, immediate or exclusive).
|
78
82
|
# @return [Any] the given block's return value
|
79
83
|
def transaction(mode = :immediate)
|
80
|
-
execute "begin #{mode} transaction"
|
81
|
-
|
82
84
|
abort = false
|
85
|
+
execute "begin #{mode} transaction"
|
83
86
|
yield self
|
84
87
|
rescue => e
|
85
88
|
abort = true
|
86
|
-
|
89
|
+
e.is_a?(Rollback) ? nil : raise
|
87
90
|
ensure
|
88
91
|
execute(abort ? 'rollback' : 'commit')
|
89
92
|
end
|
90
93
|
|
91
|
-
# Creates a savepoint with the given name.
|
94
|
+
# Creates a savepoint with the given name. For more information on
|
95
|
+
# savepoints see: https://sqlite.org/lang_savepoint.html
|
96
|
+
#
|
97
|
+
# db.savepoint(:savepoint1)
|
98
|
+
# db.execute('insert into foo values (42)')
|
99
|
+
# db.rollback_to(:savepoint1)
|
100
|
+
# db.release(:savepoint1)
|
92
101
|
#
|
93
102
|
# @param name [String, Symbol] savepoint name
|
94
103
|
# @return [Extralite::Database] database
|
@@ -97,7 +106,8 @@ module Extralite
|
|
97
106
|
self
|
98
107
|
end
|
99
108
|
|
100
|
-
# Release a savepoint with the given name.
|
109
|
+
# Release a savepoint with the given name. For more information on
|
110
|
+
# savepoints see: https://sqlite.org/lang_savepoint.html
|
101
111
|
#
|
102
112
|
# @param name [String, Symbol] savepoint name
|
103
113
|
# @return [Extralite::Database] database
|
@@ -106,7 +116,8 @@ module Extralite
|
|
106
116
|
self
|
107
117
|
end
|
108
118
|
|
109
|
-
# Rolls back changes to a savepoint with the given name.
|
119
|
+
# Rolls back changes to a savepoint with the given name. For more
|
120
|
+
# information on savepoints see: https://sqlite.org/lang_savepoint.html
|
110
121
|
#
|
111
122
|
# @param name [String, Symbol] savepoint name
|
112
123
|
# @return [Extralite::Database] database
|
@@ -116,14 +127,14 @@ module Extralite
|
|
116
127
|
end
|
117
128
|
|
118
129
|
# Rolls back the currently active transaction. This method should only be
|
119
|
-
# called from within a block passed to Database#transaction
|
130
|
+
# called from within a block passed to `Database#transaction`. This method
|
120
131
|
# raises a Extralite::Rollback exception, which will stop execution of the
|
121
132
|
# transaction block without propagating the exception.
|
122
133
|
#
|
123
|
-
#
|
124
|
-
#
|
125
|
-
#
|
126
|
-
#
|
134
|
+
# db.transaction do
|
135
|
+
# db.execute('insert into foo (42)')
|
136
|
+
# db.rollback!
|
137
|
+
# end
|
127
138
|
#
|
128
139
|
# @param name [String, Symbol] savepoint name
|
129
140
|
# @return [Extralite::Database] database
|
@@ -139,7 +150,7 @@ module Extralite
|
|
139
150
|
end
|
140
151
|
|
141
152
|
def pragma_get(key)
|
142
|
-
|
153
|
+
query_single_argv("pragma #{key}")
|
143
154
|
end
|
144
155
|
end
|
145
156
|
|
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)')
|