Skip to content

Commit a313b9a

Browse files
authored
Merge pull request #463 from fractaledmind/statement-timeout
Add `statement_timeout=` method
2 parents e9e722e + c80aeef commit a313b9a

9 files changed

+85
-2
lines changed

bin/test-gem-file-contents

+2-2
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ describe File.basename(gemfile) do
123123

124124
it "contains extension C and header files" do
125125
assert_equal(6, gemfile_contents.count { |f| File.fnmatch?("ext/**/*.c", f) })
126-
assert_equal(6, gemfile_contents.count { |f| File.fnmatch?("ext/**/*.h", f) })
126+
assert_equal(7, gemfile_contents.count { |f| File.fnmatch?("ext/**/*.h", f) })
127127
end
128128

129129
it "includes C files in extra_rdoc_files" do
@@ -165,7 +165,7 @@ describe File.basename(gemfile) do
165165

166166
it "contains extension C and header files" do
167167
assert_equal(6, gemfile_contents.count { |f| File.fnmatch?("ext/**/*.c", f) })
168-
assert_equal(6, gemfile_contents.count { |f| File.fnmatch?("ext/**/*.h", f) })
168+
assert_equal(7, gemfile_contents.count { |f| File.fnmatch?("ext/**/*.h", f) })
169169
end
170170

171171
it "includes C files in extra_rdoc_files" do

ext/sqlite3/database.c

+41
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,44 @@ busy_handler(int argc, VALUE *argv, VALUE self)
259259
return self;
260260
}
261261

262+
static int
263+
rb_sqlite3_statement_timeout(void *context)
264+
{
265+
sqlite3RubyPtr ctx = (sqlite3RubyPtr)context;
266+
struct timespec currentTime;
267+
clock_gettime(CLOCK_MONOTONIC, &currentTime);
268+
269+
if (!timespecisset(&ctx->stmt_deadline)) {
270+
// Set stmt_deadline if not already set
271+
ctx->stmt_deadline = currentTime;
272+
} else if (timespecafter(&currentTime, &ctx->stmt_deadline)) {
273+
return 1;
274+
}
275+
276+
return 0;
277+
}
278+
279+
/* call-seq: db.statement_timeout = ms
280+
*
281+
* Indicates that if a query lasts longer than the indicated number of
282+
* milliseconds, SQLite should interrupt that query and return an error.
283+
* By default, SQLite does not interrupt queries. To restore the default
284+
* behavior, send 0 as the +ms+ parameter.
285+
*/
286+
static VALUE
287+
set_statement_timeout(VALUE self, VALUE milliseconds)
288+
{
289+
sqlite3RubyPtr ctx;
290+
TypedData_Get_Struct(self, sqlite3Ruby, &database_type, ctx);
291+
292+
ctx->stmt_timeout = NUM2INT(milliseconds);
293+
int n = NUM2INT(milliseconds) == 0 ? -1 : 1000;
294+
295+
sqlite3_progress_handler(ctx->db, n, rb_sqlite3_statement_timeout, (void *)ctx);
296+
297+
return self;
298+
}
299+
262300
/* call-seq: last_insert_row_id
263301
*
264302
* Obtains the unique row ID of the last row to be inserted by this Database
@@ -869,6 +907,9 @@ init_sqlite3_database(void)
869907
rb_define_method(cSqlite3Database, "authorizer=", set_authorizer, 1);
870908
rb_define_method(cSqlite3Database, "busy_handler", busy_handler, -1);
871909
rb_define_method(cSqlite3Database, "busy_timeout=", set_busy_timeout, 1);
910+
#ifndef SQLITE_OMIT_PROGRESS_CALLBACK
911+
rb_define_method(cSqlite3Database, "statement_timeout=", set_statement_timeout, 1);
912+
#endif
872913
rb_define_method(cSqlite3Database, "extended_result_codes=", set_extended_result_codes, 1);
873914
rb_define_method(cSqlite3Database, "transaction_active?", transaction_active_p, 0);
874915
rb_define_private_method(cSqlite3Database, "exec_batch", exec_batch, 2);

ext/sqlite3/database.h

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
struct _sqlite3Ruby {
77
sqlite3 *db;
88
VALUE busy_handler;
9+
int stmt_timeout;
10+
struct timespec stmt_deadline;
911
};
1012

1113
typedef struct _sqlite3Ruby sqlite3Ruby;

ext/sqlite3/sqlite3_ruby.h

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ extern VALUE cSqlite3Blob;
4141
#include <statement.h>
4242
#include <exception.h>
4343
#include <backup.h>
44+
#include <timespec.h>
4445

4546
int bignum_to_int64(VALUE big, sqlite3_int64 *result);
4647

ext/sqlite3/statement.c

+1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ prepare(VALUE self, VALUE db, VALUE sql)
7070
);
7171

7272
CHECK(db_ctx->db, status);
73+
timespecclear(&db_ctx->stmt_deadline);
7374

7475
return rb_str_new2(tail);
7576
}

ext/sqlite3/timespec.h

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#define timespecclear(tsp) (tsp)->tv_sec = (tsp)->tv_nsec = 0
2+
#define timespecisset(tsp) ((tsp)->tv_sec || (tsp)->tv_nsec)
3+
#define timespecisvalid(tsp) \
4+
((tsp)->tv_nsec >= 0 && (tsp)->tv_nsec < 1000000000L)
5+
#define timespeccmp(tsp, usp, cmp) \
6+
(((tsp)->tv_sec == (usp)->tv_sec) ? \
7+
((tsp)->tv_nsec cmp (usp)->tv_nsec) : \
8+
((tsp)->tv_sec cmp (usp)->tv_sec))
9+
#define timespecsub(tsp, usp, vsp) \
10+
do { \
11+
(vsp)->tv_sec = (tsp)->tv_sec - (usp)->tv_sec; \
12+
(vsp)->tv_nsec = (tsp)->tv_nsec - (usp)->tv_nsec; \
13+
if ((vsp)->tv_nsec < 0) { \
14+
(vsp)->tv_sec--; \
15+
(vsp)->tv_nsec += 1000000000L; \
16+
} \
17+
} while (0)
18+
#define timespecafter(tsp, usp) \
19+
(((tsp)->tv_sec > (usp)->tv_sec) || \
20+
((tsp)->tv_sec == (usp)->tv_sec && (tsp)->tv_nsec > (usp)->tv_nsec))

lib/sqlite3/database.rb

+1
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ def initialize file, options = {}, zvfs = nil
126126
@tracefunc = nil
127127
@authorizer = nil
128128
@busy_handler = nil
129+
@progress_handler = nil
129130
@collations = {}
130131
@functions = {}
131132
@results_as_hash = options[:results_as_hash]

sqlite3.gemspec

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ Gem::Specification.new do |s|
5858
"ext/sqlite3/sqlite3_ruby.h",
5959
"ext/sqlite3/statement.c",
6060
"ext/sqlite3/statement.h",
61+
"ext/sqlite3/timespec.h",
6162
"lib/sqlite3.rb",
6263
"lib/sqlite3/constants.rb",
6364
"lib/sqlite3/database.rb",

test/test_integration_statement.rb

+16
Original file line numberDiff line numberDiff line change
@@ -191,4 +191,20 @@ def test_committing_tx_with_statement_active
191191
end
192192
assert called
193193
end
194+
195+
def test_long_running_statements_get_interrupted_when_statement_timeout_set
196+
@db.statement_timeout = 10
197+
assert_raises(SQLite3::InterruptException) do
198+
@db.execute <<~SQL
199+
WITH RECURSIVE r(i) AS (
200+
VALUES(0)
201+
UNION ALL
202+
SELECT i FROM r
203+
LIMIT 100000
204+
)
205+
SELECT i FROM r ORDER BY i LIMIT 1;
206+
SQL
207+
end
208+
@db.statement_timeout = 0
209+
end
194210
end

0 commit comments

Comments
 (0)