extralite 1.21 → 1.23

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9b705a2d1c970cb3e73c784a6322ea92e8f7618531b4d8b9c2d4e036bf135b0f
4
- data.tar.gz: fceaaf8976cac3d5e5328d64e8e76d07fe5979e4897ad7f20bb6295f697659b7
3
+ metadata.gz: 7466fb655d5f2cc65862d604190d52208773a9cc8fb611b184bc46a20ccb8d1f
4
+ data.tar.gz: bbc9b59c5c2297df34d716a19b53ccc95008bea3be4d38d3cdd91340089962b5
5
5
  SHA512:
6
- metadata.gz: c1b1bc996aa69b8f4abd4888a23a3aeae0aa15069b5d7c13027389e6165529186949ed8679571b55c00f3b76bd5360f396e874dd4c73f3d88cd8443c8931fca9
7
- data.tar.gz: aca5d4470770f5c635d2d3b42a1897c45535efd29a38137c3164c07629f3f68a3cc1f74f95cef180efe06b9cb5423bb8fe95e183c7001c98a05baa0f3532718e
6
+ metadata.gz: 227a7fb703cca6eed8b25f0adb32cf8421f3c503149a715006c381d5cca475e692048650acc561ccfcb5ff1203c489fa6fe429922aecb2c0486cd0ec83c85360
7
+ data.tar.gz: 131aa35e939c4623a306be6e7a756d40cfe77a5b199ffc90cb1937194e7066ba6fa4de3d4cc8618b8123b68dcfe9e92a60b3e821c49b51145497deb005467f24
@@ -8,7 +8,7 @@ jobs:
8
8
  fail-fast: false
9
9
  matrix:
10
10
  os: [ubuntu-latest, macos-latest]
11
- ruby: ['2.7', '3.0', '3.1', truffleruby]
11
+ ruby: ['3.0', '3.1', '3.2', truffleruby]
12
12
 
13
13
  name: >-
14
14
  ${{matrix.os}}, ${{matrix.ruby}}
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ # 1.23 2023-01-26
2
+
3
+ - Add `Database#trace` (#21)
4
+ - Add `Database#total_changes` (#20)
5
+ - Add `Database#busy_timeout=` (#19)
6
+ - Add `Database#limit` (#16)
7
+ - Improve error handling
8
+
9
+ # 1.22 2023-01-23
10
+
11
+ - Improve documentation (#17)
12
+
1
13
  # 1.21 2023-01-23
2
14
 
3
15
  - Update bundled sqlite to version 3.40.1 (#18)
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- extralite (1.21)
4
+ extralite (1.23)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -1,64 +1,42 @@
1
- <h1 align="center">
2
- Extralite
3
- </h1>
4
-
5
- <h4 align="center">A fast Ruby gem for working with SQLite3 databases</h4>
6
-
7
- <p align="center">
8
- <a href="http://rubygems.org/gems/extralite">
9
- <img src="https://badge.fury.io/rb/extralite.svg" alt="Ruby gem">
10
- </a>
11
- <a href="https://github.com/digital-fabric/extralite/actions?query=workflow%3ATests">
12
- <img src="https://github.com/digital-fabric/extralite/workflows/Tests/badge.svg" alt="Tests">
13
- </a>
14
- <a href="https://github.com/digital-fabric/extralite/blob/master/LICENSE">
15
- <img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License">
16
- </a>
17
- </p>
18
-
19
- <p align="center">
20
- <a href="https://www.rubydoc.info/gems/extralite">DOCS</a> |
21
- <a href="https://noteflakes.com/articles/2021-12-15-extralite">BLOG POST</a>
22
- </p>
1
+ # Extralite - a Super Fast Ruby Gem for Working with SQLite3 Databases
2
+
3
+ * Source code: https://github.com/digital-fabric/extralite
4
+ * Documentation: http://www.rubydoc.info/gems/extralite
5
+
6
+ [![Ruby gem](https://badge.fury.io/rb/extralite.svg)](https://rubygems.org/gems/extralite) [![Tests](https://github.com/digital-fabric/extralite/workflows/Tests/badge.svg)](https://github.com/digital-fabric/extralite/actions?query=workflow%3ATests) [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/digital-fabric/extralite/blob/master/LICENSE)
23
7
 
24
8
  ## What is Extralite?
25
9
 
26
- Extralite is a fast, extra-lightweight (about 600 lines of C-code) SQLite3
27
- wrapper for Ruby. It provides a minimal set of methods for interacting with an
28
- SQLite3 database, as well as prepared statements.
10
+ Extralite is a super fast, extra-lightweight (about 1300 lines of C-code)
11
+ SQLite3 wrapper for Ruby. It provides a minimal set of methods for interacting
12
+ with an SQLite3 database, as well as prepared statements.
29
13
 
30
14
  Extralite comes in two flavors: the `extralite` gem which uses the
31
15
  system-installed sqlite3 library, and the `extralite-bundle` gem which bundles
32
16
  the latest version of SQLite
33
- ([3.38.0](https://sqlite.org/releaselog/3_38_0.html)), offering access to the
17
+ ([3.40.1](https://sqlite.org/releaselog/3_40_1.html)), offering access to the
34
18
  latest features and enhancements.
35
19
 
36
20
  ## Features
37
21
 
22
+ - Super fast - [up to 11x faster](#performance) than the
23
+ [sqlite3](https://github.com/sparklemotion/sqlite3-ruby) gem (see also
24
+ [comparison](#why-not-just-use-the-sqlite3-gem).)
38
25
  - A variety of methods for different data access patterns: rows as hashes, rows
39
26
  as arrays, single row, single column, single value.
40
27
  - Prepared statements.
28
+ - Parameter binding.
41
29
  - Use system-installed sqlite3, or the [bundled latest version of
42
30
  SQLite3](#installing-the-extralite-sqlite3-bundle).
43
- - Super fast - [up to 12.5x faster](#performance) than the
44
- [sqlite3](https://github.com/sparklemotion/sqlite3-ruby) gem (see also
45
- [comparison](#why-not-just-use-the-sqlite3-gem).)
46
31
  - Improved [concurrency](#concurrency) for multithreaded apps: the Ruby GVL is
47
32
  released while preparing SQL statements and while iterating over results.
48
33
  - Iterate over records with a block, or collect records into an array.
49
- - Parameter binding.
50
34
  - Automatically execute SQL strings containing multiple semicolon-separated
51
35
  queries (handy for creating/modifying schemas).
52
- - Get last insert rowid.
53
- - Get number of rows changed by last query.
54
36
  - Execute the same query with multiple parameter lists (useful for inserting records).
55
37
  - Load extensions (loading of extensions is autmatically enabled. You can find
56
38
  some useful extensions here: https://github.com/nalgeon/sqlean.)
57
39
  - Includes a [Sequel adapter](#usage-with-sequel).
58
- - Other features:
59
- - Backup databases
60
- - Interrupt long-running queries (from another thread)
61
- - Get runtime status, database status and prepared statement status values.
62
40
 
63
41
  ## Installation
64
42
 
@@ -70,20 +48,21 @@ gem 'extralite'
70
48
 
71
49
  You can also run `gem install extralite` if you just want to check it out.
72
50
 
73
- ### Installing the Extralite-SQLite3 bundle
51
+ ### Installing the Extralite-SQLite3 Bundle
74
52
 
75
53
  If you don't have sqlite3 installed on your system, do not want to use the
76
54
  system-installed version of SQLite3, or would like to use the latest version of
77
55
  SQLite3, you can install the `extralite-bundle` gem, which integrates the
78
56
  SQLite3 source code.
79
57
 
80
- > **Important note**: The `extralite-bundle` will take a while to install (on my
81
- > modest machine it takes about a minute), due to the size of the sqlite3 code.
58
+ > **Important note**: The `extralite-bundle` gem will take a while to install
59
+ > (on my modest machine it takes about a minute), due to the size of the sqlite3
60
+ > code.
82
61
 
83
62
  Usage of the `extralite-bundle` gem is identical to the usage of the normal
84
- `extralite` gem.
63
+ `extralite` gem, using `require 'extralite'` to load the gem.
85
64
 
86
- ## Usage
65
+ ## Synopsis
87
66
 
88
67
  ```ruby
89
68
  require 'extralite'
@@ -144,6 +123,9 @@ rowid = db.last_insert_rowid
144
123
  # get number of rows changed in last query
145
124
  number_of_rows_affected = db.changes
146
125
 
126
+ # get column names for the given sql
127
+ db.columns('select a, b, c from foo') => [:a, :b, :c]
128
+
147
129
  # get db filename
148
130
  db.filename #=> "/tmp/my.db"
149
131
 
@@ -163,6 +145,114 @@ db.close
163
145
  db.closed? #=> true
164
146
  ```
165
147
 
148
+ ## More Features
149
+
150
+ ### Interrupting Long-running Queries
151
+
152
+ When running long-running queries, you can use `Database#interrupt` to interrupt
153
+ the query:
154
+
155
+ ```ruby
156
+ timeout_thread = Thread.new do
157
+ sleep 10
158
+ db.interrupt
159
+ end
160
+
161
+ result = begin
162
+ db.query(super_slow_sql)
163
+ rescue Extralite::InterruptError
164
+ nil
165
+ ensure
166
+ timeout_thread.kill
167
+ timeout_thread.join
168
+ end
169
+ ```
170
+
171
+ ### Creating Backups
172
+
173
+ You can use `Database#backup` to create backup copies of a database. The
174
+ `#backup` method takes either a filename or a database instance:
175
+
176
+ ```ruby
177
+ # with a filename
178
+ db.backup('backup.db')
179
+
180
+ # with an instance
181
+ target = Extralite::Database.new('backup.db')
182
+ db.backup(target)
183
+ ```
184
+
185
+ For big databases, you can also track the backup progress by providing a block
186
+ that takes two arguments - the number of remaining pages, and the total number pages:
187
+
188
+ ```ruby
189
+ db.backup('backup.db') do |remaining, total|
190
+ puts "backup progress: #{(remaining.to_f/total * 100).round}%"
191
+ end
192
+ ```
193
+
194
+ ### Retrieving Status Information
195
+
196
+ Extralite provides methods for retrieving status information about the sqlite
197
+ runtime, database-specific status and prepared statement-specific status,
198
+ `Extralite.runtime_status`, `Database#status` and `PreparedStatement#status`
199
+ respectively. You can also reset the high water mark for the specific status
200
+ code by providing true as the reset argument. The status codes mirror those
201
+ defined by the SQLite API. Some examples:
202
+
203
+ ```ruby
204
+ # The Extralite.runtime_status returns a tuple consisting of the current value
205
+ # and the high water mark value.
206
+ current, high_watermark = Extralite.runtime_status(Extralite::SQLITE_STATUS_MEMORY_USED)
207
+
208
+ # To reset the high water mark, pass true as a second argument:
209
+ current, high_watermark = Extralite.runtime_status(Extralite::SQLITE_STATUS_MEMORY_USED, true)
210
+
211
+ # Similarly, you can interrogate a database's status (pass true as a second
212
+ # argument in order to reset the high watermark):
213
+ current, high_watermark = db.status(Extralite::SQLITE_DBSTATUS_CACHE_USED)
214
+
215
+ # The PreparedStatement#status method returns a single value (pass true as a
216
+ # second argument in order to reset the high watermark):
217
+ value = stmt.status(Extralite::SQLITE_STMTSTATUS_RUN)
218
+ ```
219
+
220
+ ### Working with Database Limits
221
+
222
+ The `Database#limit` can be used to get and set various database limits, as
223
+ [discussed in the SQLite docs](https://www.sqlite.org/limits.html):
224
+
225
+ ```ruby
226
+ # get limit
227
+ value = db.limit(Extralite::SQLITE_LIMIT_ATTACHED)
228
+
229
+ # set limit
230
+ db.limit(Extralite::SQLITE_LIMIT_ATTACHED, new_value)
231
+ ```
232
+
233
+ ### Setting the Busy Timeout
234
+
235
+ When accessing a database concurrently it can be handy to set a busy timeout, in
236
+ order to not have to deal with rescuing `Extralite::BusyError` exceptions. The
237
+ timeout is given in seconds:
238
+
239
+ ```ruby
240
+ db.busy_timeout = 5
241
+ ```
242
+
243
+ ### Tracing SQL Statements
244
+
245
+ To trace all SQL statements executed on the database, pass a block to
246
+ `Database#trace`. To disable tracing, call `Database#trace` without a block:
247
+
248
+ ```ruby
249
+ # enable tracing
250
+ db.trace { |sql| puts sql: sql }
251
+
252
+ # disable tracing
253
+ db.trace
254
+ ```
255
+
166
256
  ## Usage with Sequel
167
257
 
168
258
  Extralite includes an adapter for
@@ -177,82 +267,53 @@ p articles.to_a
177
267
 
178
268
  (Make sure you include `extralite` as a dependency in your `Gemfile`.)
179
269
 
180
- ## Why not just use the sqlite3 gem?
181
-
182
- The [sqlite3-ruby](https://github.com/sparklemotion/sqlite3-ruby) gem is a
183
- popular, solid, well-maintained project, used by thousands of developers. I've
184
- been doing a lot of work with SQLite3 databases lately, and wanted to have a
185
- simpler API that gives me query results in a variety of ways. Thus extralite was
186
- born.
187
-
188
- Extralite is quite a bit [faster](#performance) than sqlite3-ruby and is also
189
- [thread-friendly](#concurrency). On the other hand, Extralite does not have
190
- support for defining custom functions, aggregates and collations. If you're
191
- using any of those features, you'll have to stick to sqlite3-ruby.
192
-
193
- Here's a table summarizing the differences between the two gems:
194
-
195
- | |sqlite3-ruby|Extralite|
196
- |-|-|-|
197
- |SQLite3 dependency|depends on OS-installed libsqlite3|Use either system sqlite3 or [bundled latest version of SQLite3](#installing-the-extralite-sqlite3-bundle)|
198
- |API design|multiple classes|single class|
199
- |Query results|row as hash, row as array, single row, single value|row as hash, row as array, __single column__, single row, single value|
200
- |Execute multiple statements|separate API (#execute_batch)|integrated|
201
- |Prepared statements|yes|yes|
202
- |custom functions in Ruby|yes|no|
203
- |custom collations|yes|no|
204
- |custom aggregate functions|yes|no|
205
- |Multithread friendly|no|[yes](#concurrency)|
206
- |Code size|~2650LoC|~600LoC|
207
- |Performance|1x|1.5x to 12.5x (see [below](#performance))|
208
-
209
270
  ## Concurrency
210
271
 
211
272
  Extralite releases the GVL while making blocking calls to the sqlite3 library,
212
273
  that is while preparing SQL statements and fetching rows. Releasing the GVL
213
274
  allows other threads to run while the sqlite3 library is busy compiling SQL into
214
- bytecode, or fetching the next row. This does not seem to hurt Extralite's
215
- performance:
275
+ bytecode, or fetching the next row. This *does not* hurt Extralite's
276
+ performance, as you can see:
216
277
 
217
278
  ## Performance
218
279
 
219
280
  A benchmark script is included, creating a table of various row counts, then
220
281
  fetching the entire table using either `sqlite3` or `extralite`. This benchmark
221
- shows Extralite to be up to ~12 times faster than `sqlite3` when fetching a
282
+ shows Extralite to be up to ~11 times faster than `sqlite3` when fetching a
222
283
  large number of rows.
223
284
 
224
- ### Rows as hashes
285
+ ### Rows as Hashes
225
286
 
226
287
  [Benchmark source code](https://github.com/digital-fabric/extralite/blob/main/test/perf_hash.rb)
227
288
 
228
- |Row count|sqlite3-ruby|Extralite|Advantage|
289
+ |Row count|sqlite3 1.6.0|Extralite 1.21|Advantage|
229
290
  |-:|-:|-:|-:|
230
- |10|75.3K rows/s|134.2K rows/s|__1.78x__|
231
- |1K|286.8K rows/s|2106.4K rows/s|__7.35x__|
232
- |100K|181.0K rows/s|2275.3K rows/s|__12.53x__|
291
+ |10|63.7K rows/s|94.0K rows/s|__1.48x__|
292
+ |1K|299.2K rows/s|1.983M rows/s|__6.63x__|
293
+ |100K|185.4K rows/s|2.033M rows/s|__10.97x__|
233
294
 
234
- ### Rows as arrays
295
+ ### Rows as Arrays
235
296
 
236
297
  [Benchmark source code](https://github.com/digital-fabric/extralite/blob/main/test/perf_ary.rb)
237
298
 
238
- |Row count|sqlite3-ruby|Extralite|Advantage|
299
+ |Row count|sqlite3 1.6.0|Extralite 1.21|Advantage|
239
300
  |-:|-:|-:|-:|
240
- |10|64.3K rows/s|94.0K rows/s|__1.46x__|
241
- |1K|498.9K rows/s|2478.2K rows/s|__4.97x__|
242
- |100K|441.1K rows/s|3023.4K rows/s|__6.85x__|
301
+ |10|71.2K rows/s|92.1K rows/s|__1.29x__|
302
+ |1K|502.1K rows/s|2.065M rows/s|__4.11x__|
303
+ |100K|455.7K rows/s|2.511M rows/s|__5.51x__|
243
304
 
244
- ### Prepared statements
305
+ ### Prepared Statements
245
306
 
246
307
  [Benchmark source code](https://github.com/digital-fabric/extralite/blob/main/test/perf_prepared.rb)
247
308
 
248
- |Row count|sqlite3-ruby|Extralite|Advantage|
309
+ |Row count|sqlite3 1.6.0|Extralite 1.21|Advantage|
249
310
  |-:|-:|-:|-:|
250
- |10|241.8K rows/s|888K rows/s|__3.67x__|
251
- |1K|298.6K rows/s|2606K rows/s|__8.73x__|
252
- |100K|201.6K rows/s|1934K rows/s|__9.6x__|
311
+ |10|232.2K rows/s|741.6K rows/s|__3.19x__|
312
+ |1K|299.8K rows/s|2386.0M rows/s|__7.96x__|
313
+ |100K|183.1K rows/s|1.893M rows/s|__10.34x__|
253
314
 
254
- As those benchmarks show, Extralite is capabale of reading up to 3M rows/second
255
- when fetching rows as arrays, and up to 2.6M rows/second when fetching
315
+ As those benchmarks show, Extralite is capabale of reading up to 2.5M
316
+ rows/second when fetching rows as arrays, and up to 2M rows/second when fetching
256
317
  rows as hashes.
257
318
 
258
319
  ## License
@@ -174,7 +174,7 @@ void prepare_multi_stmt(sqlite3 *db, sqlite3_stmt **stmt, VALUE sql) {
174
174
  case SQLITE_ERROR:
175
175
  rb_raise(cSQLError, "%s", sqlite3_errmsg(db));
176
176
  default:
177
- rb_raise(cError, "Invalid return code for prepare_multi_stmt_without_gvl: %d (please open an issue on https://github.com/digital-fabric/extralite)", ctx.rc);
177
+ rb_raise(cError, "%s", sqlite3_errmsg(db));
178
178
  }
179
179
  }
180
180
 
@@ -216,9 +216,9 @@ void prepare_single_stmt(sqlite3 *db, sqlite3_stmt **stmt, VALUE sql) {
216
216
  case SQLITE_ERROR:
217
217
  rb_raise(cSQLError, "%s", sqlite3_errmsg(db));
218
218
  case SQLITE_MULTI_STMT:
219
- rb_raise(cSQLError, "A prepared statement does not accept SQL strings with multiple queries");
219
+ rb_raise(cError, "A prepared statement does not accept SQL strings with multiple queries");
220
220
  default:
221
- rb_raise(cError, "Invalid return code for prepare_multi_stmt_without_gvl: %d (please open an issue on https://github.com/digital-fabric/extralite)", ctx.rc);
221
+ rb_raise(cError, "%s", sqlite3_errmsg(db));
222
222
  }
223
223
  }
224
224
 
@@ -248,7 +248,7 @@ int stmt_iterate(sqlite3_stmt *stmt, sqlite3 *db) {
248
248
  case SQLITE_ERROR:
249
249
  rb_raise(cSQLError, "%s", sqlite3_errmsg(db));
250
250
  default:
251
- rb_raise(cError, "Invalid return code for sqlite3_step: %d (please open an issue on https://github.com/digital-fabric/extralite)", ctx.rc);
251
+ rb_raise(cError, "%s", sqlite3_errmsg(db));
252
252
  }
253
253
 
254
254
  return 0;
@@ -7,6 +7,7 @@ VALUE cSQLError;
7
7
  VALUE cBusyError;
8
8
  VALUE cInterruptError;
9
9
 
10
+ ID ID_CALL;
10
11
  ID ID_KEYS;
11
12
  ID ID_NEW;
12
13
  ID ID_STRIP;
@@ -45,6 +46,12 @@ static VALUE Database_allocate(VALUE klass) {
45
46
  } \
46
47
  }
47
48
 
49
+ Database_t *Database_struct(VALUE self) {
50
+ Database_t *db;
51
+ GetDatabase(self, db);
52
+ return db;
53
+ }
54
+
48
55
  sqlite3 *Database_sqlite3_db(VALUE self) {
49
56
  Database_t *db;
50
57
  GetDatabase(self, db);
@@ -86,6 +93,8 @@ VALUE Database_initialize(VALUE self, VALUE path) {
86
93
  }
87
94
  #endif
88
95
 
96
+ db->trace_block = Qnil;
97
+
89
98
  return Qnil;
90
99
  }
91
100
 
@@ -134,6 +143,7 @@ static inline VALUE Database_perform_query(int argc, VALUE *argv, VALUE self, VA
134
143
 
135
144
  // prepare query ctx
136
145
  GetOpenDatabase(self, db);
146
+ if (db->trace_block != Qnil) rb_funcall(db->trace_block, ID_CALL, 1, sql);
137
147
  prepare_multi_stmt(db->sqlite3_db, &stmt, sql);
138
148
  bind_all_parameters(stmt, argc - 1, argv + 1);
139
149
  query_ctx ctx = { self, db->sqlite3_db, stmt };
@@ -470,6 +480,17 @@ VALUE backup_cleanup(VALUE ptr) {
470
480
  return Qnil;
471
481
  }
472
482
 
483
+ /* call-seq:
484
+ * db.backup(dest) -> db
485
+ * db.backup(dest) { |remaining, total| } -> db
486
+ *
487
+ * Creates a backup of the database to the given destination, which can be
488
+ * either a filename or a database instance. In order to monitor the backup
489
+ * progress you can pass a block that will be called periodically by the backup
490
+ * method with two arguments: the remaining page count, and the total page
491
+ * count, which can be used to display the progress to the user or to collect
492
+ * statistics.
493
+ */
473
494
  VALUE Database_backup(int argc, VALUE *argv, VALUE self) {
474
495
  VALUE dst;
475
496
  VALUE src_name;
@@ -513,8 +534,8 @@ VALUE Database_backup(int argc, VALUE *argv, VALUE self) {
513
534
  return self;
514
535
  }
515
536
 
516
- /*
517
- * Extralite.runtime_status(op[, reset]) -> [value, highwatermark]
537
+ /* call-seq:
538
+ * Extralite.runtime_status(op[, reset]) -> [value, highwatermark]
518
539
  *
519
540
  * Returns runtime status values for the given op as an array containing the
520
541
  * current value and the high water mark value. To reset the high water mark,
@@ -539,7 +560,7 @@ VALUE Extralite_runtime_status(int argc, VALUE* argv, VALUE self) {
539
560
  * current value and the high water mark value. To reset the high water mark,
540
561
  * pass true as reset.
541
562
  */
542
- VALUE Database_status(int argc, VALUE* argv, VALUE self) {
563
+ VALUE Database_status(int argc, VALUE *argv, VALUE self) {
543
564
  VALUE op, reset;
544
565
  int cur, hwm;
545
566
 
@@ -554,6 +575,75 @@ VALUE Database_status(int argc, VALUE* argv, VALUE self) {
554
575
  return rb_ary_new3(2, INT2NUM(cur), INT2NUM(hwm));
555
576
  }
556
577
 
578
+ /* call-seq:
579
+ * db.limit(category) -> value
580
+ * db.limit(category, new_value) -> prev_value
581
+ *
582
+ * Returns the current limit for the given category. If a new value is given,
583
+ * sets the limit to the new value and returns the previous value.
584
+ */
585
+ VALUE Database_limit(int argc, VALUE *argv, VALUE self) {
586
+ VALUE category, new_value;
587
+
588
+ rb_scan_args(argc, argv, "11", &category, &new_value);
589
+
590
+ Database_t *db;
591
+ GetOpenDatabase(self, db);
592
+
593
+ int value = sqlite3_limit(db->sqlite3_db, NUM2INT(category), RTEST(new_value) ? NUM2INT(new_value) : -1);
594
+
595
+ if (value == -1) rb_raise(cError, "Invalid limit category");
596
+
597
+ return INT2NUM(value);
598
+ }
599
+
600
+ /* call-seq:
601
+ * db.busy_timeout=(sec) -> db
602
+ * db.busy_timeout=nil -> db
603
+ *
604
+ * Sets the busy timeout for the database, in seconds or fractions thereof. To
605
+ * disable the busy timeout, set it to 0 or nil.
606
+ */
607
+ VALUE Database_busy_timeout_set(VALUE self, VALUE sec) {
608
+ Database_t *db;
609
+ GetOpenDatabase(self, db);
610
+
611
+ int ms = (sec == Qnil) ? 0 : (int)(NUM2DBL(sec) * 1000);
612
+
613
+ int rc = sqlite3_busy_timeout(db->sqlite3_db, ms);
614
+ if (rc != SQLITE_OK) rb_raise(cError, "Failed to set busy timeout");
615
+
616
+ return self;
617
+ }
618
+
619
+ /* call-seq:
620
+ * db.total_changes -> value
621
+ *
622
+ * Returns the total number of changes made to the database since opening it.
623
+ */
624
+ VALUE Database_total_changes(VALUE self) {
625
+ Database_t *db;
626
+ GetOpenDatabase(self, db);
627
+
628
+ int value = sqlite3_total_changes(db->sqlite3_db);
629
+ return INT2NUM(value);
630
+ }
631
+
632
+ /* call-seq:
633
+ * db.trace { |sql| } -> db
634
+ * db.trace -> db
635
+ *
636
+ * Installs or removes a block that will be invoked for every SQL statement
637
+ * executed.
638
+ */
639
+ VALUE Database_trace(VALUE self) {
640
+ Database_t *db;
641
+ GetOpenDatabase(self, db);
642
+
643
+ db->trace_block = rb_block_given_p() ? rb_block_proc() : Qnil;
644
+ return self;
645
+ }
646
+
557
647
  void Init_ExtraliteDatabase(void) {
558
648
  VALUE mExtralite = rb_define_module("Extralite");
559
649
  rb_define_singleton_method(mExtralite, "runtime_status", Extralite_runtime_status, -1);
@@ -563,6 +653,7 @@ void Init_ExtraliteDatabase(void) {
563
653
  rb_define_alloc_func(cDatabase, Database_allocate);
564
654
 
565
655
  rb_define_method(cDatabase, "backup", Database_backup, -1);
656
+ rb_define_method(cDatabase, "busy_timeout=", Database_busy_timeout_set, 1);
566
657
  rb_define_method(cDatabase, "changes", Database_changes, 0);
567
658
  rb_define_method(cDatabase, "close", Database_close, 0);
568
659
  rb_define_method(cDatabase, "closed?", Database_closed_p, 0);
@@ -572,6 +663,7 @@ void Init_ExtraliteDatabase(void) {
572
663
  rb_define_method(cDatabase, "initialize", Database_initialize, 1);
573
664
  rb_define_method(cDatabase, "interrupt", Database_interrupt, 0);
574
665
  rb_define_method(cDatabase, "last_insert_rowid", Database_last_insert_rowid, 0);
666
+ rb_define_method(cDatabase, "limit", Database_limit, -1);
575
667
  rb_define_method(cDatabase, "prepare", Database_prepare, 1);
576
668
  rb_define_method(cDatabase, "query", Database_query_hash, -1);
577
669
  rb_define_method(cDatabase, "query_ary", Database_query_ary, -1);
@@ -580,6 +672,8 @@ void Init_ExtraliteDatabase(void) {
580
672
  rb_define_method(cDatabase, "query_single_row", Database_query_single_row, -1);
581
673
  rb_define_method(cDatabase, "query_single_value", Database_query_single_value, -1);
582
674
  rb_define_method(cDatabase, "status", Database_status, -1);
675
+ rb_define_method(cDatabase, "total_changes", Database_total_changes, 0);
676
+ rb_define_method(cDatabase, "trace", Database_trace, 0);
583
677
  rb_define_method(cDatabase, "transaction_active?", Database_transaction_active_p, 0);
584
678
 
585
679
  #ifdef HAVE_SQLITE3_LOAD_EXTENSION
@@ -595,6 +689,7 @@ void Init_ExtraliteDatabase(void) {
595
689
  rb_gc_register_mark_object(cBusyError);
596
690
  rb_gc_register_mark_object(cInterruptError);
597
691
 
692
+ ID_CALL = rb_intern("call");
598
693
  ID_KEYS = rb_intern("keys");
599
694
  ID_NEW = rb_intern("new");
600
695
  ID_STRIP = rb_intern("strip");
@@ -27,6 +27,7 @@ extern VALUE cSQLError;
27
27
  extern VALUE cBusyError;
28
28
  extern VALUE cInterruptError;
29
29
 
30
+ extern ID ID_CALL;
30
31
  extern ID ID_KEYS;
31
32
  extern ID ID_NEW;
32
33
  extern ID ID_STRIP;
@@ -34,11 +35,13 @@ extern ID ID_TO_S;
34
35
 
35
36
  typedef struct {
36
37
  sqlite3 *sqlite3_db;
38
+ VALUE trace_block;
37
39
  } Database_t;
38
40
 
39
41
  typedef struct {
40
42
  VALUE db;
41
43
  VALUE sql;
44
+ Database_t *db_struct;
42
45
  sqlite3 *sqlite3_db;
43
46
  sqlite3_stmt *stmt;
44
47
  } PreparedStatement_t;
@@ -72,5 +75,6 @@ int stmt_iterate(sqlite3_stmt *stmt, sqlite3 *db);
72
75
  VALUE cleanup_stmt(query_ctx *ctx);
73
76
 
74
77
  sqlite3 *Database_sqlite3_db(VALUE self);
78
+ Database_t *Database_struct(VALUE self);
75
79
 
76
80
  #endif /* EXTRALITE_H */
@@ -50,6 +50,7 @@ VALUE PreparedStatement_initialize(VALUE self, VALUE db, VALUE sql) {
50
50
  rb_raise(cError, "Cannot prepare an empty SQL query");
51
51
 
52
52
  stmt->db = db;
53
+ stmt->db_struct = Database_struct(db);
53
54
  stmt->sqlite3_db = Database_sqlite3_db(db);
54
55
  stmt->sql = sql;
55
56
 
@@ -65,6 +66,8 @@ static inline VALUE PreparedStatement_perform_query(int argc, VALUE *argv, VALUE
65
66
  if (!stmt->stmt)
66
67
  rb_raise(cError, "Prepared statement is closed");
67
68
 
69
+ if (stmt->db_struct->trace_block != Qnil) rb_funcall(stmt->db_struct->trace_block, ID_CALL, 1, stmt->sql);
70
+
68
71
  sqlite3_reset(stmt->stmt);
69
72
  sqlite3_clear_bindings(stmt->stmt);
70
73
  bind_all_parameters(stmt->stmt, argc, argv);
@@ -0,0 +1,50 @@
1
+ module Extralite
2
+
3
+ SQLITE_STATUS_MEMORY_USED = 0
4
+ SQLITE_STATUS_PAGECACHE_USED = 1
5
+ SQLITE_STATUS_PAGECACHE_OVERFLOW = 2
6
+ SQLITE_STATUS_SCRATCH_USED = 3 # NOT USED
7
+ SQLITE_STATUS_SCRATCH_OVERFLOW = 4 # NOT USED
8
+ SQLITE_STATUS_MALLOC_SIZE = 5
9
+ SQLITE_STATUS_PARSER_STACK = 6
10
+ SQLITE_STATUS_PAGECACHE_SIZE = 7
11
+ SQLITE_STATUS_SCRATCH_SIZE = 8 # NOT USED
12
+ SQLITE_STATUS_MALLOC_COUNT = 9
13
+
14
+ SQLITE_DBSTATUS_LOOKASIDE_USED = 0
15
+ SQLITE_DBSTATUS_CACHE_USED = 1
16
+ SQLITE_DBSTATUS_SCHEMA_USED = 2
17
+ SQLITE_DBSTATUS_STMT_USED = 3
18
+ SQLITE_DBSTATUS_LOOKASIDE_HIT = 4
19
+ SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE = 5
20
+ SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL = 6
21
+ SQLITE_DBSTATUS_CACHE_HIT = 7
22
+ SQLITE_DBSTATUS_CACHE_MISS = 8
23
+ SQLITE_DBSTATUS_CACHE_WRITE = 9
24
+ SQLITE_DBSTATUS_DEFERRED_FKS = 10
25
+ SQLITE_DBSTATUS_CACHE_USED_SHARED = 11
26
+ SQLITE_DBSTATUS_CACHE_SPILL = 12
27
+
28
+ SQLITE_STMTSTATUS_FULLSCAN_STEP = 1
29
+ SQLITE_STMTSTATUS_SORT = 2
30
+ SQLITE_STMTSTATUS_AUTOINDEX = 3
31
+ SQLITE_STMTSTATUS_VM_STEP = 4
32
+ SQLITE_STMTSTATUS_REPREPARE = 5
33
+ SQLITE_STMTSTATUS_RUN = 6
34
+ SQLITE_STMTSTATUS_FILTER_MISS = 7
35
+ SQLITE_STMTSTATUS_FILTER_HIT = 8
36
+ SQLITE_STMTSTATUS_MEMUSED = 99
37
+
38
+ SQLITE_LIMIT_LENGTH = 0
39
+ SQLITE_LIMIT_SQL_LENGTH = 1
40
+ SQLITE_LIMIT_COLUMN = 2
41
+ SQLITE_LIMIT_EXPR_DEPTH = 3
42
+ SQLITE_LIMIT_COMPOUND_SELECT = 4
43
+ SQLITE_LIMIT_VDBE_OP = 5
44
+ SQLITE_LIMIT_FUNCTION_ARG = 6
45
+ SQLITE_LIMIT_ATTACHED = 7
46
+ SQLITE_LIMIT_LIKE_PATTERN_LENGTH = 8
47
+ SQLITE_LIMIT_VARIABLE_NUMBER = 9
48
+ SQLITE_LIMIT_TRIGGER_DEPTH = 10
49
+ SQLITE_LIMIT_WORKER_THREADS = 11
50
+ end
@@ -1,3 +1,3 @@
1
1
  module Extralite
2
- VERSION = '1.21'
2
+ VERSION = '1.23'
3
3
  end
data/lib/extralite.rb CHANGED
@@ -1,46 +1,11 @@
1
1
  require_relative './extralite_ext'
2
+ require_relative './extralite/sqlite3_constants'
2
3
 
3
4
  # Extralite is a Ruby gem for working with SQLite databases
4
5
  module Extralite
5
-
6
- SQLITE_STATUS_MEMORY_USED = 0
7
- SQLITE_STATUS_PAGECACHE_USED = 1
8
- SQLITE_STATUS_PAGECACHE_OVERFLOW = 2
9
- SQLITE_STATUS_SCRATCH_USED = 3 # NOT USED
10
- SQLITE_STATUS_SCRATCH_OVERFLOW = 4 # NOT USED
11
- SQLITE_STATUS_MALLOC_SIZE = 5
12
- SQLITE_STATUS_PARSER_STACK = 6
13
- SQLITE_STATUS_PAGECACHE_SIZE = 7
14
- SQLITE_STATUS_SCRATCH_SIZE = 8 # NOT USED
15
- SQLITE_STATUS_MALLOC_COUNT = 9
16
6
 
17
- SQLITE_DBSTATUS_LOOKASIDE_USED = 0
18
- SQLITE_DBSTATUS_CACHE_USED = 1
19
- SQLITE_DBSTATUS_SCHEMA_USED = 2
20
- SQLITE_DBSTATUS_STMT_USED = 3
21
- SQLITE_DBSTATUS_LOOKASIDE_HIT = 4
22
- SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE = 5
23
- SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL = 6
24
- SQLITE_DBSTATUS_CACHE_HIT = 7
25
- SQLITE_DBSTATUS_CACHE_MISS = 8
26
- SQLITE_DBSTATUS_CACHE_WRITE = 9
27
- SQLITE_DBSTATUS_DEFERRED_FKS = 10
28
- SQLITE_DBSTATUS_CACHE_USED_SHARED = 11
29
- SQLITE_DBSTATUS_CACHE_SPILL = 12
30
-
31
- SQLITE_STMTSTATUS_FULLSCAN_STEP = 1
32
- SQLITE_STMTSTATUS_SORT = 2
33
- SQLITE_STMTSTATUS_AUTOINDEX = 3
34
- SQLITE_STMTSTATUS_VM_STEP = 4
35
- SQLITE_STMTSTATUS_REPREPARE = 5
36
- SQLITE_STMTSTATUS_RUN = 6
37
- SQLITE_STMTSTATUS_FILTER_MISS = 7
38
- SQLITE_STMTSTATUS_FILTER_HIT = 8
39
- SQLITE_STMTSTATUS_MEMUSED = 99
40
-
41
- # The following class definitions are not really needed, as they're already
42
- # defined in the C extension. We put them here for the sake of generating
43
- # docs.
7
+ # The following error classes are already defined in the C extension. We put
8
+ # them here for the sake of generating docs.
44
9
 
45
10
  # A base class for Extralite exceptions
46
11
  class Error < ::StandardError
@@ -70,14 +35,26 @@ module Extralite
70
35
  AND name NOT LIKE 'sqlite_%';
71
36
  SQL
72
37
 
38
+ # Returns the list of currently defined tables.
39
+ #
40
+ # @return [Array] list of tables
73
41
  def tables
74
42
  query_single_column(TABLES_SQL)
75
43
  end
76
44
 
45
+ # Gets or sets one or more pragmas:
46
+ #
47
+ # db.pragma(:cache_size) # get
48
+ # db.pragma(cache_size: -2000) # set
49
+ #
50
+ # @param value [Symbol, String, Hash] pragma name or hash mapping names to values
51
+ # @return [Hash] query result
77
52
  def pragma(value)
78
53
  value.is_a?(Hash) ? pragma_set(value) : pragma_get(value)
79
54
  end
80
55
 
56
+ private
57
+
81
58
  def pragma_set(values)
82
59
  sql = values.inject(+'') { |s, (k, v)| s += "pragma #{k}=#{v}; " }
83
60
  query(sql)
@@ -87,18 +64,4 @@ module Extralite
87
64
  query("pragma #{key}")
88
65
  end
89
66
  end
90
-
91
- # An SQLite backup
92
- class Backup
93
- # def initialize(dst, dst_name, src, src_name); end
94
-
95
- # def dst; end
96
- # def src; end
97
-
98
- # def step(pages); end
99
- # def finish; end
100
-
101
- # def pagecount; end
102
- # def remaining; end
103
- end
104
67
  end
@@ -269,6 +269,68 @@ end
269
269
  def test_database_status
270
270
  assert_operator 0, :<, @db.status(Extralite::SQLITE_DBSTATUS_SCHEMA_USED).first
271
271
  end
272
+
273
+ def test_database_limit
274
+ result = @db.limit(Extralite::SQLITE_LIMIT_ATTACHED)
275
+ assert_equal 10, result
276
+
277
+ result = @db.limit(Extralite::SQLITE_LIMIT_ATTACHED, 5)
278
+ assert_equal 10, result
279
+
280
+ result = @db.limit(Extralite::SQLITE_LIMIT_ATTACHED)
281
+ assert_equal 5, result
282
+
283
+ assert_raises(Extralite::Error) { @db.limit(-999) }
284
+ end
285
+
286
+ def test_database_busy_timeout
287
+ fn = "/tmp/extralite-#{rand(10000)}.db"
288
+ db1 = Extralite::Database.new(fn)
289
+ db2 = Extralite::Database.new(fn)
290
+
291
+ db1.query('begin exclusive')
292
+ assert_raises(Extralite::BusyError) { db2.query('begin exclusive') }
293
+
294
+ db2.busy_timeout = 0.3
295
+ t0 = Time.now
296
+ t = Thread.new { sleep 0.1; db1.query('rollback') }
297
+ result = db2.query('begin exclusive')
298
+ t1 = Time.now
299
+
300
+ assert_equal [], result
301
+ assert t1 - t0 >= 0.1
302
+ db2.query('rollback')
303
+
304
+ # try to provoke a timeout
305
+ db1.query('begin exclusive')
306
+ db2.busy_timeout = 0.1
307
+ t0 = Time.now
308
+ t = Thread.new do
309
+ sleep 0.5
310
+ ensure
311
+ db1.query('rollback')
312
+ end
313
+ assert_raises(Extralite::BusyError) { db2.query('begin exclusive') }
314
+ t1 = Time.now
315
+ assert t1 - t0 >= 0.1
316
+ t.kill
317
+ t.join
318
+
319
+ db1.query('begin exclusive')
320
+ db2.busy_timeout = 0
321
+ assert_raises(Extralite::BusyError) { db2.query('begin exclusive') }
322
+
323
+ db2.busy_timeout = nil
324
+ assert_raises(Extralite::BusyError) { db2.query('begin exclusive') }
325
+ end
326
+
327
+ def test_database_total_changes
328
+ assert_equal 2, @db.total_changes
329
+
330
+ @db.query('insert into t values (7, 8, 9)')
331
+
332
+ assert_equal 3, @db.total_changes
333
+ end
272
334
  end
273
335
 
274
336
  class ScenarioTest < MiniTest::Test
@@ -336,6 +398,30 @@ class ScenarioTest < MiniTest::Test
336
398
  result = @db.query_single_column('select x from t')
337
399
  assert_equal [1, 4, 7], result
338
400
  end
401
+
402
+ def test_database_trace
403
+ sqls = []
404
+ @db.trace { |sql| sqls << sql }
405
+
406
+ @db.query('select 1')
407
+ assert_equal ['select 1'], sqls
408
+
409
+ @db.query('select 2')
410
+ assert_equal ['select 1', 'select 2'], sqls
411
+
412
+ stmt = @db.prepare('select 3')
413
+
414
+ stmt.query
415
+ assert_equal ['select 1', 'select 2', 'select 3'], sqls
416
+
417
+ # turn off
418
+ @db.trace
419
+
420
+ stmt.query
421
+
422
+ @db.query('select 4')
423
+ assert_equal ['select 1', 'select 2', 'select 3'], sqls
424
+ end
339
425
  end
340
426
 
341
427
  class BackupTest < MiniTest::Test
@@ -37,6 +37,11 @@ class PreparedStatementTest < MiniTest::Test
37
37
  assert_raises(Extralite::SQLError) { @db.prepare('blah') }
38
38
  end
39
39
 
40
+ def test_prepared_statement_with_multiple_queries
41
+ error = begin; @db.prepare('select 1; select 2'); rescue => e; error = e; end
42
+ assert_equal Extralite::Error, error.class
43
+ end
44
+
40
45
  def test_prepared_statement_query_hash
41
46
  r = @stmt.query_hash(4)
42
47
  assert_equal [{x: 4, y: 5, z: 6}], r
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: extralite
3
3
  version: !ruby/object:Gem::Version
4
- version: '1.21'
4
+ version: '1.23'
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-01-23 00:00:00.000000000 Z
11
+ date: 2023-01-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake-compiler
@@ -113,6 +113,7 @@ files:
113
113
  - extralite.gemspec
114
114
  - gemspec.rb
115
115
  - lib/extralite.rb
116
+ - lib/extralite/sqlite3_constants.rb
116
117
  - lib/extralite/version.rb
117
118
  - lib/sequel/adapters/extralite.rb
118
119
  - test/extensions/text.dylib