extralite-bundle 2.6 → 2.7
Sign up to get free protection for your applications and to get access to all the features.
- 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)')
|