extralite-bundle 3.0.1 → 3.1.0

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: c98b87050195caee2a3ac6bbbefdd3bf77ed3fe29add59cf18d5162299856d93
4
+ data.tar.gz: b0796d7fba1df64d3095d29ff6d8f00bb6f66320467cf54d0ab6f31a164c7c59
5
5
  SHA512:
6
- metadata.gz: ef666c54decf0410bf8ae2394a777fe0de00b08ed45c6ea0f267c1e208294ca1b1223be919b97c36f19a767b417fc501a75e9c30738c366bb1a5ccd40b954411
7
- data.tar.gz: 4fb11e51cbb6a5a1992b0764694cb0efaf168435aad18957234646db30409bf9b3cc49e4b0092d2cf872c8eb5b581ea5bb3fd00253cdefd1db44dfeb1067581a
6
+ metadata.gz: 34f204b0d4abf91ac7b2f114c2e6838515498a068e750c476c7c473a3823e33f871088974b204aed8e98192c1bcfbdec2f8d5f2df1bff770a523724e662a79b7
7
+ data.tar.gz: c6b48bf9540dd37a663ab21db610f6aeee0193a3cf539b5cff8ba600475b15cc91c9e8679f7c0a897af0b227b39f597aa8d2da4b378c6a5c2d8aaf64d6a6dc49
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ # 3.1.0 2026-09-22
2
+
3
+ - Update bundled SQLite to 3.53.4
4
+ - Add `stmt_cache` option to `Database#initialize`
5
+ - Implement automatic stmt caching for parametric queries
6
+ - Change handling of multi stmt query and execute invocations.
7
+ - For #execute, accept multiple statements but raise error if parameters are given
8
+ - For #query, raise error on multiple statements
9
+
1
10
  ## 3.0.1 2026-07-21
2
11
 
3
12
  - 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] for queries with parameters.
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)
@@ -653,6 +655,13 @@ query.batch_query([[42, 3], [43, 6]])
653
655
  #=> [{ x: 42, y: 2, z: 3 }, { x: 43, y: 5, z: 6 }]
654
656
  ```
655
657
 
658
+ ## Automatic query caching
659
+
660
+ Extralite implements automatic caching of parametric queries. This means that
661
+ any time you run one of the `execute` or `query_xxx` methods with parameters,
662
+ Extralite will only prepare the query once, and the underlying `sqlite_stmt`
663
+ object will be cached for reuse.
664
+
656
665
  ## Transactions and Savepoints
657
666
 
658
667
  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,202 @@ 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->rc = res;
322
+ goto done;
323
+ }
324
+ }
325
+ rb_thread_call_with_gvl(exec_bind_parameters, ctx);
235
326
  }
236
327
 
237
- if (rest == end) return NULL;
328
+ ctx->rc = exec_stmt_iterate(*(ctx->stmtptr));
329
+ if (ctx->rc != SQLITE_OK) goto done;
238
330
 
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
- }
331
+ ctx->total_changes += sqlite3_changes(ctx->db);
332
+ finalize_stmt_ctx(ctx);
333
+
334
+ if (rest == end) return NULL;
248
335
  str = rest;
249
336
  }
337
+ done:
338
+ finalize_stmt(&next_stmt);
339
+ finalize_stmt_ctx(ctx);
250
340
  return NULL;
251
341
  }
252
342
 
343
+ inline int raise_error(stmt_ctx *ctx) {
344
+ switch (ctx->rc) {
345
+ case SQLITE_BUSY:
346
+ rb_raise(cBusyError, "Database is busy");
347
+ case SQLITE_ERROR:
348
+ rb_raise(cSQLError, "%s", sqlite3_errmsg(ctx->db));
349
+ case SQLITE_MISUSE:
350
+ rb_raise(cError, "Multiple statements cannot take parameters");
351
+ default:
352
+ rb_raise(cError, "%s", sqlite3_errmsg(ctx->db));
353
+ }
354
+ return 0;
355
+ }
356
+
253
357
  /*
254
358
  This function prepares a statement from an SQL string containing one or more SQL
255
359
  statements. It will release the GVL while the statements are being prepared and
256
360
  executed. All statements excluding the last one are executed. The last statement
257
361
  is not executed, but instead handed back to the caller for looping over results.
362
+
363
+ @return [int] total changes
258
364
  */
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);
365
+ int exec_multi_stmt(stmt_ctx *ctx) {
366
+ gvl_call(ctx->gvl_mode, exec_multi_stmt_impl, (void *)ctx);
367
+ if (ctx->rc == SQLITE_OK) return ctx->total_changes;
263
368
 
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
- }
369
+ if (*(ctx->stmtptr)) sqlite3_finalize(*(ctx->stmtptr));
370
+ return raise_error(ctx);
274
371
  }
275
372
 
276
- #define SQLITE_MULTI_STMT -1
373
+ ////////////////////////////////////////////////////////////////////////////////
374
+
375
+ void *prep_single_stmt_impl(void *ptr) {
376
+ stmt_ctx *ctx = (stmt_ctx *)ptr;
377
+
378
+ if (ctx->flags & STMT_CTX_F_CACHE_HIT) return NULL;
277
379
 
278
- void *prepare_single_stmt_impl(void *ptr) {
279
- prepare_stmt_ctx *ctx = (prepare_stmt_ctx *)ptr;
280
380
  const char *rest = NULL;
281
381
  const char *str = ctx->str;
282
382
  const char *end = ctx->str + ctx->len;
283
383
 
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;
384
+ ctx->rc = sqlite3_prepare_v2(ctx->db, str, end - str, ctx->stmtptr, &rest);
385
+ if (ctx->rc != SQLITE_OK) {
386
+ finalize_stmt(ctx->stmtptr);
387
+ return NULL;
290
388
  }
291
- goto end;
292
- discard_stmt:
293
- if (*ctx->stmt) {
294
- sqlite3_finalize(*ctx->stmt);
295
- *ctx->stmt = NULL;
389
+ if (rest != end) {
390
+ sqlite3_stmt *next = NULL;
391
+ ctx->rc = sqlite3_prepare_v2(ctx->db, rest, end - rest, &next, NULL);
392
+ if (next) {
393
+ sqlite3_finalize(next);
394
+ ctx->rc = SQLITE_MISUSE;
395
+ }
396
+ if (ctx->rc != SQLITE_OK) finalize_stmt(ctx->stmtptr);
296
397
  }
297
- end:
298
398
  return NULL;
299
399
  }
300
400
 
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);
401
+ void prep_single_stmt(stmt_ctx *ctx) {
402
+ gvl_call(ctx->gvl_mode, prep_single_stmt_impl, (void *)ctx);
403
+ if (ctx->rc == SQLITE_OK) return;
305
404
 
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
- }
405
+ if (*(ctx->stmtptr)) sqlite3_finalize(*(ctx->stmtptr));
406
+ raise_error(ctx);
318
407
  }
319
408
 
320
409
  struct step_ctx {
@@ -362,7 +451,15 @@ inline int stmt_iterate(query_ctx *ctx) {
362
451
  }
363
452
 
364
453
  VALUE cleanup_stmt(query_ctx *ctx) {
365
- if (ctx->stmt) sqlite3_finalize(ctx->stmt);
454
+ if (!ctx->stmt) goto done;
455
+
456
+ if (ctx->flags & STMT_CTX_F_USE_CACHE) {
457
+ if (!(ctx->flags & STMT_CTX_F_CACHE_HIT))
458
+ rb_hash_aset(ctx->db->stmt_cache, ctx->sql, ULONG2NUM((uint64_t)(ctx->stmt)));
459
+ }
460
+ else
461
+ sqlite3_finalize(ctx->stmt);
462
+ done:
366
463
  return Qnil;
367
464
  }
368
465
 
@@ -517,7 +614,7 @@ VALUE safe_query_transform(query_ctx *ctx) {
517
614
  VALUE array = rb_ary_new();
518
615
  VALUE identity_storage = rb_hash_new();
519
616
  VALUE row = Qnil;
520
- // int column_count = sqlite3_column_count(ctx->stmt);
617
+ // int column_count = sqlite3_column_count(ctx->stmtptr);
521
618
  struct transform_node *transform_root = get_transform_root(ctx->transform);
522
619
 
523
620
  int row_count = 0;
@@ -542,17 +639,17 @@ done:
542
639
  return rb_ary_entry(array, 0);
543
640
  }
544
641
 
545
- return Qnil;
546
642
  RB_GC_GUARD(identity_storage);
547
643
  RB_GC_GUARD(row);
548
644
  RB_GC_GUARD(array);
645
+ return Qnil;
549
646
  }
550
647
 
551
648
  VALUE safe_query_single_row_transform(query_ctx *ctx) {
552
649
  VALUE array = rb_ary_new();
553
650
  VALUE identity_storage = rb_hash_new();
554
651
  VALUE row = Qnil;
555
- // int column_count = sqlite3_column_count(ctx->stmt);
652
+ // int column_count = sqlite3_column_count(ctx->stmtptr);
556
653
  struct transform_node *transform_root = get_transform_root(ctx->transform);
557
654
 
558
655
  int row_count = 0;
@@ -572,10 +669,10 @@ VALUE safe_query_single_row_transform(query_ctx *ctx) {
572
669
  return row;
573
670
  }
574
671
 
575
- return Qnil;
576
672
  RB_GC_GUARD(identity_storage);
577
673
  RB_GC_GUARD(row);
578
674
  RB_GC_GUARD(array);
675
+ return Qnil;
579
676
  }
580
677
 
581
678
  VALUE safe_query_splat(query_ctx *ctx);
@@ -1008,3 +1105,7 @@ VALUE safe_query_changes(query_ctx *ctx) {
1008
1105
  while (stmt_iterate(ctx));
1009
1106
  return INT2FIX(sqlite3_changes(ctx->sqlite3_db));
1010
1107
  }
1108
+
1109
+ VALUE safe_total_changes(query_ctx *ctx) {
1110
+ return INT2FIX(ctx->total_changes);
1111
+ }