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
data/ext/extralite/database.c
CHANGED
@@ -13,11 +13,19 @@ VALUE eArgumentError;
|
|
13
13
|
|
14
14
|
ID ID_bind;
|
15
15
|
ID ID_call;
|
16
|
+
ID ID_each;
|
16
17
|
ID ID_keys;
|
17
18
|
ID ID_new;
|
18
19
|
ID ID_strip;
|
20
|
+
ID ID_to_s;
|
21
|
+
ID ID_track;
|
19
22
|
|
23
|
+
VALUE SYM_gvl_release_threshold;
|
20
24
|
VALUE SYM_read_only;
|
25
|
+
VALUE SYM_synchronous;
|
26
|
+
VALUE SYM_wal_journal_mode;
|
27
|
+
|
28
|
+
#define DB_GVL_MODE(db) Database_prepare_gvl_mode(db)
|
21
29
|
|
22
30
|
static size_t Database_size(const void *ptr) {
|
23
31
|
return sizeof(Database_t);
|
@@ -25,7 +33,14 @@ static size_t Database_size(const void *ptr) {
|
|
25
33
|
|
26
34
|
static void Database_mark(void *ptr) {
|
27
35
|
Database_t *db = ptr;
|
28
|
-
|
36
|
+
rb_gc_mark_movable(db->trace_proc);
|
37
|
+
rb_gc_mark_movable(db->progress_handler_proc);
|
38
|
+
}
|
39
|
+
|
40
|
+
static void Database_compact(void *ptr) {
|
41
|
+
Database_t *db = ptr;
|
42
|
+
db->trace_proc = rb_gc_location(db->trace_proc);
|
43
|
+
db->progress_handler_proc = rb_gc_location(db->progress_handler_proc);
|
29
44
|
}
|
30
45
|
|
31
46
|
static void Database_free(void *ptr) {
|
@@ -36,7 +51,7 @@ static void Database_free(void *ptr) {
|
|
36
51
|
|
37
52
|
static const rb_data_type_t Database_type = {
|
38
53
|
"Database",
|
39
|
-
{Database_mark, Database_free, Database_size,},
|
54
|
+
{Database_mark, Database_free, Database_size, Database_compact},
|
40
55
|
0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED
|
41
56
|
};
|
42
57
|
|
@@ -85,14 +100,40 @@ default_flags:
|
|
85
100
|
return SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
|
86
101
|
}
|
87
102
|
|
103
|
+
VALUE Database_execute(int argc, VALUE *argv, VALUE self);
|
104
|
+
|
105
|
+
void Database_apply_opts(VALUE self, Database_t *db, VALUE opts) {
|
106
|
+
VALUE value = Qnil;
|
107
|
+
|
108
|
+
value = rb_hash_aref(opts, SYM_gvl_release_threshold);
|
109
|
+
if (!NIL_P(value)) db->gvl_release_threshold = NUM2INT(value);
|
110
|
+
|
111
|
+
value = rb_hash_aref(opts, SYM_wal_journal_mode);
|
112
|
+
if (RTEST(value)) {
|
113
|
+
value = rb_str_new_literal("PRAGMA journal_mode=wal");
|
114
|
+
Database_execute(1, &value, self);
|
115
|
+
}
|
116
|
+
|
117
|
+
value = rb_hash_aref(opts, SYM_synchronous);
|
118
|
+
if (RTEST(value)) {
|
119
|
+
value = rb_str_new_literal("PRAGMA synchronous=1");
|
120
|
+
Database_execute(1, &value, self);
|
121
|
+
}
|
122
|
+
|
123
|
+
RB_GC_GUARD(value);
|
124
|
+
}
|
125
|
+
|
88
126
|
/* Initializes a new SQLite database with the given path and options.
|
89
127
|
*
|
90
128
|
* @overload initialize(path)
|
91
129
|
* @param path [String] file path (or ':memory:' for memory database)
|
92
130
|
* @return [void]
|
93
|
-
* @overload initialize(path, read_only:
|
131
|
+
* @overload initialize(path, gvl_release_threshold: , read_only: , synchronous: , wal_journal_mode: )
|
94
132
|
* @param path [String] file path (or ':memory:' for memory database)
|
133
|
+
* @param gvl_release_threshold [Integer] GVL release threshold
|
95
134
|
* @param read_only [boolean] true for opening the database for reading only
|
135
|
+
* @param synchronous [boolean] true to set PRAGMA synchronous=1
|
136
|
+
* @param wal_journal_mode [boolean] true to set PRAGMA journal_mode=wal
|
96
137
|
* @return [void]
|
97
138
|
*/
|
98
139
|
VALUE Database_initialize(int argc, VALUE *argv, VALUE self) {
|
@@ -124,9 +165,12 @@ VALUE Database_initialize(int argc, VALUE *argv, VALUE self) {
|
|
124
165
|
}
|
125
166
|
#endif
|
126
167
|
|
127
|
-
db->
|
168
|
+
db->trace_proc = Qnil;
|
169
|
+
db->progress_handler_proc = Qnil;
|
128
170
|
db->gvl_release_threshold = DEFAULT_GVL_RELEASE_THRESHOLD;
|
129
171
|
|
172
|
+
if (!NIL_P(opts)) Database_apply_opts(self, db, opts);
|
173
|
+
|
130
174
|
return Qnil;
|
131
175
|
}
|
132
176
|
|
@@ -170,6 +214,10 @@ VALUE Database_closed_p(VALUE self) {
|
|
170
214
|
return db->sqlite3_db ? Qfalse : Qtrue;
|
171
215
|
}
|
172
216
|
|
217
|
+
inline enum gvl_mode Database_prepare_gvl_mode(Database_t *db) {
|
218
|
+
return db->gvl_release_threshold < 0 ? GVL_HOLD : GVL_RELEASE;
|
219
|
+
}
|
220
|
+
|
173
221
|
static inline VALUE Database_perform_query(int argc, VALUE *argv, VALUE self, VALUE (*call)(query_ctx *)) {
|
174
222
|
Database_t *db = self_to_open_database(self);
|
175
223
|
sqlite3_stmt *stmt;
|
@@ -180,9 +228,8 @@ static inline VALUE Database_perform_query(int argc, VALUE *argv, VALUE self, VA
|
|
180
228
|
sql = rb_funcall(argv[0], ID_strip, 0);
|
181
229
|
if (RSTRING_LEN(sql) == 0) return Qnil;
|
182
230
|
|
183
|
-
|
184
|
-
|
185
|
-
prepare_multi_stmt(db->sqlite3_db, &stmt, sql);
|
231
|
+
TRACE_SQL(db, sql);
|
232
|
+
prepare_multi_stmt(DB_GVL_MODE(db), db->sqlite3_db, &stmt, sql);
|
186
233
|
RB_GC_GUARD(sql);
|
187
234
|
|
188
235
|
bind_all_parameters(stmt, argc - 1, argv + 1);
|
@@ -342,30 +389,150 @@ VALUE Database_execute(int argc, VALUE *argv, VALUE self) {
|
|
342
389
|
}
|
343
390
|
|
344
391
|
/* call-seq:
|
345
|
-
* db.
|
392
|
+
* db.batch_execute(sql, params_array) -> changes
|
393
|
+
* db.batch_execute(sql, enumerable) -> changes
|
394
|
+
* db.batch_execute(sql, callable) -> changes
|
395
|
+
*
|
396
|
+
* Executes the given query for each list of parameters in the paramter source.
|
397
|
+
* If an enumerable is given, it is iterated and each of its values is used as
|
398
|
+
* the parameters for running the query. If a callable is given, it is called
|
399
|
+
* repeatedly and each of its return values is used as the parameters, until nil
|
400
|
+
* is returned.
|
346
401
|
*
|
347
|
-
*
|
348
|
-
*
|
349
|
-
* multiple records.
|
402
|
+
* Returns the number of changes effected. This method is designed for inserting
|
403
|
+
* multiple records or performing other mass operations.
|
350
404
|
*
|
351
405
|
* records = [
|
352
406
|
* [1, 2, 3],
|
353
407
|
* [4, 5, 6]
|
354
408
|
* ]
|
355
|
-
* db.
|
409
|
+
* db.batch_execute('insert into foo values (?, ?, ?)', records)
|
356
410
|
*
|
411
|
+
* source = [
|
412
|
+
* [1, 2, 3],
|
413
|
+
* [4, 5, 6]
|
414
|
+
* ]
|
415
|
+
* db.batch_execute('insert into foo values (?, ?, ?)', -> { records.shift })
|
416
|
+
*
|
417
|
+
* @param sql [String] query SQL
|
418
|
+
* @param parameters [Array<Array, Hash>, Enumerable, Enumerator, Callable] parameters to run query with
|
419
|
+
* @return [Integer] Total number of changes effected
|
357
420
|
*/
|
358
|
-
VALUE
|
421
|
+
VALUE Database_batch_execute(VALUE self, VALUE sql, VALUE parameters) {
|
359
422
|
Database_t *db = self_to_open_database(self);
|
360
423
|
sqlite3_stmt *stmt;
|
361
424
|
|
362
425
|
if (RSTRING_LEN(sql) == 0) return Qnil;
|
363
426
|
|
364
|
-
|
365
|
-
|
366
|
-
|
427
|
+
prepare_single_stmt(DB_GVL_MODE(db), db->sqlite3_db, &stmt, sql);
|
428
|
+
query_ctx ctx = QUERY_CTX(self, db, stmt, parameters, QUERY_MULTI_ROW, ALL_ROWS);
|
429
|
+
|
430
|
+
return rb_ensure(SAFE(safe_batch_execute), (VALUE)&ctx, SAFE(cleanup_stmt), (VALUE)&ctx);
|
431
|
+
}
|
432
|
+
|
433
|
+
/* call-seq:
|
434
|
+
* db.batch_query(sql, params_array) -> rows
|
435
|
+
* db.batch_query(sql, enumerable) -> rows
|
436
|
+
* db.batch_query(sql, callable) -> rows
|
437
|
+
* db.batch_query(sql, params_array) { |rows| ... } -> changes
|
438
|
+
* db.batch_query(sql, enumerable) { |rows| ... } -> changes
|
439
|
+
* db.batch_query(sql, callable) { |rows| ... } -> changes
|
440
|
+
*
|
441
|
+
* Executes the given query for each list of parameters in the given paramter
|
442
|
+
* source. If a block is given, it is called with the resulting rows for each
|
443
|
+
* invocation of the query, and the total number of changes is returned.
|
444
|
+
* Otherwise, an array containing the resulting rows for each invocation is
|
445
|
+
* returned.
|
446
|
+
*
|
447
|
+
* records = [
|
448
|
+
* [1, 2],
|
449
|
+
* [3, 4]
|
450
|
+
* ]
|
451
|
+
* db.batch_query('insert into foo values (?, ?) returning bar, baz', records)
|
452
|
+
* #=> [{ bar: 1, baz: 2 }, { bar: 3, baz: 4}]
|
453
|
+
* *
|
454
|
+
* @param sql [String] query SQL
|
455
|
+
* @param parameters [Array<Array, Hash>, Enumerable, Enumerator, Callable] parameters to run query with
|
456
|
+
* @return [Array<Hash>, Integer] Total number of changes effected
|
457
|
+
*/
|
458
|
+
VALUE Database_batch_query(VALUE self, VALUE sql, VALUE parameters) {
|
459
|
+
Database_t *db = self_to_open_database(self);
|
460
|
+
sqlite3_stmt *stmt;
|
461
|
+
|
462
|
+
prepare_single_stmt(DB_GVL_MODE(db), db->sqlite3_db, &stmt, sql);
|
463
|
+
query_ctx ctx = QUERY_CTX(self, db, stmt, parameters, QUERY_MULTI_ROW, ALL_ROWS);
|
464
|
+
|
465
|
+
return rb_ensure(SAFE(safe_batch_query), (VALUE)&ctx, SAFE(cleanup_stmt), (VALUE)&ctx);
|
466
|
+
}
|
467
|
+
|
468
|
+
/* call-seq:
|
469
|
+
* db.batch_query_ary(sql, params_array) -> rows
|
470
|
+
* db.batch_query_ary(sql, enumerable) -> rows
|
471
|
+
* db.batch_query_ary(sql, callable) -> rows
|
472
|
+
* db.batch_query_ary(sql, params_array) { |rows| ... } -> changes
|
473
|
+
* db.batch_query_ary(sql, enumerable) { |rows| ... } -> changes
|
474
|
+
* db.batch_query_ary(sql, callable) { |rows| ... } -> changes
|
475
|
+
*
|
476
|
+
* Executes the given query for each list of parameters in the given paramter
|
477
|
+
* source. If a block is given, it is called with the resulting rows for each
|
478
|
+
* invocation of the query, and the total number of changes is returned.
|
479
|
+
* Otherwise, an array containing the resulting rows for each invocation is
|
480
|
+
* returned. Rows are represented as arrays.
|
481
|
+
*
|
482
|
+
* records = [
|
483
|
+
* [1, 2],
|
484
|
+
* [3, 4]
|
485
|
+
* ]
|
486
|
+
* db.batch_query_ary('insert into foo values (?, ?) returning bar, baz', records)
|
487
|
+
* #=> [[1, 2], [3, 4]]
|
488
|
+
* *
|
489
|
+
* @param sql [String] query SQL
|
490
|
+
* @param parameters [Array<Array, Hash>, Enumerable, Enumerator, Callable] parameters to run query with
|
491
|
+
* @return [Array<Array>, Integer] Total number of changes effected
|
492
|
+
*/
|
493
|
+
VALUE Database_batch_query_ary(VALUE self, VALUE sql, VALUE parameters) {
|
494
|
+
Database_t *db = self_to_open_database(self);
|
495
|
+
sqlite3_stmt *stmt;
|
496
|
+
|
497
|
+
prepare_single_stmt(DB_GVL_MODE(db), db->sqlite3_db, &stmt, sql);
|
498
|
+
query_ctx ctx = QUERY_CTX(self, db, stmt, parameters, QUERY_MULTI_ROW, ALL_ROWS);
|
499
|
+
|
500
|
+
return rb_ensure(SAFE(safe_batch_query_ary), (VALUE)&ctx, SAFE(cleanup_stmt), (VALUE)&ctx);
|
501
|
+
}
|
502
|
+
|
503
|
+
/* call-seq:
|
504
|
+
* db.batch_query_single_column(sql, params_array) -> rows
|
505
|
+
* db.batch_query_single_column(sql, enumerable) -> rows
|
506
|
+
* db.batch_query_single_column(sql, callable) -> rows
|
507
|
+
* db.batch_query_single_column(sql, params_array) { |rows| ... } -> changes
|
508
|
+
* db.batch_query_single_column(sql, enumerable) { |rows| ... } -> changes
|
509
|
+
* db.batch_query_single_column(sql, callable) { |rows| ... } -> changes
|
510
|
+
*
|
511
|
+
* Executes the given query for each list of parameters in the given paramter
|
512
|
+
* source. If a block is given, it is called with the resulting rows for each
|
513
|
+
* invocation of the query, and the total number of changes is returned.
|
514
|
+
* Otherwise, an array containing the resulting rows for each invocation is
|
515
|
+
* returned. Rows are single values.
|
516
|
+
*
|
517
|
+
* records = [
|
518
|
+
* [1, 2],
|
519
|
+
* [3, 4]
|
520
|
+
* ]
|
521
|
+
* db.batch_query_ary('insert into foo values (?, ?) returning baz', records)
|
522
|
+
* #=> [2, 4]
|
523
|
+
* *
|
524
|
+
* @param sql [String] query SQL
|
525
|
+
* @param parameters [Array<Array, Hash>, Enumerable, Enumerator, Callable] parameters to run query with
|
526
|
+
* @return [Array<any>, Integer] Total number of changes effected
|
527
|
+
*/
|
528
|
+
VALUE Database_batch_query_single_column(VALUE self, VALUE sql, VALUE parameters) {
|
529
|
+
Database_t *db = self_to_open_database(self);
|
530
|
+
sqlite3_stmt *stmt;
|
531
|
+
|
532
|
+
prepare_single_stmt(DB_GVL_MODE(db), db->sqlite3_db, &stmt, sql);
|
533
|
+
query_ctx ctx = QUERY_CTX(self, db, stmt, parameters, QUERY_MULTI_ROW, ALL_ROWS);
|
367
534
|
|
368
|
-
return rb_ensure(SAFE(
|
535
|
+
return rb_ensure(SAFE(safe_batch_query_single_column), (VALUE)&ctx, SAFE(cleanup_stmt), (VALUE)&ctx);
|
369
536
|
}
|
370
537
|
|
371
538
|
/* call-seq:
|
@@ -449,8 +616,10 @@ VALUE Database_load_extension(VALUE self, VALUE path) {
|
|
449
616
|
|
450
617
|
/* call-seq:
|
451
618
|
* db.prepare(sql) -> Extralite::Query
|
619
|
+
* db.prepare(sql, ...) -> Extralite::Query
|
452
620
|
*
|
453
|
-
* Creates a prepared statement with the given SQL query.
|
621
|
+
* Creates a prepared statement with the given SQL query. If query parameters
|
622
|
+
* are given, they are bound to the query.
|
454
623
|
*/
|
455
624
|
VALUE Database_prepare(int argc, VALUE *argv, VALUE self) {
|
456
625
|
rb_check_arity(argc, 1, UNLIMITED_ARGUMENTS);
|
@@ -697,7 +866,127 @@ VALUE Database_total_changes(VALUE self) {
|
|
697
866
|
VALUE Database_trace(VALUE self) {
|
698
867
|
Database_t *db = self_to_open_database(self);
|
699
868
|
|
700
|
-
RB_OBJ_WRITE(self, &db->
|
869
|
+
RB_OBJ_WRITE(self, &db->trace_proc, rb_block_given_p() ? rb_block_proc() : Qnil);
|
870
|
+
return self;
|
871
|
+
}
|
872
|
+
|
873
|
+
#ifdef EXTRALITE_ENABLE_CHANGESET
|
874
|
+
/* Tracks changes to the database and returns a changeset. The changeset can
|
875
|
+
* then be used to store the changes to a file, apply them to another database,
|
876
|
+
* or undo the changes. The given table names specify which tables should be
|
877
|
+
* tracked for changes. Passing a value of nil causes all tables to be tracked.
|
878
|
+
*
|
879
|
+
* changeset = db.track_changes(:foo, :bar) do
|
880
|
+
* perform_a_bunch_of_queries
|
881
|
+
* end
|
882
|
+
*
|
883
|
+
* File.open('my.changes', 'w+') { |f| f << changeset.to_blob }
|
884
|
+
*
|
885
|
+
* @param table [String, Symbol] table to track
|
886
|
+
* @return [Extralite::Changeset] changeset
|
887
|
+
*/
|
888
|
+
VALUE Database_track_changes(int argc, VALUE *argv, VALUE self) {
|
889
|
+
self_to_open_database(self);
|
890
|
+
|
891
|
+
VALUE changeset = rb_funcall(cChangeset, ID_new, 0);
|
892
|
+
VALUE tables = rb_ary_new_from_values(argc, argv);
|
893
|
+
|
894
|
+
rb_funcall(changeset, ID_track, 2, self, tables);
|
895
|
+
|
896
|
+
RB_GC_GUARD(changeset);
|
897
|
+
RB_GC_GUARD(tables);
|
898
|
+
return changeset;
|
899
|
+
}
|
900
|
+
#endif
|
901
|
+
|
902
|
+
int Database_progress_handler(void *ptr) {
|
903
|
+
Database_t *db = (Database_t *)ptr;
|
904
|
+
rb_funcall(db->progress_handler_proc, ID_call, 0);
|
905
|
+
return 0;
|
906
|
+
}
|
907
|
+
|
908
|
+
int Database_busy_handler(void *ptr, int v) {
|
909
|
+
Database_t *db = (Database_t *)ptr;
|
910
|
+
rb_funcall(db->progress_handler_proc, ID_call, 0);
|
911
|
+
return 1;
|
912
|
+
}
|
913
|
+
|
914
|
+
void Database_reset_progress_handler(VALUE self, Database_t *db) {
|
915
|
+
RB_OBJ_WRITE(self, &db->progress_handler_proc, Qnil);
|
916
|
+
sqlite3_progress_handler(db->sqlite3_db, 0, NULL, NULL);
|
917
|
+
sqlite3_busy_handler(db->sqlite3_db, NULL, NULL);
|
918
|
+
}
|
919
|
+
|
920
|
+
/* call-seq:
|
921
|
+
* db.on_progress(period) { } -> db
|
922
|
+
* db.on_progress(0) -> db
|
923
|
+
*
|
924
|
+
* Installs or removes a progress handler that will be executed periodically
|
925
|
+
* while a query is running. This method can be used to support switching
|
926
|
+
* between fibers and threads or implementing timeouts for running queries.
|
927
|
+
*
|
928
|
+
* The given period parameter specifies the approximate number of SQLite virtual
|
929
|
+
* machine instructions that are evaluated between successive invocations of the
|
930
|
+
* progress handler. A period of less than 1 removes the progress handler.
|
931
|
+
*
|
932
|
+
* The progress handler is called also when the database is busy. This lets the
|
933
|
+
* application perform work while waiting for the database to become unlocked,
|
934
|
+
* or implement a timeout. Note that setting the database's busy_timeout _after_
|
935
|
+
* setting a progress handler may lead to undefined behaviour in a concurrent
|
936
|
+
* application.
|
937
|
+
*
|
938
|
+
* When the progress handler is set, the gvl release threshold value is set to
|
939
|
+
* -1, which means that the GVL will not be released at all when preparing or
|
940
|
+
* running queries. It is the application's responsibility to let other threads
|
941
|
+
* or fibers run by calling e.g. Thread.pass:
|
942
|
+
*
|
943
|
+
* db.on_progress(1000) do
|
944
|
+
* do_something_interesting
|
945
|
+
* Thread.pass # let other threads run
|
946
|
+
* end
|
947
|
+
*
|
948
|
+
* Note that the progress handler is set globally for the database and that
|
949
|
+
* Extralite does provide any hooks for telling which queries are currently
|
950
|
+
* running or at what time they were started. This means that you'll need
|
951
|
+
* to wrap the stock #query_xxx and #execute methods with your own code that
|
952
|
+
* calculates timeouts, for example:
|
953
|
+
*
|
954
|
+
* def setup_progress_handler
|
955
|
+
* @db.on_progress(1000) do
|
956
|
+
* raise TimeoutError if Time.now - @t0 >= @timeout
|
957
|
+
* Thread.pass
|
958
|
+
* end
|
959
|
+
* end
|
960
|
+
*
|
961
|
+
* def query(sql, *)
|
962
|
+
* @t0 = Time.now
|
963
|
+
* @db.query(sql, *)
|
964
|
+
* end
|
965
|
+
*
|
966
|
+
* If the gvl release threshold is set to a value equal to or larger than 0
|
967
|
+
* after setting the progress handler, the progress handler will be reset.
|
968
|
+
*
|
969
|
+
* @param period [Integer] progress handler period
|
970
|
+
* @returns [Extralite::Database] database
|
971
|
+
*/
|
972
|
+
VALUE Database_on_progress(VALUE self, VALUE period) {
|
973
|
+
Database_t *db = self_to_open_database(self);
|
974
|
+
int period_int = NUM2INT(period);
|
975
|
+
|
976
|
+
if (period_int > 0 && rb_block_given_p()) {
|
977
|
+
RB_OBJ_WRITE(self, &db->progress_handler_proc, rb_block_proc());
|
978
|
+
db->gvl_release_threshold = -1;
|
979
|
+
|
980
|
+
sqlite3_progress_handler(db->sqlite3_db, period_int, &Database_progress_handler, db);
|
981
|
+
sqlite3_busy_handler(db->sqlite3_db, &Database_busy_handler, db);
|
982
|
+
}
|
983
|
+
else {
|
984
|
+
RB_OBJ_WRITE(self, &db->progress_handler_proc, Qnil);
|
985
|
+
db->gvl_release_threshold = DEFAULT_GVL_RELEASE_THRESHOLD;
|
986
|
+
sqlite3_progress_handler(db->sqlite3_db, 0, NULL, NULL);
|
987
|
+
sqlite3_busy_handler(db->sqlite3_db, NULL, NULL);
|
988
|
+
}
|
989
|
+
|
701
990
|
return self;
|
702
991
|
}
|
703
992
|
|
@@ -777,8 +1066,16 @@ VALUE Database_gvl_release_threshold_set(VALUE self, VALUE value) {
|
|
777
1066
|
|
778
1067
|
switch (TYPE(value)) {
|
779
1068
|
case T_FIXNUM:
|
780
|
-
|
781
|
-
|
1069
|
+
{
|
1070
|
+
int value_int = NUM2INT(value);
|
1071
|
+
if (value_int < -1)
|
1072
|
+
rb_raise(eArgumentError, "Invalid GVL release threshold value (expect integer >= -1)");
|
1073
|
+
|
1074
|
+
if (value_int > -1 && !NIL_P(db->progress_handler_proc))
|
1075
|
+
Database_reset_progress_handler(self, db);
|
1076
|
+
db->gvl_release_threshold = value_int;
|
1077
|
+
break;
|
1078
|
+
}
|
782
1079
|
case T_NIL:
|
783
1080
|
db->gvl_release_threshold = DEFAULT_GVL_RELEASE_THRESHOLD;
|
784
1081
|
break;
|
@@ -811,7 +1108,10 @@ void Init_ExtraliteDatabase(void) {
|
|
811
1108
|
#endif
|
812
1109
|
|
813
1110
|
rb_define_method(cDatabase, "execute", Database_execute, -1);
|
814
|
-
rb_define_method(cDatabase, "
|
1111
|
+
rb_define_method(cDatabase, "batch_execute", Database_batch_execute, 2);
|
1112
|
+
rb_define_method(cDatabase, "batch_query", Database_batch_query, 2);
|
1113
|
+
rb_define_method(cDatabase, "batch_query_ary", Database_batch_query_ary, 2);
|
1114
|
+
rb_define_method(cDatabase, "batch_query_single_column", Database_batch_query_single_column, 2);
|
815
1115
|
rb_define_method(cDatabase, "filename", Database_filename, -1);
|
816
1116
|
rb_define_method(cDatabase, "gvl_release_threshold", Database_gvl_release_threshold_get, 0);
|
817
1117
|
rb_define_method(cDatabase, "gvl_release_threshold=", Database_gvl_release_threshold_set, 1);
|
@@ -820,6 +1120,7 @@ void Init_ExtraliteDatabase(void) {
|
|
820
1120
|
rb_define_method(cDatabase, "interrupt", Database_interrupt, 0);
|
821
1121
|
rb_define_method(cDatabase, "last_insert_rowid", Database_last_insert_rowid, 0);
|
822
1122
|
rb_define_method(cDatabase, "limit", Database_limit, -1);
|
1123
|
+
rb_define_method(cDatabase, "on_progress", Database_on_progress, 1);
|
823
1124
|
rb_define_method(cDatabase, "prepare", Database_prepare, -1);
|
824
1125
|
rb_define_method(cDatabase, "query", Database_query_hash, -1);
|
825
1126
|
rb_define_method(cDatabase, "query_ary", Database_query_ary, -1);
|
@@ -831,6 +1132,11 @@ void Init_ExtraliteDatabase(void) {
|
|
831
1132
|
rb_define_method(cDatabase, "status", Database_status, -1);
|
832
1133
|
rb_define_method(cDatabase, "total_changes", Database_total_changes, 0);
|
833
1134
|
rb_define_method(cDatabase, "trace", Database_trace, 0);
|
1135
|
+
|
1136
|
+
#ifdef EXTRALITE_ENABLE_CHANGESET
|
1137
|
+
rb_define_method(cDatabase, "track_changes", Database_track_changes, -1);
|
1138
|
+
#endif
|
1139
|
+
|
834
1140
|
rb_define_method(cDatabase, "transaction_active?", Database_transaction_active_p, 0);
|
835
1141
|
|
836
1142
|
#ifdef HAVE_SQLITE3_LOAD_EXTENSION
|
@@ -849,12 +1155,22 @@ void Init_ExtraliteDatabase(void) {
|
|
849
1155
|
|
850
1156
|
ID_bind = rb_intern("bind");
|
851
1157
|
ID_call = rb_intern("call");
|
1158
|
+
ID_each = rb_intern("each");
|
852
1159
|
ID_keys = rb_intern("keys");
|
853
1160
|
ID_new = rb_intern("new");
|
854
1161
|
ID_strip = rb_intern("strip");
|
1162
|
+
ID_to_s = rb_intern("to_s");
|
1163
|
+
ID_track = rb_intern("track");
|
1164
|
+
|
1165
|
+
SYM_gvl_release_threshold = ID2SYM(rb_intern("gvl_release_threshold"));
|
1166
|
+
SYM_read_only = ID2SYM(rb_intern("read_only"));
|
1167
|
+
SYM_synchronous = ID2SYM(rb_intern("synchronous"));
|
1168
|
+
SYM_wal_journal_mode = ID2SYM(rb_intern("wal_journal_mode"));
|
855
1169
|
|
856
|
-
|
1170
|
+
rb_gc_register_mark_object(SYM_gvl_release_threshold);
|
857
1171
|
rb_gc_register_mark_object(SYM_read_only);
|
1172
|
+
rb_gc_register_mark_object(SYM_synchronous);
|
1173
|
+
rb_gc_register_mark_object(SYM_wal_journal_mode);
|
858
1174
|
|
859
1175
|
UTF8_ENCODING = rb_utf8_encoding();
|
860
1176
|
}
|
@@ -6,11 +6,17 @@ $CFLAGS << ' -Wno-undef'
|
|
6
6
|
$CFLAGS << ' -Wno-discarded-qualifiers'
|
7
7
|
$CFLAGS << ' -Wno-unused-function'
|
8
8
|
|
9
|
-
|
10
|
-
$defs <<
|
11
|
-
$defs <<
|
9
|
+
# enable the session extension
|
10
|
+
$defs << '-DSQLITE_ENABLE_SESSION'
|
11
|
+
$defs << '-DSQLITE_ENABLE_PREUPDATE_HOOK'
|
12
|
+
$defs << '-DEXTRALITE_ENABLE_CHANGESET'
|
12
13
|
|
13
|
-
|
14
|
+
$defs << '-DHAVE_SQLITE3_ENABLE_LOAD_EXTENSION'
|
15
|
+
$defs << '-DHAVE_SQLITE3_LOAD_EXTENSION'
|
16
|
+
$defs << '-DHAVE_SQLITE3_PREPARE_V2'
|
17
|
+
$defs << '-DHAVE_SQLITE3_ERROR_OFFSET'
|
18
|
+
$defs << '-DHAVE_SQLITE3SESSION_CHANGESET'
|
14
19
|
|
20
|
+
have_func('usleep')
|
15
21
|
dir_config('extralite_ext')
|
16
22
|
create_makefile('extralite_ext')
|
data/ext/extralite/extconf.rb
CHANGED
@@ -52,43 +52,47 @@ else
|
|
52
52
|
$CFLAGS << ' -W3'
|
53
53
|
end
|
54
54
|
|
55
|
-
if RUBY_VERSION < '2.7'
|
56
|
-
$CFLAGS << ' -DTAINTING_SUPPORT'
|
57
|
-
end
|
58
|
-
|
59
55
|
# @!visibility private
|
60
56
|
def asplode missing
|
61
57
|
if RUBY_PLATFORM =~ /mingw|mswin/
|
62
58
|
abort "#{missing} is missing. Install SQLite3 from " +
|
63
59
|
"http://www.sqlite.org/ first."
|
64
60
|
else
|
65
|
-
abort
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
end
|
61
|
+
abort <<~error
|
62
|
+
#{missing} is missing. Try 'brew install sqlite3',
|
63
|
+
'yum install sqlite-devel' or 'apt-get install libsqlite3-dev'
|
64
|
+
and check your shared library search path (the location where
|
65
|
+
your sqlite3 shared library is located).
|
66
|
+
error
|
72
67
|
end
|
68
|
+
end
|
73
69
|
|
74
|
-
|
75
|
-
|
70
|
+
asplode('sqlite3.h') unless find_header('sqlite3.h')
|
71
|
+
# find_library 'pthread', 'pthread_create' # 1.8 support. *shrug*
|
76
72
|
|
77
|
-
|
73
|
+
have_library 'dl' # for static builds
|
78
74
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
75
|
+
if with_config('sqlcipher')
|
76
|
+
asplode('sqlcipher') unless find_library 'sqlcipher', 'sqlite3_libversion_number'
|
77
|
+
else
|
78
|
+
asplode('sqlite3') unless find_library 'sqlite3', 'sqlite3_libversion_number'
|
79
|
+
end
|
80
|
+
|
81
|
+
have_func('sqlite3_enable_load_extension')
|
82
|
+
have_func('sqlite3_load_extension')
|
83
|
+
have_func('sqlite3_prepare_v2')
|
84
|
+
have_func('sqlite3_error_offset')
|
85
|
+
have_func('sqlite3session_changeset')
|
86
|
+
|
87
|
+
if have_type('sqlite3_session', 'sqlite.h')
|
88
|
+
$defs << '-DEXTRALITE_ENABLE_CHANGESET'
|
89
|
+
end
|
90
|
+
# have_macro('__SQLITESESSION_H_')
|
91
|
+
# have_macro('SQLITE3_H')
|
84
92
|
|
85
|
-
have_func('sqlite3_enable_load_extension')
|
86
|
-
have_func('sqlite3_load_extension')
|
87
|
-
have_func('sqlite3_prepare_v2')
|
88
|
-
have_func('sqlite3_error_offset')
|
89
93
|
|
90
|
-
|
94
|
+
$defs << "-DEXTRALITE_NO_BUNDLE"
|
91
95
|
|
92
|
-
|
93
|
-
|
94
|
-
|
96
|
+
dir_config('extralite_ext')
|
97
|
+
create_makefile('extralite_ext')
|
98
|
+
end
|
data/ext/extralite/extralite.h
CHANGED
@@ -23,6 +23,7 @@
|
|
23
23
|
extern VALUE cDatabase;
|
24
24
|
extern VALUE cQuery;
|
25
25
|
extern VALUE cIterator;
|
26
|
+
extern VALUE cChangeset;
|
26
27
|
extern VALUE cBlob;
|
27
28
|
|
28
29
|
extern VALUE cError;
|
@@ -32,17 +33,21 @@ extern VALUE cInterruptError;
|
|
32
33
|
extern VALUE cParameterError;
|
33
34
|
|
34
35
|
extern ID ID_call;
|
36
|
+
extern ID ID_each;
|
35
37
|
extern ID ID_keys;
|
36
38
|
extern ID ID_new;
|
37
39
|
extern ID ID_strip;
|
40
|
+
extern ID ID_to_s;
|
41
|
+
extern ID ID_track;
|
38
42
|
|
39
|
-
extern VALUE SYM_hash;
|
40
43
|
extern VALUE SYM_ary;
|
44
|
+
extern VALUE SYM_hash;
|
41
45
|
extern VALUE SYM_single_column;
|
42
46
|
|
43
47
|
typedef struct {
|
44
48
|
sqlite3 *sqlite3_db;
|
45
|
-
VALUE
|
49
|
+
VALUE trace_proc;
|
50
|
+
VALUE progress_handler_proc;
|
46
51
|
int gvl_release_threshold;
|
47
52
|
} Database_t;
|
48
53
|
|
@@ -67,6 +72,13 @@ typedef struct {
|
|
67
72
|
enum iterator_mode mode;
|
68
73
|
} Iterator_t;
|
69
74
|
|
75
|
+
#ifdef EXTRALITE_ENABLE_CHANGESET
|
76
|
+
typedef struct {
|
77
|
+
int changeset_len;
|
78
|
+
void *changeset_ptr;
|
79
|
+
} Changeset_t;
|
80
|
+
#endif
|
81
|
+
|
70
82
|
enum query_mode {
|
71
83
|
QUERY_YIELD,
|
72
84
|
QUERY_MULTI_ROW,
|
@@ -96,11 +108,18 @@ enum gvl_mode {
|
|
96
108
|
#define MULTI_ROW_P(mode) (mode == QUERY_MULTI_ROW)
|
97
109
|
#define QUERY_CTX(self, db, stmt, params, mode, max_rows) \
|
98
110
|
{ self, db->sqlite3_db, stmt, params, mode, max_rows, 0, db->gvl_release_threshold, 0 }
|
111
|
+
#define TRACE_SQL(db, sql) \
|
112
|
+
if (db->trace_proc != Qnil) rb_funcall(db->trace_proc, ID_call, 1, sql);
|
113
|
+
|
99
114
|
#define DEFAULT_GVL_RELEASE_THRESHOLD 1000
|
100
115
|
|
116
|
+
|
101
117
|
extern rb_encoding *UTF8_ENCODING;
|
102
118
|
|
103
|
-
VALUE
|
119
|
+
VALUE safe_batch_execute(query_ctx *ctx);
|
120
|
+
VALUE safe_batch_query(query_ctx *ctx);
|
121
|
+
VALUE safe_batch_query_ary(query_ctx *ctx);
|
122
|
+
VALUE safe_batch_query_single_column(query_ctx *ctx);
|
104
123
|
VALUE safe_query_ary(query_ctx *ctx);
|
105
124
|
VALUE safe_query_changes(query_ctx *ctx);
|
106
125
|
VALUE safe_query_columns(query_ctx *ctx);
|
@@ -121,14 +140,15 @@ VALUE Query_to_a_hash(VALUE self);
|
|
121
140
|
VALUE Query_to_a_ary(VALUE self);
|
122
141
|
VALUE Query_to_a_single_column(VALUE self);
|
123
142
|
|
124
|
-
void prepare_single_stmt(sqlite3 *db, sqlite3_stmt **stmt, VALUE sql);
|
125
|
-
void prepare_multi_stmt(sqlite3 *db, sqlite3_stmt **stmt, VALUE sql);
|
143
|
+
void prepare_single_stmt(enum gvl_mode mode, sqlite3 *db, sqlite3_stmt **stmt, VALUE sql);
|
144
|
+
void prepare_multi_stmt(enum gvl_mode mode, sqlite3 *db, sqlite3_stmt **stmt, VALUE sql);
|
126
145
|
void bind_all_parameters(sqlite3_stmt *stmt, int argc, VALUE *argv);
|
127
146
|
void bind_all_parameters_from_object(sqlite3_stmt *stmt, VALUE obj);
|
128
147
|
int stmt_iterate(query_ctx *ctx);
|
129
148
|
VALUE cleanup_stmt(query_ctx *ctx);
|
130
149
|
|
131
150
|
sqlite3 *Database_sqlite3_db(VALUE self);
|
151
|
+
enum gvl_mode Database_prepare_gvl_mode(Database_t *db);
|
132
152
|
Database_t *self_to_database(VALUE self);
|
133
153
|
|
134
154
|
void *gvl_call(enum gvl_mode mode, void *(*fn)(void *), void *data);
|