extralite 1.5 → 1.6

Sign up to get free protection for your applications and to get access to all the features.
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