extralite-bundle 3.1.1 → 3.1.2
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/CHANGELOG.md +7 -0
- data/Gemfile +0 -4
- data/LICENSE +1 -1
- data/README.md +26 -1
- data/TODO.md +6 -0
- data/ext/extralite/common.c +9 -7
- data/ext/extralite/database.c +6 -6
- data/gemspec.rb +3 -3
- data/lib/extralite/version.rb +1 -1
- data/lib/extralite.rb +25 -1
- data/test/test_database.rb +112 -1
- metadata +11 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9eac653308555c057d121db6b0e17905fff645a62d926bdd3ac07323f40f8f9e
|
|
4
|
+
data.tar.gz: 2bcfeb952ce57afa647b969016891edae0fd615fc78f97a0a6b0336cc35791d6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aa4d929c51e1f2c05fc0e67aa37447281c50d697b9dff242ca6f96e3723027e03b3db5b4c437399f28045a12d251dfff3e0f3a47709302b132347cca9c7fa514
|
|
7
|
+
data.tar.gz: 9809ca98906d5b853e646814057b976a55171249ae85b0502eea8ab39032d725889d736d3e9eff71ee46c9ca31d64e81f42102936753e69a2714188abec3bca5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# 3.1.2 2026-10-03
|
|
2
|
+
|
|
3
|
+
- Fix handling of failed BEGIN in #transaction (#89)
|
|
4
|
+
- Reset of cached statements post-query (#88)
|
|
5
|
+
- Add missing wal checkpoint, integrity_check docs to README
|
|
6
|
+
- Implement `Database#integrity_check`, `Database#quick_check`
|
|
7
|
+
|
|
1
8
|
# 3.1.1 2026-09-26
|
|
2
9
|
|
|
3
10
|
- Fix clearing of stmt cache after `DB#close` or after GC (#87)
|
data/Gemfile
CHANGED
data/LICENSE
CHANGED
data/README.md
CHANGED
|
@@ -56,7 +56,7 @@ latest features and enhancements.
|
|
|
56
56
|
[backups](#creating-backups), retrieve [status
|
|
57
57
|
information](#retrieving-status-information), work with
|
|
58
58
|
[changesets](#working-with-changesets), interrogate [database
|
|
59
|
-
limits](#working-with-database-limits), [trace](#tracing-sql-statements)
|
|
59
|
+
limits](#working-with-database-limits), [trace](#tracing-sql-statements), [WAL checkpoints](#wal-checkpoints), and run [integrity checks](#integrity-checks).
|
|
60
60
|
queries.
|
|
61
61
|
- [Sequel](#usage-with-sequel) adapter.
|
|
62
62
|
|
|
@@ -1313,6 +1313,31 @@ db.query('select ?, ?', 1, 2)
|
|
|
1313
1313
|
# { sql: "select ?, ?", args: [1, 2] }
|
|
1314
1314
|
```
|
|
1315
1315
|
|
|
1316
|
+
### WAL Checkpoints
|
|
1317
|
+
|
|
1318
|
+
Exrtralite lets you manually perform a WAL checkpoint in order to truncate the
|
|
1319
|
+
WAL file at any given moment using the `Database#wal_checkpoint` method:
|
|
1320
|
+
|
|
1321
|
+
```ruby
|
|
1322
|
+
db.wal_checkpoint(:full)
|
|
1323
|
+
```
|
|
1324
|
+
|
|
1325
|
+
The `#wal_checkpoint` method takes a mode argument, which should be one
|
|
1326
|
+
`:passive`, `:full`, `:restart`, `:truncate`. For a discussion of the different
|
|
1327
|
+
modes, you can refer to the [SQLite
|
|
1328
|
+
documentation](https://sqlite.org/c3ref/wal_checkpoint_v2.html).
|
|
1329
|
+
|
|
1330
|
+
### Integrity Checks
|
|
1331
|
+
|
|
1332
|
+
Extralite provides two methods for performing integrity checks,
|
|
1333
|
+
`Database#integrity_check` and `Database#quick_check`, which map to the
|
|
1334
|
+
corresponding pragmas. The two methods return an array of error messages. In the
|
|
1335
|
+
case of a normal, uncorrupted database, the methods will return `['ok']`:
|
|
1336
|
+
|
|
1337
|
+
```ruby
|
|
1338
|
+
db.integrity_check #=> ['ok'] (hopefully)
|
|
1339
|
+
```
|
|
1340
|
+
|
|
1316
1341
|
## Usage with Sequel
|
|
1317
1342
|
|
|
1318
1343
|
Extralite includes an adapter for
|
data/TODO.md
CHANGED
data/ext/extralite/common.c
CHANGED
|
@@ -226,6 +226,8 @@ static inline void finalize_stmt_ctx(stmt_ctx *ctx) {
|
|
|
226
226
|
return;
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
sqlite3_reset(*(ctx->stmtptr));
|
|
230
|
+
sqlite3_clear_bindings(*(ctx->stmtptr));
|
|
229
231
|
if (!(ctx->flags & STMT_CTX_F_CACHE_HIT))
|
|
230
232
|
rb_hash_aset(ctx->stmt_cache, ctx->sql, ULONG2NUM((uint64_t)*(ctx->stmtptr)));
|
|
231
233
|
}
|
|
@@ -241,11 +243,7 @@ void make_stmt_ctx(
|
|
|
241
243
|
|
|
242
244
|
int use_cache = (argc > 0) && (db->flags & DB_F_STMT_CACHE);
|
|
243
245
|
ctx->flags = use_cache ? STMT_CTX_F_USE_CACHE : 0;
|
|
244
|
-
if (use_cache)
|
|
245
|
-
lookup_cache_entry(ctx);
|
|
246
|
-
if (ctx->flags & STMT_CTX_F_CACHE_HIT)
|
|
247
|
-
sqlite3_clear_bindings(*(ctx->stmtptr));
|
|
248
|
-
}
|
|
246
|
+
if (use_cache) lookup_cache_entry(ctx);
|
|
249
247
|
|
|
250
248
|
if (!use_cache || !(*(ctx->stmtptr))) {
|
|
251
249
|
ctx->str = RSTRING_PTR(sql);
|
|
@@ -458,8 +456,12 @@ VALUE cleanup_stmt(query_ctx *ctx) {
|
|
|
458
456
|
|
|
459
457
|
if (!(ctx->flags & STMT_CTX_F_USE_CACHE))
|
|
460
458
|
sqlite3_finalize(ctx->stmt);
|
|
461
|
-
else
|
|
462
|
-
|
|
459
|
+
else {
|
|
460
|
+
sqlite3_reset(ctx->stmt);
|
|
461
|
+
sqlite3_clear_bindings(ctx->stmt);
|
|
462
|
+
if (!(ctx->flags & STMT_CTX_F_CACHE_HIT))
|
|
463
|
+
rb_hash_aset(ctx->db->stmt_cache, ctx->sql, ULONG2NUM((uint64_t)(ctx->stmt)));
|
|
464
|
+
}
|
|
463
465
|
done:
|
|
464
466
|
return Qnil;
|
|
465
467
|
}
|
data/ext/extralite/database.c
CHANGED
|
@@ -78,19 +78,18 @@ static int stmt_cache_iter(VALUE key, VALUE value, VALUE _param) {
|
|
|
78
78
|
return ST_CONTINUE;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
static void
|
|
81
|
+
static void stmt_cache_finalize(sqlite3 *db, VALUE stmt_cache) {
|
|
82
82
|
if (stmt_cache == Qnil) return;
|
|
83
83
|
if (rb_hash_size_num(stmt_cache) == 0) return;
|
|
84
84
|
|
|
85
85
|
rb_hash_foreach(stmt_cache, stmt_cache_iter, Qnil);
|
|
86
|
-
rb_hash_clear(stmt_cache);
|
|
87
86
|
RB_GC_GUARD(stmt_cache);
|
|
88
87
|
}
|
|
89
88
|
|
|
90
89
|
static void Database_free(void *ptr) {
|
|
91
90
|
Database_t *db = ptr;
|
|
92
91
|
if (db->sqlite3_db) {
|
|
93
|
-
|
|
92
|
+
stmt_cache_finalize(db->sqlite3_db, db->stmt_cache);
|
|
94
93
|
sqlite3_close_v2(db->sqlite3_db);
|
|
95
94
|
}
|
|
96
95
|
free(ptr);
|
|
@@ -279,7 +278,8 @@ VALUE Database_close(VALUE self) {
|
|
|
279
278
|
int rc;
|
|
280
279
|
Database_t *db = self_to_database(self);
|
|
281
280
|
|
|
282
|
-
|
|
281
|
+
stmt_cache_finalize(db->sqlite3_db, db->stmt_cache);
|
|
282
|
+
rb_hash_clear(db->stmt_cache);
|
|
283
283
|
rc = sqlite3_close_v2(db->sqlite3_db);
|
|
284
284
|
if (rc) {
|
|
285
285
|
rb_raise(cError, "%s", sqlite3_errmsg(db->sqlite3_db));
|
|
@@ -1565,11 +1565,11 @@ int checkpoint_mode_symbol_to_int(VALUE mode) {
|
|
|
1565
1565
|
*
|
|
1566
1566
|
* @overload wal_checkpoint(mode)
|
|
1567
1567
|
* @param mode [Symbol] checkpoint mode (`:passive`, `:full`, `:restart`, `:truncate`)
|
|
1568
|
-
* @return [Array<
|
|
1568
|
+
* @return [Array<Integer>] total and checkpointed frame count
|
|
1569
1569
|
* @overload wal_checkpoint(mode, db_name)
|
|
1570
1570
|
* @param mode [Symbol] checkpoint mode (`:passive`, `:full`, `:restart`, `:truncate`)
|
|
1571
1571
|
* @param db_name [String] attached database name
|
|
1572
|
-
* @return [Array<
|
|
1572
|
+
* @return [Array<Integer>] total and checkpointed frame count
|
|
1573
1573
|
*/
|
|
1574
1574
|
VALUE Database_wal_checkpoint(int argc, VALUE *argv, VALUE self) {
|
|
1575
1575
|
Database_t *db = self_to_open_database(self);
|
data/gemspec.rb
CHANGED
|
@@ -19,7 +19,7 @@ def common_spec(s)
|
|
|
19
19
|
|
|
20
20
|
s.add_development_dependency 'rake-compiler', '1.3.1'
|
|
21
21
|
s.add_development_dependency 'minitest'
|
|
22
|
-
s.add_development_dependency 'simplecov'
|
|
23
|
-
s.add_development_dependency 'yard'
|
|
24
|
-
s.add_development_dependency 'sequel', '5.
|
|
22
|
+
s.add_development_dependency 'simplecov'
|
|
23
|
+
s.add_development_dependency 'yard'
|
|
24
|
+
s.add_development_dependency 'sequel', '5.108.0'
|
|
25
25
|
end
|
data/lib/extralite/version.rb
CHANGED
data/lib/extralite.rb
CHANGED
|
@@ -215,6 +215,29 @@ module Extralite
|
|
|
215
215
|
value.is_a?(Hash) ? pragma_set(value) : pragma_get(value)
|
|
216
216
|
end
|
|
217
217
|
|
|
218
|
+
# Performs a low-level formatting and consistency check of the database
|
|
219
|
+
# using PRAGMA integrity_check. See:
|
|
220
|
+
# https://www.sqlite.org/pragma.html#pragma_integrity_check
|
|
221
|
+
#
|
|
222
|
+
# @param v [Integer, String, nil] max errors or table name
|
|
223
|
+
# @return [Array<String>] array of error strings or 'ok'
|
|
224
|
+
def integrity_check(v = nil)
|
|
225
|
+
query_splat(
|
|
226
|
+
v ? "pragma integrity_check(#{v})" : 'pragma integrity_check'
|
|
227
|
+
)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Performs a quicker integrity check of the database using PRAGMA
|
|
231
|
+
# quick_check. See: https://www.sqlite.org/pragma.html#pragma_quick_check
|
|
232
|
+
#
|
|
233
|
+
# @param v [Integer, String, nil] max errors or table name
|
|
234
|
+
# @return [Array<String>] array of error strings or 'ok'
|
|
235
|
+
def quick_check(v = nil)
|
|
236
|
+
query_splat(
|
|
237
|
+
v ? "pragma quick_check(#{v})" : 'pragma quick_check'
|
|
238
|
+
)
|
|
239
|
+
end
|
|
240
|
+
|
|
218
241
|
# Error class used to roll back a transaction without propagating an
|
|
219
242
|
# exception.
|
|
220
243
|
class Rollback < Error
|
|
@@ -237,12 +260,13 @@ module Extralite
|
|
|
237
260
|
def transaction(mode = :immediate)
|
|
238
261
|
abort = false
|
|
239
262
|
execute "begin #{mode} transaction"
|
|
263
|
+
started = true
|
|
240
264
|
yield self
|
|
241
265
|
rescue => e
|
|
242
266
|
abort = true
|
|
243
267
|
e.is_a?(Rollback) ? nil : raise
|
|
244
268
|
ensure
|
|
245
|
-
execute(abort ? 'rollback' : 'commit')
|
|
269
|
+
execute(abort ? 'rollback' : 'commit') if started
|
|
246
270
|
end
|
|
247
271
|
|
|
248
272
|
# Creates a savepoint with the given name. For more information on
|
data/test/test_database.rb
CHANGED
|
@@ -372,6 +372,86 @@ class DatabaseTest < Minitest::Test
|
|
|
372
372
|
# assert_equal 1, @db.pragma('foreign_keys')
|
|
373
373
|
end
|
|
374
374
|
|
|
375
|
+
def test_integrity_check_ok
|
|
376
|
+
r = @db.integrity_check
|
|
377
|
+
assert_equal ['ok'], r
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def test_integrity_check_bad_var
|
|
381
|
+
assert_raises(Extralite::Error) { @db.integrity_check('foobar') }
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def test_integrity_check_damaged
|
|
385
|
+
fn = Tempfile.new('extralite_test_integrity_check_damaged').path
|
|
386
|
+
db = Extralite::Database.new(fn)
|
|
387
|
+
|
|
388
|
+
db.execute <<~SQL
|
|
389
|
+
create table foo (x);
|
|
390
|
+
SQL
|
|
391
|
+
|
|
392
|
+
db.batch_execute("insert into foo values (?)", 1..100)
|
|
393
|
+
|
|
394
|
+
assert_equal (1..100).to_a, db.query_splat('select * from foo')
|
|
395
|
+
db.close
|
|
396
|
+
|
|
397
|
+
File.open(fn, 'r+b') do |f|
|
|
398
|
+
1.times do
|
|
399
|
+
f.seek(8015)
|
|
400
|
+
f.write('\x00' * 16)
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
db = Extralite::Database.new(fn, legacy: true)
|
|
405
|
+
assert_raises(Extralite::Error) { db.query_splat('select * from foo') }
|
|
406
|
+
r = db.integrity_check
|
|
407
|
+
assert_equal 2, r.size
|
|
408
|
+
assert_equal 'database disk image is malformed', r[1]
|
|
409
|
+
|
|
410
|
+
r = db.integrity_check(1)
|
|
411
|
+
assert_equal 1, r.size
|
|
412
|
+
assert_match /^\*\*\* in database main \*\*\*/, r[0]
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def test_quick_check_ok
|
|
416
|
+
r = @db.quick_check
|
|
417
|
+
assert_equal ['ok'], r
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def test_quick_check_bad_var
|
|
421
|
+
assert_raises(Extralite::Error) { @db.quick_check('foobar') }
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def test_quick_check_damaged
|
|
425
|
+
fn = Tempfile.new('extralite_test_quick_check_damaged').path
|
|
426
|
+
db = Extralite::Database.new(fn)
|
|
427
|
+
|
|
428
|
+
db.execute <<~SQL
|
|
429
|
+
create table foo (x);
|
|
430
|
+
SQL
|
|
431
|
+
|
|
432
|
+
db.batch_execute("insert into foo values (?)", 1..100)
|
|
433
|
+
|
|
434
|
+
assert_equal (1..100).to_a, db.query_splat('select * from foo')
|
|
435
|
+
db.close
|
|
436
|
+
|
|
437
|
+
File.open(fn, 'r+b') do |f|
|
|
438
|
+
1.times do
|
|
439
|
+
f.seek(8015)
|
|
440
|
+
f.write('\x00' * 16)
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
db = Extralite::Database.new(fn, legacy: true)
|
|
445
|
+
assert_raises(Extralite::Error) { db.query_splat('select * from foo') }
|
|
446
|
+
r = db.quick_check
|
|
447
|
+
assert_equal 2, r.size
|
|
448
|
+
assert_equal 'database disk image is malformed', r[1]
|
|
449
|
+
|
|
450
|
+
r = db.integrity_check(1)
|
|
451
|
+
assert_equal 1, r.size
|
|
452
|
+
assert_match /^\*\*\* in database main \*\*\*/, r[0]
|
|
453
|
+
end
|
|
454
|
+
|
|
375
455
|
def test_execute
|
|
376
456
|
changes = @db.execute('update t set x = 42')
|
|
377
457
|
assert_equal 2, changes
|
|
@@ -2244,7 +2324,7 @@ class StmtCacheText < Minitest::Test
|
|
|
2244
2324
|
end
|
|
2245
2325
|
|
|
2246
2326
|
def test_stmt_cache_gc
|
|
2247
|
-
fn = Tempfile.new('
|
|
2327
|
+
fn = Tempfile.new('extralite_test_stmt_cache_gc').path
|
|
2248
2328
|
|
|
2249
2329
|
db = Extralite::Database.new(fn)
|
|
2250
2330
|
db.query('create table t (x)')
|
|
@@ -2260,4 +2340,35 @@ class StmtCacheText < Minitest::Test
|
|
|
2260
2340
|
GC.start
|
|
2261
2341
|
refute File.file?("#{fn}-wal"), "WAL file should not exist after GC"
|
|
2262
2342
|
end
|
|
2343
|
+
|
|
2344
|
+
def test_stmt_cache_concurrent_access
|
|
2345
|
+
# https://github.com/digital-fabric/extralite/issues/88
|
|
2346
|
+
fn = Tempfile.new('extralite_test_stmt_cache_concurrent_access').path
|
|
2347
|
+
|
|
2348
|
+
a = Extralite::Database.new(fn)
|
|
2349
|
+
a.execute("pragma journal_mode=wal")
|
|
2350
|
+
a.execute("create table t (id integer primary key, x)")
|
|
2351
|
+
a.execute("insert into t (x) values (1)")
|
|
2352
|
+
a.query_single("select * from t where id = ?", 1)
|
|
2353
|
+
|
|
2354
|
+
b = Extralite::Database.new(fn)
|
|
2355
|
+
b.execute("insert into t (x) values (2)")
|
|
2356
|
+
|
|
2357
|
+
assert_equal 2, b.query_single_splat("select count(*) from t")
|
|
2358
|
+
assert_equal 2, a.query_single_splat("select count(*) from t")
|
|
2359
|
+
end
|
|
2360
|
+
|
|
2361
|
+
def test_stmt_cache_concurrent_transactions
|
|
2362
|
+
# https://github.com/digital-fabric/extralite/issues/89
|
|
2363
|
+
fn = Tempfile.new('extralite_test_stmt_cache_concurrent_access').path
|
|
2364
|
+
|
|
2365
|
+
a = Extralite::Database.new(fn)
|
|
2366
|
+
a.execute('create table t (x)')
|
|
2367
|
+
b = Extralite::Database.new(fn)
|
|
2368
|
+
b.execute('begin immediate transaction')
|
|
2369
|
+
|
|
2370
|
+
assert_raises(Extralite::BusyError) {
|
|
2371
|
+
a.transaction { a.execute("insert into t values (1)") }
|
|
2372
|
+
}
|
|
2373
|
+
end
|
|
2263
2374
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: extralite-bundle
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.1.
|
|
4
|
+
version: 3.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sharon Rosner
|
|
@@ -41,44 +41,44 @@ dependencies:
|
|
|
41
41
|
name: simplecov
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
|
-
- -
|
|
44
|
+
- - ">="
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: 0
|
|
46
|
+
version: '0'
|
|
47
47
|
type: :development
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
|
-
- -
|
|
51
|
+
- - ">="
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: 0
|
|
53
|
+
version: '0'
|
|
54
54
|
- !ruby/object:Gem::Dependency
|
|
55
55
|
name: yard
|
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
|
57
57
|
requirements:
|
|
58
|
-
- -
|
|
58
|
+
- - ">="
|
|
59
59
|
- !ruby/object:Gem::Version
|
|
60
|
-
version: 0
|
|
60
|
+
version: '0'
|
|
61
61
|
type: :development
|
|
62
62
|
prerelease: false
|
|
63
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
64
64
|
requirements:
|
|
65
|
-
- -
|
|
65
|
+
- - ">="
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
|
-
version: 0
|
|
67
|
+
version: '0'
|
|
68
68
|
- !ruby/object:Gem::Dependency
|
|
69
69
|
name: sequel
|
|
70
70
|
requirement: !ruby/object:Gem::Requirement
|
|
71
71
|
requirements:
|
|
72
72
|
- - '='
|
|
73
73
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: 5.
|
|
74
|
+
version: 5.108.0
|
|
75
75
|
type: :development
|
|
76
76
|
prerelease: false
|
|
77
77
|
version_requirements: !ruby/object:Gem::Requirement
|
|
78
78
|
requirements:
|
|
79
79
|
- - '='
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
|
-
version: 5.
|
|
81
|
+
version: 5.108.0
|
|
82
82
|
email: sharon@noteflakes.com
|
|
83
83
|
executables: []
|
|
84
84
|
extensions:
|