extralite 1.18 → 1.20

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: a40752d79cd5d28d8c9367c973f86b7e6a80d05e3878b5c70a6a98e0a70f3838
4
- data.tar.gz: d72e040c85a2e34a07ea513fd7b28cc558a0b8c71a8b4f211b21aa3a9f492387
3
+ metadata.gz: f9dba60a33188d5c3c7cfb09c7c746f64bcd723d53a20da4b110037ca395a50b
4
+ data.tar.gz: 2b5d8de6e19fe38800fbd30b2465ef6788cf50ac73f79be7e07d8d1f2165ad8b
5
5
  SHA512:
6
- metadata.gz: 4bc536fb4a5590fbae92abb1f99189f57812f5440addb49964908fcb6c45f1bfea575fc2421bfd4ddddcaaf7d00e9285ea3db4db9162afe4df71c27349dd1342
7
- data.tar.gz: a1a158d144c3f97f3bd281ba050419d42c95566408cb168f35f3a0fe8d328705f662fbc1c9ac22e788a44f1120ff90db7e237e8ad1f2505b29a643c05589b331
6
+ metadata.gz: 4723dd3278f61a7dc95aa48260a5d974f3da7383463f538f3e0d647f1591f6759f239e633b6d3efe4e17d81c7841f0670c2847d318b1fdd78e7f1365fe509aad
7
+ data.tar.gz: 5fface9ad5152b0a11bb259267a1db2d6a214a02ca3a6ad0207f707e6de7aa674a28180fd6e109d55745bb3453d17b4e3244aaeba89bf75b40ec472dda1789bc
@@ -15,7 +15,7 @@ jobs:
15
15
 
16
16
  runs-on: ${{matrix.os}}
17
17
  steps:
18
- - uses: actions/checkout@v1
18
+ - uses: actions/checkout@v3
19
19
  - uses: ruby/setup-ruby@v1
20
20
  with:
21
21
  ruby-version: ${{matrix.ruby}}
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ # 1.20 2023-01-21
2
+
3
+ - Fix compilation error (#15 @sitano)
4
+ - Add status methods `Extralite.runtime_status`, `Database#status`, `PreparedStatement#status` (#14 @sitano)
5
+ - Add `Database#interrupt` (#13 @sitano)
6
+ - Add `Database#backup` (#11 @sitano)
7
+ - Derive `Extralite::Error` from `StandardError` (#10 @sitano)
8
+
9
+ ## 1.19 2022-12-01
10
+
11
+ - Add `Database#execute_multi`
12
+ - Add `PreparedStatement#execute_multi`
13
+
1
14
  ## 1.18 2022-12-01
2
15
 
3
16
  - Fix usage with system sqlite3 lib where `load_extension` is disabled
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- extralite (1.18)
4
+ extralite (1.20)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -51,9 +51,14 @@ latest features and enhancements.
51
51
  queries (handy for creating/modifying schemas).
52
52
  - Get last insert rowid.
53
53
  - Get number of rows changed by last query.
54
+ - Execute the same query with multiple parameter lists (useful for inserting records).
54
55
  - Load extensions (loading of extensions is autmatically enabled. You can find
55
56
  some useful extensions here: https://github.com/nalgeon/sqlean.)
56
57
  - Includes a [Sequel adapter](#usage-with-sequel).
58
+ - Other features:
59
+ - Backup databases
60
+ - Interrupt long-running queries (from another thread)
61
+ - Get runtime status, database status and prepared statement status values.
57
62
 
58
63
  ## Installation
59
64
 
@@ -123,6 +128,10 @@ db.query('select * from foo where bar = :bar', bar: 42)
123
128
  db.query('select * from foo where bar = :bar', 'bar' => 42)
124
129
  db.query('select * from foo where bar = :bar', ':bar' => 42)
125
130
 
131
+ # insert multiple rows
132
+ db.execute_multi('insert into foo values (?)', ['bar', 'baz'])
133
+ db.execute_multi('insert into foo values (?, ?)', [[1, 2], [3, 4]])
134
+
126
135
  # prepared statements
127
136
  stmt = db.prepare('select ? as foo, ? as bar') #=> Extralite::PreparedStatement
128
137
  stmt.query(1, 2) #=> [{ :foo => 1, :bar => 2 }]
@@ -81,6 +81,16 @@ void bind_all_parameters(sqlite3_stmt *stmt, int argc, VALUE *argv) {
81
81
  }
82
82
  }
83
83
 
84
+ void bind_all_parameters_from_object(sqlite3_stmt *stmt, VALUE obj) {
85
+ if (TYPE(obj) == T_ARRAY) {
86
+ int count = RARRAY_LEN(obj);
87
+ for (int i = 0; i < count; i++)
88
+ bind_parameter_value(stmt, i + 1, RARRAY_AREF(obj, i));
89
+ }
90
+ else
91
+ bind_parameter_value(stmt, 1, obj);
92
+ }
93
+
84
94
  static inline VALUE get_column_names(sqlite3_stmt *stmt, int column_count) {
85
95
  VALUE arr = rb_ary_new2(column_count);
86
96
  for (int i = 0; i < column_count; i++) {
@@ -223,7 +233,7 @@ void *stmt_iterate_without_gvl(void *ptr) {
223
233
  return NULL;
224
234
  }
225
235
 
226
- static inline int stmt_iterate(sqlite3_stmt *stmt, sqlite3 *db) {
236
+ int stmt_iterate(sqlite3_stmt *stmt, sqlite3 *db) {
227
237
  struct step_ctx ctx = {stmt, 0};
228
238
  rb_thread_call_without_gvl(stmt_iterate_without_gvl, (void *)&ctx, RUBY_UBF_IO, 0);
229
239
  switch (ctx.rc) {
@@ -233,6 +243,8 @@ static inline int stmt_iterate(sqlite3_stmt *stmt, sqlite3 *db) {
233
243
  return 0;
234
244
  case SQLITE_BUSY:
235
245
  rb_raise(cBusyError, "Database is busy");
246
+ case SQLITE_INTERRUPT:
247
+ rb_raise(cInterruptError, "Query was interrupted");
236
248
  case SQLITE_ERROR:
237
249
  rb_raise(cSQLError, "%s", sqlite3_errmsg(db));
238
250
  default:
@@ -262,7 +274,8 @@ VALUE safe_query_hash(query_ctx *ctx) {
262
274
 
263
275
  while (stmt_iterate(ctx->stmt, ctx->sqlite3_db)) {
264
276
  row = row_to_hash(ctx->stmt, column_count, column_names);
265
- if (yield_to_block) rb_yield(row); else rb_ary_push(result, row);
277
+ if (yield_to_block) rb_yield(row);
278
+ else rb_ary_push(result, row);
266
279
  }
267
280
 
268
281
  RB_GC_GUARD(column_names);
@@ -284,7 +297,8 @@ VALUE safe_query_ary(query_ctx *ctx) {
284
297
 
285
298
  while (stmt_iterate(ctx->stmt, ctx->sqlite3_db)) {
286
299
  row = row_to_ary(ctx->stmt, column_count);
287
- if (yield_to_block) rb_yield(row); else rb_ary_push(result, row);
300
+ if (yield_to_block) rb_yield(row);
301
+ else rb_ary_push(result, row);
288
302
  }
289
303
 
290
304
  RB_GC_GUARD(row);
@@ -346,6 +360,22 @@ VALUE safe_query_single_value(query_ctx *ctx) {
346
360
  return value;
347
361
  }
348
362
 
363
+ VALUE safe_execute_multi(query_ctx *ctx) {
364
+ int count = RARRAY_LEN(ctx->params);
365
+ int changes = 0;
366
+
367
+ for (int i = 0; i < count; i++) {
368
+ sqlite3_reset(ctx->stmt);
369
+ sqlite3_clear_bindings(ctx->stmt);
370
+ bind_all_parameters_from_object(ctx->stmt, RARRAY_AREF(ctx->params, i));
371
+
372
+ while (stmt_iterate(ctx->stmt, ctx->sqlite3_db));
373
+ changes += sqlite3_changes(ctx->sqlite3_db);
374
+ }
375
+
376
+ return INT2FIX(changes);
377
+ }
378
+
349
379
  VALUE safe_query_columns(query_ctx *ctx) {
350
380
  return get_column_names(ctx->stmt, sqlite3_column_count(ctx->stmt));
351
381
  }
@@ -5,6 +5,7 @@ VALUE cDatabase;
5
5
  VALUE cError;
6
6
  VALUE cSQLError;
7
7
  VALUE cBusyError;
8
+ VALUE cInterruptError;
8
9
 
9
10
  ID ID_KEYS;
10
11
  ID ID_NEW;
@@ -265,6 +266,34 @@ VALUE Database_query_single_value(int argc, VALUE *argv, VALUE self) {
265
266
  return Database_perform_query(argc, argv, self, safe_query_single_value);
266
267
  }
267
268
 
269
+ /* call-seq:
270
+ * db.execute_multi(sql, params_array) -> changes
271
+ *
272
+ * Executes the given query for each list of parameters in params_array. Returns
273
+ * the number of changes effected. This method is designed for inserting
274
+ * multiple records.
275
+ *
276
+ * records = [
277
+ * [1, 2, 3],
278
+ * [4, 5, 6]
279
+ * ]
280
+ * db.execute_multi_query('insert into foo values (?, ?, ?)', records)
281
+ *
282
+ */
283
+ VALUE Database_execute_multi(VALUE self, VALUE sql, VALUE params_array) {
284
+ Database_t *db;
285
+ sqlite3_stmt *stmt;
286
+
287
+ if (RSTRING_LEN(sql) == 0) return Qnil;
288
+
289
+ // prepare query ctx
290
+ GetOpenDatabase(self, db);
291
+ prepare_single_stmt(db->sqlite3_db, &stmt, sql);
292
+ query_ctx ctx = { self, db->sqlite3_db, stmt, params_array };
293
+
294
+ return rb_ensure(SAFE(safe_execute_multi), (VALUE)&ctx, SAFE(cleanup_stmt), (VALUE)&ctx);
295
+ }
296
+
268
297
  /* call-seq:
269
298
  * db.columns(sql) -> columns
270
299
  *
@@ -358,42 +387,213 @@ VALUE Database_prepare(VALUE self, VALUE sql) {
358
387
  return rb_funcall(cPreparedStatement, ID_NEW, 2, self, sql);
359
388
  }
360
389
 
361
- void Init_ExtraliteDatabase() {
390
+ /* call-seq:
391
+ * db.interrupt -> db
392
+ *
393
+ * Interrupts a long running query. This method is to be called from a different
394
+ * thread than the one running the query. Upon calling `#interrupt` the running
395
+ * query will stop and raise an `Extralite::InterruptError` exception.
396
+ *
397
+ * It is not safe to call `#interrupt` on a database that is about to be closed.
398
+ * For more information, consult the [sqlite3 API docs](https://sqlite.org/c3ref/interrupt.html).
399
+ */
400
+ VALUE Database_interrupt(VALUE self) {
401
+ Database_t *db;
402
+ GetOpenDatabase(self, db);
403
+
404
+ sqlite3_interrupt(db->sqlite3_db);
405
+ return self;
406
+ }
407
+
408
+ typedef struct {
409
+ sqlite3 *dst;
410
+ int close_dst_on_cleanup;
411
+ sqlite3_backup *backup;
412
+ int block_given;
413
+ int rc;
414
+ } backup_ctx;
415
+
416
+ #define BACKUP_STEP_MAX_PAGES 16
417
+ #define BACKUP_SLEEP_MS 100
418
+
419
+ void *backup_step_without_gvl(void *ptr) {
420
+ backup_ctx *ctx = (backup_ctx *)ptr;
421
+ ctx->rc = sqlite3_backup_step(ctx->backup, BACKUP_STEP_MAX_PAGES);
422
+ return NULL;
423
+ }
424
+
425
+ void *backup_sleep_without_gvl(void *unused) {
426
+ sqlite3_sleep(BACKUP_SLEEP_MS);
427
+ return NULL;
428
+ }
429
+
430
+ VALUE backup_safe_iterate(VALUE ptr) {
431
+ backup_ctx *ctx = (backup_ctx *)ptr;
432
+ int done = 0;
433
+
434
+ while (!done) {
435
+ rb_thread_call_without_gvl(backup_step_without_gvl, (void *)ctx, RUBY_UBF_IO, 0);
436
+ switch(ctx->rc) {
437
+ case SQLITE_DONE:
438
+ if (ctx->block_given) {
439
+ VALUE total = INT2FIX(sqlite3_backup_pagecount(ctx->backup));
440
+ rb_yield_values(2, total, total);
441
+ }
442
+ done = 1;
443
+ continue;
444
+ case SQLITE_OK:
445
+ if (ctx->block_given) {
446
+ VALUE remaining = INT2FIX(sqlite3_backup_remaining(ctx->backup));
447
+ VALUE total = INT2FIX(sqlite3_backup_pagecount(ctx->backup));
448
+ rb_yield_values(2, remaining, total);
449
+ }
450
+ continue;
451
+ case SQLITE_BUSY:
452
+ case SQLITE_LOCKED:
453
+ rb_thread_call_without_gvl(backup_sleep_without_gvl, NULL, RUBY_UBF_IO, 0);
454
+ continue;
455
+ default:
456
+ rb_raise(cError, "%s", sqlite3_errstr(ctx->rc));
457
+ }
458
+ };
459
+
460
+ return Qnil;
461
+ }
462
+
463
+ VALUE backup_cleanup(VALUE ptr) {
464
+ backup_ctx *ctx = (backup_ctx *)ptr;
465
+
466
+ sqlite3_backup_finish(ctx->backup);
467
+
468
+ if (ctx->close_dst_on_cleanup)
469
+ sqlite3_close(ctx->dst);
470
+ return Qnil;
471
+ }
472
+
473
+ VALUE Database_backup(int argc, VALUE *argv, VALUE self) {
474
+ VALUE dst;
475
+ VALUE src_name;
476
+ VALUE dst_name;
477
+ rb_scan_args(argc, argv, "12", &dst, &src_name, &dst_name);
478
+ if (src_name == Qnil) src_name = rb_str_new_literal("main");
479
+ if (dst_name == Qnil) dst_name = rb_str_new_literal("main");
480
+
481
+ int dst_is_fn = TYPE(dst) == T_STRING;
482
+
483
+ Database_t *src;
484
+ GetOpenDatabase(self, src);
485
+ sqlite3 *dst_db;
486
+
487
+ if (dst_is_fn) {
488
+ int rc = sqlite3_open(StringValueCStr(dst), &dst_db);
489
+ if (rc) {
490
+ sqlite3_close(dst_db);
491
+ rb_raise(cError, "%s", sqlite3_errmsg(dst_db));
492
+ }
493
+ }
494
+ else {
495
+ Database_t *dst_struct;
496
+ GetOpenDatabase(dst, dst_struct);
497
+ dst_db = dst_struct->sqlite3_db;
498
+ }
499
+
500
+ // TODO: add possibility to use different src and dest db names (main, tmp, or
501
+ // attached db's).
502
+ sqlite3_backup *backup;
503
+ backup = sqlite3_backup_init(dst_db, StringValueCStr(dst_name), src->sqlite3_db, StringValueCStr(src_name));
504
+ if (!backup) {
505
+ if (dst_is_fn)
506
+ sqlite3_close(dst_db);
507
+ rb_raise(cError, "%s", sqlite3_errmsg(dst_db));
508
+ }
509
+
510
+ backup_ctx ctx = { dst_db, dst_is_fn, backup, rb_block_given_p(), 0 };
511
+ rb_ensure(SAFE(backup_safe_iterate), (VALUE)&ctx, SAFE(backup_cleanup), (VALUE)&ctx);
512
+
513
+ return self;
514
+ }
515
+
516
+ /*
517
+ * Extralite.runtime_status(op[, reset]) -> [value, highwatermark]
518
+ *
519
+ * Returns runtime status values for the given op as an array containing the
520
+ * current value and the high water mark value. To reset the high water mark,
521
+ * pass true as reset.
522
+ */
523
+ VALUE Extralite_runtime_status(int argc, VALUE* argv, VALUE self) {
524
+ VALUE op, reset;
525
+ sqlite3_int64 cur, hwm;
526
+
527
+ rb_scan_args(argc, argv, "11", &op, &reset);
528
+
529
+ int rc = sqlite3_status64(NUM2INT(op), &cur, &hwm, RTEST(reset) ? 1 : 0);
530
+ if (rc != SQLITE_OK) rb_raise(cError, "%s", sqlite3_errstr(rc));
531
+
532
+ return rb_ary_new3(2, LONG2FIX(cur), LONG2FIX(hwm));
533
+ }
534
+
535
+ /* call-seq:
536
+ * db.status(op[, reset]) -> [value, highwatermark]
537
+ *
538
+ * Returns database status values for the given op as an array containing the
539
+ * current value and the high water mark value. To reset the high water mark,
540
+ * pass true as reset.
541
+ */
542
+ VALUE Database_status(int argc, VALUE* argv, VALUE self) {
543
+ VALUE op, reset;
544
+ int cur, hwm;
545
+
546
+ rb_scan_args(argc, argv, "11", &op, &reset);
547
+
548
+ Database_t *db;
549
+ GetOpenDatabase(self, db);
550
+
551
+ int rc = sqlite3_db_status(db->sqlite3_db, NUM2INT(op), &cur, &hwm, RTEST(reset) ? 1 : 0);
552
+ if (rc != SQLITE_OK) rb_raise(cError, "%s", sqlite3_errstr(rc));
553
+
554
+ return rb_ary_new3(2, INT2NUM(cur), INT2NUM(hwm));
555
+ }
556
+
557
+ void Init_ExtraliteDatabase(void) {
362
558
  VALUE mExtralite = rb_define_module("Extralite");
559
+ rb_define_singleton_method(mExtralite, "runtime_status", Extralite_runtime_status, -1);
363
560
  rb_define_singleton_method(mExtralite, "sqlite3_version", Extralite_sqlite3_version, 0);
364
561
 
365
562
  cDatabase = rb_define_class_under(mExtralite, "Database", rb_cObject);
366
563
  rb_define_alloc_func(cDatabase, Database_allocate);
367
564
 
368
- rb_define_method(cDatabase, "initialize", Database_initialize, 1);
565
+ rb_define_method(cDatabase, "backup", Database_backup, -1);
566
+ rb_define_method(cDatabase, "changes", Database_changes, 0);
369
567
  rb_define_method(cDatabase, "close", Database_close, 0);
370
568
  rb_define_method(cDatabase, "closed?", Database_closed_p, 0);
371
-
569
+ rb_define_method(cDatabase, "columns", Database_columns, 1);
570
+ rb_define_method(cDatabase, "execute_multi", Database_execute_multi, 2);
571
+ rb_define_method(cDatabase, "filename", Database_filename, -1);
572
+ rb_define_method(cDatabase, "initialize", Database_initialize, 1);
573
+ rb_define_method(cDatabase, "interrupt", Database_interrupt, 0);
574
+ rb_define_method(cDatabase, "last_insert_rowid", Database_last_insert_rowid, 0);
575
+ rb_define_method(cDatabase, "prepare", Database_prepare, 1);
372
576
  rb_define_method(cDatabase, "query", Database_query_hash, -1);
373
- rb_define_method(cDatabase, "query_hash", Database_query_hash, -1);
374
577
  rb_define_method(cDatabase, "query_ary", Database_query_ary, -1);
375
- rb_define_method(cDatabase, "query_single_row", Database_query_single_row, -1);
578
+ rb_define_method(cDatabase, "query_hash", Database_query_hash, -1);
376
579
  rb_define_method(cDatabase, "query_single_column", Database_query_single_column, -1);
580
+ rb_define_method(cDatabase, "query_single_row", Database_query_single_row, -1);
377
581
  rb_define_method(cDatabase, "query_single_value", Database_query_single_value, -1);
378
- rb_define_method(cDatabase, "columns", Database_columns, 1);
379
-
380
- rb_define_method(cDatabase, "last_insert_rowid", Database_last_insert_rowid, 0);
381
- rb_define_method(cDatabase, "changes", Database_changes, 0);
382
- rb_define_method(cDatabase, "filename", Database_filename, -1);
582
+ rb_define_method(cDatabase, "status", Database_status, -1);
383
583
  rb_define_method(cDatabase, "transaction_active?", Database_transaction_active_p, 0);
384
584
 
385
585
  #ifdef HAVE_SQLITE3_LOAD_EXTENSION
386
586
  rb_define_method(cDatabase, "load_extension", Database_load_extension, 1);
387
587
  #endif
388
588
 
389
- rb_define_method(cDatabase, "prepare", Database_prepare, 1);
390
-
391
- cError = rb_define_class_under(mExtralite, "Error", rb_eRuntimeError);
589
+ cError = rb_define_class_under(mExtralite, "Error", rb_eStandardError);
392
590
  cSQLError = rb_define_class_under(mExtralite, "SQLError", cError);
393
591
  cBusyError = rb_define_class_under(mExtralite, "BusyError", cError);
592
+ cInterruptError = rb_define_class_under(mExtralite, "InterruptError", cError);
394
593
  rb_gc_register_mark_object(cError);
395
594
  rb_gc_register_mark_object(cSQLError);
396
595
  rb_gc_register_mark_object(cBusyError);
596
+ rb_gc_register_mark_object(cInterruptError);
397
597
 
398
598
  ID_KEYS = rb_intern("keys");
399
599
  ID_NEW = rb_intern("new");
@@ -25,6 +25,7 @@ extern VALUE cPreparedStatement;
25
25
  extern VALUE cError;
26
26
  extern VALUE cSQLError;
27
27
  extern VALUE cBusyError;
28
+ extern VALUE cInterruptError;
28
29
 
29
30
  extern ID ID_KEYS;
30
31
  extern ID ID_NEW;
@@ -46,18 +47,28 @@ typedef struct {
46
47
  VALUE self;
47
48
  sqlite3 *sqlite3_db;
48
49
  sqlite3_stmt *stmt;
50
+ VALUE params;
49
51
  } query_ctx;
50
52
 
53
+ typedef struct {
54
+ VALUE dst;
55
+ VALUE src;
56
+ sqlite3_backup *p;
57
+ } backup_t;
58
+
51
59
  VALUE safe_query_ary(query_ctx *ctx);
52
60
  VALUE safe_query_hash(query_ctx *ctx);
53
61
  VALUE safe_query_single_column(query_ctx *ctx);
54
62
  VALUE safe_query_single_row(query_ctx *ctx);
55
63
  VALUE safe_query_single_value(query_ctx *ctx);
64
+ VALUE safe_execute_multi(query_ctx *ctx);
56
65
  VALUE safe_query_columns(query_ctx *ctx);
57
66
 
58
67
  void prepare_single_stmt(sqlite3 *db, sqlite3_stmt **stmt, VALUE sql);
59
68
  void prepare_multi_stmt(sqlite3 *db, sqlite3_stmt **stmt, VALUE sql);
60
69
  void bind_all_parameters(sqlite3_stmt *stmt, int argc, VALUE *argv);
70
+ void bind_all_parameters_from_object(sqlite3_stmt *stmt, VALUE obj);
71
+ int stmt_iterate(sqlite3_stmt *stmt, sqlite3 *db);
61
72
  VALUE cleanup_stmt(query_ctx *ctx);
62
73
 
63
74
  sqlite3 *Database_sqlite3_db(VALUE self);
@@ -1,7 +1,7 @@
1
1
  void Init_ExtraliteDatabase();
2
2
  void Init_ExtralitePreparedStatement();
3
3
 
4
- void Init_extralite_ext() {
4
+ void Init_extralite_ext(void) {
5
5
  Init_ExtraliteDatabase();
6
6
  Init_ExtralitePreparedStatement();
7
7
  }
@@ -197,6 +197,32 @@ VALUE PreparedStatement_query_single_value(int argc, VALUE *argv, VALUE self) {
197
197
  return PreparedStatement_perform_query(argc, argv, self, safe_query_single_value);
198
198
  }
199
199
 
200
+ /* call-seq:
201
+ * stmt.execute_multi(params_array) -> changes
202
+ *
203
+ * Executes the prepared statment for each list of parameters in params_array.
204
+ * Returns the number of changes effected. This method is designed for inserting
205
+ * multiple records.
206
+ *
207
+ * stmt = db.prepare('insert into foo values (?, ?, ?)')
208
+ * records = [
209
+ * [1, 2, 3],
210
+ * [4, 5, 6]
211
+ * ]
212
+ * stmt.execute_multi_query(records)
213
+ *
214
+ */
215
+ VALUE PreparedStatement_execute_multi(VALUE self, VALUE params_array) {
216
+ PreparedStatement_t *stmt;
217
+ GetPreparedStatement(self, stmt);
218
+
219
+ if (!stmt->stmt)
220
+ rb_raise(cError, "Prepared statement is closed");
221
+
222
+ query_ctx ctx = { self, stmt->sqlite3_db, stmt->stmt, params_array };
223
+ return safe_execute_multi(&ctx);
224
+ }
225
+
200
226
  /* call-seq:
201
227
  * stmt.database -> database
202
228
  * stmt.db -> database
@@ -257,26 +283,43 @@ VALUE PreparedStatement_closed_p(VALUE self) {
257
283
  return stmt->stmt ? Qfalse : Qtrue;
258
284
  }
259
285
 
260
- void Init_ExtralitePreparedStatement() {
286
+ /* call-seq:
287
+ * stmt.status(op[, reset]) -> value
288
+ *
289
+ * Returns the current status value for the given op. To reset the value, pass
290
+ * true as reset.
291
+ */
292
+ VALUE PreparedStatement_status(int argc, VALUE* argv, VALUE self) {
293
+ VALUE op, reset;
294
+
295
+ rb_scan_args(argc, argv, "11", &op, &reset);
296
+
297
+ PreparedStatement_t *stmt;
298
+ GetPreparedStatement(self, stmt);
299
+
300
+ int value = sqlite3_stmt_status(stmt->stmt, NUM2INT(op), RTEST(reset) ? 1 : 0);
301
+ return INT2NUM(value);
302
+ }
303
+
304
+ void Init_ExtralitePreparedStatement(void) {
261
305
  VALUE mExtralite = rb_define_module("Extralite");
262
306
 
263
307
  cPreparedStatement = rb_define_class_under(mExtralite, "PreparedStatement", rb_cObject);
264
308
  rb_define_alloc_func(cPreparedStatement, PreparedStatement_allocate);
265
309
 
266
- rb_define_method(cPreparedStatement, "initialize", PreparedStatement_initialize, 2);
310
+ rb_define_method(cPreparedStatement, "close", PreparedStatement_close, 0);
311
+ rb_define_method(cPreparedStatement, "closed?", PreparedStatement_closed_p, 0);
312
+ rb_define_method(cPreparedStatement, "columns", PreparedStatement_columns, 0);
267
313
  rb_define_method(cPreparedStatement, "database", PreparedStatement_database, 0);
268
314
  rb_define_method(cPreparedStatement, "db", PreparedStatement_database, 0);
269
- rb_define_method(cPreparedStatement, "sql", PreparedStatement_sql, 0);
270
-
315
+ rb_define_method(cPreparedStatement, "execute_multi", PreparedStatement_execute_multi, 1);
316
+ rb_define_method(cPreparedStatement, "initialize", PreparedStatement_initialize, 2);
271
317
  rb_define_method(cPreparedStatement, "query", PreparedStatement_query_hash, -1);
272
318
  rb_define_method(cPreparedStatement, "query_hash", PreparedStatement_query_hash, -1);
273
319
  rb_define_method(cPreparedStatement, "query_ary", PreparedStatement_query_ary, -1);
274
320
  rb_define_method(cPreparedStatement, "query_single_row", PreparedStatement_query_single_row, -1);
275
321
  rb_define_method(cPreparedStatement, "query_single_column", PreparedStatement_query_single_column, -1);
276
322
  rb_define_method(cPreparedStatement, "query_single_value", PreparedStatement_query_single_value, -1);
277
-
278
- rb_define_method(cPreparedStatement, "columns", PreparedStatement_columns, 0);
279
-
280
- rb_define_method(cPreparedStatement, "close", PreparedStatement_close, 0);
281
- rb_define_method(cPreparedStatement, "closed?", PreparedStatement_closed_p, 0);
323
+ rb_define_method(cPreparedStatement, "sql", PreparedStatement_sql, 0);
324
+ rb_define_method(cPreparedStatement, "status", PreparedStatement_status, -1);
282
325
  }
@@ -1,3 +1,3 @@
1
1
  module Extralite
2
- VERSION = '1.18'
2
+ VERSION = '1.20'
3
3
  end
data/lib/extralite.rb CHANGED
@@ -2,8 +2,48 @@ require_relative './extralite_ext'
2
2
 
3
3
  # Extralite is a Ruby gem for working with SQLite databases
4
4
  module Extralite
5
+
6
+ SQLITE_STATUS_MEMORY_USED = 0
7
+ SQLITE_STATUS_PAGECACHE_USED = 1
8
+ SQLITE_STATUS_PAGECACHE_OVERFLOW = 2
9
+ SQLITE_STATUS_SCRATCH_USED = 3 # NOT USED
10
+ SQLITE_STATUS_SCRATCH_OVERFLOW = 4 # NOT USED
11
+ SQLITE_STATUS_MALLOC_SIZE = 5
12
+ SQLITE_STATUS_PARSER_STACK = 6
13
+ SQLITE_STATUS_PAGECACHE_SIZE = 7
14
+ SQLITE_STATUS_SCRATCH_SIZE = 8 # NOT USED
15
+ SQLITE_STATUS_MALLOC_COUNT = 9
16
+
17
+ SQLITE_DBSTATUS_LOOKASIDE_USED = 0
18
+ SQLITE_DBSTATUS_CACHE_USED = 1
19
+ SQLITE_DBSTATUS_SCHEMA_USED = 2
20
+ SQLITE_DBSTATUS_STMT_USED = 3
21
+ SQLITE_DBSTATUS_LOOKASIDE_HIT = 4
22
+ SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE = 5
23
+ SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL = 6
24
+ SQLITE_DBSTATUS_CACHE_HIT = 7
25
+ SQLITE_DBSTATUS_CACHE_MISS = 8
26
+ SQLITE_DBSTATUS_CACHE_WRITE = 9
27
+ SQLITE_DBSTATUS_DEFERRED_FKS = 10
28
+ SQLITE_DBSTATUS_CACHE_USED_SHARED = 11
29
+ SQLITE_DBSTATUS_CACHE_SPILL = 12
30
+
31
+ SQLITE_STMTSTATUS_FULLSCAN_STEP = 1
32
+ SQLITE_STMTSTATUS_SORT = 2
33
+ SQLITE_STMTSTATUS_AUTOINDEX = 3
34
+ SQLITE_STMTSTATUS_VM_STEP = 4
35
+ SQLITE_STMTSTATUS_REPREPARE = 5
36
+ SQLITE_STMTSTATUS_RUN = 6
37
+ SQLITE_STMTSTATUS_FILTER_MISS = 7
38
+ SQLITE_STMTSTATUS_FILTER_HIT = 8
39
+ SQLITE_STMTSTATUS_MEMUSED = 99
40
+
41
+ # The following class definitions are not really needed, as they're already
42
+ # defined in the C extension. We put them here for the sake of generating
43
+ # docs.
44
+
5
45
  # A base class for Extralite exceptions
6
- class Error < RuntimeError
46
+ class Error < ::StandardError
7
47
  end
8
48
 
9
49
  # An exception representing an SQL error emitted by SQLite
@@ -15,6 +55,11 @@ module Extralite
15
55
  class BusyError < Error
16
56
  end
17
57
 
58
+ # An exception raised when a query is interrupted by calling
59
+ # `Database#interrupt` from another thread
60
+ class InterruptError < Error
61
+ end
62
+
18
63
  # An SQLite database
19
64
  class Database
20
65
  alias_method :execute, :query
@@ -42,4 +87,18 @@ module Extralite
42
87
  query("pragma #{key}")
43
88
  end
44
89
  end
90
+
91
+ # An SQLite backup
92
+ class Backup
93
+ # def initialize(dst, dst_name, src, src_name); end
94
+
95
+ # def dst; end
96
+ # def src; end
97
+
98
+ # def step(pages); end
99
+ # def finish; end
100
+
101
+ # def pagecount; end
102
+ # def remaining; end
103
+ end
45
104
  end
@@ -207,6 +207,68 @@ end
207
207
  assert_equal [{schema_version: 33}], @db.pragma(:schema_version)
208
208
  assert_equal [{recursive_triggers: 1}], @db.pragma(:recursive_triggers)
209
209
  end
210
+
211
+ def test_execute_multi
212
+ @db.query('create table foo (a, b, c)')
213
+ assert_equal [], @db.query('select * from foo')
214
+
215
+ records = [
216
+ [1, '2', 3],
217
+ ['4', 5, 6]
218
+ ]
219
+
220
+ changes = @db.execute_multi('insert into foo values (?, ?, ?)', records)
221
+
222
+ assert_equal 2, changes
223
+ assert_equal [
224
+ { a: 1, b: '2', c: 3 },
225
+ { a: '4', b: 5, c: 6 }
226
+ ], @db.query('select * from foo')
227
+ end
228
+
229
+ def test_execute_multi_single_values
230
+ @db.query('create table foo (bar)')
231
+ assert_equal [], @db.query('select * from foo')
232
+
233
+ records = [
234
+ 'hi',
235
+ 'bye'
236
+ ]
237
+
238
+ changes = @db.execute_multi('insert into foo values (?)', records)
239
+
240
+ assert_equal 2, changes
241
+ assert_equal [
242
+ { bar: 'hi' },
243
+ { bar: 'bye' }
244
+ ], @db.query('select * from foo')
245
+ end
246
+
247
+ def test_interrupt
248
+ t = Thread.new do
249
+ sleep 0.5
250
+ @db.interrupt
251
+ end
252
+
253
+ n = 2**31
254
+ assert_raises(Extralite::InterruptError) {
255
+ @db.query <<-SQL
256
+ WITH RECURSIVE
257
+ fibo (curr, next)
258
+ AS
259
+ ( SELECT 1,1
260
+ UNION ALL
261
+ SELECT next, curr+next FROM fibo
262
+ LIMIT #{n} )
263
+ SELECT curr, next FROM fibo LIMIT 1 OFFSET #{n}-1;
264
+ SQL
265
+ }
266
+ t.join
267
+ end
268
+
269
+ def test_database_status
270
+ assert_operator 0, :<, @db.status(Extralite::SQLITE_DBSTATUS_SCHEMA_USED).first
271
+ end
210
272
  end
211
273
 
212
274
  class ScenarioTest < MiniTest::Test
@@ -275,3 +337,39 @@ class ScenarioTest < MiniTest::Test
275
337
  assert_equal [1, 4, 7], result
276
338
  end
277
339
  end
340
+
341
+ class BackupTest < MiniTest::Test
342
+ def setup
343
+ @src = Extralite::Database.new(':memory:')
344
+ @dst = Extralite::Database.new(':memory:')
345
+
346
+ @src.query('create table t (x,y,z)')
347
+ @src.query('insert into t values (1, 2, 3)')
348
+ @src.query('insert into t values (4, 5, 6)')
349
+ end
350
+
351
+ def test_backup
352
+ @src.backup(@dst)
353
+ assert_equal [[1, 2, 3], [4, 5, 6]], @dst.query_ary('select * from t')
354
+ end
355
+
356
+ def test_backup_with_block
357
+ progress = []
358
+ @src.backup(@dst) { |r, t| progress << [r, t] }
359
+ assert_equal [[1, 2, 3], [4, 5, 6]], @dst.query_ary('select * from t')
360
+ assert_equal [[2, 2]], progress
361
+ end
362
+
363
+ def test_backup_with_schema_names
364
+ @src.backup(@dst, 'main', 'temp')
365
+ assert_equal [[1, 2, 3], [4, 5, 6]], @dst.query_ary('select * from temp.t')
366
+ end
367
+
368
+ def test_backup_with_fn
369
+ tmp_fn = "/tmp/#{rand(86400)}.db"
370
+ @src.backup(tmp_fn)
371
+
372
+ db = Extralite::Database.new(tmp_fn)
373
+ assert_equal [[1, 2, 3], [4, 5, 6]], db.query_ary('select * from t')
374
+ end
375
+ end
@@ -6,4 +6,28 @@ class ExtraliteTest < MiniTest::Test
6
6
  def test_sqlite3_version
7
7
  assert_match /^3\.\d+\.\d+$/, Extralite.sqlite3_version
8
8
  end
9
+
10
+ def test_status
11
+ db = Extralite::Database.new(':memory:')
12
+ db.query('create table if not exists t (x,y,z)')
13
+ db.query('insert into t values (1, 2, 3)')
14
+
15
+ begin
16
+ a = Extralite::runtime_status(Extralite::SQLITE_STATUS_MEMORY_USED, false)
17
+ b = Extralite::runtime_status(Extralite::SQLITE_STATUS_MEMORY_USED)
18
+ c = Extralite::runtime_status(Extralite::SQLITE_STATUS_MEMORY_USED, true)
19
+ d = Extralite::runtime_status(Extralite::SQLITE_STATUS_MEMORY_USED, true)
20
+
21
+ assert_operator 0, :<, a[0]
22
+ assert_operator a[0], :<=, a[1]
23
+
24
+ assert_equal a, b
25
+ assert_equal a, c
26
+
27
+ assert_equal a[0], d[0]
28
+ assert_equal a[0], d[1]
29
+ ensure
30
+ db.close
31
+ end
32
+ end
9
33
  end
@@ -181,4 +181,34 @@ end
181
181
 
182
182
  assert_raises { p.query_single_value }
183
183
  end
184
+
185
+ def test_prepared_statement_execute_multi
186
+ @db.query('create table foo (a, b, c)')
187
+ assert_equal [], @db.query('select * from foo')
188
+
189
+ records = [
190
+ [1, '2', 3],
191
+ ['4', 5, 6]
192
+ ]
193
+
194
+ p = @db.prepare('insert into foo values (?, ?, ?)')
195
+ changes = p.execute_multi(records)
196
+
197
+ assert_equal 2, changes
198
+ assert_equal [
199
+ { a: 1, b: '2', c: 3 },
200
+ { a: '4', b: 5, c: 6 }
201
+ ], @db.query('select * from foo')
202
+ end
203
+
204
+ def test_prepared_statement_status
205
+ assert_equal 0, @stmt.status(Extralite::SQLITE_STMTSTATUS_RUN)
206
+ @stmt.query
207
+ assert_equal 1, @stmt.status(Extralite::SQLITE_STMTSTATUS_RUN)
208
+ @stmt.query
209
+ assert_equal 2, @stmt.status(Extralite::SQLITE_STMTSTATUS_RUN)
210
+ @stmt.query
211
+ assert_equal 3, @stmt.status(Extralite::SQLITE_STMTSTATUS_RUN, true)
212
+ assert_equal 0, @stmt.status(Extralite::SQLITE_STMTSTATUS_RUN)
213
+ end
184
214
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: extralite
3
3
  version: !ruby/object:Gem::Version
4
- version: '1.18'
4
+ version: '1.20'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-12-01 00:00:00.000000000 Z
11
+ date: 2023-01-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake-compiler
@@ -153,7 +153,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
153
153
  - !ruby/object:Gem::Version
154
154
  version: '0'
155
155
  requirements: []
156
- rubygems_version: 3.3.7
156
+ rubygems_version: 3.4.1
157
157
  signing_key:
158
158
  specification_version: 4
159
159
  summary: Extra-lightweight SQLite3 wrapper for Ruby