extralite 1.18 → 1.20

Sign up to get free protection for your applications and to get access to all the features.
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