Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 32 additions & 10 deletions ext/sqlite3/database.c
Original file line number Diff line number Diff line change
Expand Up @@ -524,24 +524,46 @@ set_sqlite3_func_result(sqlite3_context *ctx, VALUE result)
}
}

static void
rb_sqlite3_func(sqlite3_context *ctx, int argc, sqlite3_value **argv)
typedef struct {
sqlite3_context *ctx;
int argc;
sqlite3_value **argv;
} rb_sqlite3_func_args_t;

static VALUE
rb_sqlite3_func_protected(VALUE func_args_value)
{
VALUE callable = (VALUE)sqlite3_user_data(ctx);
VALUE params = rb_ary_new2(argc);
rb_sqlite3_func_args_t *args = (rb_sqlite3_func_args_t *)func_args_value;
VALUE callable = (VALUE)sqlite3_user_data(args->ctx);
VALUE params = rb_ary_new2(args->argc);
VALUE result;
int i;

if (argc > 0) {
for (i = 0; i < argc; i++) {
VALUE param = sqlite3val2rb(argv[i]);
rb_ary_push(params, param);
}
for (i = 0; i < args->argc; i++) {
VALUE param = sqlite3val2rb(args->argv[i]);
rb_ary_push(params, param);
}

result = rb_apply(callable, rb_intern("call"), params);

set_sqlite3_func_result(ctx, result);
set_sqlite3_func_result(args->ctx, result);

return Qnil;
}

static void
rb_sqlite3_func(sqlite3_context *ctx, int argc, sqlite3_value **argv)
{
rb_sqlite3_func_args_t args = { .ctx = ctx, .argc = argc, .argv = argv };
int exc_status;

rb_protect(rb_sqlite3_func_protected, (VALUE)&args, &exc_status);

if (exc_status) {
/* the user should never see this message, because Statement#step will
* re-raise the exception still in rb_errinfo */
sqlite3_result_error(ctx, "Ruby Exception occurred", -1);
}
}

#ifndef HAVE_RB_PROC_ARITY
Expand Down
39 changes: 39 additions & 0 deletions test/test_database.rb
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,45 @@ def test_function_return_types
end
end

def test_function_raise_propagates_and_connection_remains_usable
@db.define_function("boom") { |a| raise "boom: #{a}" }

error = assert_raise(RuntimeError) { @db.execute("select boom(1)") }
assert_equal("boom: 1", error.message)

assert_equal([[2]], @db.execute("select 1 + 1"))
end

def test_function_raise_does_not_deadlock_other_threads_using_the_connection
skip("interpreter doesn't support fork") unless Process.respond_to?(:fork)
skip("valgrind doesn't handle forking") if i_am_running_in_valgrind

@db.close
read, write = IO.pipe
old_stderr, $stderr = $stderr, StringIO.new
pid = Process.fork do
read.close
db = SQLite3::Database.new(":memory:")
db.define_function("boom") { |a| raise "boom" }
begin
db.execute("select boom(1)")
rescue RuntimeError
end
Thread.new { db.execute("select 1") }.join
write.write("ok")
exit!
end
$stderr = old_stderr
write.close

result = IO.select([read], nil, nil, 10) && read.gets
Process.kill(:KILL, pid) unless result
Process.waitpid(pid)
read.close

assert_equal("ok", result, "second thread deadlocked on the connection after a function raised")
end

def test_function_gc_segfault
@db.create_function("bug", -1) { |func, *values| func.result = values.join }
# With a lot of data and a lot of threads, try to induce a GC segfault.
Expand Down
Loading