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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7fffde36ad772833bc816f85c26c6472d1c9e6c8e4ba91092734efeb24e2541a
4
- data.tar.gz: c4200d06ffece67d1241297567b37b3ddb6bc882d72dd6b4ba402a2611f4a1a1
3
+ metadata.gz: 9eac653308555c057d121db6b0e17905fff645a62d926bdd3ac07323f40f8f9e
4
+ data.tar.gz: 2bcfeb952ce57afa647b969016891edae0fd615fc78f97a0a6b0336cc35791d6
5
5
  SHA512:
6
- metadata.gz: 97fa8bbe57df8c8f679703764a7b6a185c235cf53c40738bf46f06bea82385fe1a7deed5d2990cb9837a1e1aaf858a5a4a88e5baccbb6117526dce8c3591fd32
7
- data.tar.gz: 07716c4ba47c25b2ee796ce287e7a87e051176cdbd79badb906ea86fe45fd7526b3cd0180f077cfae3ff6311a7e182b351c36d8696c10feb8ac50714f8dd1dd0
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
@@ -1,7 +1,3 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec name: 'extralite'
4
-
5
- group :development do
6
- gem "ruby_memcheck", "2.3.0" if Gem::Platform.local.os == "linux"
7
- end
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Sharon Rosner
3
+ Copyright (c) 2026 Sharon Rosner
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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
@@ -1,3 +1,9 @@
1
+ ## Remove Query objects
2
+
3
+ - Remove Query
4
+ - Cleanup code
5
+ - Update README
6
+
1
7
  ## Transforms
2
8
 
3
9
  - Add ability to exclude a column:
@@ -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 if (!(ctx->flags & STMT_CTX_F_CACHE_HIT))
462
- rb_hash_aset(ctx->db->stmt_cache, ctx->sql, ULONG2NUM((uint64_t)(ctx->stmt)));
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
  }
@@ -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 stmt_cache_clear(sqlite3 *db, VALUE stmt_cache) {
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
- stmt_cache_clear(db->sqlite3_db, db->stmt_cache);
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
- stmt_cache_clear(db->sqlite3_db, db->stmt_cache);
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<int>] total and checkpointed frame count
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<int>] total and checkpointed frame count
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', '0.22.0'
23
- s.add_development_dependency 'yard', '0.9.38'
24
- s.add_development_dependency 'sequel', '5.105.0'
22
+ s.add_development_dependency 'simplecov'
23
+ s.add_development_dependency 'yard'
24
+ s.add_development_dependency 'sequel', '5.108.0'
25
25
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Extralite
4
- VERSION = '3.1.1'
4
+ VERSION = '3.1.2'
5
5
  end
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
@@ -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('extralite_test_stmt_cache_close').path
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.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.22.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.22.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.9.38
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.9.38
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.105.0
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.105.0
81
+ version: 5.108.0
82
82
  email: sharon@noteflakes.com
83
83
  executables: []
84
84
  extensions: