extralite-bundle 3.1.1 → 3.1.3

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: 476ad3a137ab5292ac1e7a5b6053db39b8136141154e42c06502e0d723875680
4
+ data.tar.gz: d33bae3131575f702577d7e1960a54f64d4d2ac6d3903eb41b3f8fb9f896288f
5
5
  SHA512:
6
- metadata.gz: 97fa8bbe57df8c8f679703764a7b6a185c235cf53c40738bf46f06bea82385fe1a7deed5d2990cb9837a1e1aaf858a5a4a88e5baccbb6117526dce8c3591fd32
7
- data.tar.gz: 07716c4ba47c25b2ee796ce287e7a87e051176cdbd79badb906ea86fe45fd7526b3cd0180f077cfae3ff6311a7e182b351c36d8696c10feb8ac50714f8dd1dd0
6
+ metadata.gz: cbeb90a19c16e0355825de837ef6488614c1576df06ab2ba4dba1e6868568b839ee124d1713f4884156aa58ae3d7c8314e5675d90022fe3a08c6a54ec3448498
7
+ data.tar.gz: 885f3d695eec965ef271c3500eb07a56f9f06890b514f52d27d8f3a6e25c71c43b8034d730048b1a7f9db7e8f25ddebb5f69f1ddd2a9f53fe8adb926d323ad4a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ # 3.1.3 2026-10-03
2
+
3
+ - Fix database finalization of stmts (on free/close)
4
+ - Use stmt_cache for storing Query stmts
5
+
6
+ # 3.1.2 2026-10-03
7
+
8
+ - Fix handling of failed BEGIN in #transaction (#89)
9
+ - Reset of cached statements post-query (#88)
10
+ - Add missing wal checkpoint, integrity_check docs to README
11
+ - Implement `Database#integrity_check`, `Database#quick_check`
12
+
1
13
  # 3.1.1 2026-09-26
2
14
 
3
15
  - 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
  }
@@ -72,25 +72,19 @@ static void Database_compact(void *ptr) {
72
72
  db->progress_handler.proc = rb_gc_location(db->progress_handler.proc);
73
73
  }
74
74
 
75
- static int stmt_cache_iter(VALUE key, VALUE value, VALUE _param) {
76
- sqlite3_stmt *stmt = (sqlite3_stmt *)NUM2ULONG(value);
77
- sqlite3_finalize(stmt);
78
- return ST_CONTINUE;
79
- }
80
-
81
- static void stmt_cache_clear(sqlite3 *db, VALUE stmt_cache) {
82
- if (stmt_cache == Qnil) return;
83
- if (rb_hash_size_num(stmt_cache) == 0) return;
84
-
85
- rb_hash_foreach(stmt_cache, stmt_cache_iter, Qnil);
86
- rb_hash_clear(stmt_cache);
87
- RB_GC_GUARD(stmt_cache);
75
+ static inline void finalize_all_stmts(sqlite3 *db) {
76
+ sqlite3_stmt *stmt = sqlite3_next_stmt(db, NULL);
77
+ while (stmt) {
78
+ sqlite3_stmt *next = sqlite3_next_stmt(db, stmt);
79
+ sqlite3_finalize(stmt);
80
+ stmt = next;
81
+ }
88
82
  }
89
83
 
90
84
  static void Database_free(void *ptr) {
91
85
  Database_t *db = ptr;
92
86
  if (db->sqlite3_db) {
93
- stmt_cache_clear(db->sqlite3_db, db->stmt_cache);
87
+ finalize_all_stmts(db->sqlite3_db);
94
88
  sqlite3_close_v2(db->sqlite3_db);
95
89
  }
96
90
  free(ptr);
@@ -279,7 +273,8 @@ VALUE Database_close(VALUE self) {
279
273
  int rc;
280
274
  Database_t *db = self_to_database(self);
281
275
 
282
- stmt_cache_clear(db->sqlite3_db, db->stmt_cache);
276
+ finalize_all_stmts(db->sqlite3_db);
277
+ rb_hash_clear(db->stmt_cache);
283
278
  rc = sqlite3_close_v2(db->sqlite3_db);
284
279
  if (rc) {
285
280
  rb_raise(cError, "%s", sqlite3_errmsg(db->sqlite3_db));
@@ -1565,11 +1560,11 @@ int checkpoint_mode_symbol_to_int(VALUE mode) {
1565
1560
  *
1566
1561
  * @overload wal_checkpoint(mode)
1567
1562
  * @param mode [Symbol] checkpoint mode (`:passive`, `:full`, `:restart`, `:truncate`)
1568
- * @return [Array<int>] total and checkpointed frame count
1563
+ * @return [Array<Integer>] total and checkpointed frame count
1569
1564
  * @overload wal_checkpoint(mode, db_name)
1570
1565
  * @param mode [Symbol] checkpoint mode (`:passive`, `:full`, `:restart`, `:truncate`)
1571
1566
  * @param db_name [String] attached database name
1572
- * @return [Array<int>] total and checkpointed frame count
1567
+ * @return [Array<Integer>] total and checkpointed frame count
1573
1568
  */
1574
1569
  VALUE Database_wal_checkpoint(int argc, VALUE *argv, VALUE self) {
1575
1570
  Database_t *db = self_to_open_database(self);
@@ -40,8 +40,7 @@ static void Query_compact(void *ptr) {
40
40
  }
41
41
 
42
42
  static void Query_free(void *ptr) {
43
- Query_t *query = ptr;
44
- if (query->stmt) sqlite3_finalize(query->stmt);
43
+ // Query_t *query = ptr;
45
44
  free(ptr);
46
45
  }
47
46
 
@@ -70,7 +69,7 @@ static inline Query_t *self_to_query(VALUE obj) {
70
69
  // verifies that the query is not closed
71
70
  static inline Query_t *self_to_query_verify(VALUE obj) {
72
71
  Query_t *query = self_to_query(obj);
73
- if (query->closed) rb_raise(cError, "Query is closed");
72
+ if (query->closed || !(query->db_struct->sqlite3_db)) rb_raise(cError, "Query is closed");
74
73
  return query;
75
74
  }
76
75
 
@@ -133,8 +132,10 @@ VALUE Query_initialize(VALUE self, VALUE db, VALUE sql, VALUE mode) {
133
132
 
134
133
  static inline void prep_query(Query_t *query) {
135
134
  stmt_ctx stmt_ctx;
136
- make_stmt_ctx(&stmt_ctx, query->db_struct, &(query->stmt), query->sql, 0, NULL);
135
+ make_stmt_ctx(&stmt_ctx, query->db_struct, &(query->stmt), query->sql, 1, NULL);
137
136
  prep_single_stmt(&stmt_ctx);
137
+ if (!(stmt_ctx.flags & STMT_CTX_F_CACHE_HIT))
138
+ rb_hash_aset(query->db_struct->stmt_cache, query->sql, ULONG2NUM((uint64_t)(query->stmt)));
138
139
  }
139
140
 
140
141
  static inline void query_reset(Query_t *query) {
@@ -531,7 +532,6 @@ VALUE Query_clone(VALUE self) {
531
532
  VALUE Query_close(VALUE self) {
532
533
  Query_t *query = self_to_query(self);
533
534
  if (query->stmt) {
534
- sqlite3_finalize(query->stmt);
535
535
  query->stmt = NULL;
536
536
  }
537
537
  query->closed = 1;
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.3'
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
data/test/test_query.rb CHANGED
@@ -933,7 +933,7 @@ class QueryTest < Minitest::Test
933
933
  def test_query_after_db_close
934
934
  assert_equal [{ x: 4, y: 5, z: 6}], @query.bind(4).to_a
935
935
  @db.close
936
- assert_equal [{ x: 4, y: 5, z: 6}], @query.bind(4).to_a
936
+ assert_raises(Extralite::Error) { @query.bind(4).to_a }
937
937
  end
938
938
 
939
939
  def test_query_eof
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.3
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: