extralite 2.0 → 2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d399062a5652d11c0e969dfb74f6c998015980e8831e221d2aece7ddb6cfc83d
4
- data.tar.gz: '0759cd16b15124934c52d5e2a3f36620b165f5a205aaea707c3d957503038173'
3
+ metadata.gz: bd8cf56eff701ddf57586d6f7510d5959cdf81ff15e6a64c6ed8ff698f93b43b
4
+ data.tar.gz: 42000f8c83849577fd1672996969961357c5bbadb82d89d2168c74be7acfa303
5
5
  SHA512:
6
- metadata.gz: 62ad641bd7661d326c3ba83eff0e7ecf5befdd12d7267608e748d6a7cf586d96e9f870cd179989cabb230b822e9d245ff9d1ed8877632ca33c7577858a639ded
7
- data.tar.gz: 2984632a7bb790b354713396ac00856f0646c79aa22bb3a9bcd26995a127eb8cb86a5ca64efcbd91b1fcc8dd5a41f95e0bacefed7b5bacd477e9b689d79186a6
6
+ metadata.gz: 1159c965d8ce9bfade2c81de97b6ef311dcfdb84ce65eb85238aa6a23b29b786d31de2bb8f30f87a24a800b88d01475a7cb400b97245401d587bef365b140087
7
+ data.tar.gz: 74f975529a9efb7291524eaa4c285fadc6976377a607cd28e72cbd3010cbc9fd061264aabb9a5df1ce864f097ca330c9acd9dded62b8de7cc63c1217be88412a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ # 2.1 2023-07-11
2
+
3
+ - Implement `Database#execute`, `Query#execute` for data-manipulation queries
4
+ - Add option for opening databases for read only access
5
+
1
6
  # 2.0 2023-07-08
2
7
 
3
8
  - Fix Sequel migrations (#8)
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- extralite (2.0)
4
+ extralite (2.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/TODO.md CHANGED
@@ -1,5 +1,21 @@
1
- - Add option to open database in readonly mode, use sqlite3_open_v2
2
- https://sqlite.org/c3ref/open.html
3
-
4
1
  - Improve tracing
5
- - Add `#inspect` method for `Database`, `Query`, `Iterator` classes
2
+ - Transactions and savepoints:
3
+
4
+ - `DB#transaction {}` - does a `BEGIN..COMMIT` - non-reentrant!
5
+ - `DB#savepoint(name)` - creates a savepoint
6
+ - `DB#release(name)` - releases a savepoint
7
+ - `DB#rollback` - raises `Extralite::Rollback`, which is rescued by `DB#transaction`
8
+ - `DB#rollback_to(name)` - rolls back to a savepoint
9
+
10
+ - More database methods:
11
+
12
+ - `Database#quote`
13
+ - `Database#busy_timeout=` https://sqlite.org/c3ref/busy_timeout.html
14
+ - `Database#cache_flush` https://sqlite.org/c3ref/db_cacheflush.html
15
+ - `Database#release_memory` https://sqlite.org/c3ref/db_release_memory.html
16
+
17
+ - Security
18
+
19
+ - Enable extension loading by using
20
+ [SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION](https://www.sqlite.org/c3ref/c_dbconfig_defensive.html#sqlitedbconfigenableloadextension)
21
+ in order to prevent usage of `load_extension()` SQL function.
@@ -396,3 +396,8 @@ VALUE safe_execute_multi(query_ctx *ctx) {
396
396
  VALUE safe_query_columns(query_ctx *ctx) {
397
397
  return get_column_names(ctx->stmt, sqlite3_column_count(ctx->stmt));
398
398
  }
399
+
400
+ VALUE safe_query_changes(query_ctx *ctx) {
401
+ while (stmt_iterate(ctx));
402
+ return INT2FIX(sqlite3_changes(ctx->sqlite3_db));
403
+ }
@@ -6,6 +6,7 @@ VALUE cError;
6
6
  VALUE cSQLError;
7
7
  VALUE cBusyError;
8
8
  VALUE cInterruptError;
9
+ VALUE eArgumentError;
9
10
 
10
11
  ID ID_bind;
11
12
  ID ID_call;
@@ -14,6 +15,8 @@ ID ID_new;
14
15
  ID ID_strip;
15
16
  ID ID_to_s;
16
17
 
18
+ VALUE SYM_read_only;
19
+
17
20
  static size_t Database_size(const void *ptr) {
18
21
  return sizeof(Database_t);
19
22
  }
@@ -36,27 +39,21 @@ static VALUE Database_allocate(VALUE klass) {
36
39
  return TypedData_Wrap_Struct(klass, &Database_type, db);
37
40
  }
38
41
 
39
- #define GetDatabase(obj, database) \
40
- TypedData_Get_Struct((obj), Database_t, &Database_type, (database))
41
-
42
- // make sure the database is open
43
- #define GetOpenDatabase(obj, database) { \
44
- TypedData_Get_Struct((obj), Database_t, &Database_type, (database)); \
45
- if (!(database)->sqlite3_db) { \
46
- rb_raise(cError, "Database is closed"); \
47
- } \
42
+ inline Database_t *self_to_database(VALUE self) {
43
+ Database_t *db;
44
+ TypedData_Get_Struct(self, Database_t, &Database_type, db);
45
+ return db;
48
46
  }
49
47
 
50
- Database_t *Database_struct(VALUE self) {
51
- Database_t *db;
52
- GetDatabase(self, db);
48
+ inline Database_t *self_to_open_database(VALUE self) {
49
+ Database_t *db = self_to_database(self);
50
+ if (!(db)->sqlite3_db) rb_raise(cError, "Database is closed");
51
+
53
52
  return db;
54
53
  }
55
54
 
56
- sqlite3 *Database_sqlite3_db(VALUE self) {
57
- Database_t *db;
58
- GetDatabase(self, db);
59
- return db->sqlite3_db;
55
+ inline sqlite3 *Database_sqlite3_db(VALUE self) {
56
+ return self_to_database(self)->sqlite3_db;
60
57
  }
61
58
 
62
59
  /* call-seq:
@@ -69,18 +66,37 @@ VALUE Extralite_sqlite3_version(VALUE self) {
69
66
  return rb_str_new_cstr(sqlite3_version);
70
67
  }
71
68
 
72
- /* call-seq:
73
- * db.initialize(path)
69
+ static inline int db_open_flags_from_opts(VALUE opts) {
70
+ if (opts == Qnil) goto default_flags;
71
+
72
+ if (TYPE(opts) != T_HASH)
73
+ rb_raise(eArgumentError, "Expected hash as database initialization options");
74
+
75
+ VALUE read_only = rb_hash_aref(opts, SYM_read_only);
76
+ if (RTEST(read_only)) return SQLITE_OPEN_READONLY;
77
+ default_flags:
78
+ return SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
79
+ }
80
+
81
+ /* Initializes a new SQLite database with the given path and options.
74
82
  *
75
- * Initializes a new SQLite database with the given path.
83
+ * @overload initialize(path)
84
+ * @param path [String] file path (or ':memory:' for memory database)
85
+ * @return [void]
86
+ * @overload initialize(path, read_only: false)
87
+ * @param path [String] file path (or ':memory:' for memory database)
88
+ * @param read_only [boolean] true for opening the database for reading only
89
+ * @return [void]
76
90
  */
91
+ VALUE Database_initialize(int argc, VALUE *argv, VALUE self) {
92
+ Database_t *db = self_to_database(self);
93
+ VALUE path;
94
+ VALUE opts = Qnil;
77
95
 
78
- VALUE Database_initialize(VALUE self, VALUE path) {
79
- int rc;
80
- Database_t *db;
81
- GetDatabase(self, db);
96
+ rb_scan_args(argc, argv, "11", &path, &opts);
97
+ int flags = db_open_flags_from_opts(opts);
82
98
 
83
- rc = sqlite3_open(StringValueCStr(path), &db->sqlite3_db);
99
+ int rc = sqlite3_open_v2(StringValueCStr(path), &db->sqlite3_db, flags, NULL);
84
100
  if (rc) {
85
101
  sqlite3_close_v2(db->sqlite3_db);
86
102
  rb_raise(cError, "%s", sqlite3_errstr(rc));
@@ -106,6 +122,16 @@ VALUE Database_initialize(VALUE self, VALUE path) {
106
122
  return Qnil;
107
123
  }
108
124
 
125
+ /* Returns true if the database was open for read only access.
126
+ *
127
+ * @return [boolean] true if database is open for read only access
128
+ */
129
+ VALUE Database_read_only_p(VALUE self) {
130
+ Database_t *db = self_to_database(self);
131
+ int open = sqlite3_db_readonly(db->sqlite3_db, "main");
132
+ return (open == 1) ? Qtrue : Qfalse;
133
+ }
134
+
109
135
  /* call-seq:
110
136
  * db.close -> db
111
137
  *
@@ -113,8 +139,7 @@ VALUE Database_initialize(VALUE self, VALUE path) {
113
139
  */
114
140
  VALUE Database_close(VALUE self) {
115
141
  int rc;
116
- Database_t *db;
117
- GetDatabase(self, db);
142
+ Database_t *db = self_to_database(self);
118
143
 
119
144
  rc = sqlite3_close_v2(db->sqlite3_db);
120
145
  if (rc) {
@@ -133,14 +158,12 @@ VALUE Database_close(VALUE self) {
133
158
  * @return [bool] is database closed
134
159
  */
135
160
  VALUE Database_closed_p(VALUE self) {
136
- Database_t *db;
137
- GetDatabase(self, db);
138
-
161
+ Database_t *db = self_to_database(self);
139
162
  return db->sqlite3_db ? Qfalse : Qtrue;
140
163
  }
141
164
 
142
165
  static inline VALUE Database_perform_query(int argc, VALUE *argv, VALUE self, VALUE (*call)(query_ctx *)) {
143
- Database_t *db;
166
+ Database_t *db = self_to_open_database(self);
144
167
  sqlite3_stmt *stmt;
145
168
  VALUE sql;
146
169
 
@@ -150,7 +173,6 @@ static inline VALUE Database_perform_query(int argc, VALUE *argv, VALUE self, VA
150
173
  if (RSTRING_LEN(sql) == 0) return Qnil;
151
174
 
152
175
  // prepare query ctx
153
- GetOpenDatabase(self, db);
154
176
  if (db->trace_block != Qnil) rb_funcall(db->trace_block, ID_call, 1, sql);
155
177
  prepare_multi_stmt(db->sqlite3_db, &stmt, sql);
156
178
  RB_GC_GUARD(sql);
@@ -286,6 +308,31 @@ VALUE Database_query_single_value(int argc, VALUE *argv, VALUE self) {
286
308
  return Database_perform_query(argc, argv, self, safe_query_single_value);
287
309
  }
288
310
 
311
+ /* call-seq:
312
+ * db.execute(sql, *parameters) -> changes
313
+ *
314
+ * Runs a query returning the total changes effected. This method should be used
315
+ * for data- or schema-manipulation queries.
316
+ *
317
+ * Query parameters to be bound to placeholders in the query can be specified as
318
+ * a list of values or as a hash mapping parameter names to values. When
319
+ * parameters are given as an array, the query should specify parameters using
320
+ * `?`:
321
+ *
322
+ * db.execute('update foo set x = ? where y = ?', 42, 43)
323
+ *
324
+ * Named placeholders are specified using `:`. The placeholder values are
325
+ * specified using a hash, where keys are either strings are symbols. String
326
+ * keys can include or omit the `:` prefix. The following are equivalent:
327
+ *
328
+ * db.execute('update foo set x = :bar', bar: 42)
329
+ * db.execute('update foo set x = :bar', 'bar' => 42)
330
+ * db.execute('update foo set x = :bar', ':bar' => 42)
331
+ */
332
+ VALUE Database_execute(int argc, VALUE *argv, VALUE self) {
333
+ return Database_perform_query(argc, argv, self, safe_query_changes);
334
+ }
335
+
289
336
  /* call-seq:
290
337
  * db.execute_multi(sql, params_array) -> changes
291
338
  *
@@ -301,13 +348,12 @@ VALUE Database_query_single_value(int argc, VALUE *argv, VALUE self) {
301
348
  *
302
349
  */
303
350
  VALUE Database_execute_multi(VALUE self, VALUE sql, VALUE params_array) {
304
- Database_t *db;
351
+ Database_t *db = self_to_open_database(self);
305
352
  sqlite3_stmt *stmt;
306
353
 
307
354
  if (RSTRING_LEN(sql) == 0) return Qnil;
308
355
 
309
356
  // prepare query ctx
310
- GetOpenDatabase(self, db);
311
357
  prepare_single_stmt(db->sqlite3_db, &stmt, sql);
312
358
  query_ctx ctx = { self, db->sqlite3_db, stmt, params_array, QUERY_MODE(QUERY_MULTI_ROW), ALL_ROWS };
313
359
 
@@ -329,8 +375,7 @@ VALUE Database_columns(VALUE self, VALUE sql) {
329
375
  * Returns the rowid of the last inserted row.
330
376
  */
331
377
  VALUE Database_last_insert_rowid(VALUE self) {
332
- Database_t *db;
333
- GetOpenDatabase(self, db);
378
+ Database_t *db = self_to_open_database(self);
334
379
 
335
380
  return INT2FIX(sqlite3_last_insert_rowid(db->sqlite3_db));
336
381
  }
@@ -341,8 +386,7 @@ VALUE Database_last_insert_rowid(VALUE self) {
341
386
  * Returns the number of changes made to the database by the last operation.
342
387
  */
343
388
  VALUE Database_changes(VALUE self) {
344
- Database_t *db;
345
- GetOpenDatabase(self, db);
389
+ Database_t *db = self_to_open_database(self);
346
390
 
347
391
  return INT2FIX(sqlite3_changes(db->sqlite3_db));
348
392
  }
@@ -355,8 +399,7 @@ VALUE Database_changes(VALUE self) {
355
399
  VALUE Database_filename(int argc, VALUE *argv, VALUE self) {
356
400
  const char *db_name;
357
401
  const char *filename;
358
- Database_t *db;
359
- GetOpenDatabase(self, db);
402
+ Database_t *db = self_to_open_database(self);
360
403
 
361
404
  rb_check_arity(argc, 0, 1);
362
405
  db_name = (argc == 1) ? StringValueCStr(argv[0]) : "main";
@@ -370,8 +413,7 @@ VALUE Database_filename(int argc, VALUE *argv, VALUE self) {
370
413
  * Returns true if a transaction is currently in progress.
371
414
  */
372
415
  VALUE Database_transaction_active_p(VALUE self) {
373
- Database_t *db;
374
- GetOpenDatabase(self, db);
416
+ Database_t *db = self_to_open_database(self);
375
417
 
376
418
  return sqlite3_get_autocommit(db->sqlite3_db) ? Qfalse : Qtrue;
377
419
  }
@@ -383,8 +425,7 @@ VALUE Database_transaction_active_p(VALUE self) {
383
425
  * Loads an extension with the given path.
384
426
  */
385
427
  VALUE Database_load_extension(VALUE self, VALUE path) {
386
- Database_t *db;
387
- GetOpenDatabase(self, db);
428
+ Database_t *db = self_to_open_database(self);
388
429
  char *err_msg;
389
430
 
390
431
  int rc = sqlite3_load_extension(db->sqlite3_db, RSTRING_PTR(path), 0, &err_msg);
@@ -422,8 +463,7 @@ VALUE Database_prepare(int argc, VALUE *argv, VALUE self) {
422
463
  * For more information, consult the [sqlite3 API docs](https://sqlite.org/c3ref/interrupt.html).
423
464
  */
424
465
  VALUE Database_interrupt(VALUE self) {
425
- Database_t *db;
426
- GetOpenDatabase(self, db);
466
+ Database_t *db = self_to_open_database(self);
427
467
 
428
468
  sqlite3_interrupt(db->sqlite3_db);
429
469
  return self;
@@ -515,8 +555,7 @@ VALUE Database_backup(int argc, VALUE *argv, VALUE self) {
515
555
 
516
556
  int dst_is_fn = TYPE(dst) == T_STRING;
517
557
 
518
- Database_t *src;
519
- GetOpenDatabase(self, src);
558
+ Database_t *src = self_to_open_database(self);
520
559
  sqlite3 *dst_db;
521
560
 
522
561
  if (dst_is_fn) {
@@ -527,8 +566,7 @@ VALUE Database_backup(int argc, VALUE *argv, VALUE self) {
527
566
  }
528
567
  }
529
568
  else {
530
- Database_t *dst_struct;
531
- GetOpenDatabase(dst, dst_struct);
569
+ Database_t *dst_struct = self_to_open_database(dst);
532
570
  dst_db = dst_struct->sqlite3_db;
533
571
  }
534
572
 
@@ -583,8 +621,7 @@ VALUE Database_status(int argc, VALUE *argv, VALUE self) {
583
621
 
584
622
  rb_scan_args(argc, argv, "11", &op, &reset);
585
623
 
586
- Database_t *db;
587
- GetOpenDatabase(self, db);
624
+ Database_t *db = self_to_open_database(self);
588
625
 
589
626
  int rc = sqlite3_db_status(db->sqlite3_db, NUM2INT(op), &cur, &hwm, RTEST(reset) ? 1 : 0);
590
627
  if (rc != SQLITE_OK) rb_raise(cError, "%s", sqlite3_errstr(rc));
@@ -604,8 +641,7 @@ VALUE Database_limit(int argc, VALUE *argv, VALUE self) {
604
641
 
605
642
  rb_scan_args(argc, argv, "11", &category, &new_value);
606
643
 
607
- Database_t *db;
608
- GetOpenDatabase(self, db);
644
+ Database_t *db = self_to_open_database(self);
609
645
 
610
646
  int value = sqlite3_limit(db->sqlite3_db, NUM2INT(category), RTEST(new_value) ? NUM2INT(new_value) : -1);
611
647
 
@@ -622,8 +658,7 @@ VALUE Database_limit(int argc, VALUE *argv, VALUE self) {
622
658
  * disable the busy timeout, set it to 0 or nil.
623
659
  */
624
660
  VALUE Database_busy_timeout_set(VALUE self, VALUE sec) {
625
- Database_t *db;
626
- GetOpenDatabase(self, db);
661
+ Database_t *db = self_to_open_database(self);
627
662
 
628
663
  int ms = (sec == Qnil) ? 0 : (int)(NUM2DBL(sec) * 1000);
629
664
  int rc = sqlite3_busy_timeout(db->sqlite3_db, ms);
@@ -638,8 +673,7 @@ VALUE Database_busy_timeout_set(VALUE self, VALUE sec) {
638
673
  * Returns the total number of changes made to the database since opening it.
639
674
  */
640
675
  VALUE Database_total_changes(VALUE self) {
641
- Database_t *db;
642
- GetOpenDatabase(self, db);
676
+ Database_t *db = self_to_open_database(self);
643
677
 
644
678
  int value = sqlite3_total_changes(db->sqlite3_db);
645
679
  return INT2NUM(value);
@@ -653,8 +687,7 @@ VALUE Database_total_changes(VALUE self) {
653
687
  * executed.
654
688
  */
655
689
  VALUE Database_trace(VALUE self) {
656
- Database_t *db;
657
- GetOpenDatabase(self, db);
690
+ Database_t *db = self_to_open_database(self);
658
691
 
659
692
  db->trace_block = rb_block_given_p() ? rb_block_proc() : Qnil;
660
693
  return self;
@@ -666,8 +699,7 @@ VALUE Database_trace(VALUE self) {
666
699
  * Returns the last error code for the database.
667
700
  */
668
701
  VALUE Database_errcode(VALUE self) {
669
- Database_t *db;
670
- GetOpenDatabase(self, db);
702
+ Database_t *db = self_to_open_database(self);
671
703
 
672
704
  return INT2NUM(sqlite3_errcode(db->sqlite3_db));
673
705
  }
@@ -678,8 +710,7 @@ VALUE Database_errcode(VALUE self) {
678
710
  * Returns the last error message for the database.
679
711
  */
680
712
  VALUE Database_errmsg(VALUE self) {
681
- Database_t *db;
682
- GetOpenDatabase(self, db);
713
+ Database_t *db = self_to_open_database(self);
683
714
 
684
715
  return rb_str_new2(sqlite3_errmsg(db->sqlite3_db));
685
716
  }
@@ -691,13 +722,25 @@ VALUE Database_errmsg(VALUE self) {
691
722
  * Returns the offset for the last error
692
723
  */
693
724
  VALUE Database_error_offset(VALUE self) {
694
- Database_t *db;
695
- GetOpenDatabase(self, db);
725
+ Database_t *db = self_to_open_database(self);
696
726
 
697
727
  return INT2NUM(sqlite3_error_offset(db->sqlite3_db));
698
728
  }
699
729
  #endif
700
730
 
731
+ /* Returns a short string representation of the database instance, including the
732
+ * database filename.
733
+ *
734
+ * @return [String] string representation
735
+ */
736
+ VALUE Database_inspect(VALUE self) {
737
+ VALUE cname = rb_class_name(CLASS_OF(self));
738
+ VALUE filename = Database_filename(0, NULL, self);
739
+ if (RSTRING_LEN(filename) == 0) filename = rb_str_new_literal(":memory:");
740
+
741
+ return rb_sprintf("#<%"PRIsVALUE":%p %"PRIsVALUE">", cname, (void*)self, filename);
742
+ }
743
+
701
744
  void Init_ExtraliteDatabase(void) {
702
745
  VALUE mExtralite = rb_define_module("Extralite");
703
746
  rb_define_singleton_method(mExtralite, "runtime_status", Extralite_runtime_status, -1);
@@ -719,9 +762,11 @@ void Init_ExtraliteDatabase(void) {
719
762
  rb_define_method(cDatabase, "error_offset", Database_error_offset, 0);
720
763
  #endif
721
764
 
765
+ rb_define_method(cDatabase, "execute", Database_execute, -1);
722
766
  rb_define_method(cDatabase, "execute_multi", Database_execute_multi, 2);
723
767
  rb_define_method(cDatabase, "filename", Database_filename, -1);
724
- rb_define_method(cDatabase, "initialize", Database_initialize, 1);
768
+ rb_define_method(cDatabase, "initialize", Database_initialize, -1);
769
+ rb_define_method(cDatabase, "inspect", Database_inspect, 0);
725
770
  rb_define_method(cDatabase, "interrupt", Database_interrupt, 0);
726
771
  rb_define_method(cDatabase, "last_insert_rowid", Database_last_insert_rowid, 0);
727
772
  rb_define_method(cDatabase, "limit", Database_limit, -1);
@@ -732,6 +777,7 @@ void Init_ExtraliteDatabase(void) {
732
777
  rb_define_method(cDatabase, "query_single_column", Database_query_single_column, -1);
733
778
  rb_define_method(cDatabase, "query_single_row", Database_query_single_row, -1);
734
779
  rb_define_method(cDatabase, "query_single_value", Database_query_single_value, -1);
780
+ rb_define_method(cDatabase, "read_only?", Database_read_only_p, 0);
735
781
  rb_define_method(cDatabase, "status", Database_status, -1);
736
782
  rb_define_method(cDatabase, "total_changes", Database_total_changes, 0);
737
783
  rb_define_method(cDatabase, "trace", Database_trace, 0);
@@ -750,10 +796,15 @@ void Init_ExtraliteDatabase(void) {
750
796
  rb_gc_register_mark_object(cBusyError);
751
797
  rb_gc_register_mark_object(cInterruptError);
752
798
 
799
+ eArgumentError = rb_const_get(rb_cObject, rb_intern("ArgumentError"));
800
+
753
801
  ID_bind = rb_intern("bind");
754
802
  ID_call = rb_intern("call");
755
803
  ID_keys = rb_intern("keys");
756
804
  ID_new = rb_intern("new");
757
805
  ID_strip = rb_intern("strip");
758
806
  ID_to_s = rb_intern("to_s");
807
+
808
+ SYM_read_only = ID2SYM(rb_intern("read_only"));
809
+ rb_gc_register_mark_object(SYM_read_only);
759
810
  }
@@ -94,6 +94,7 @@ typedef struct {
94
94
 
95
95
  VALUE safe_execute_multi(query_ctx *ctx);
96
96
  VALUE safe_query_ary(query_ctx *ctx);
97
+ VALUE safe_query_changes(query_ctx *ctx);
97
98
  VALUE safe_query_columns(query_ctx *ctx);
98
99
  VALUE safe_query_hash(query_ctx *ctx);
99
100
  VALUE safe_query_single_column(query_ctx *ctx);
@@ -120,6 +121,6 @@ int stmt_iterate(query_ctx *ctx);
120
121
  VALUE cleanup_stmt(query_ctx *ctx);
121
122
 
122
123
  sqlite3 *Database_sqlite3_db(VALUE self);
123
- Database_t *Database_struct(VALUE self);
124
+ Database_t *self_to_database(VALUE self);
124
125
 
125
126
  #endif /* EXTRALITE_H */
@@ -34,7 +34,7 @@ static VALUE Iterator_allocate(VALUE klass) {
34
34
  return TypedData_Wrap_Struct(klass, &Iterator_type, iterator);
35
35
  }
36
36
 
37
- static inline Iterator_t *value_to_iterator(VALUE obj) {
37
+ static inline Iterator_t *self_to_iterator(VALUE obj) {
38
38
  Iterator_t *iterator;
39
39
  TypedData_Get_Struct((obj), Iterator_t, &Iterator_type, (iterator));
40
40
  return iterator;
@@ -61,7 +61,7 @@ static inline enum iterator_mode symbol_to_mode(VALUE sym) {
61
61
  * @return [void]
62
62
  */
63
63
  VALUE Iterator_initialize(VALUE self, VALUE query, VALUE mode) {
64
- Iterator_t *iterator = value_to_iterator(self);
64
+ Iterator_t *iterator = self_to_iterator(self);
65
65
 
66
66
  iterator->query = query;
67
67
  iterator->mode = symbol_to_mode(mode);
@@ -92,7 +92,7 @@ inline each_method mode_to_each_method(enum iterator_mode mode) {
92
92
  */
93
93
  VALUE Iterator_each(VALUE self) {
94
94
  if (rb_block_given_p()) {
95
- Iterator_t *iterator = value_to_iterator(self);
95
+ Iterator_t *iterator = self_to_iterator(self);
96
96
  each_method method = mode_to_each_method(iterator->mode);
97
97
  method(iterator->query);
98
98
  }
@@ -130,7 +130,7 @@ inline next_method mode_to_next_method(enum iterator_mode mode) {
130
130
  * @return [Array, Extralite::Iterator] next rows or self if block is given
131
131
  */
132
132
  VALUE Iterator_next(int argc, VALUE *argv, VALUE self) {
133
- Iterator_t *iterator = value_to_iterator(self);
133
+ Iterator_t *iterator = self_to_iterator(self);
134
134
  next_method method = mode_to_next_method(iterator->mode);
135
135
  VALUE result = method(argc, argv, iterator->query);
136
136
 
@@ -156,11 +156,34 @@ inline to_a_method mode_to_to_a_method(enum iterator_mode mode) {
156
156
  * @return [Array] array of query result set rows
157
157
  */
158
158
  VALUE Iterator_to_a(VALUE self) {
159
- Iterator_t *iterator = value_to_iterator(self);
159
+ Iterator_t *iterator = self_to_iterator(self);
160
160
  to_a_method method = mode_to_to_a_method(iterator->mode);
161
161
  return method(iterator->query);
162
162
  }
163
163
 
164
+ inline VALUE mode_to_symbol(Iterator_t *iterator) {
165
+ switch (iterator->mode) {
166
+ case ITERATOR_ARY:
167
+ return SYM_ary;
168
+ case ITERATOR_SINGLE_COLUMN:
169
+ return SYM_single_column;
170
+ default:
171
+ return SYM_hash;
172
+ }
173
+ }
174
+
175
+ /* Returns a short string representation of the iterator instance, including the
176
+ * SQL string.
177
+ *
178
+ * @return [String] string representation
179
+ */
180
+ VALUE Iterator_inspect(VALUE self) {
181
+ VALUE cname = rb_class_name(CLASS_OF(self));
182
+ VALUE sym = mode_to_symbol(self_to_iterator(self));
183
+
184
+ return rb_sprintf("#<%"PRIsVALUE":%p %"PRIsVALUE">", cname, (void*)self, sym);
185
+ }
186
+
164
187
  void Init_ExtraliteIterator(void) {
165
188
  VALUE mExtralite = rb_define_module("Extralite");
166
189
 
@@ -171,10 +194,15 @@ void Init_ExtraliteIterator(void) {
171
194
 
172
195
  rb_define_method(cIterator, "initialize", Iterator_initialize, 2);
173
196
  rb_define_method(cIterator, "each", Iterator_each, 0);
197
+ rb_define_method(cIterator, "inspect", Iterator_inspect, 0);
174
198
  rb_define_method(cIterator, "next", Iterator_next, -1);
175
199
  rb_define_method(cIterator, "to_a", Iterator_to_a, 0);
176
200
 
177
201
  SYM_hash = ID2SYM(rb_intern("hash"));
178
202
  SYM_ary = ID2SYM(rb_intern("ary"));
179
203
  SYM_single_column = ID2SYM(rb_intern("single_column"));
204
+
205
+ rb_gc_register_mark_object(SYM_hash);
206
+ rb_gc_register_mark_object(SYM_ary);
207
+ rb_gc_register_mark_object(SYM_single_column);
180
208
  }
@@ -11,6 +11,9 @@
11
11
 
12
12
  VALUE cQuery;
13
13
 
14
+ ID ID_inspect;
15
+ ID ID_slice;
16
+
14
17
  static size_t Query_size(const void *ptr) {
15
18
  return sizeof(Query_t);
16
19
  }
@@ -42,7 +45,7 @@ static VALUE Query_allocate(VALUE klass) {
42
45
  return TypedData_Wrap_Struct(klass, &Query_type, query);
43
46
  }
44
47
 
45
- static inline Query_t *value_to_query(VALUE obj) {
48
+ static inline Query_t *self_to_query(VALUE obj) {
46
49
  Query_t *query;
47
50
  TypedData_Get_Struct((obj), Query_t, &Query_type, (query));
48
51
  return query;
@@ -58,14 +61,14 @@ static inline Query_t *value_to_query(VALUE obj) {
58
61
  * @return [void]
59
62
  */
60
63
  VALUE Query_initialize(VALUE self, VALUE db, VALUE sql) {
61
- Query_t *query = value_to_query(self);
64
+ Query_t *query = self_to_query(self);
62
65
 
63
66
  sql = rb_funcall(sql, ID_strip, 0);
64
67
  if (!RSTRING_LEN(sql))
65
68
  rb_raise(cError, "Cannot prepare an empty SQL query");
66
69
 
67
70
  query->db = db;
68
- query->db_struct = Database_struct(db);
71
+ query->db_struct = self_to_database(db);
69
72
  query->sqlite3_db = Database_sqlite3_db(db);
70
73
  query->sql = sql;
71
74
  query->stmt = NULL;
@@ -113,7 +116,7 @@ static inline void query_reset_and_bind(Query_t *query, int argc, VALUE * argv)
113
116
  * @return [Extralite::Query] self
114
117
  */
115
118
  VALUE Query_reset(VALUE self) {
116
- Query_t *query = value_to_query(self);
119
+ Query_t *query = self_to_query(self);
117
120
  if (query->closed) rb_raise(cError, "Query is closed");
118
121
 
119
122
  query_reset(query);
@@ -145,7 +148,7 @@ VALUE Query_reset(VALUE self) {
145
148
  * @return [Extralite::Query] self
146
149
  */
147
150
  VALUE Query_bind(int argc, VALUE *argv, VALUE self) {
148
- Query_t *query = value_to_query(self);
151
+ Query_t *query = self_to_query(self);
149
152
  if (query->closed) rb_raise(cError, "Query is closed");
150
153
 
151
154
  query_reset_and_bind(query, argc, argv);
@@ -157,7 +160,7 @@ VALUE Query_bind(int argc, VALUE *argv, VALUE self) {
157
160
  * @return [boolean] true if iteration has reached the end of the result set
158
161
  */
159
162
  VALUE Query_eof_p(VALUE self) {
160
- Query_t *query = value_to_query(self);
163
+ Query_t *query = self_to_query(self);
161
164
  if (query->closed) rb_raise(cError, "Query is closed");
162
165
 
163
166
  return query->eof ? Qtrue : Qfalse;
@@ -166,7 +169,7 @@ VALUE Query_eof_p(VALUE self) {
166
169
  #define MAX_ROWS(max_rows) (max_rows == SINGLE_ROW ? 1 : max_rows)
167
170
 
168
171
  static inline VALUE Query_perform_next(VALUE self, int max_rows, VALUE (*call)(query_ctx *)) {
169
- Query_t *query = value_to_query(self);
172
+ Query_t *query = self_to_query(self);
170
173
  if (query->closed) rb_raise(cError, "Query is closed");
171
174
 
172
175
  if (!query->stmt) query_reset(query);
@@ -265,7 +268,7 @@ VALUE Query_next_single_column(int argc, VALUE *argv, VALUE self) {
265
268
  * @return [Array<Hash>] all rows
266
269
  */
267
270
  VALUE Query_to_a_hash(VALUE self) {
268
- Query_t *query = value_to_query(self);
271
+ Query_t *query = self_to_query(self);
269
272
  query_reset(query);
270
273
  return Query_perform_next(self, ALL_ROWS, safe_query_hash);
271
274
  }
@@ -275,7 +278,7 @@ VALUE Query_to_a_hash(VALUE self) {
275
278
  * @return [Array<Array>] all rows
276
279
  */
277
280
  VALUE Query_to_a_ary(VALUE self) {
278
- Query_t *query = value_to_query(self);
281
+ Query_t *query = self_to_query(self);
279
282
  query_reset(query);
280
283
  return Query_perform_next(self, ALL_ROWS, safe_query_ary);
281
284
  }
@@ -286,7 +289,7 @@ VALUE Query_to_a_ary(VALUE self) {
286
289
  * @return [Array<Object>] all rows
287
290
  */
288
291
  VALUE Query_to_a_single_column(VALUE self) {
289
- Query_t *query = value_to_query(self);
292
+ Query_t *query = self_to_query(self);
290
293
  query_reset(query);
291
294
  return Query_perform_next(self, ALL_ROWS, safe_query_single_column);
292
295
  }
@@ -300,7 +303,7 @@ VALUE Query_to_a_single_column(VALUE self) {
300
303
  VALUE Query_each_hash(VALUE self) {
301
304
  if (!rb_block_given_p()) return rb_funcall(cIterator, ID_new, 2, self, SYM_hash);
302
305
 
303
- Query_t *query = value_to_query(self);
306
+ Query_t *query = self_to_query(self);
304
307
  query_reset(query);
305
308
  return Query_perform_next(self, ALL_ROWS, safe_query_hash);
306
309
  }
@@ -314,7 +317,7 @@ VALUE Query_each_hash(VALUE self) {
314
317
  VALUE Query_each_ary(VALUE self) {
315
318
  if (!rb_block_given_p()) return rb_funcall(cIterator, ID_new, 2, self, SYM_ary);
316
319
 
317
- Query_t *query = value_to_query(self);
320
+ Query_t *query = self_to_query(self);
318
321
  query_reset(query);
319
322
  return Query_perform_next(self, ALL_ROWS, safe_query_ary);
320
323
  }
@@ -329,11 +332,40 @@ VALUE Query_each_ary(VALUE self) {
329
332
  VALUE Query_each_single_column(VALUE self) {
330
333
  if (!rb_block_given_p()) return rb_funcall(cIterator, ID_new, 2, self, SYM_single_column);
331
334
 
332
- Query_t *query = value_to_query(self);
335
+ Query_t *query = self_to_query(self);
333
336
  query_reset(query);
334
337
  return Query_perform_next(self, ALL_ROWS, safe_query_single_column);
335
338
  }
336
339
 
340
+ /* call-seq:
341
+ * query.execute(*parameters) -> changes
342
+ *
343
+ * Runs a query returning the total changes effected. This method should be used
344
+ * for data- or schema-manipulation queries.
345
+ *
346
+ * Query parameters to be bound to placeholders in the query can be specified as
347
+ * a list of values or as a hash mapping parameter names to values. When
348
+ * parameters are given as an array, the query should specify parameters using
349
+ * `?`:
350
+ *
351
+ * query = db.prepare('update foo set x = ? where y = ?')
352
+ * query.execute(42, 43)
353
+ *
354
+ * Named placeholders are specified using `:`. The placeholder values are
355
+ * specified using a hash, where keys are either strings are symbols. String
356
+ * keys can include or omit the `:` prefix. The following are equivalent:
357
+ *
358
+ * query = db.prepare('update foo set x = :bar')
359
+ * query.execute(bar: 42)
360
+ * query.execute('bar' => 42)
361
+ * query.execute(':bar' => 42)
362
+ */
363
+ VALUE Query_execute(int argc, VALUE *argv, VALUE self) {
364
+ Query_t *query = self_to_query(self);
365
+ query_reset_and_bind(query, argc, argv);
366
+ return Query_perform_next(self, ALL_ROWS, safe_query_changes);
367
+ }
368
+
337
369
  /* Executes the query for each set of parameters in the given array. Parameters
338
370
  * can be specified as either an array (for unnamed parameters) or a hash (for
339
371
  * named parameters). Returns the number of changes effected. This method is
@@ -350,7 +382,7 @@ VALUE Query_each_single_column(VALUE self) {
350
382
  * @return [Integer] number of changes effected
351
383
  */
352
384
  VALUE Query_execute_multi(VALUE self, VALUE parameters) {
353
- Query_t *query = value_to_query(self);
385
+ Query_t *query = self_to_query(self);
354
386
  if (query->closed) rb_raise(cError, "Query is closed");
355
387
 
356
388
  if (!query->stmt)
@@ -368,7 +400,7 @@ VALUE Query_execute_multi(VALUE self, VALUE parameters) {
368
400
  * @return [Extralite::Database] associated database
369
401
  */
370
402
  VALUE Query_database(VALUE self) {
371
- Query_t *query = value_to_query(self);
403
+ Query_t *query = self_to_query(self);
372
404
  return query->db;
373
405
  }
374
406
 
@@ -377,7 +409,7 @@ VALUE Query_database(VALUE self) {
377
409
  * @return [String] SQL string
378
410
  */
379
411
  VALUE Query_sql(VALUE self) {
380
- Query_t *query = value_to_query(self);
412
+ Query_t *query = self_to_query(self);
381
413
  return query->sql;
382
414
  }
383
415
 
@@ -386,7 +418,7 @@ VALUE Query_sql(VALUE self) {
386
418
  * @return [Array<Symbol>] column names
387
419
  */
388
420
  VALUE Query_columns(VALUE self) {
389
- Query_t *query = value_to_query(self);
421
+ Query_t *query = self_to_query(self);
390
422
  query_reset(query);
391
423
  return Query_perform_next(self, ALL_ROWS, safe_query_columns);
392
424
  }
@@ -396,7 +428,7 @@ VALUE Query_columns(VALUE self) {
396
428
  * @return [Extralite::Query] self
397
429
  */
398
430
  VALUE Query_close(VALUE self) {
399
- Query_t *query = value_to_query(self);
431
+ Query_t *query = self_to_query(self);
400
432
  if (query->stmt) {
401
433
  sqlite3_finalize(query->stmt);
402
434
  query->stmt = NULL;
@@ -410,7 +442,7 @@ VALUE Query_close(VALUE self) {
410
442
  * @return [boolean] true if query is closed
411
443
  */
412
444
  VALUE Query_closed_p(VALUE self) {
413
- Query_t *query = value_to_query(self);
445
+ Query_t *query = self_to_query(self);
414
446
  return query->closed ? Qtrue : Qfalse;
415
447
  }
416
448
 
@@ -431,7 +463,7 @@ VALUE Query_status(int argc, VALUE* argv, VALUE self) {
431
463
 
432
464
  rb_scan_args(argc, argv, "11", &op, &reset);
433
465
 
434
- Query_t *query = value_to_query(self);
466
+ Query_t *query = self_to_query(self);
435
467
  if (query->closed) rb_raise(cError, "Query is closed");
436
468
 
437
469
  if (!query->stmt)
@@ -441,6 +473,24 @@ VALUE Query_status(int argc, VALUE* argv, VALUE self) {
441
473
  return INT2NUM(value);
442
474
  }
443
475
 
476
+ /* Returns a short string representation of the query instance, including the
477
+ * SQL string.
478
+ *
479
+ * @return [String] string representation
480
+ */
481
+ VALUE Query_inspect(VALUE self) {
482
+ VALUE cname = rb_class_name(CLASS_OF(self));
483
+ VALUE sql = self_to_query(self)->sql;
484
+ if (RSTRING_LEN(sql) > 48) {
485
+ sql = rb_funcall(sql, ID_slice, 2, INT2FIX(0), INT2FIX(45));
486
+ rb_str_cat2(sql, "...");
487
+ }
488
+ sql = rb_funcall(sql, ID_inspect, 0);
489
+
490
+ RB_GC_GUARD(sql);
491
+ return rb_sprintf("#<%"PRIsVALUE":%p %"PRIsVALUE">", cname, (void*)self, sql);
492
+ }
493
+
444
494
  void Init_ExtraliteQuery(void) {
445
495
  VALUE mExtralite = rb_define_module("Extralite");
446
496
 
@@ -460,8 +510,10 @@ void Init_ExtraliteQuery(void) {
460
510
  rb_define_method(cQuery, "each_single_column", Query_each_single_column, 0);
461
511
 
462
512
  rb_define_method(cQuery, "eof?", Query_eof_p, 0);
513
+ rb_define_method(cQuery, "execute", Query_execute, -1);
463
514
  rb_define_method(cQuery, "execute_multi", Query_execute_multi, 1);
464
515
  rb_define_method(cQuery, "initialize", Query_initialize, 2);
516
+ rb_define_method(cQuery, "inspect", Query_inspect, 0);
465
517
 
466
518
  rb_define_method(cQuery, "next", Query_next_hash, -1);
467
519
  rb_define_method(cQuery, "next_ary", Query_next_ary, -1);
@@ -476,4 +528,7 @@ void Init_ExtraliteQuery(void) {
476
528
  rb_define_method(cQuery, "to_a_ary", Query_to_a_ary, 0);
477
529
  rb_define_method(cQuery, "to_a_hash", Query_to_a_hash, 0);
478
530
  rb_define_method(cQuery, "to_a_single_column", Query_to_a_single_column, 0);
531
+
532
+ ID_inspect = rb_intern("inspect");
533
+ ID_slice = rb_intern("slice");
479
534
  }
@@ -1,4 +1,4 @@
1
1
  module Extralite
2
2
  # Extralite version
3
- VERSION = '2.0'
3
+ VERSION = '2.1'
4
4
  end
data/lib/extralite.rb CHANGED
@@ -27,8 +27,6 @@ module Extralite
27
27
 
28
28
  # An SQLite database
29
29
  class Database
30
- alias_method :execute, :query
31
-
32
30
  # @!visibility private
33
31
  TABLES_SQL = <<~SQL
34
32
  SELECT name FROM sqlite_master
@@ -206,6 +206,17 @@ end
206
206
  assert_equal [{recursive_triggers: 1}], @db.pragma(:recursive_triggers)
207
207
  end
208
208
 
209
+ def test_execute
210
+ changes = @db.execute('update t set x = 42')
211
+ assert_equal 2, changes
212
+ end
213
+
214
+ def test_execute_with_params
215
+ changes = @db.execute('update t set x = ? where z = ?', 42, 6)
216
+ assert_equal 1, changes
217
+ assert_equal [[1, 2, 3], [42, 5, 6]], @db.query_ary('select * from t order by x')
218
+ end
219
+
209
220
  def test_execute_multi
210
221
  @db.query('create table foo (a, b, c)')
211
222
  assert_equal [], @db.query('select * from foo')
@@ -361,6 +372,21 @@ end
361
372
  query.next
362
373
  @db.close
363
374
  end
375
+
376
+ def test_read_only_database
377
+ db = Extralite::Database.new(':memory:')
378
+ db.query('create table foo (bar)')
379
+ assert_equal false, db.read_only?
380
+
381
+ db = Extralite::Database.new(':memory:', read_only: true)
382
+ assert_raises(Extralite::Error) { db.query('create table foo (bar)') }
383
+ assert_equal true, db.read_only?
384
+ end
385
+
386
+ def test_database_inspect
387
+ db = Extralite::Database.new(':memory:')
388
+ assert_match /^\#\<Extralite::Database:0x[0-9a-f]+ :memory:\>$/, db.inspect
389
+ end
364
390
  end
365
391
 
366
392
  class ScenarioTest < MiniTest::Test
@@ -96,4 +96,9 @@ class IteratorTest < MiniTest::Test
96
96
  mapped = query.each_single_column.map { |v| v * 10 }
97
97
  assert_equal [30, 60, 90], mapped
98
98
  end
99
+
100
+ def test_iterator_inspect
101
+ i = @query.each_ary
102
+ assert_match /^\#\<Extralite::Iterator:0x[0-9a-f]+ ary\>$/, i.inspect
103
+ end
99
104
  end
data/test/test_query.rb CHANGED
@@ -430,6 +430,18 @@ class QueryTest < MiniTest::Test
430
430
  assert_raises(Extralite::Error) { p.next }
431
431
  end
432
432
 
433
+ def test_query_execute
434
+ q = @db.prepare('update t set x = 42')
435
+ assert_equal 3, q.execute
436
+ assert_equal [[42, 2, 3], [42, 5, 6], [42, 8, 9]], @db.query_ary('select * from t order by z')
437
+ end
438
+
439
+ def test_query_execute_with_params
440
+ q = @db.prepare('update t set x = ? where z = ?')
441
+ assert_equal 1, q.execute(42, 9)
442
+ assert_equal [[1, 2, 3], [4, 5, 6], [42, 8, 9]], @db.query_ary('select * from t order by z')
443
+ end
444
+
433
445
  def test_query_execute_multi
434
446
  @db.query('create table foo (a, b, c)')
435
447
  assert_equal [], @db.query('select * from foo')
@@ -499,4 +511,9 @@ class QueryTest < MiniTest::Test
499
511
  assert_equal [1, 4, 7], query.to_a_single_column
500
512
  assert_equal true, query.eof?
501
513
  end
514
+
515
+ def test_query_inspect
516
+ q = @db.prepare('select x from t')
517
+ assert_match /^\#\<Extralite::Query:0x[0-9a-f]+ #{q.sql.inspect}\>$/, q.inspect
518
+ end
502
519
  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: '2.0'
4
+ version: '2.1'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-07-08 00:00:00.000000000 Z
11
+ date: 2023-07-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake-compiler