extralite 2.4 → 2.6
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-bundle.yml +30 -0
- data/.github/workflows/test.yml +2 -12
- data/CHANGELOG.md +49 -10
- data/Gemfile.lock +1 -1
- data/LICENSE +1 -1
- data/README.md +876 -217
- data/TODO.md +2 -3
- data/ext/extralite/changeset.c +463 -0
- data/ext/extralite/common.c +226 -19
- data/ext/extralite/database.c +339 -23
- data/ext/extralite/extconf-bundle.rb +10 -4
- data/ext/extralite/extconf.rb +31 -27
- data/ext/extralite/extralite.h +25 -5
- data/ext/extralite/extralite_ext.c +10 -0
- data/ext/extralite/iterator.c +8 -3
- data/ext/extralite/query.c +222 -22
- data/gemspec.rb +1 -1
- data/lib/extralite/version.rb +1 -1
- data/lib/extralite.rb +64 -8
- data/test/helper.rb +8 -0
- data/test/issue-54.rb +21 -0
- data/test/issue-59.rb +70 -0
- data/test/perf_ary.rb +14 -12
- data/test/perf_hash.rb +17 -15
- data/test/perf_hash_prepared.rb +58 -0
- data/test/test_changeset.rb +161 -0
- data/test/test_database.rb +672 -13
- data/test/test_query.rb +367 -2
- metadata +10 -5
- data/test/perf_prepared.rb +0 -64
@@ -1,9 +1,19 @@
|
|
1
|
+
#include "ruby.h"
|
2
|
+
|
1
3
|
void Init_ExtraliteDatabase();
|
2
4
|
void Init_ExtraliteQuery();
|
3
5
|
void Init_ExtraliteIterator();
|
6
|
+
#ifdef EXTRALITE_ENABLE_CHANGESET
|
7
|
+
void Init_ExtraliteChangeset();
|
8
|
+
#endif
|
4
9
|
|
5
10
|
void Init_extralite_ext(void) {
|
11
|
+
rb_ext_ractor_safe(true);
|
12
|
+
|
6
13
|
Init_ExtraliteDatabase();
|
7
14
|
Init_ExtraliteQuery();
|
8
15
|
Init_ExtraliteIterator();
|
16
|
+
#ifdef EXTRALITE_ENABLE_CHANGESET
|
17
|
+
Init_ExtraliteChangeset();
|
18
|
+
#endif
|
9
19
|
}
|
data/ext/extralite/iterator.c
CHANGED
@@ -19,13 +19,18 @@ static size_t Iterator_size(const void *ptr) {
|
|
19
19
|
|
20
20
|
static void Iterator_mark(void *ptr) {
|
21
21
|
Iterator_t *iterator = ptr;
|
22
|
-
|
22
|
+
rb_gc_mark_movable(iterator->query);
|
23
|
+
}
|
24
|
+
|
25
|
+
static void Iterator_compact(void *ptr) {
|
26
|
+
Iterator_t *iterator = ptr;
|
27
|
+
iterator->query = rb_gc_location(iterator->query);
|
23
28
|
}
|
24
29
|
|
25
30
|
static const rb_data_type_t Iterator_type = {
|
26
31
|
"Iterator",
|
27
|
-
{Iterator_mark, free, Iterator_size,},
|
28
|
-
0, 0, RUBY_TYPED_FREE_IMMEDIATELY
|
32
|
+
{Iterator_mark, free, Iterator_size, Iterator_compact},
|
33
|
+
0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED
|
29
34
|
};
|
30
35
|
|
31
36
|
static VALUE Iterator_allocate(VALUE klass) {
|
data/ext/extralite/query.c
CHANGED
@@ -14,14 +14,22 @@ VALUE cQuery;
|
|
14
14
|
ID ID_inspect;
|
15
15
|
ID ID_slice;
|
16
16
|
|
17
|
+
#define DB_GVL_MODE(query) Database_prepare_gvl_mode(query->db_struct)
|
18
|
+
|
17
19
|
static size_t Query_size(const void *ptr) {
|
18
20
|
return sizeof(Query_t);
|
19
21
|
}
|
20
22
|
|
21
23
|
static void Query_mark(void *ptr) {
|
22
24
|
Query_t *query = ptr;
|
23
|
-
|
24
|
-
|
25
|
+
rb_gc_mark_movable(query->db);
|
26
|
+
rb_gc_mark_movable(query->sql);
|
27
|
+
}
|
28
|
+
|
29
|
+
static void Query_compact(void *ptr) {
|
30
|
+
Query_t *query = ptr;
|
31
|
+
query->db = rb_gc_location(query->db);
|
32
|
+
query->sql = rb_gc_location(query->sql);
|
25
33
|
}
|
26
34
|
|
27
35
|
static void Query_free(void *ptr) {
|
@@ -32,7 +40,7 @@ static void Query_free(void *ptr) {
|
|
32
40
|
|
33
41
|
static const rb_data_type_t Query_type = {
|
34
42
|
"Query",
|
35
|
-
{Query_mark, Query_free, Query_size,},
|
43
|
+
{Query_mark, Query_free, Query_size, Query_compact},
|
36
44
|
0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED
|
37
45
|
};
|
38
46
|
|
@@ -82,19 +90,17 @@ VALUE Query_initialize(VALUE self, VALUE db, VALUE sql) {
|
|
82
90
|
|
83
91
|
static inline void query_reset(Query_t *query) {
|
84
92
|
if (!query->stmt)
|
85
|
-
prepare_single_stmt(query->sqlite3_db, &query->stmt, query->sql);
|
86
|
-
|
87
|
-
rb_funcall(query->db_struct->trace_block, ID_call, 1, query->sql);
|
93
|
+
prepare_single_stmt(DB_GVL_MODE(query), query->sqlite3_db, &query->stmt, query->sql);
|
94
|
+
TRACE_SQL(query->db_struct, query->sql);
|
88
95
|
sqlite3_reset(query->stmt);
|
89
96
|
query->eof = 0;
|
90
97
|
}
|
91
98
|
|
92
99
|
static inline void query_reset_and_bind(Query_t *query, int argc, VALUE * argv) {
|
93
100
|
if (!query->stmt)
|
94
|
-
prepare_single_stmt(query->sqlite3_db, &query->stmt, query->sql);
|
101
|
+
prepare_single_stmt(DB_GVL_MODE(query), query->sqlite3_db, &query->stmt, query->sql);
|
95
102
|
|
96
|
-
|
97
|
-
rb_funcall(query->db_struct->trace_block, ID_call, 1, query->sql);
|
103
|
+
TRACE_SQL(query->db_struct, query->sql);
|
98
104
|
|
99
105
|
sqlite3_reset(query->stmt);
|
100
106
|
query->eof = 0;
|
@@ -122,8 +128,7 @@ VALUE Query_reset(VALUE self) {
|
|
122
128
|
if (query->closed) rb_raise(cError, "Query is closed");
|
123
129
|
|
124
130
|
query_reset(query);
|
125
|
-
|
126
|
-
rb_funcall(query->db_struct->trace_block, ID_call, 1, query->sql);
|
131
|
+
TRACE_SQL(query->db_struct, query->sql);
|
127
132
|
|
128
133
|
return self;
|
129
134
|
}
|
@@ -367,27 +372,115 @@ VALUE Query_execute(int argc, VALUE *argv, VALUE self) {
|
|
367
372
|
return Query_perform_next(self, ALL_ROWS, safe_query_changes);
|
368
373
|
}
|
369
374
|
|
370
|
-
/*
|
371
|
-
*
|
372
|
-
*
|
373
|
-
*
|
375
|
+
/* call-seq:
|
376
|
+
* query << [...] -> query
|
377
|
+
* query << { ... } -> query
|
378
|
+
*
|
379
|
+
* Runs the with the given parameters, returning the total changes effected.
|
380
|
+
* This method should be used for data- or schema-manipulation queries.
|
381
|
+
*
|
382
|
+
* Query parameters to be bound to placeholders in the query can be specified as
|
383
|
+
* a list of values or as a hash mapping parameter names to values. When
|
384
|
+
* parameters are given as an array, the query should specify parameters using
|
385
|
+
* `?`:
|
386
|
+
*
|
387
|
+
* query = db.prepare('update foo set x = ? where y = ?')
|
388
|
+
* query << [42, 43]
|
389
|
+
*
|
390
|
+
* Named placeholders are specified using `:`. The placeholder values are
|
391
|
+
* specified using a hash, where keys are either strings are symbols. String
|
392
|
+
* keys can include or omit the `:` prefix. The following are equivalent:
|
393
|
+
*
|
394
|
+
* query = db.prepare('update foo set x = :bar')
|
395
|
+
* query << { bar: 42 }
|
396
|
+
* query << { 'bar' => 42 }
|
397
|
+
* query << { ':bar' => 42 }
|
398
|
+
*/
|
399
|
+
VALUE Query_execute_chevrons(VALUE self, VALUE params) {
|
400
|
+
Query_execute(1, ¶ms, self);
|
401
|
+
return self;
|
402
|
+
}
|
403
|
+
|
404
|
+
/* call-seq:
|
405
|
+
* query.batch_execute(params_array) -> changes
|
406
|
+
* query.batch_execute(enumerable) -> changes
|
407
|
+
* query.batch_execute(callable) -> changes
|
408
|
+
*
|
409
|
+
* Executes the query for each set of parameters in the paramter source. If an
|
410
|
+
* enumerable is given, it is iterated and each of its values is used as the
|
411
|
+
* parameters for running the query. If a callable is given, it is called
|
412
|
+
* repeatedly and each of its return values is used as the parameters, until nil
|
413
|
+
* is returned.
|
414
|
+
*
|
415
|
+
* Returns the number of changes effected. This method is designed for inserting
|
416
|
+
* multiple records.
|
374
417
|
*
|
375
418
|
* query = db.prepare('insert into foo values (?, ?, ?)')
|
376
419
|
* records = [
|
377
420
|
* [1, 2, 3],
|
378
421
|
* [4, 5, 6]
|
379
422
|
* ]
|
380
|
-
* query.
|
423
|
+
* query.batch_execute(records)
|
424
|
+
*
|
425
|
+
* source = [
|
426
|
+
* [1, 2, 3],
|
427
|
+
* [4, 5, 6]
|
428
|
+
* ]
|
429
|
+
* query.batch_execute { records.shift }
|
381
430
|
*
|
382
|
-
* @param parameters [Array<Array, Hash
|
431
|
+
* @param parameters [Array<Array, Hash>, Enumerable, Enumerator, Callable] array of parameters to run query with
|
383
432
|
* @return [Integer] number of changes effected
|
384
433
|
*/
|
385
|
-
VALUE
|
434
|
+
VALUE Query_batch_execute(VALUE self, VALUE parameters) {
|
435
|
+
Query_t *query = self_to_query(self);
|
436
|
+
if (query->closed) rb_raise(cError, "Query is closed");
|
437
|
+
|
438
|
+
if (!query->stmt)
|
439
|
+
prepare_single_stmt(DB_GVL_MODE(query), query->sqlite3_db, &query->stmt, query->sql);
|
440
|
+
|
441
|
+
query_ctx ctx = QUERY_CTX(
|
442
|
+
self,
|
443
|
+
query->db_struct,
|
444
|
+
query->stmt,
|
445
|
+
parameters,
|
446
|
+
QUERY_MODE(QUERY_MULTI_ROW),
|
447
|
+
ALL_ROWS
|
448
|
+
);
|
449
|
+
return safe_batch_execute(&ctx);
|
450
|
+
}
|
451
|
+
|
452
|
+
/* call-seq:
|
453
|
+
* query.batch_query(sql, params_array) -> rows
|
454
|
+
* query.batch_query(sql, enumerable) -> rows
|
455
|
+
* query.batch_query(sql, callable) -> rows
|
456
|
+
* query.batch_query(sql, params_array) { |rows| ... } -> changes
|
457
|
+
* query.batch_query(sql, enumerable) { |rows| ... } -> changes
|
458
|
+
* query.batch_query(sql, callable) { |rows| ... } -> changes
|
459
|
+
*
|
460
|
+
* Executes the prepared query for each list of parameters in the given paramter
|
461
|
+
* source. If a block is given, it is called with the resulting rows for each
|
462
|
+
* invocation of the query, and the total number of changes is returned.
|
463
|
+
* Otherwise, an array containing the resulting rows for each invocation is
|
464
|
+
* returned.
|
465
|
+
*
|
466
|
+
* q = db.prepare('insert into foo values (?, ?) returning bar, baz')
|
467
|
+
* records = [
|
468
|
+
* [1, 2],
|
469
|
+
* [3, 4]
|
470
|
+
* ]
|
471
|
+
* q.batch_query(records)
|
472
|
+
* #=> [{ bar: 1, baz: 2 }, { bar: 3, baz: 4}]
|
473
|
+
* *
|
474
|
+
* @param sql [String] query SQL
|
475
|
+
* @param parameters [Array<Array, Hash>, Enumerable, Enumerator, Callable] parameters to run query with
|
476
|
+
* @return [Array<Hash>, Integer] Total number of changes effected
|
477
|
+
*/
|
478
|
+
VALUE Query_batch_query(VALUE self, VALUE parameters) {
|
386
479
|
Query_t *query = self_to_query(self);
|
387
480
|
if (query->closed) rb_raise(cError, "Query is closed");
|
388
481
|
|
389
482
|
if (!query->stmt)
|
390
|
-
prepare_single_stmt(query->sqlite3_db, &query->stmt, query->sql);
|
483
|
+
prepare_single_stmt(DB_GVL_MODE(query), query->sqlite3_db, &query->stmt, query->sql);
|
391
484
|
|
392
485
|
query_ctx ctx = QUERY_CTX(
|
393
486
|
self,
|
@@ -397,7 +490,95 @@ VALUE Query_execute_multi(VALUE self, VALUE parameters) {
|
|
397
490
|
QUERY_MODE(QUERY_MULTI_ROW),
|
398
491
|
ALL_ROWS
|
399
492
|
);
|
400
|
-
return
|
493
|
+
return safe_batch_query(&ctx);
|
494
|
+
}
|
495
|
+
|
496
|
+
/* call-seq:
|
497
|
+
* query.batch_query_ary(sql, params_array) -> rows
|
498
|
+
* query.batch_query_ary(sql, enumerable) -> rows
|
499
|
+
* query.batch_query_ary(sql, callable) -> rows
|
500
|
+
* query.batch_query_ary(sql, params_array) { |rows| ... } -> changes
|
501
|
+
* query.batch_query_ary(sql, enumerable) { |rows| ... } -> changes
|
502
|
+
* query.batch_query_ary(sql, callable) { |rows| ... } -> changes
|
503
|
+
*
|
504
|
+
* Executes the prepared query for each list of parameters in the given paramter
|
505
|
+
* source. If a block is given, it is called with the resulting rows for each
|
506
|
+
* invocation of the query, and the total number of changes is returned.
|
507
|
+
* Otherwise, an array containing the resulting rows for each invocation is
|
508
|
+
* returned. Rows are represented as arrays.
|
509
|
+
*
|
510
|
+
* q = db.prepare('insert into foo values (?, ?) returning bar, baz')
|
511
|
+
* records = [
|
512
|
+
* [1, 2],
|
513
|
+
* [3, 4]
|
514
|
+
* ]
|
515
|
+
* q.batch_query_ary(records)
|
516
|
+
* #=> [{ bar: 1, baz: 2 }, { bar: 3, baz: 4}]
|
517
|
+
* *
|
518
|
+
* @param sql [String] query SQL
|
519
|
+
* @param parameters [Array<Array, Hash>, Enumerable, Enumerator, Callable] parameters to run query with
|
520
|
+
* @return [Array<Hash>, Integer] Total number of changes effected
|
521
|
+
*/
|
522
|
+
VALUE Query_batch_query_ary(VALUE self, VALUE parameters) {
|
523
|
+
Query_t *query = self_to_query(self);
|
524
|
+
if (query->closed) rb_raise(cError, "Query is closed");
|
525
|
+
|
526
|
+
if (!query->stmt)
|
527
|
+
prepare_single_stmt(DB_GVL_MODE(query), query->sqlite3_db, &query->stmt, query->sql);
|
528
|
+
|
529
|
+
query_ctx ctx = QUERY_CTX(
|
530
|
+
self,
|
531
|
+
query->db_struct,
|
532
|
+
query->stmt,
|
533
|
+
parameters,
|
534
|
+
QUERY_MODE(QUERY_MULTI_ROW),
|
535
|
+
ALL_ROWS
|
536
|
+
);
|
537
|
+
return safe_batch_query_ary(&ctx);
|
538
|
+
}
|
539
|
+
|
540
|
+
/* call-seq:
|
541
|
+
* query.batch_query_single_column(sql, params_array) -> rows
|
542
|
+
* query.batch_query_single_column(sql, enumerable) -> rows
|
543
|
+
* query.batch_query_single_column(sql, callable) -> rows
|
544
|
+
* query.batch_query_single_column(sql, params_array) { |rows| ... } -> changes
|
545
|
+
* query.batch_query_single_column(sql, enumerable) { |rows| ... } -> changes
|
546
|
+
* query.batch_query_single_column(sql, callable) { |rows| ... } -> changes
|
547
|
+
*
|
548
|
+
* Executes the prepared query for each list of parameters in the given paramter
|
549
|
+
* source. If a block is given, it is called with the resulting rows for each
|
550
|
+
* invocation of the query, and the total number of changes is returned.
|
551
|
+
* Otherwise, an array containing the resulting rows for each invocation is
|
552
|
+
* returned. Rows are represented as single values.
|
553
|
+
*
|
554
|
+
* q = db.prepare('insert into foo values (?, ?) returning bar, baz')
|
555
|
+
* records = [
|
556
|
+
* [1, 2],
|
557
|
+
* [3, 4]
|
558
|
+
* ]
|
559
|
+
* q.batch_query_single_column(records)
|
560
|
+
* #=> [{ bar: 1, baz: 2 }, { bar: 3, baz: 4}]
|
561
|
+
* *
|
562
|
+
* @param sql [String] query SQL
|
563
|
+
* @param parameters [Array<Array, Hash>, Enumerable, Enumerator, Callable] parameters to run query with
|
564
|
+
* @return [Array<Hash>, Integer] Total number of changes effected
|
565
|
+
*/
|
566
|
+
VALUE Query_batch_query_single_column(VALUE self, VALUE parameters) {
|
567
|
+
Query_t *query = self_to_query(self);
|
568
|
+
if (query->closed) rb_raise(cError, "Query is closed");
|
569
|
+
|
570
|
+
if (!query->stmt)
|
571
|
+
prepare_single_stmt(DB_GVL_MODE(query), query->sqlite3_db, &query->stmt, query->sql);
|
572
|
+
|
573
|
+
query_ctx ctx = QUERY_CTX(
|
574
|
+
self,
|
575
|
+
query->db_struct,
|
576
|
+
query->stmt,
|
577
|
+
parameters,
|
578
|
+
QUERY_MODE(QUERY_MULTI_ROW),
|
579
|
+
ALL_ROWS
|
580
|
+
);
|
581
|
+
return safe_batch_query_single_column(&ctx);
|
401
582
|
}
|
402
583
|
|
403
584
|
/* Returns the database associated with the query.
|
@@ -431,6 +612,19 @@ VALUE Query_columns(VALUE self) {
|
|
431
612
|
return Query_perform_next(self, ALL_ROWS, safe_query_columns);
|
432
613
|
}
|
433
614
|
|
615
|
+
/* call-seq:
|
616
|
+
* query.clone -> copy
|
617
|
+
* query.dup -> copy
|
618
|
+
*
|
619
|
+
* Returns a new query instance for the same SQL as the original query.
|
620
|
+
*
|
621
|
+
* @return [Extralite::Query] copy of query
|
622
|
+
*/
|
623
|
+
VALUE Query_clone(VALUE self) {
|
624
|
+
Query_t *query = self_to_query(self);
|
625
|
+
return rb_funcall(cQuery, ID_new, 2, query->db, query->sql);
|
626
|
+
}
|
627
|
+
|
434
628
|
/* Closes the query. Attempting to run a closed query will raise an error.
|
435
629
|
*
|
436
630
|
* @return [Extralite::Query] self
|
@@ -475,7 +669,7 @@ VALUE Query_status(int argc, VALUE* argv, VALUE self) {
|
|
475
669
|
if (query->closed) rb_raise(cError, "Query is closed");
|
476
670
|
|
477
671
|
if (!query->stmt)
|
478
|
-
prepare_single_stmt(query->sqlite3_db, &query->stmt, query->sql);
|
672
|
+
prepare_single_stmt(DB_GVL_MODE(query), query->sqlite3_db, &query->stmt, query->sql);
|
479
673
|
|
480
674
|
int value = sqlite3_stmt_status(query->stmt, NUM2INT(op), RTEST(reset) ? 1 : 0);
|
481
675
|
return INT2NUM(value);
|
@@ -509,8 +703,10 @@ void Init_ExtraliteQuery(void) {
|
|
509
703
|
rb_define_method(cQuery, "close", Query_close, 0);
|
510
704
|
rb_define_method(cQuery, "closed?", Query_closed_p, 0);
|
511
705
|
rb_define_method(cQuery, "columns", Query_columns, 0);
|
706
|
+
rb_define_method(cQuery, "clone", Query_clone, 0);
|
512
707
|
rb_define_method(cQuery, "database", Query_database, 0);
|
513
708
|
rb_define_method(cQuery, "db", Query_database, 0);
|
709
|
+
rb_define_method(cQuery, "dup", Query_clone, 0);
|
514
710
|
|
515
711
|
rb_define_method(cQuery, "each", Query_each_hash, 0);
|
516
712
|
rb_define_method(cQuery, "each_ary", Query_each_ary, 0);
|
@@ -519,7 +715,11 @@ void Init_ExtraliteQuery(void) {
|
|
519
715
|
|
520
716
|
rb_define_method(cQuery, "eof?", Query_eof_p, 0);
|
521
717
|
rb_define_method(cQuery, "execute", Query_execute, -1);
|
522
|
-
rb_define_method(cQuery, "
|
718
|
+
rb_define_method(cQuery, "<<", Query_execute_chevrons, 1);
|
719
|
+
rb_define_method(cQuery, "batch_execute", Query_batch_execute, 1);
|
720
|
+
rb_define_method(cQuery, "batch_query", Query_batch_query, 1);
|
721
|
+
rb_define_method(cQuery, "batch_query_ary", Query_batch_query_ary, 1);
|
722
|
+
rb_define_method(cQuery, "batch_query_single_column", Query_batch_query_single_column, 1);
|
523
723
|
rb_define_method(cQuery, "initialize", Query_initialize, 2);
|
524
724
|
rb_define_method(cQuery, "inspect", Query_inspect, 0);
|
525
725
|
|
data/gemspec.rb
CHANGED
@@ -16,7 +16,7 @@ def common_spec(s)
|
|
16
16
|
s.rdoc_options = ["--title", "extralite", "--main", "README.md"]
|
17
17
|
s.extra_rdoc_files = ["README.md"]
|
18
18
|
s.require_paths = ["lib"]
|
19
|
-
s.required_ruby_version = '>=
|
19
|
+
s.required_ruby_version = '>= 3.0'
|
20
20
|
|
21
21
|
s.add_development_dependency 'rake-compiler', '1.1.6'
|
22
22
|
s.add_development_dependency 'minitest', '5.15.0'
|
data/lib/extralite/version.rb
CHANGED
data/lib/extralite.rb
CHANGED
@@ -33,16 +33,20 @@ module Extralite
|
|
33
33
|
class Database
|
34
34
|
# @!visibility private
|
35
35
|
TABLES_SQL = <<~SQL
|
36
|
-
SELECT name FROM sqlite_master
|
36
|
+
SELECT name FROM %<db>s.sqlite_master
|
37
37
|
WHERE type ='table'
|
38
|
-
AND name NOT LIKE 'sqlite_
|
38
|
+
AND name NOT LIKE 'sqlite_%%';
|
39
39
|
SQL
|
40
40
|
|
41
|
-
|
41
|
+
alias_method :execute_multi, :batch_execute
|
42
|
+
|
43
|
+
# Returns the list of currently defined tables. If a database name is given,
|
44
|
+
# returns the list of tables for the relevant attached database.
|
42
45
|
#
|
46
|
+
# @param db [String] name of attached database
|
43
47
|
# @return [Array] list of tables
|
44
|
-
def tables
|
45
|
-
query_single_column(TABLES_SQL)
|
48
|
+
def tables(db = 'main')
|
49
|
+
query_single_column(format(TABLES_SQL, db: db))
|
46
50
|
end
|
47
51
|
|
48
52
|
# Gets or sets one or more pragmas:
|
@@ -56,6 +60,11 @@ module Extralite
|
|
56
60
|
value.is_a?(Hash) ? pragma_set(value) : pragma_get(value)
|
57
61
|
end
|
58
62
|
|
63
|
+
# Error class used to roll back a transaction without propagating an
|
64
|
+
# exception.
|
65
|
+
class Rollback < Error
|
66
|
+
end
|
67
|
+
|
59
68
|
# Starts a transaction and runs the given block. If an exception is raised
|
60
69
|
# in the block, the transaction is rolled back. Otherwise, the transaction
|
61
70
|
# is commited after running the block.
|
@@ -72,13 +81,56 @@ module Extralite
|
|
72
81
|
|
73
82
|
abort = false
|
74
83
|
yield self
|
75
|
-
rescue
|
84
|
+
rescue => e
|
76
85
|
abort = true
|
77
|
-
raise
|
86
|
+
raise unless e.is_a?(Rollback)
|
78
87
|
ensure
|
79
88
|
execute(abort ? 'rollback' : 'commit')
|
80
89
|
end
|
81
90
|
|
91
|
+
# Creates a savepoint with the given name.
|
92
|
+
#
|
93
|
+
# @param name [String, Symbol] savepoint name
|
94
|
+
# @return [Extralite::Database] database
|
95
|
+
def savepoint(name)
|
96
|
+
execute "savepoint #{name}"
|
97
|
+
self
|
98
|
+
end
|
99
|
+
|
100
|
+
# Release a savepoint with the given name.
|
101
|
+
#
|
102
|
+
# @param name [String, Symbol] savepoint name
|
103
|
+
# @return [Extralite::Database] database
|
104
|
+
def release(name)
|
105
|
+
execute "release #{name}"
|
106
|
+
self
|
107
|
+
end
|
108
|
+
|
109
|
+
# Rolls back changes to a savepoint with the given name.
|
110
|
+
#
|
111
|
+
# @param name [String, Symbol] savepoint name
|
112
|
+
# @return [Extralite::Database] database
|
113
|
+
def rollback_to(name)
|
114
|
+
execute "rollback to #{name}"
|
115
|
+
self
|
116
|
+
end
|
117
|
+
|
118
|
+
# Rolls back the currently active transaction. This method should only be
|
119
|
+
# called from within a block passed to Database#transaction. This method
|
120
|
+
# raises a Extralite::Rollback exception, which will stop execution of the
|
121
|
+
# transaction block without propagating the exception.
|
122
|
+
#
|
123
|
+
# db.transaction do
|
124
|
+
# db.execute('insert into foo (42)')
|
125
|
+
# db.rollback!
|
126
|
+
# end
|
127
|
+
#
|
128
|
+
# @param name [String, Symbol] savepoint name
|
129
|
+
# @return [Extralite::Database] database
|
130
|
+
def rollback!
|
131
|
+
raise Rollback
|
132
|
+
end
|
133
|
+
|
82
134
|
private
|
83
135
|
|
84
136
|
def pragma_set(values)
|
@@ -87,7 +139,11 @@ module Extralite
|
|
87
139
|
end
|
88
140
|
|
89
141
|
def pragma_get(key)
|
90
|
-
|
142
|
+
query_single_value("pragma #{key}")
|
91
143
|
end
|
92
144
|
end
|
145
|
+
|
146
|
+
class Query
|
147
|
+
alias_method :execute_multi, :batch_execute
|
148
|
+
end
|
93
149
|
end
|
data/test/helper.rb
CHANGED
@@ -7,3 +7,11 @@ require 'minitest/autorun'
|
|
7
7
|
puts "sqlite3 version: #{Extralite.sqlite3_version}"
|
8
8
|
|
9
9
|
IS_LINUX = RUBY_PLATFORM =~ /linux/
|
10
|
+
SKIP_RACTOR_TESTS = !IS_LINUX || (RUBY_VERSION =~ /^3\.[01]/)
|
11
|
+
|
12
|
+
module Minitest::Assertions
|
13
|
+
def assert_in_range exp_range, act
|
14
|
+
msg = message(msg) { "Expected #{mu_pp(act)} to be in range #{mu_pp(exp_range)}" }
|
15
|
+
assert exp_range.include?(act), msg
|
16
|
+
end
|
17
|
+
end
|
data/test/issue-54.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "./lib/extralite"
|
4
|
+
|
5
|
+
puts 'Connecting to database...'
|
6
|
+
|
7
|
+
connection_1 = Extralite::Database.new("test.sqlite3")
|
8
|
+
puts "#{connection_1} connected"
|
9
|
+
connection_2 = Extralite::Database.new("test.sqlite3")
|
10
|
+
connection_2.busy_timeout = 0
|
11
|
+
puts "#{connection_2} connected"
|
12
|
+
|
13
|
+
[connection_1, connection_2].each do |connection|
|
14
|
+
puts "#{connection} beginning transaction..."
|
15
|
+
connection.execute "begin immediate transaction"
|
16
|
+
end
|
17
|
+
|
18
|
+
[connection_1, connection_2].each do |connection|
|
19
|
+
puts "#{connection} rolling back transaction..."
|
20
|
+
connection.execute "rollback transaction"
|
21
|
+
end
|
data/test/issue-59.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "./lib/extralite"
|
4
|
+
require "benchmark"
|
5
|
+
require "tempfile"
|
6
|
+
require "fileutils"
|
7
|
+
|
8
|
+
p sqlite_version: Extralite.sqlite3_version
|
9
|
+
|
10
|
+
N = (ENV['N'] || 1000).to_i
|
11
|
+
p N: N
|
12
|
+
|
13
|
+
fn1 = '/tmp/db1'
|
14
|
+
fn2 = '/tmp/db2'
|
15
|
+
|
16
|
+
FileUtils.rm(fn1) rescue nil
|
17
|
+
FileUtils.rm(fn2) rescue nil
|
18
|
+
|
19
|
+
p fn1: fn1
|
20
|
+
p fn2: fn2
|
21
|
+
|
22
|
+
db1 = Extralite::Database.new fn1
|
23
|
+
db1.execute "pragma journal_mode = wal;"
|
24
|
+
db1.transaction do
|
25
|
+
db1.execute "create table t1 ( a integer primary key, b text );"
|
26
|
+
values = N.times.map { |i| "#{i}-#{rand(1000)}" }
|
27
|
+
db1.execute_multi "insert into t1 ( b ) values ( ? );", values
|
28
|
+
|
29
|
+
p count: db1.query_single_value("select count(*) from t1")
|
30
|
+
p some_rows: db1.query("select * from t1 limit 5")
|
31
|
+
end
|
32
|
+
|
33
|
+
db2 = Extralite::Database.new fn2
|
34
|
+
db2.execute "pragma journal_mode = wal;"
|
35
|
+
db2.execute "attach '#{fn1}' as db1;"
|
36
|
+
db2.execute "create table t2 ( a integer primary key, b text );"
|
37
|
+
|
38
|
+
p main_tables: db2.tables
|
39
|
+
p db1_tables: db2.tables('db1')
|
40
|
+
|
41
|
+
overall = Benchmark.realtime do
|
42
|
+
t1 = Thread.new do
|
43
|
+
time1 = Benchmark.realtime do
|
44
|
+
db2.execute "create unique index db1.t1_b_unique on t1 (b);"
|
45
|
+
end
|
46
|
+
p({ indexing: time1 })
|
47
|
+
end
|
48
|
+
|
49
|
+
t2 = Thread.new do
|
50
|
+
time2 = Benchmark.realtime do
|
51
|
+
(N / 10000).times do |i|
|
52
|
+
values = 10000.times.map { |i| "#{i}-#{rand(1000)}" }
|
53
|
+
db2.transaction do
|
54
|
+
db2.execute_multi "insert into main.t2 ( b ) values ( ? );", values
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
p({ inserting: time2 })
|
59
|
+
p count_t2: db2.query_single_value("select count(*) from main.t2")
|
60
|
+
p some_rows_t2: db2.query("select * from main.t2 limit 5")
|
61
|
+
end
|
62
|
+
|
63
|
+
t1.join
|
64
|
+
t2.join
|
65
|
+
end
|
66
|
+
|
67
|
+
p({ overall: overall })
|
68
|
+
|
69
|
+
db1.close
|
70
|
+
db2.close
|
data/test/perf_ary.rb
CHANGED
@@ -15,26 +15,25 @@ require 'fileutils'
|
|
15
15
|
DB_PATH = "/tmp/extralite_sqlite3_perf-#{Time.now.to_i}-#{rand(10000)}.db"
|
16
16
|
puts "DB_PATH = #{DB_PATH.inspect}"
|
17
17
|
|
18
|
+
$sqlite3_db = SQLite3::Database.new(DB_PATH)
|
19
|
+
$extralite_db = Extralite::Database.new(DB_PATH, gvl_release_threshold: -1)
|
18
20
|
|
19
21
|
def prepare_database(count)
|
20
22
|
db = Extralite::Database.new(DB_PATH)
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
count.times {
|
25
|
-
|
26
|
-
db.close
|
23
|
+
$extralite_db.query('create table if not exists foo ( a integer primary key, b text )')
|
24
|
+
$extralite_db.query('delete from foo')
|
25
|
+
$extralite_db.query('begin')
|
26
|
+
count.times { $extralite_db.query('insert into foo (b) values (?)', "hello#{rand(1000)}" )}
|
27
|
+
$extralite_db.query('commit')
|
27
28
|
end
|
28
29
|
|
29
30
|
def sqlite3_run(count)
|
30
|
-
|
31
|
-
results = db.execute('select * from foo')
|
31
|
+
results = $sqlite3_db.execute('select * from foo')
|
32
32
|
raise unless results.size == count
|
33
33
|
end
|
34
34
|
|
35
35
|
def extralite_run(count)
|
36
|
-
|
37
|
-
results = db.query_ary('select * from foo')
|
36
|
+
results = $extralite_db.query('select * from foo')
|
38
37
|
raise unless results.size == count
|
39
38
|
end
|
40
39
|
|
@@ -43,12 +42,15 @@ end
|
|
43
42
|
|
44
43
|
prepare_database(c)
|
45
44
|
|
46
|
-
Benchmark.ips do |x|
|
47
|
-
x.config(:time =>
|
45
|
+
bm = Benchmark.ips do |x|
|
46
|
+
x.config(:time => 5, :warmup => 2)
|
48
47
|
|
49
48
|
x.report("sqlite3") { sqlite3_run(c) }
|
50
49
|
x.report("extralite") { extralite_run(c) }
|
51
50
|
|
52
51
|
x.compare!
|
53
52
|
end
|
53
|
+
puts;
|
54
|
+
bm.entries.each { |e| puts "#{e.label}: #{(e.ips * c).round.to_i} rows/s" }
|
55
|
+
puts;
|
54
56
|
end
|