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 +4 -4
- data/.github/workflows/test.yml +1 -1
- data/CHANGELOG.md +13 -0
- data/Gemfile.lock +1 -1
- data/README.md +9 -0
- data/ext/extralite/common.c +33 -3
- data/ext/extralite/database.c +213 -13
- data/ext/extralite/extralite.h +11 -0
- data/ext/extralite/extralite_ext.c +1 -1
- data/ext/extralite/prepared_statement.c +52 -9
- data/lib/extralite/version.rb +1 -1
- data/lib/extralite.rb +60 -1
- data/test/test_database.rb +98 -0
- data/test/test_extralite.rb +24 -0
- data/test/test_prepared_statement.rb +30 -0
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f9dba60a33188d5c3c7cfb09c7c746f64bcd723d53a20da4b110037ca395a50b
|
4
|
+
data.tar.gz: 2b5d8de6e19fe38800fbd30b2465ef6788cf50ac73f79be7e07d8d1f2165ad8b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4723dd3278f61a7dc95aa48260a5d974f3da7383463f538f3e0d647f1591f6759f239e633b6d3efe4e17d81c7841f0670c2847d318b1fdd78e7f1365fe509aad
|
7
|
+
data.tar.gz: 5fface9ad5152b0a11bb259267a1db2d6a214a02ca3a6ad0207f707e6de7aa674a28180fd6e109d55745bb3453d17b4e3244aaeba89bf75b40ec472dda1789bc
|
data/.github/workflows/test.yml
CHANGED
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
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 }]
|
data/ext/extralite/common.c
CHANGED
@@ -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
|
-
|
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);
|
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);
|
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
|
}
|
data/ext/extralite/database.c
CHANGED
@@ -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
|
-
|
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, "
|
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, "
|
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, "
|
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
|
-
|
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");
|
data/ext/extralite/extralite.h
CHANGED
@@ -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);
|
@@ -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
|
-
|
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, "
|
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, "
|
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, "
|
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
|
}
|
data/lib/extralite/version.rb
CHANGED
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 <
|
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
|
data/test/test_database.rb
CHANGED
@@ -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
|
data/test/test_extralite.rb
CHANGED
@@ -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.
|
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:
|
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.
|
156
|
+
rubygems_version: 3.4.1
|
157
157
|
signing_key:
|
158
158
|
specification_version: 4
|
159
159
|
summary: Extra-lightweight SQLite3 wrapper for Ruby
|