diff --git a/.rdoc_options b/.rdoc_options
index 1f489341..0d2ded97 100644
--- a/.rdoc_options
+++ b/.rdoc_options
@@ -17,6 +17,7 @@ exclude:
- "vendor"
- "ports"
- "tmp"
+- "pkg"
hyperlink_all: false
line_numbers: false
locale:
diff --git a/ext/sqlite3/database.c b/ext/sqlite3/database.c
index 621dc7aa..724c4c2f 100644
--- a/ext/sqlite3/database.c
+++ b/ext/sqlite3/database.c
@@ -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;
@@ -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
diff --git a/lib/sqlite3/database.rb b/lib/sqlite3/database.rb
index 1cf9e62e..98759792 100644
--- a/lib/sqlite3/database.rb
+++ b/lib/sqlite3/database.rb
@@ -8,8 +8,10 @@
require "sqlite3/fork_safety"
module SQLite3
- # The Database class encapsulates a single connection to a SQLite3 database.
- # Its usage is very straightforward:
+ # == Overview
+ #
+ # The Database class encapsulates a single connection to a SQLite3 database. Here's a
+ # straightforward example of usage:
#
# require 'sqlite3'
#
@@ -19,28 +21,59 @@ module SQLite3
# end
# end
#
- # It wraps the lower-level methods provided by the selected driver, and
- # includes the Pragmas module for access to various pragma convenience
- # methods.
+ # It wraps the lower-level methods provided by the selected driver, and includes the Pragmas
+ # module for access to various pragma convenience methods.
#
- # The Database class provides type translation services as well, by which
- # the SQLite3 data types (which are all represented as strings) may be
- # converted into their corresponding types (as defined in the schemas
- # for their tables). This translation only occurs when querying data from
+ # The Database class provides type translation services as well, by which the SQLite3 data types
+ # (which are all represented as strings) may be converted into their corresponding types (as
+ # defined in the schemas for their tables). This translation only occurs when querying data from
# the database--insertions and updates are all still typeless.
#
- # Furthermore, the Database class has been designed to work well with the
- # ArrayFields module from Ara Howard. If you require the ArrayFields
- # module before performing a query, and if you have not enabled results as
- # hashes, then the results will all be indexible by field name.
+ # Furthermore, the Database class has been designed to work well with the ArrayFields module from
+ # Ara Howard. If you require the ArrayFields module before performing a query, and if you have not
+ # enabled results as hashes, then the results will all be indexible by field name.
+ #
+ # == Thread safety
+ #
+ # When SQLite3.threadsafe? returns true, it is safe to share instances of the database class
+ # among threads without adding specific locking. Other object instances may require applications
+ # to provide their own locks if they are to be shared among threads. Please see the README.md for
+ # more information.
+ #
+ # == SQLite Extensions
+ #
+ # SQLite3::Database supports the universe of {sqlite
+ # extensions}[https://www.sqlite.org/loadext.html]. It's possible to load an extension into an
+ # existing Database object using the #load_extension method and passing a filesystem path:
+ #
+ # db = SQLite3::Database.new(":memory:")
+ # db.enable_load_extension(true)
+ # db.load_extension("/path/to/extension")
+ #
+ # As of v2.4.0, it's also possible to pass an object that responds to +#to_path+. This
+ # documentation will refer to the supported interface as +_ExtensionSpecifier+, which can be
+ # expressed in RBS syntax as:
+ #
+ # interface _ExtensionSpecifier
+ # def to_path: () → String
+ # end
#
- # Thread safety:
+ # So, for example, if you are using the {sqlean gem}[https://github.com/flavorjones/sqlean-ruby]
+ # which provides modules that implement this interface, you can pass the module directly:
+ #
+ # db = SQLite3::Database.new(":memory:")
+ # db.enable_load_extension(true)
+ # db.load_extension(SQLean::Crypto)
+ #
+ # It's also possible in v2.4.0+ to load extensions via the SQLite3::Database constructor by using
+ # the +extensions:+ keyword argument to pass an array of String paths or extension specifiers:
+ #
+ # db = SQLite3::Database.new(":memory:", extensions: ["/path/to/extension", SQLean::Crypto])
+ #
+ # Note that when loading extensions via the constructor, there is no need to call
+ # #enable_load_extension; however it is still necessary to call #enable_load_extensions before any
+ # subsequently invocations of #load_extension on the initialized Database object.
#
- # When `SQLite3.threadsafe?` returns true, it is safe to share instances of
- # the database class among threads without adding specific locking. Other
- # object instances may require applications to provide their own locks if
- # they are to be shared among threads. Please see the README.md for more
- # information.
class Database
attr_reader :collations
@@ -76,23 +109,25 @@ def quote(string)
# as hashes or not. By default, rows are returned as arrays.
attr_accessor :results_as_hash
- # call-seq: SQLite3::Database.new(file, options = {})
+ # call-seq:
+ # SQLite3::Database.new(file, options = {})
#
# Create a new Database object that opens the given file.
#
# Supported permissions +options+:
# - the default mode is READWRITE | CREATE
- # - +:readonly+: boolean (default false), true to set the mode to +READONLY+
- # - +:readwrite+: boolean (default false), true to set the mode to +READWRITE+
- # - +:flags+: set the mode to a combination of SQLite3::Constants::Open flags.
+ # - +readonly:+ boolean (default false), true to set the mode to +READONLY+
+ # - +readwrite:+ boolean (default false), true to set the mode to +READWRITE+
+ # - +flags:+ set the mode to a combination of SQLite3::Constants::Open flags.
#
# Supported encoding +options+:
- # - +:utf16+: boolean (default false), is the filename's encoding UTF-16 (only needed if the filename encoding is not UTF_16LE or BE)
+ # - +utf16:+ +boolish+ (default false), is the filename's encoding UTF-16 (only needed if the filename encoding is not UTF_16LE or BE)
#
# Other supported +options+:
- # - +:strict+: boolean (default false), disallow the use of double-quoted string literals (see https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted)
- # - +:results_as_hash+: boolean (default false), return rows as hashes instead of arrays
- # - +:default_transaction_mode+: one of +:deferred+ (default), +:immediate+, or +:exclusive+. If a mode is not specified in a call to #transaction, this will be the default transaction mode.
+ # - +strict:+ +boolish+ (default false), disallow the use of double-quoted string literals (see https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted)
+ # - +results_as_hash:+ +boolish+ (default false), return rows as hashes instead of arrays
+ # - +default_transaction_mode:+ one of +:deferred+ (default), +:immediate+, or +:exclusive+. If a mode is not specified in a call to #transaction, this will be the default transaction mode.
+ # - +extensions:+ Array[String | _ExtensionSpecifier] SQLite extensions to load into the database. See Database@SQLite+Extensions for more information.
#
def initialize file, options = {}, zvfs = nil
mode = Constants::Open::READWRITE | Constants::Open::CREATE
@@ -135,6 +170,8 @@ def initialize file, options = {}, zvfs = nil
@readonly = mode & Constants::Open::READONLY != 0
@default_transaction_mode = options[:default_transaction_mode] || :deferred
+ initialize_extensions(options[:extensions])
+
ForkSafety.track(self)
if block_given?
@@ -658,6 +695,52 @@ def busy_handler_timeout=(milliseconds)
end
end
+ # call-seq:
+ # load_extension(extension_specifier) -> self
+ #
+ # Loads an SQLite extension library from the named file. Extension loading must be enabled using
+ # #enable_load_extension prior to using this method.
+ #
+ # See also: Database@SQLite+Extensions
+ #
+ # [Parameters]
+ # - +extension_specifier+: (String | +_ExtensionSpecifier+) If a String, it is the filesystem path
+ # to the sqlite extension file. If an object that responds to #to_path, the
+ # return value of that method is used as the filesystem path to the sqlite extension file.
+ #
+ # [Example] Using a filesystem path:
+ #
+ # db.load_extension("/path/to/my_extension.so")
+ #
+ # [Example] Using the {sqlean gem}[https://github.com/flavorjones/sqlean-ruby]:
+ #
+ # db.load_extension(SQLean::VSV)
+ #
+ def load_extension(extension_specifier)
+ if extension_specifier.respond_to?(:to_path)
+ extension_specifier = extension_specifier.to_path
+ elsif !extension_specifier.is_a?(String)
+ raise TypeError, "extension_specifier #{extension_specifier.inspect} is not a String or a valid extension specifier object"
+ end
+ load_extension_internal(extension_specifier)
+ end
+
+ def initialize_extensions(extensions) # :nodoc:
+ return if extensions.nil?
+ raise TypeError, "extensions must be an Array" unless extensions.is_a?(Array)
+ return if extensions.empty?
+
+ begin
+ enable_load_extension(true)
+
+ extensions.each do |extension|
+ load_extension(extension)
+ end
+ ensure
+ enable_load_extension(false)
+ 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/lib/sqlite3/version.rb b/lib/sqlite3/version.rb
index 20bc7adb..ccecebfa 100644
--- a/lib/sqlite3/version.rb
+++ b/lib/sqlite3/version.rb
@@ -1,4 +1,4 @@
module SQLite3
# (String) the version of the sqlite3 gem, e.g. "2.1.1"
- VERSION = "2.3.1"
+ VERSION = "2.4.0.dev"
end
diff --git a/test/test_database.rb b/test/test_database.rb
index 27f0479a..15765621 100644
--- a/test/test_database.rb
+++ b/test/test_database.rb
@@ -3,6 +3,12 @@
require "pathname"
module SQLite3
+ class FakeExtensionSpecifier
+ def self.to_path
+ "/path/to/extension"
+ end
+ end
+
class TestDatabase < SQLite3::TestCase
attr_reader :db
@@ -15,6 +21,17 @@ def teardown
@db.close unless @db.closed?
end
+ def mock_database_load_extension_internal(db)
+ class << db
+ attr_reader :load_extension_internal_path
+
+ def load_extension_internal(path)
+ @load_extension_internal_path ||= []
+ @load_extension_internal_path << path
+ end
+ end
+ end
+
def test_custom_function_encoding
@db.execute("CREATE TABLE
sourceTable(
@@ -650,16 +667,117 @@ def test_strict_mode
assert_match(/no such column: "?nope"?/, error.message)
end
- def test_load_extension_with_nonstring_argument
- db = SQLite3::Database.new(":memory:")
+ def test_load_extension_error_with_nonexistent_path
+ skip("extensions are not enabled") unless db.respond_to?(:load_extension)
+ db.enable_load_extension(true)
+
+ assert_raises(SQLite3::Exception) { db.load_extension("/nonexistent/path") }
+ assert_raises(SQLite3::Exception) { db.load_extension(Pathname.new("nonexistent")) }
+ end
+
+ def test_load_extension_error_with_invalid_argument
skip("extensions are not enabled") unless db.respond_to?(:load_extension)
+ db.enable_load_extension(true)
+
assert_raises(TypeError) { db.load_extension(1) }
- assert_raises(TypeError) { db.load_extension(Pathname.new("foo.so")) }
+ assert_raises(TypeError) { db.load_extension({a: 1}) }
+ assert_raises(TypeError) { db.load_extension([]) }
+ assert_raises(TypeError) { db.load_extension(Object.new) }
end
- def test_load_extension_error
- db = SQLite3::Database.new(":memory:")
- assert_raises(SQLite3::Exception) { db.load_extension("path/to/foo.so") }
+ def test_load_extension_with_an_extension_descriptor
+ mock_database_load_extension_internal(db)
+
+ db.load_extension(Pathname.new("/path/to/ext2"))
+ assert_equal(["/path/to/ext2"], db.load_extension_internal_path)
+
+ db.load_extension_internal_path.clear # reset
+
+ db.load_extension(FakeExtensionSpecifier)
+ assert_equal(["/path/to/extension"], db.load_extension_internal_path)
+ end
+
+ def test_initialize_extensions_with_extensions_calls_enable_load_extension
+ mock_database_load_extension_internal(db)
+ class << db
+ attr_accessor :enable_load_extension_called
+ attr_reader :enable_load_extension_arg
+
+ def reset_test
+ @enable_load_extension_called = 0
+ @enable_load_extension_arg = []
+ end
+
+ def enable_load_extension(val)
+ @enable_load_extension_called += 1
+ @enable_load_extension_arg << val
+ end
+ end
+
+ db.reset_test
+ db.initialize_extensions(nil)
+ assert_equal(0, db.enable_load_extension_called)
+
+ db.reset_test
+ db.initialize_extensions([])
+ assert_equal(0, db.enable_load_extension_called)
+
+ db.reset_test
+ db.initialize_extensions(["/path/to/extension"])
+ assert_equal(2, db.enable_load_extension_called)
+ assert_equal([true, false], db.enable_load_extension_arg)
+
+ db.reset_test
+ db.initialize_extensions([FakeExtensionSpecifier])
+ assert_equal(2, db.enable_load_extension_called)
+ assert_equal([true, false], db.enable_load_extension_arg)
+ end
+
+ def test_initialize_extensions_object_is_an_extension_specifier
+ mock_database_load_extension_internal(db)
+
+ db.initialize_extensions([Pathname.new("/path/to/extension")])
+ assert_equal(["/path/to/extension"], db.load_extension_internal_path)
+
+ db.load_extension_internal_path.clear # reset
+
+ db.initialize_extensions([FakeExtensionSpecifier])
+ assert_equal(["/path/to/extension"], db.load_extension_internal_path)
+ end
+
+ def test_initialize_extensions_object_not_an_extension_specifier
+ mock_database_load_extension_internal(db)
+
+ db.initialize_extensions(["/path/to/extension"])
+ assert_equal(["/path/to/extension"], db.load_extension_internal_path)
+
+ assert_raises(TypeError) { db.initialize_extensions([Class.new]) }
+
+ assert_raises(TypeError) { db.initialize_extensions(FakeExtensionSpecifier) }
+ end
+
+ def test_initialize_with_extensions_calls_initialize_extensions
+ # ephemeral class to capture arguments passed to initialize_extensions
+ klass = Class.new(SQLite3::Database) do
+ attr :initialize_extensions_called, :initialize_extensions_arg
+
+ def initialize_extensions(extensions)
+ @initialize_extensions_called = true
+ @initialize_extensions_arg = extensions
+ end
+ end
+
+ db = klass.new(":memory:")
+ assert(db.initialize_extensions_called)
+ assert_nil(db.initialize_extensions_arg)
+
+ db = klass.new(":memory:", extensions: [])
+ assert(db.initialize_extensions_called)
+ assert_empty(db.initialize_extensions_arg)
+
+ db = klass.new(":memory:", extensions: ["path/to/ext1", "path/to/ext2", FakeExtensionSpecifier])
+ assert(db.initialize_extensions_called)
+ assert_equal(["path/to/ext1", "path/to/ext2", FakeExtensionSpecifier], db.initialize_extensions_arg)
end
def test_raw_float_infinity