From 6c4e1553ef80c0369b7b14be25de12212f95be68 Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Sun, 7 Jan 2024 18:57:15 +0100 Subject: [PATCH 01/18] Add the progress_handler interface --- ext/sqlite3/database.c | 46 +++++++++++++++++++++++++++++++++++++++++ lib/sqlite3/database.rb | 1 + 2 files changed, 47 insertions(+) diff --git a/ext/sqlite3/database.c b/ext/sqlite3/database.c index bd7b2ea6..15b318c4 100644 --- a/ext/sqlite3/database.c +++ b/ext/sqlite3/database.c @@ -237,6 +237,51 @@ static VALUE busy_handler(int argc, VALUE *argv, VALUE self) return self; } +static int +rb_sqlite3_progress_handler(void *ctx) +{ + VALUE self = (VALUE)(ctx); + VALUE handle = rb_iv_get(self, "@progress_handler"); + VALUE result = rb_funcall(handle, rb_intern("call"), 0); + + if (Qfalse == result) return 1; + + return 0; +} + +/* call-seq: + * progress_handler([n]) { ... } + * progress_handler([n,] Class.new { def call; end }.new) + * + * Register a progress handler with this database instance. + * This handler will be invoked periodically during a long-running query or operation. + * If the handler returns +false+, the operation will be interrupted; otherwise, it continues. + * The parameter 'n' specifies the number of SQLite virtual machine instructions between invocations. + * If 'n' is not provided, the default value is 1. + */ +static VALUE +progress_handler(int argc, VALUE *argv, VALUE self) +{ + sqlite3RubyPtr ctx; + VALUE block, n_value; + + TypedData_Get_Struct(self, sqlite3Ruby, &database_type, ctx); + REQUIRE_OPEN_DB(ctx); + + rb_scan_args(argc, argv, "02", &n_value, &block); + + int n = NIL_P(n_value) ? 1 : NUM2INT(n_value); + if(NIL_P(block) && rb_block_given_p()) block = rb_block_proc(); + + rb_iv_set(self, "@progress_handler", block); + + sqlite3_progress_handler( + ctx->db, n, NIL_P(block) ? NULL : rb_sqlite3_progress_handler, (void *)self); + + return self; +} + + /* call-seq: last_insert_row_id * * Obtains the unique row ID of the last row to be inserted by this Database @@ -854,6 +899,7 @@ void init_sqlite3_database(void) rb_define_method(cSqlite3Database, "changes", changes, 0); rb_define_method(cSqlite3Database, "authorizer=", set_authorizer, 1); rb_define_method(cSqlite3Database, "busy_handler", busy_handler, -1); + rb_define_method(cSqlite3Database, "progress_handler", progress_handler, -1); rb_define_method(cSqlite3Database, "busy_timeout=", set_busy_timeout, 1); rb_define_method(cSqlite3Database, "extended_result_codes=", set_extended_result_codes, 1); rb_define_method(cSqlite3Database, "transaction_active?", transaction_active_p, 0); diff --git a/lib/sqlite3/database.rb b/lib/sqlite3/database.rb index 5d004a23..7cad9403 100644 --- a/lib/sqlite3/database.rb +++ b/lib/sqlite3/database.rb @@ -124,6 +124,7 @@ def initialize file, options = {}, zvfs = nil @authorizer = nil @encoding = nil @busy_handler = nil + @progress_handler = nil @collations = {} @functions = {} @results_as_hash = options[:results_as_hash] From 046bd67a574399b2b05a4f5c32181d7a0af73437 Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Sun, 7 Jan 2024 18:57:26 +0100 Subject: [PATCH 02/18] Add tests for the progress_handler interface --- test/test_integration.rb | 54 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/test/test_integration.rb b/test/test_integration.rb index d5ea3a53..a7b94a4e 100644 --- a/test/test_integration.rb +++ b/test/test_integration.rb @@ -470,7 +470,7 @@ def test_transaction_active def test_transaction_implicit_rollback assert !@db.transaction_active? - @db.transaction + @db.transaction @db.execute('create table bar (x CHECK(1 = 0))') assert @db.transaction_active? assert_raises( SQLite3::ConstraintException ) do @@ -504,4 +504,56 @@ def test_bind_array_parameter [ 1, "foo" ] ) assert_equal "foo", result end + + def test_progress_handler_used + progress_calls = [] + @db.progress_handler do + progress_calls << nil + true + end + @db.execute "create table test1(a, b)" + + assert_operator 1, :<, progress_calls.size + end + + def test_progress_handler_opcode_arg + progress_calls = [] + handler = Proc.new do + progress_calls << nil + true + end + @db.progress_handler(1, handler) + @db.execute "create table test1(a, b)" + first_count = progress_calls.size + + progress_calls = [] + @db.progress_handler(2, handler) + @db.execute "create table test2(a, b)" + second_count = progress_calls.size + + assert_operator first_count, :>, second_count + end + + def test_progress_handler_interrupts_operation + @db.progress_handler do + false + end + + assert_raises(SQLite3::InterruptException) do + @db.execute "create table test1(a, b)" + end + end + + def test_clear_handler + progress_calls = [] + @db.progress_handler do + progress_calls << nil + true + end + @db.progress_handler(nil) + + @db.execute "create table test1(a, b)" + + assert_equal 0, progress_calls.size + end end From 50f1ef011d8f7cfbc96c5e6078109a0a231560c8 Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Sun, 7 Jan 2024 21:02:15 +0100 Subject: [PATCH 03/18] Remove extra line --- ext/sqlite3/database.c | 1 - 1 file changed, 1 deletion(-) diff --git a/ext/sqlite3/database.c b/ext/sqlite3/database.c index 3a0c6a39..139d7f3d 100644 --- a/ext/sqlite3/database.c +++ b/ext/sqlite3/database.c @@ -887,7 +887,6 @@ init_sqlite3_database(void) #if 0 VALUE mSqlite3 = rb_define_module("SQLite3"); #endif - cSqlite3Database = rb_define_class_under(mSqlite3, "Database", rb_cObject); rb_define_alloc_func(cSqlite3Database, allocate); From c6bf834b052e0f05249acfe9807fc2c802b6191f Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Sun, 7 Jan 2024 21:16:25 +0100 Subject: [PATCH 04/18] Bump the frequency for the opcode arg test --- test/test_integration.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_integration.rb b/test/test_integration.rb index a7b94a4e..dbe5e175 100644 --- a/test/test_integration.rb +++ b/test/test_integration.rb @@ -527,7 +527,7 @@ def test_progress_handler_opcode_arg first_count = progress_calls.size progress_calls = [] - @db.progress_handler(2, handler) + @db.progress_handler(4, handler) @db.execute "create table test2(a, b)" second_count = progress_calls.size From 3681b2a792faf3843256ba05798e887f852949b5 Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Sun, 7 Jan 2024 21:18:47 +0100 Subject: [PATCH 05/18] Bump the frequency for the opcode arg test and make assertion more resilient --- test/test_integration.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_integration.rb b/test/test_integration.rb index dbe5e175..29bd9ee3 100644 --- a/test/test_integration.rb +++ b/test/test_integration.rb @@ -527,11 +527,11 @@ def test_progress_handler_opcode_arg first_count = progress_calls.size progress_calls = [] - @db.progress_handler(4, handler) + @db.progress_handler(10, handler) @db.execute "create table test2(a, b)" second_count = progress_calls.size - assert_operator first_count, :>, second_count + assert_operator first_count, :>=, second_count end def test_progress_handler_interrupts_operation From c0d51448ae14799bdaf3d81e599f2ea46fcafe7c Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Sun, 7 Jan 2024 22:00:23 +0100 Subject: [PATCH 06/18] Only define the ruby progress_handler method if the OMIT compilation flag is not set --- ext/sqlite3/database.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ext/sqlite3/database.c b/ext/sqlite3/database.c index 139d7f3d..921d0ec3 100644 --- a/ext/sqlite3/database.c +++ b/ext/sqlite3/database.c @@ -911,7 +911,9 @@ init_sqlite3_database(void) rb_define_method(cSqlite3Database, "changes", changes, 0); rb_define_method(cSqlite3Database, "authorizer=", set_authorizer, 1); rb_define_method(cSqlite3Database, "busy_handler", busy_handler, -1); +#ifndef SQLITE_OMIT_PROGRESS_CALLBACK rb_define_method(cSqlite3Database, "progress_handler", progress_handler, -1); +#endif rb_define_method(cSqlite3Database, "busy_timeout=", set_busy_timeout, 1); rb_define_method(cSqlite3Database, "extended_result_codes=", set_extended_result_codes, 1); rb_define_method(cSqlite3Database, "transaction_active?", transaction_active_p, 0); From e3f95ca8deb9d2a0972c333a1d15f305c6939f13 Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Mon, 8 Jan 2024 14:47:09 +0100 Subject: [PATCH 07/18] Add statement_timeout= method that leverages progress_handler to interrupt long-running statements --- lib/sqlite3/database.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/sqlite3/database.rb b/lib/sqlite3/database.rb index 206918f9..034bbf8c 100644 --- a/lib/sqlite3/database.rb +++ b/lib/sqlite3/database.rb @@ -749,6 +749,21 @@ def translate_from_db types, row @type_translator.call types, row end + def statement_timeout=( milliseconds ) + timeout_seconds = milliseconds.fdiv(1000) + + progress_handler do + now = Process.clock_gettime(Process::CLOCK_MONOTONIC) + if @statement_timeout_deadline.nil? + @statement_timeout_deadline = now + timeout_seconds + elsif now > @statement_timeout_deadline + next false + else + true + end + end + end + private NULL_TRANSLATOR = lambda { |_, row| row } From b819265e75bb080eb9fa1eae594b66996faf030d Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Mon, 8 Jan 2024 14:53:07 +0100 Subject: [PATCH 08/18] Add a test for statement_timeout= --- test/test_integration_statement.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/test_integration_statement.rb b/test/test_integration_statement.rb index 759941f3..f04dde25 100644 --- a/test/test_integration_statement.rb +++ b/test/test_integration_statement.rb @@ -191,4 +191,20 @@ def test_committing_tx_with_statement_active end assert called end + + def test_long_running_statements_get_interrupted_when_statement_timeout_set + @db.statement_timeout = 100 + assert_raises(SQLite3::InterruptException) do + @db.execute <<~SQL + WITH RECURSIVE r(i) AS ( + VALUES(0) + UNION ALL + SELECT i FROM r + LIMIT 10000000 + ) + SELECT i FROM r ORDER BY i LIMIT 1; + SQL + end + @db.statement_timeout = nil + end end From 3ba847c13b04c42a2c98dc9f3260145c3deabcda Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Mon, 8 Jan 2024 14:53:26 +0100 Subject: [PATCH 09/18] Allow statement_timeout= to accept a nil to reset the progress_handler --- lib/sqlite3/database.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/sqlite3/database.rb b/lib/sqlite3/database.rb index bbf1ef9c..6fb18e31 100644 --- a/lib/sqlite3/database.rb +++ b/lib/sqlite3/database.rb @@ -751,6 +751,8 @@ def translate_from_db types, row end def statement_timeout=( milliseconds ) + progress_handler(nil) and return if milliseconds.nil? + timeout_seconds = milliseconds.fdiv(1000) progress_handler do From bf1f0e630cb5ae9c3a517a4fd56374cd78755cfe Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Thu, 11 Jan 2024 17:02:39 +0100 Subject: [PATCH 10/18] Implement that statement_timeout= in pure C --- ext/sqlite3/database.c | 60 ++++++++++++++---------------- ext/sqlite3/database.h | 2 + lib/sqlite3/database.rb | 17 --------- test/test_integration_statement.rb | 4 +- 4 files changed, 32 insertions(+), 51 deletions(-) diff --git a/ext/sqlite3/database.c b/ext/sqlite3/database.c index aa103989..1896a58e 100644 --- a/ext/sqlite3/database.c +++ b/ext/sqlite3/database.c @@ -17,6 +17,8 @@ database_mark(void *ctx) { sqlite3RubyPtr c = (sqlite3RubyPtr)ctx; rb_gc_mark(c->busy_handler); + rb_gc_mark(c->stmt_timeout); + rb_gc_mark(c->stmt_deadline); } static void @@ -260,50 +262,44 @@ busy_handler(int argc, VALUE *argv, VALUE self) } static int -rb_sqlite3_progress_handler(void *ctx) +rb_sqlite3_statement_timeout(void *context) { - VALUE self = (VALUE)(ctx); - VALUE handle = rb_iv_get(self, "@progress_handler"); - VALUE result = rb_funcall(handle, rb_intern("call"), 0); + sqlite3RubyPtr ctx = (sqlite3RubyPtr)context; + struct timespec currentTime; + clock_gettime(CLOCK_MONOTONIC, ¤tTime); + + if (ctx->stmt_deadline == 0) { + ctx->stmt_deadline = currentTime.tv_sec * 1e9 + currentTime.tv_nsec + (long long)ctx->stmt_timeout * 1e6; + } else { + long long timeDiff = ctx->stmt_deadline - (currentTime.tv_sec * 1e9 + currentTime.tv_nsec); - if (Qfalse == result) return 1; + if (timeDiff < 0) { return 1; } + } - return 0; + return 0; } -/* call-seq: - * progress_handler([n]) { ... } - * progress_handler([n,] Class.new { def call; end }.new) +/* call-seq: db.statement_timeout = ms * - * Register a progress handler with this database instance. - * This handler will be invoked periodically during a long-running query or operation. - * If the handler returns +false+, the operation will be interrupted; otherwise, it continues. - * The parameter 'n' specifies the number of SQLite virtual machine instructions between invocations. - * If 'n' is not provided, the default value is 1. + * Indicates that if a query lasts longer than the indicated number of + * milliseconds, SQLite should interrupt that query and return an error. + * By default, SQLite does not interrupt queries. To restore the default + * behavior, send 0 as the +ms+ parameter. */ static VALUE -progress_handler(int argc, VALUE *argv, VALUE self) +set_statement_timeout(VALUE self, VALUE milliseconds) { - sqlite3RubyPtr ctx; - VALUE block, n_value; - - TypedData_Get_Struct(self, sqlite3Ruby, &database_type, ctx); - REQUIRE_OPEN_DB(ctx); - - rb_scan_args(argc, argv, "02", &n_value, &block); - - int n = NIL_P(n_value) ? 1 : NUM2INT(n_value); - if(NIL_P(block) && rb_block_given_p()) block = rb_block_proc(); + sqlite3RubyPtr ctx; + TypedData_Get_Struct(self, sqlite3Ruby, &database_type, ctx); - rb_iv_set(self, "@progress_handler", block); + ctx->stmt_timeout = NUM2INT(milliseconds); + int n = NUM2INT(milliseconds) == 0 ? -1 : 1000; - sqlite3_progress_handler( - ctx->db, n, NIL_P(block) ? NULL : rb_sqlite3_progress_handler, (void *)self); + sqlite3_progress_handler(ctx->db, n, rb_sqlite3_statement_timeout, (void *)ctx); - return self; + return self; } - /* call-seq: last_insert_row_id * * Obtains the unique row ID of the last row to be inserted by this Database @@ -919,10 +915,10 @@ init_sqlite3_database(void) rb_define_method(cSqlite3Database, "changes", changes, 0); rb_define_method(cSqlite3Database, "authorizer=", set_authorizer, 1); rb_define_method(cSqlite3Database, "busy_handler", busy_handler, -1); + rb_define_method(cSqlite3Database, "busy_timeout=", set_busy_timeout, 1); #ifndef SQLITE_OMIT_PROGRESS_CALLBACK - rb_define_method(cSqlite3Database, "progress_handler", progress_handler, -1); + rb_define_method(cSqlite3Database, "statement_timeout=", set_statement_timeout, 1); #endif - rb_define_method(cSqlite3Database, "busy_timeout=", set_busy_timeout, 1); rb_define_method(cSqlite3Database, "extended_result_codes=", set_extended_result_codes, 1); rb_define_method(cSqlite3Database, "transaction_active?", transaction_active_p, 0); rb_define_private_method(cSqlite3Database, "exec_batch", exec_batch, 2); diff --git a/ext/sqlite3/database.h b/ext/sqlite3/database.h index 56833020..9fbdb60d 100644 --- a/ext/sqlite3/database.h +++ b/ext/sqlite3/database.h @@ -6,6 +6,8 @@ struct _sqlite3Ruby { sqlite3 *db; VALUE busy_handler; + int stmt_timeout; + long long stmt_deadline; }; typedef struct _sqlite3Ruby sqlite3Ruby; diff --git a/lib/sqlite3/database.rb b/lib/sqlite3/database.rb index 58668e41..5b97226a 100644 --- a/lib/sqlite3/database.rb +++ b/lib/sqlite3/database.rb @@ -692,23 +692,6 @@ def busy_handler_timeout=(milliseconds) end end - def statement_timeout=( milliseconds ) - progress_handler(nil) and return if milliseconds.nil? - - timeout_seconds = milliseconds.fdiv(1000) - - progress_handler do - now = Process.clock_gettime(Process::CLOCK_MONOTONIC) - if @statement_timeout_deadline.nil? - @statement_timeout_deadline = now + timeout_seconds - elsif now > @statement_timeout_deadline - next false - else - true - end - end - end - # A helper class for dealing with custom functions (see #create_function, # #create_aggregate, and #create_aggregate_handler). It encapsulates the # opaque function object that represents the current invocation. It also diff --git a/test/test_integration_statement.rb b/test/test_integration_statement.rb index ba1a8349..a766de54 100644 --- a/test/test_integration_statement.rb +++ b/test/test_integration_statement.rb @@ -200,11 +200,11 @@ def test_long_running_statements_get_interrupted_when_statement_timeout_set VALUES(0) UNION ALL SELECT i FROM r - LIMIT 10000000 + LIMIT 1000000 ) SELECT i FROM r ORDER BY i LIMIT 1; SQL end - @db.statement_timeout = nil + @db.statement_timeout = 0 end end From c8007e78b5697181120020a9145c92112ea6d764 Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Thu, 11 Jan 2024 17:13:09 +0100 Subject: [PATCH 11/18] Remove old progress_handler tests --- test/test_integration.rb | 52 ---------------------------------------- 1 file changed, 52 deletions(-) diff --git a/test/test_integration.rb b/test/test_integration.rb index a593f0fe..f0c005ab 100644 --- a/test/test_integration.rb +++ b/test/test_integration.rb @@ -505,56 +505,4 @@ def test_bind_array_parameter [1, "foo"]) assert_equal "foo", result end - - def test_progress_handler_used - progress_calls = [] - @db.progress_handler do - progress_calls << nil - true - end - @db.execute "create table test1(a, b)" - - assert_operator 1, :<, progress_calls.size - end - - def test_progress_handler_opcode_arg - progress_calls = [] - handler = Proc.new do - progress_calls << nil - true - end - @db.progress_handler(1, handler) - @db.execute "create table test1(a, b)" - first_count = progress_calls.size - - progress_calls = [] - @db.progress_handler(10, handler) - @db.execute "create table test2(a, b)" - second_count = progress_calls.size - - assert_operator first_count, :>=, second_count - end - - def test_progress_handler_interrupts_operation - @db.progress_handler do - false - end - - assert_raises(SQLite3::InterruptException) do - @db.execute "create table test1(a, b)" - end - end - - def test_clear_handler - progress_calls = [] - @db.progress_handler do - progress_calls << nil - true - end - @db.progress_handler(nil) - - @db.execute "create table test1(a, b)" - - assert_equal 0, progress_calls.size - end end From ae76a01df1cb813f15d7bc4b96bb46ac5f62b8a6 Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Thu, 11 Jan 2024 19:51:23 +0100 Subject: [PATCH 12/18] Update ext/sqlite3/database.c Co-authored-by: Jean Boussier --- ext/sqlite3/database.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/ext/sqlite3/database.c b/ext/sqlite3/database.c index 1896a58e..ae00762b 100644 --- a/ext/sqlite3/database.c +++ b/ext/sqlite3/database.c @@ -17,8 +17,6 @@ database_mark(void *ctx) { sqlite3RubyPtr c = (sqlite3RubyPtr)ctx; rb_gc_mark(c->busy_handler); - rb_gc_mark(c->stmt_timeout); - rb_gc_mark(c->stmt_deadline); } static void From cd47d9b22fa896d94aa3c1011ebfb9f2e7b45ae7 Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Thu, 11 Jan 2024 20:30:59 +0100 Subject: [PATCH 13/18] Reset stmt_deadline when preparing a statement --- ext/sqlite3/statement.c | 1 + 1 file changed, 1 insertion(+) diff --git a/ext/sqlite3/statement.c b/ext/sqlite3/statement.c index ee3d8896..5616e15b 100644 --- a/ext/sqlite3/statement.c +++ b/ext/sqlite3/statement.c @@ -70,6 +70,7 @@ prepare(VALUE self, VALUE db, VALUE sql) ); CHECK(db_ctx->db, status); + db_ctx->stmt_deadline = 0; return rb_str_new2(tail); } From 13d4067be5823bf058d6cc71a96173210f5c6f99 Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Thu, 11 Jan 2024 20:32:48 +0100 Subject: [PATCH 14/18] Make test faster --- test/test_integration_statement.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_integration_statement.rb b/test/test_integration_statement.rb index a766de54..2e180acd 100644 --- a/test/test_integration_statement.rb +++ b/test/test_integration_statement.rb @@ -193,14 +193,14 @@ def test_committing_tx_with_statement_active end def test_long_running_statements_get_interrupted_when_statement_timeout_set - @db.statement_timeout = 100 + @db.statement_timeout = 10 assert_raises(SQLite3::InterruptException) do @db.execute <<~SQL WITH RECURSIVE r(i) AS ( VALUES(0) UNION ALL SELECT i FROM r - LIMIT 1000000 + LIMIT 100000 ) SELECT i FROM r ORDER BY i LIMIT 1; SQL From bd0267e2923a19a247c5dee0745dbfd6e5b8bbe2 Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Sun, 21 Jan 2024 18:41:55 +0100 Subject: [PATCH 15/18] Implement statement_timeout with timespecs --- ext/sqlite3/database.c | 16 ++++++++++------ ext/sqlite3/database.h | 2 +- ext/sqlite3/sqlite3_ruby.h | 1 + ext/sqlite3/statement.c | 2 +- ext/sqlite3/timespec.h | 20 ++++++++++++++++++++ 5 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 ext/sqlite3/timespec.h diff --git a/ext/sqlite3/database.c b/ext/sqlite3/database.c index ae00762b..7f48cef1 100644 --- a/ext/sqlite3/database.c +++ b/ext/sqlite3/database.c @@ -12,6 +12,11 @@ VALUE cSqlite3Database; +// Function to check if two timespec structures are equal +int timespec_equal(const struct timespec* ts1, const struct timespec* ts2) { + return (ts1->tv_sec == ts2->tv_sec) && (ts1->tv_nsec == ts2->tv_nsec); +} + static void database_mark(void *ctx) { @@ -266,12 +271,11 @@ rb_sqlite3_statement_timeout(void *context) struct timespec currentTime; clock_gettime(CLOCK_MONOTONIC, ¤tTime); - if (ctx->stmt_deadline == 0) { - ctx->stmt_deadline = currentTime.tv_sec * 1e9 + currentTime.tv_nsec + (long long)ctx->stmt_timeout * 1e6; - } else { - long long timeDiff = ctx->stmt_deadline - (currentTime.tv_sec * 1e9 + currentTime.tv_nsec); - - if (timeDiff < 0) { return 1; } + if (!timespecisset(&ctx->stmt_deadline)) { + // Set stmt_deadline if not already set + ctx->stmt_deadline = currentTime; + } else if (timespecafter(¤tTime, &ctx->stmt_deadline)) { + return 1; } return 0; diff --git a/ext/sqlite3/database.h b/ext/sqlite3/database.h index 9fbdb60d..3123f4fe 100644 --- a/ext/sqlite3/database.h +++ b/ext/sqlite3/database.h @@ -7,7 +7,7 @@ struct _sqlite3Ruby { sqlite3 *db; VALUE busy_handler; int stmt_timeout; - long long stmt_deadline; + struct timespec stmt_deadline; }; typedef struct _sqlite3Ruby sqlite3Ruby; diff --git a/ext/sqlite3/sqlite3_ruby.h b/ext/sqlite3/sqlite3_ruby.h index bcf53e63..088d3cd5 100644 --- a/ext/sqlite3/sqlite3_ruby.h +++ b/ext/sqlite3/sqlite3_ruby.h @@ -41,6 +41,7 @@ extern VALUE cSqlite3Blob; #include #include #include +#include int bignum_to_int64(VALUE big, sqlite3_int64 *result); diff --git a/ext/sqlite3/statement.c b/ext/sqlite3/statement.c index 9367bc64..d049dec1 100644 --- a/ext/sqlite3/statement.c +++ b/ext/sqlite3/statement.c @@ -70,7 +70,7 @@ prepare(VALUE self, VALUE db, VALUE sql) ); CHECK(db_ctx->db, status); - db_ctx->stmt_deadline = 0; + timespecclear(&db_ctx->stmt_deadline); return rb_str_new2(tail); } diff --git a/ext/sqlite3/timespec.h b/ext/sqlite3/timespec.h new file mode 100644 index 00000000..322fe758 --- /dev/null +++ b/ext/sqlite3/timespec.h @@ -0,0 +1,20 @@ +#define timespecclear(tsp) (tsp)->tv_sec = (tsp)->tv_nsec = 0 +#define timespecisset(tsp) ((tsp)->tv_sec || (tsp)->tv_nsec) +#define timespecisvalid(tsp) \ + ((tsp)->tv_nsec >= 0 && (tsp)->tv_nsec < 1000000000L) +#define timespeccmp(tsp, usp, cmp) \ + (((tsp)->tv_sec == (usp)->tv_sec) ? \ + ((tsp)->tv_nsec cmp (usp)->tv_nsec) : \ + ((tsp)->tv_sec cmp (usp)->tv_sec)) +#define timespecsub(tsp, usp, vsp) \ + do { \ + (vsp)->tv_sec = (tsp)->tv_sec - (usp)->tv_sec; \ + (vsp)->tv_nsec = (tsp)->tv_nsec - (usp)->tv_nsec; \ + if ((vsp)->tv_nsec < 0) { \ + (vsp)->tv_sec--; \ + (vsp)->tv_nsec += 1000000000L; \ + } \ + } while (0) +#define timespecafter(tsp, usp) \ + (((tsp)->tv_sec > (usp)->tv_sec) || \ + ((tsp)->tv_sec == (usp)->tv_sec && (tsp)->tv_nsec > (usp)->tv_nsec)) From 6456dec01287c4463195467da5ddbaffad08f6ad Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Mon, 22 Jan 2024 22:23:20 +0100 Subject: [PATCH 16/18] Add the new timespec.h file to the gemspec --- sqlite3.gemspec | 1 + 1 file changed, 1 insertion(+) diff --git a/sqlite3.gemspec b/sqlite3.gemspec index d0746454..5555e6c5 100644 --- a/sqlite3.gemspec +++ b/sqlite3.gemspec @@ -61,6 +61,7 @@ Gem::Specification.new do |s| "ext/sqlite3/sqlite3_ruby.h", "ext/sqlite3/statement.c", "ext/sqlite3/statement.h", + "ext/sqlite3/timespec.h", "lib/sqlite3.rb", "lib/sqlite3/constants.rb", "lib/sqlite3/database.rb", From 832fe6a8328acc41a73660926db6babda1d07c1e Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Mon, 22 Jan 2024 23:16:34 +0100 Subject: [PATCH 17/18] Update test-gem-file-contents --- bin/test-gem-file-contents | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/test-gem-file-contents b/bin/test-gem-file-contents index cbe98c24..f4ad822e 100755 --- a/bin/test-gem-file-contents +++ b/bin/test-gem-file-contents @@ -112,7 +112,7 @@ describe File.basename(gemfile) do it "contains extension C and header files" do assert_equal(6, gemfile_contents.count { |f| File.fnmatch?("ext/**/*.c", f) }) - assert_equal(6, gemfile_contents.count { |f| File.fnmatch?("ext/**/*.h", f) }) + assert_equal(7, gemfile_contents.count { |f| File.fnmatch?("ext/**/*.h", f) }) end it "includes C files in extra_rdoc_files" do @@ -154,7 +154,7 @@ describe File.basename(gemfile) do it "contains extension C and header files" do assert_equal(6, gemfile_contents.count { |f| File.fnmatch?("ext/**/*.c", f) }) - assert_equal(6, gemfile_contents.count { |f| File.fnmatch?("ext/**/*.h", f) }) + assert_equal(7, gemfile_contents.count { |f| File.fnmatch?("ext/**/*.h", f) }) end it "includes C files in extra_rdoc_files" do From c80aeef62c1db64bd7836b581d7450cc792483fb Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Tue, 23 Jan 2024 00:07:25 +0100 Subject: [PATCH 18/18] Update ext/sqlite3/database.c Co-authored-by: Jean Boussier --- ext/sqlite3/database.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/ext/sqlite3/database.c b/ext/sqlite3/database.c index 7f48cef1..9f6f134b 100644 --- a/ext/sqlite3/database.c +++ b/ext/sqlite3/database.c @@ -12,11 +12,6 @@ VALUE cSqlite3Database; -// Function to check if two timespec structures are equal -int timespec_equal(const struct timespec* ts1, const struct timespec* ts2) { - return (ts1->tv_sec == ts2->tv_sec) && (ts1->tv_nsec == ts2->tv_nsec); -} - static void database_mark(void *ctx) {