Skip to content

Commit

Permalink
feat: Database#load_extension supports an extension specifier
Browse files Browse the repository at this point in the history
    interface _ExtensionSpecifier
      def sqlite_extension_path: () → String
    end

So when passed an object that responds to `#sqlite_extension_path`,
that method's return value is used as the filesystem path for the
extension, allowing a calling convention like:

    db.load_extension(SQLean::VSV)

where prior to this change that would need to be the more verbose:

    db.load_extension(SQLean::VSV.sqlite_extension_path)

I think supporting this calling convention will also make it easier to
inject sqlite extensions via a Rails database config file. Stay tuned.
  • Loading branch information
flavorjones committed Nov 24, 2024
1 parent e609300 commit ceb76cc
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 9 deletions.
1 change: 1 addition & 0 deletions .rdoc_options
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ exclude:
- "vendor"
- "ports"
- "tmp"
- "pkg"
hyperlink_all: false
line_numbers: false
locale:
Expand Down
10 changes: 2 additions & 8 deletions ext/sqlite3/database.c
Original file line number Diff line number Diff line change
Expand Up @@ -771,14 +771,8 @@ collation(VALUE self, VALUE name, VALUE comparator)
}

#ifdef HAVE_SQLITE3_LOAD_EXTENSION
/* call-seq: db.load_extension(file)
*
* Loads an SQLite extension library from the named file. Extension
* loading must be enabled using db.enable_load_extension(true) prior
* to calling this API.
*/
static VALUE
load_extension(VALUE self, VALUE file)
load_extension_internal(VALUE self, VALUE file)
{
sqlite3RubyPtr ctx;
int status;
Expand Down Expand Up @@ -997,7 +991,7 @@ init_sqlite3_database(void)
rb_define_private_method(cSqlite3Database, "db_filename", db_filename, 1);

#ifdef HAVE_SQLITE3_LOAD_EXTENSION
rb_define_method(cSqlite3Database, "load_extension", load_extension, 1);
rb_define_private_method(cSqlite3Database, "load_extension_internal", load_extension_internal, 1);
#endif

#ifdef HAVE_SQLITE3_ENABLE_LOAD_EXTENSION
Expand Down
31 changes: 31 additions & 0 deletions lib/sqlite3/database.rb
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,37 @@ def busy_handler_timeout=(milliseconds)
end
end

# call-seq:
# load_extension(extension_specifier) -> Database
#
# Loads an SQLite extension library from the named file. Extension loading must be enabled using
# Database#enable_load_extension prior to using this method.
#
# See also: https://www.sqlite.org/loadext.html
#
# [Parameters]
# - extension_specifier (String | +_ExtensionSpecifier+) If a String, it is the filesystem path
# to the sqlite extension file. If an object that responds to #sqlite_extension_path, the
# return value of that method is used as the filesystem path to the sqlite extension file.
#
# +_ExtensionSpecifier+ describes the following interface:
#
# interface _ExtensionSpecifier
# def sqlite_extension_path: () → String
# end
#
# For example, the +sqlean+ ruby gem offers a set of classes that implement this interface,
# allowing a calling convention like:
#
# db.load_extension(SQLean::VSV)
#
def load_extension(extension_specifier)
if extension_specifier.respond_to?(:sqlite_extension_path)
extension_specifier = extension_specifier.sqlite_extension_path
end
load_extension_internal(extension_specifier)
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
Expand Down
2 changes: 1 addition & 1 deletion lib/sqlite3/version.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module SQLite3
# (String) the version of the sqlite3 gem, e.g. "2.1.1"
VERSION = "2.3.0"
VERSION = "2.4.0.dev"
end
21 changes: 21 additions & 0 deletions test/test_database.rb
Original file line number Diff line number Diff line change
Expand Up @@ -653,10 +653,31 @@ def test_strict_mode
def test_load_extension_with_nonstring_argument
db = SQLite3::Database.new(":memory:")
skip("extensions are not enabled") unless db.respond_to?(:load_extension)

assert_raises(TypeError) { db.load_extension(1) }
assert_raises(TypeError) { db.load_extension(Pathname.new("foo.so")) }
end

def test_load_extension_with_an_extension_descriptor
extension_descriptor = Class.new do
def sqlite_extension_path
"path/to/extension"
end
end.new

class << db
attr_reader :load_extension_internal_path

def load_extension_internal(path)
@load_extension_internal_path = path
end
end

db.load_extension(extension_descriptor)

assert_equal("path/to/extension", db.load_extension_internal_path)
end

def test_load_extension_error
db = SQLite3::Database.new(":memory:")
assert_raises(SQLite3::Exception) { db.load_extension("path/to/foo.so") }
Expand Down

0 comments on commit ceb76cc

Please sign in to comment.