extralite 1.5 → 1.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ad8337aebac5ef75132340d8336389fe0c927ed454b5792d57f80efa4f362664
4
- data.tar.gz: 602ad3648312e810398fac215353b0fac5aa916ff7a3a7901ba1990c9e3fb145
3
+ metadata.gz: e2924788672c7c3960a5baeaeacf14d297cfe461cfb1d20db828d7a67b5536c8
4
+ data.tar.gz: fb4da5551c3b1ddca95ef05520eee477bfb7a29383b9c7e5f5c0638392eba3e9
5
5
  SHA512:
6
- metadata.gz: a5831721c27d2eb27c8433bcd286ba826861946b7894eb2c1564f449d321d2eaeec7cce13a4166b10db799f9e9e391bc2ba65961edc7b144592d1994767e69b0
7
- data.tar.gz: ce77ee584ec89bae2819a2c0384ca7cc08daf00f405d0bf379f322d6182b3bf55bceb0ac0136d52fddd3fe5a43e4e48eb23cbfc00eef74a0439ce25eeeb529f7
6
+ metadata.gz: 63b73c0d05cb294b75d79bef5c1362e2c422f16b0e7ae76c3d2b577af20f612007a4fe272942958b38d545efabe4d975c1a0475ee8dec4a68e957d61800ffca7
7
+ data.tar.gz: b79c50cb5c696cf1eb505bbd126b197f1160f56af70e669cfc432e2e16d5423848c139ff9e942fb26c06c3d15f94e1a43f22ab4c317dae4fb0c2290f8d431aa6
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 1.6 2021-12-13
2
+
3
+ - Release GVL while fetching rows
4
+
1
5
  ## 1.5 2021-12-13
2
6
 
3
7
  - Release GVL while preparing statements
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- extralite (1.5)
4
+ extralite (1.6)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -6,18 +6,23 @@
6
6
 
7
7
  ## What is Extralite?
8
8
 
9
- Extralite is an extra-lightweight (less than 400 lines of C-code) SQLite3 wrapper for
10
- Ruby. It provides a single class with a minimal set of methods to interact with
11
- an SQLite3 database.
9
+ Extralite is an extra-lightweight (less than 430 lines of C-code) SQLite3
10
+ wrapper for Ruby. It provides a single class with a minimal set of methods to
11
+ interact with an SQLite3 database.
12
12
 
13
13
  ## Features
14
14
 
15
- - A variety of methods for different data access patterns: row as hash, row as
16
- array, single single row, single column, single value.
15
+ - A variety of methods for different data access patterns: rows as hashes, rows
16
+ as arrays, single row, single column, single value.
17
+ - Super fast - [up to 12.5x faster](#performance) than the
18
+ [sqlite3](https://github.com/sparklemotion/sqlite3-ruby) gem (see also
19
+ [comparison](#why-not-just-use-the-sqlite3-gem).)
20
+ - Improved [concurrency](#concurrency) for multithreaded apps: the Ruby GVL is
21
+ released while preparing SQL statements and while iterating over results.
17
22
  - Iterate over records with a block, or collect records into an array.
18
23
  - Parameter binding.
19
- - Correctly execute strings with multiple semicolon-separated queries (handy for
20
- creating/modifying schemas).
24
+ - Automatically execute SQL strings containing multiple semicolon-separated
25
+ queries (handy for creating/modifying schemas).
21
26
  - Get last insert rowid.
22
27
  - Get number of rows changed by last query.
23
28
  - Load extensions (loading of extensions is autmatically enabled. You can find
@@ -80,20 +85,55 @@ db.closed? #=> true
80
85
  ## Why not just use the sqlite3 gem?
81
86
 
82
87
  The sqlite3-ruby gem is a popular, solid, well-maintained project, used by
83
- thousands of developers. I've been doing a lot of work with SQLite3 lately, and
84
- wanted to have a simpler API that gives me query results in a variety of ways.
85
- Thus extralite was born.
88
+ thousands of developers. I've been doing a lot of work with SQLite3 databases
89
+ lately, and wanted to have a simpler API that gives me query results in a
90
+ variety of ways. Thus extralite was born.
91
+
92
+ Here's a table summarizing the differences between the two gems:
93
+
94
+ | |sqlite3-ruby|Extralite|
95
+ |-|-|-|
96
+ |API design|multiple classes|single class|
97
+ |Query results|row as hash, row as array, single row, single value|row as hash, row as array, single column, single row, single value|
98
+ |execute multiple statements|separate API (#execute_batch)|integrated|
99
+ |custom functions in Ruby|yes|no|
100
+ |custom collations|yes|no|
101
+ |custom aggregate functions|yes|no|
102
+ |Multithread friendly|no|[yes](#concurrency)|
103
+ |Code size|~2650LoC|~500LoC|
104
+ |Performance|1x|1.5x to 12.5x (see [below](#performance))|
86
105
 
87
106
  ## What about concurrency?
88
107
 
89
- Extralite currently does not release the GVL. This means that even if queries
90
- are executed on a separate thread, no other Ruby threads will be scheduled while
91
- SQLite3 is busy fetching the next record.
108
+ Extralite releases the GVL while making blocking calls to the sqlite3 library,
109
+ that is while preparing SQL statements and fetching rows. Releasing the GVL
110
+ allows other threads to run while the sqlite3 library is busy compiling SQL into
111
+ bytecode, or fetching the next row. This does not seem to hurt Extralite's
112
+ performance:
92
113
 
93
- In the future Extralite might be changed to release the GVL each time
94
- `sqlite3_step` is called.
114
+ ## Performance
115
+
116
+ A benchmark script is
117
+ [included](https://github.com/digital-fabric/extralite/blob/main/test/perf.rb),
118
+ creating a table of various row counts, then fetching the entire table using
119
+ either `sqlite3` or `extralite`. This benchmark shows Extralite to be up to 12.5
120
+ times faster than `sqlite3` when fetching a large number of rows. Here are the
121
+ results (using the `sqlite3` gem performance as baseline):
122
+
123
+ |Row count|sqlite3-ruby (baseline)|Extralite (relative - rounded)|
124
+ |-:|-:|-:|
125
+ |10|1x|1.5x|
126
+ |1K|1x|7x|
127
+ |100K|1x|12.5x|
128
+
129
+ (If you're interested in checking this yourself, just run the script and let me
130
+ know if your results are different.)
95
131
 
96
132
  ## Can I use it with an ORM like ActiveRecord or Sequel?
97
133
 
98
- Not yet, but you are welcome to contribute adapters for those projects. I will
99
- be releasing my own not-an-ORM tool in the near future.
134
+ Not yet, but you are welcome to contribute adapters for those projects.
135
+
136
+ ## Contributing
137
+
138
+ Contributions in the form of issues, PRs or comments will be greatly
139
+ appreciated!
@@ -228,10 +228,21 @@ inline void prepare_multi_stmt(sqlite3 *db, sqlite3_stmt **stmt, VALUE sql) {
228
228
  }
229
229
  }
230
230
 
231
- inline int stmt_iterate(sqlite3_stmt *stmt, sqlite3 *db) {
231
+ struct step_ctx {
232
+ sqlite3_stmt *stmt;
232
233
  int rc;
233
- rc = sqlite3_step(stmt);
234
- switch (rc) {
234
+ };
235
+
236
+ void *stmt_iterate_without_gvl(void *ptr) {
237
+ struct step_ctx *ctx = (struct step_ctx *)ptr;
238
+ ctx->rc = sqlite3_step(ctx->stmt);
239
+ return NULL;
240
+ }
241
+
242
+ inline int stmt_iterate(sqlite3_stmt *stmt, sqlite3 *db) {
243
+ struct step_ctx ctx = {stmt, 0};
244
+ rb_thread_call_without_gvl(stmt_iterate_without_gvl, (void *)&ctx, RUBY_UBF_IO, 0);
245
+ switch (ctx.rc) {
235
246
  case SQLITE_ROW:
236
247
  return 1;
237
248
  case SQLITE_DONE:
@@ -241,7 +252,7 @@ inline int stmt_iterate(sqlite3_stmt *stmt, sqlite3 *db) {
241
252
  case SQLITE_ERROR:
242
253
  rb_raise(cSQLError, "%s", sqlite3_errmsg(db));
243
254
  default:
244
- rb_raise(cError, "Invalid return code for sqlite3_step: %d (please open an issue on https://github.com/digital-fabric/extralite)", rc);
255
+ rb_raise(cError, "Invalid return code for sqlite3_step: %d (please open an issue on https://github.com/digital-fabric/extralite)", ctx.rc);
245
256
  }
246
257
 
247
258
  return 0;
@@ -1,3 +1,3 @@
1
1
  module Extralite
2
- VERSION = '1.5'
2
+ VERSION = '1.6'
3
3
  end
data/test/perf.rb CHANGED
@@ -13,36 +13,39 @@ require 'benchmark/ips'
13
13
  require 'fileutils'
14
14
 
15
15
  DB_PATH = '/tmp/extralite_sqlite3_perf.db'
16
- COUNT = 10000
17
16
 
18
- def prepare_database
17
+ def prepare_database(count)
19
18
  FileUtils.rm(DB_PATH) rescue nil
20
19
  db = Extralite::Database.new(DB_PATH)
21
20
  db.query('create table foo ( a integer primary key, b text )')
22
21
  db.query('begin')
23
- COUNT.times { db.query('insert into foo (b) values (?)', "hello#{rand(1000)}" )}
22
+ count.times { db.query('insert into foo (b) values (?)', "hello#{rand(1000)}" )}
24
23
  db.query('commit')
25
24
  end
26
25
 
27
- def sqlite3_run
26
+ def sqlite3_run(count)
28
27
  db = SQLite3::Database.new(DB_PATH, :results_as_hash => true)
29
28
  results = db.execute('select * from foo')
30
- raise unless results.size == COUNT
29
+ raise unless results.size == count
31
30
  end
32
31
 
33
- def extralite_run
32
+ def extralite_run(count)
34
33
  db = Extralite::Database.new(DB_PATH)
35
34
  results = db.query('select * from foo')
36
- raise unless results.size == COUNT
35
+ raise unless results.size == count
37
36
  end
38
37
 
39
- prepare_database
38
+ [10, 1000, 100000].each do |c|
39
+ puts; puts; puts "Record count: #{c}"
40
40
 
41
- Benchmark.ips do |x|
42
- x.config(:time => 3, :warmup => 1)
41
+ prepare_database(c)
43
42
 
44
- x.report("sqlite3") { sqlite3_run }
45
- x.report("extralite") { extralite_run }
43
+ Benchmark.ips do |x|
44
+ x.config(:time => 3, :warmup => 1)
46
45
 
47
- x.compare!
46
+ x.report("sqlite3") { sqlite3_run(c) }
47
+ x.report("extralite") { extralite_run(c) }
48
+
49
+ x.compare!
50
+ end
48
51
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: extralite
3
3
  version: !ruby/object:Gem::Version
4
- version: '1.5'
4
+ version: '1.6'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner