extralite-bundle 3.0.1 → 3.1.1

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: 4f1b8eb33bd51ba18483e487841c0fbf82afc0b72b7cdefbd14ea73dc47b71ef
4
- data.tar.gz: a3d0b76ccaa51ecd9fa3bf1d6da99bb35282eac272457fe4d115f2d395b6b991
3
+ metadata.gz: 7fffde36ad772833bc816f85c26c6472d1c9e6c8e4ba91092734efeb24e2541a
4
+ data.tar.gz: c4200d06ffece67d1241297567b37b3ddb6bc882d72dd6b4ba402a2611f4a1a1
5
5
  SHA512:
6
- metadata.gz: ef666c54decf0410bf8ae2394a777fe0de00b08ed45c6ea0f267c1e208294ca1b1223be919b97c36f19a767b417fc501a75e9c30738c366bb1a5ccd40b954411
7
- data.tar.gz: 4fb11e51cbb6a5a1992b0764694cb0efaf168435aad18957234646db30409bf9b3cc49e4b0092d2cf872c8eb5b581ea5bb3fd00253cdefd1db44dfeb1067581a
6
+ metadata.gz: 97fa8bbe57df8c8f679703764a7b6a185c235cf53c40738bf46f06bea82385fe1a7deed5d2990cb9837a1e1aaf858a5a4a88e5baccbb6117526dce8c3591fd32
7
+ data.tar.gz: 07716c4ba47c25b2ee796ce287e7a87e051176cdbd79badb906ea86fe45fd7526b3cd0180f077cfae3ff6311a7e182b351c36d8696c10feb8ac50714f8dd1dd0
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ # 3.1.1 2026-09-26
2
+
3
+ - Fix clearing of stmt cache after `DB#close` or after GC (#87)
4
+
5
+ # 3.1.0 2026-09-22
6
+
7
+ - Update bundled SQLite to 3.53.4
8
+ - Add `stmt_cache` option to `Database#initialize`
9
+ - Implement automatic stmt caching for parametric queries
10
+ - Change handling of multi stmt query and execute invocations.
11
+ - For #execute, accept multiple statements but raise error if parameters are given
12
+ - For #query, raise error on multiple statements
13
+
1
14
  ## 3.0.1 2026-07-21
2
15
 
3
16
  - Fix passing nil transform to `DB#prepare`
data/README.md CHANGED
@@ -32,7 +32,7 @@ databases.
32
32
  Extralite comes in two flavors: the `extralite` gem which uses the
33
33
  system-installed sqlite3 library, and the `extralite-bundle` gem which bundles
34
34
  the latest version of SQLite
35
- ([3.53.3](https://sqlite.org/releaselog/3_53_3.html)), offering access to the
35
+ ([3.53.4](https://sqlite.org/releaselog/3_53_4.html)), offering access to the
36
36
  latest features and enhancements.
37
37
 
38
38
  ## Features
@@ -49,6 +49,7 @@ latest features and enhancements.
49
49
  allowing iterating through single records or batches of records.
50
50
  - [Prepared queries](#prepared-queries).
51
51
  - [Parameter binding](#parameter-binding).
52
+ - [Automatic query caching](#automatic-query-caching) for parametric queries.
52
53
  - [Batch execution](#batch-execution-of-queries) of queries.
53
54
  - [transactions and savepoints](#transactions-and-savepoints).
54
55
  - Advanced features: load [SQLite extensions](#loading-extensions), create
@@ -69,6 +70,7 @@ latest features and enhancements.
69
70
  - [Data Types](#data-types)
70
71
  - [Prepared Queries](#prepared-queries)
71
72
  - [Batch Execution of Queries](#batch-execution-of-queries)
73
+ - [Automatic query caching](#automatic-query-caching)
72
74
  - [Transactions and Savepoints](#transactions-and-savepoints)
73
75
  - [Database Information](#database-information)
74
76
  - [Error Handling](#error-handling)
@@ -361,6 +363,16 @@ To use the transform, pass it along with the SQL string to `Database#query`:
361
363
  db.qurey(transform, sql) #=> [...]
362
364
  ```
363
365
 
366
+ Structured transforms support the following column types:
367
+
368
+ - `integer`
369
+ - `float`
370
+ - `text`
371
+ - `bool` - represented as an integer value of 0/1.
372
+ - `json` - represented as a string (text).
373
+ - A custom proc/lambda - e.g. `->(v) { v.split(',') }`.
374
+ - `auto` - uses the native database data type.
375
+
364
376
  Transforms can also be used with [prepared
365
377
  queries](#transforms-in-prepared-queries).
366
378
 
@@ -653,6 +665,13 @@ query.batch_query([[42, 3], [43, 6]])
653
665
  #=> [{ x: 42, y: 2, z: 3 }, { x: 43, y: 5, z: 6 }]
654
666
  ```
655
667
 
668
+ ## Automatic query caching
669
+
670
+ Extralite implements automatic caching of parametric queries. This means that
671
+ any time you run one of the `execute` or `query_xxx` methods with parameters,
672
+ Extralite will only prepare the query once, and the underlying `sqlite_stmt`
673
+ object will be cached for reuse.
674
+
656
675
  ## Transactions and Savepoints
657
676
 
658
677
  All reads and writes to SQLite databases occur within a
data/Rakefile CHANGED
@@ -28,7 +28,7 @@ end
28
28
  task :release do
29
29
  require_relative './lib/extralite/version'
30
30
  version = Extralite::VERSION
31
-
31
+
32
32
  puts 'Building extralite...'
33
33
  `gem build extralite.gemspec`
34
34
 
@@ -66,4 +66,4 @@ begin
66
66
  end
67
67
  rescue LoadError => e
68
68
  warn("NOTE: ruby_memcheck is not available in this environment: #{e}")
69
- end
69
+ end
data/TODO.md CHANGED
@@ -28,7 +28,7 @@ end
28
28
  ]
29
29
  ```
30
30
 
31
- - Can we get rid of the hash container?
31
+ - Use single value instead of hash
32
32
 
33
33
  ```ruby
34
34
  Extralite::Transform.new do
@@ -38,7 +38,7 @@ Extralite::Transform.new do
38
38
  content: text,
39
39
  _tag_id: skip,
40
40
  tags: [
41
- name: text
41
+ { name: text.unbox }
42
42
  ]
43
43
  }
44
44
  end
@@ -62,7 +62,7 @@ class PubSub
62
62
  create table if not exists messages(
63
63
  subscriber_id integer,
64
64
  topic text,
65
- message text,
65
+ message text,
66
66
  foreign key (subscriber_id, topic)
67
67
  references subscriber_topics(subscriber_id, topic)
68
68
  on delete cascade
@@ -157,11 +157,6 @@ static inline void column_names_set(struct column_names *names, int idx, VALUE v
157
157
  rb_ary_push(names->array, value);
158
158
  }
159
159
 
160
- static inline VALUE column_names_get(struct column_names *names, int idx) {
161
- return (names->count <= MAX_EMBEDDED_COLUMN_NAMES) ?
162
- names->names[idx] : RARRAY_AREF(names->array, idx);
163
- }
164
-
165
160
  static inline struct column_names get_column_names(sqlite3_stmt *stmt, int column_count) {
166
161
  struct column_names names;
167
162
  column_names_setup(&names, column_count);
@@ -213,108 +208,205 @@ static inline void row_to_splat_values(sqlite3_stmt *stmt, int column_count, VAL
213
208
  }
214
209
  }
215
210
 
216
- typedef struct {
217
- sqlite3 *db;
218
- sqlite3_stmt **stmt;
219
- const char *str;
220
- long len;
221
- int rc;
222
- } prepare_stmt_ctx;
211
+ static inline void lookup_cache_entry(stmt_ctx *ctx) {
212
+ VALUE cached = rb_hash_aref(ctx->stmt_cache, ctx->sql);
213
+ *(ctx->stmtptr) = NIL_P(cached) ? NULL : (sqlite3_stmt *)NUM2ULONG(cached);
214
+ if (*(ctx->stmtptr)) {
215
+ sqlite3_reset(*(ctx->stmtptr));
216
+ ctx->flags |= STMT_CTX_F_CACHE_HIT;
217
+ }
218
+ }
219
+
220
+ static inline void finalize_stmt_ctx(stmt_ctx *ctx) {
221
+ if (!*(ctx->stmtptr)) return;
222
+
223
+ if (!(ctx->flags & STMT_CTX_F_USE_CACHE)) {
224
+ sqlite3_finalize(*(ctx->stmtptr));
225
+ *(ctx->stmtptr) = NULL;
226
+ return;
227
+ }
228
+
229
+ if (!(ctx->flags & STMT_CTX_F_CACHE_HIT))
230
+ rb_hash_aset(ctx->stmt_cache, ctx->sql, ULONG2NUM((uint64_t)*(ctx->stmtptr)));
231
+ }
232
+
233
+ void make_stmt_ctx(
234
+ stmt_ctx *ctx, Database_t *db, sqlite3_stmt **stmt, VALUE sql, int argc, VALUE *argv
235
+ ) {
236
+ ctx->stmt_cache = db->stmt_cache;
237
+ ctx->sql = sql;
238
+
239
+ ctx->db = db->sqlite3_db;
240
+ ctx->stmtptr = stmt;
241
+
242
+ int use_cache = (argc > 0) && (db->flags & DB_F_STMT_CACHE);
243
+ 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
+ }
249
+
250
+ if (!use_cache || !(*(ctx->stmtptr))) {
251
+ ctx->str = RSTRING_PTR(sql);
252
+ ctx->len = RSTRING_LEN(sql);
253
+ }
254
+
255
+ ctx->gvl_mode = db->gvl_release_threshold < 0 ? GVL_HOLD : GVL_RELEASE;
256
+ ctx->rc = 0;
257
+ ctx->total_changes = 0;
258
+ ctx->argc = argc;
259
+ ctx->argv = argv;
260
+ }
261
+
262
+ static inline int exec_stmt_iterate(sqlite3_stmt *stmt) {
263
+ while (true) {
264
+ int rc = sqlite3_step(stmt);
265
+ switch (rc) {
266
+ case SQLITE_ROW: continue;
267
+ case SQLITE_DONE: return 0;
268
+ default: return rc;
269
+ }
270
+ }
271
+ }
272
+
273
+ static inline void finalize_stmt(sqlite3_stmt **stmt) {
274
+ if (*stmt) {
275
+ sqlite3_finalize(*stmt);
276
+ *stmt = NULL;
277
+ }
278
+ }
279
+
280
+ static inline void *exec_bind_parameters(void *ptr) {
281
+ stmt_ctx *ctx = (stmt_ctx *)ptr;
282
+ bind_all_parameters(*(ctx->stmtptr), ctx->argc, ctx->argv);
283
+ return NULL;
284
+ }
285
+
286
+ void *exec_multi_stmt_impl(void *ptr) {
287
+ stmt_ctx *ctx = (stmt_ctx *)ptr;
288
+
289
+ if (ctx->flags & STMT_CTX_F_CACHE_HIT) {
290
+ rb_thread_call_with_gvl(exec_bind_parameters, ctx);
291
+ ctx->rc = exec_stmt_iterate(*(ctx->stmtptr));
292
+ if (ctx->rc == SQLITE_OK)
293
+ ctx->total_changes += sqlite3_changes(ctx->db);
294
+ else
295
+ ctx->total_changes = 0;
296
+ finalize_stmt_ctx(ctx);
297
+ return NULL;
298
+ }
223
299
 
224
- void *prepare_multi_stmt_impl(void *ptr) {
225
- prepare_stmt_ctx *ctx = (prepare_stmt_ctx *)ptr;
226
300
  const char *rest = NULL;
227
301
  const char *str = ctx->str;
228
302
  const char *end = ctx->str + ctx->len;
303
+ sqlite3_stmt *next_stmt = NULL;
304
+ ctx->total_changes = 0;
229
305
  while (1) {
230
- ctx->rc = sqlite3_prepare_v2(ctx->db, str, end - str, ctx->stmt, &rest);
231
- if (ctx->rc) {
232
- // error
233
- sqlite3_finalize(*ctx->stmt);
234
- return NULL;
306
+ if (next_stmt) {
307
+ *(ctx->stmtptr) = next_stmt;
308
+ next_stmt = NULL;
309
+ ctx->rc = SQLITE_OK;
310
+ }
311
+ else
312
+ ctx->rc = sqlite3_prepare_v2(ctx->db, str, end - str, ctx->stmtptr, &rest);
313
+
314
+ if ((ctx->rc != SQLITE_OK) || !(*(ctx->stmtptr))) goto done;
315
+ if (ctx->argc) {
316
+ // parameters were provided - check if str contains multiple statements
317
+ if (rest != end) {
318
+ int res = sqlite3_prepare_v2(ctx->db, rest, end-rest, &next_stmt, NULL);
319
+ if (next_stmt) res = SQLITE_MISUSE;
320
+ if (res != SQLITE_OK) {
321
+ ctx->flags &= ~STMT_CTX_F_USE_CACHE;
322
+ ctx->rc = res;
323
+ goto done;
324
+ }
325
+ }
326
+ rb_thread_call_with_gvl(exec_bind_parameters, ctx);
235
327
  }
236
328
 
237
- if (rest == end) return NULL;
329
+ ctx->rc = exec_stmt_iterate(*(ctx->stmtptr));
330
+ if (ctx->rc != SQLITE_OK) goto done;
238
331
 
239
- // perform current query, but discard its results
240
- ctx->rc = sqlite3_step(*ctx->stmt);
241
- sqlite3_finalize(*ctx->stmt);
242
- switch (ctx->rc) {
243
- case SQLITE_BUSY:
244
- case SQLITE_ERROR:
245
- case SQLITE_MISUSE:
246
- return NULL;
247
- }
332
+ ctx->total_changes += sqlite3_changes(ctx->db);
333
+ finalize_stmt_ctx(ctx);
334
+
335
+ if (rest == end) return NULL;
248
336
  str = rest;
249
337
  }
338
+ done:
339
+ finalize_stmt(&next_stmt);
340
+ finalize_stmt_ctx(ctx);
250
341
  return NULL;
251
342
  }
252
343
 
344
+ inline int raise_error(stmt_ctx *ctx) {
345
+ switch (ctx->rc) {
346
+ case SQLITE_BUSY:
347
+ rb_raise(cBusyError, "Database is busy");
348
+ case SQLITE_ERROR:
349
+ rb_raise(cSQLError, "%s", sqlite3_errmsg(ctx->db));
350
+ case SQLITE_MISUSE:
351
+ rb_raise(cError, "Multiple statements cannot take parameters");
352
+ default:
353
+ rb_raise(cError, "%s", sqlite3_errmsg(ctx->db));
354
+ }
355
+ return 0;
356
+ }
357
+
253
358
  /*
254
359
  This function prepares a statement from an SQL string containing one or more SQL
255
360
  statements. It will release the GVL while the statements are being prepared and
256
361
  executed. All statements excluding the last one are executed. The last statement
257
362
  is not executed, but instead handed back to the caller for looping over results.
363
+
364
+ @return [int] total changes
258
365
  */
259
- void prepare_multi_stmt(enum gvl_mode mode, sqlite3 *db, sqlite3_stmt **stmt, VALUE sql) {
260
- prepare_stmt_ctx ctx = {db, stmt, RSTRING_PTR(sql), RSTRING_LEN(sql), 0};
261
- gvl_call(mode, prepare_multi_stmt_impl, (void *)&ctx);
262
- RB_GC_GUARD(sql);
366
+ int exec_multi_stmt(stmt_ctx *ctx) {
367
+ gvl_call(ctx->gvl_mode, exec_multi_stmt_impl, (void *)ctx);
368
+ if (ctx->rc == SQLITE_OK) return ctx->total_changes;
263
369
 
264
- switch (ctx.rc) {
265
- case 0:
266
- return;
267
- case SQLITE_BUSY:
268
- rb_raise(cBusyError, "Database is busy");
269
- case SQLITE_ERROR:
270
- rb_raise(cSQLError, "%s", sqlite3_errmsg(db));
271
- default:
272
- rb_raise(cError, "%s", sqlite3_errmsg(db));
273
- }
370
+ if (*(ctx->stmtptr)) sqlite3_finalize(*(ctx->stmtptr));
371
+
372
+ return raise_error(ctx);
274
373
  }
275
374
 
276
- #define SQLITE_MULTI_STMT -1
375
+ ////////////////////////////////////////////////////////////////////////////////
376
+
377
+ void *prep_single_stmt_impl(void *ptr) {
378
+ stmt_ctx *ctx = (stmt_ctx *)ptr;
379
+
380
+ if (ctx->flags & STMT_CTX_F_CACHE_HIT) return NULL;
277
381
 
278
- void *prepare_single_stmt_impl(void *ptr) {
279
- prepare_stmt_ctx *ctx = (prepare_stmt_ctx *)ptr;
280
382
  const char *rest = NULL;
281
383
  const char *str = ctx->str;
282
384
  const char *end = ctx->str + ctx->len;
283
385
 
284
- ctx->rc = sqlite3_prepare_v2(ctx->db, str, end - str, ctx->stmt, &rest);
285
- if (ctx->rc)
286
- goto discard_stmt;
287
- else if (rest != end) {
288
- ctx->rc = SQLITE_MULTI_STMT;
289
- goto discard_stmt;
386
+ ctx->rc = sqlite3_prepare_v2(ctx->db, str, end - str, ctx->stmtptr, &rest);
387
+ if (ctx->rc != SQLITE_OK) {
388
+ finalize_stmt(ctx->stmtptr);
389
+ return NULL;
290
390
  }
291
- goto end;
292
- discard_stmt:
293
- if (*ctx->stmt) {
294
- sqlite3_finalize(*ctx->stmt);
295
- *ctx->stmt = NULL;
391
+ if (rest != end) {
392
+ sqlite3_stmt *next = NULL;
393
+ ctx->rc = sqlite3_prepare_v2(ctx->db, rest, end - rest, &next, NULL);
394
+ if (next) {
395
+ sqlite3_finalize(next);
396
+ ctx->rc = SQLITE_MISUSE;
397
+ }
398
+ if (ctx->rc != SQLITE_OK) finalize_stmt(ctx->stmtptr);
296
399
  }
297
- end:
298
400
  return NULL;
299
401
  }
300
402
 
301
- void prepare_single_stmt(enum gvl_mode mode, sqlite3 *db, sqlite3_stmt **stmt, VALUE sql) {
302
- prepare_stmt_ctx ctx = {db, stmt, RSTRING_PTR(sql), RSTRING_LEN(sql), 0};
303
- gvl_call(mode, prepare_single_stmt_impl, (void *)&ctx);
304
- RB_GC_GUARD(sql);
403
+ void prep_single_stmt(stmt_ctx *ctx) {
404
+ gvl_call(ctx->gvl_mode, prep_single_stmt_impl, (void *)ctx);
405
+ if (ctx->rc == SQLITE_OK) return;
305
406
 
306
- switch (ctx.rc) {
307
- case 0:
308
- return;
309
- case SQLITE_BUSY:
310
- rb_raise(cBusyError, "Database is busy");
311
- case SQLITE_ERROR:
312
- rb_raise(cSQLError, "%s", sqlite3_errmsg(db));
313
- case SQLITE_MULTI_STMT:
314
- rb_raise(cError, "A prepared statement does not accept SQL strings with multiple queries");
315
- default:
316
- rb_raise(cError, "%s", sqlite3_errmsg(db));
317
- }
407
+ if (*(ctx->stmtptr)) sqlite3_finalize(*(ctx->stmtptr));
408
+
409
+ raise_error(ctx);
318
410
  }
319
411
 
320
412
  struct step_ctx {
@@ -362,7 +454,13 @@ inline int stmt_iterate(query_ctx *ctx) {
362
454
  }
363
455
 
364
456
  VALUE cleanup_stmt(query_ctx *ctx) {
365
- if (ctx->stmt) sqlite3_finalize(ctx->stmt);
457
+ if (!ctx->stmt) goto done;
458
+
459
+ if (!(ctx->flags & STMT_CTX_F_USE_CACHE))
460
+ 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)));
463
+ done:
366
464
  return Qnil;
367
465
  }
368
466
 
@@ -503,8 +601,6 @@ VALUE run_transform_no_identity(
503
601
  static inline VALUE run_transform(
504
602
  VALUE identity_storage, struct transform_node *node, sqlite3_stmt *stmt
505
603
  ) {
506
- // fprintf(stdout, "transform_container: %p flags: %02x identity_idx: %d\n", node, node->flags, node->identity_idx);
507
- // if (node->flags & TRANSFORM_F_NAME) INSPECT(" name", node->name);
508
604
  if (node->identity_node) {
509
605
  return run_transform_with_identity(identity_storage, node, stmt);
510
606
  }
@@ -517,7 +613,7 @@ VALUE safe_query_transform(query_ctx *ctx) {
517
613
  VALUE array = rb_ary_new();
518
614
  VALUE identity_storage = rb_hash_new();
519
615
  VALUE row = Qnil;
520
- // int column_count = sqlite3_column_count(ctx->stmt);
616
+ // int column_count = sqlite3_column_count(ctx->stmtptr);
521
617
  struct transform_node *transform_root = get_transform_root(ctx->transform);
522
618
 
523
619
  int row_count = 0;
@@ -542,17 +638,17 @@ done:
542
638
  return rb_ary_entry(array, 0);
543
639
  }
544
640
 
545
- return Qnil;
546
641
  RB_GC_GUARD(identity_storage);
547
642
  RB_GC_GUARD(row);
548
643
  RB_GC_GUARD(array);
644
+ return Qnil;
549
645
  }
550
646
 
551
647
  VALUE safe_query_single_row_transform(query_ctx *ctx) {
552
648
  VALUE array = rb_ary_new();
553
649
  VALUE identity_storage = rb_hash_new();
554
650
  VALUE row = Qnil;
555
- // int column_count = sqlite3_column_count(ctx->stmt);
651
+ // int column_count = sqlite3_column_count(ctx->stmtptr);
556
652
  struct transform_node *transform_root = get_transform_root(ctx->transform);
557
653
 
558
654
  int row_count = 0;
@@ -572,10 +668,10 @@ VALUE safe_query_single_row_transform(query_ctx *ctx) {
572
668
  return row;
573
669
  }
574
670
 
575
- return Qnil;
576
671
  RB_GC_GUARD(identity_storage);
577
672
  RB_GC_GUARD(row);
578
673
  RB_GC_GUARD(array);
674
+ return Qnil;
579
675
  }
580
676
 
581
677
  VALUE safe_query_splat(query_ctx *ctx);
@@ -1008,3 +1104,7 @@ VALUE safe_query_changes(query_ctx *ctx) {
1008
1104
  while (stmt_iterate(ctx));
1009
1105
  return INT2FIX(sqlite3_changes(ctx->sqlite3_db));
1010
1106
  }
1107
+
1108
+ VALUE safe_total_changes(query_ctx *ctx) {
1109
+ return INT2FIX(ctx->total_changes);
1110
+ }