Skip to content

Commit

Permalink
Support loading SQLite3 extensions with config/database.yml
Browse files Browse the repository at this point in the history
The `sqlite3` gem v2.4.0 introduces support for loading extensions
that are passed to the Database constructor. This feature leverages
that feature to allow configuration of extensions using either
filesystem paths or the names of modules that respond to `.to_path`.

This commit documents the feature in both SQLite3Adapter rdoc and the
"Configuring" guide. It also extends and improves the documentation
around general SQLite3Adapter configuration.

See sparklemotion/sqlite3-ruby#586 for more information.
  • Loading branch information
flavorjones committed Dec 4, 2024
1 parent 95deab7 commit b599d4c
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 9 deletions.
17 changes: 17 additions & 0 deletions activerecord/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
* SQLite extensions can be configured in `config/database.yml`.

The database configuration option `extensions:` allows an application to load SQLite extensions
when using `sqlite3` >= v2.4.0. The array members may be filesystem paths or the names of
modules that respond to `.to_path`:

``` yaml
development:
adapter: sqlite3
extensions:
- SQLean::UUID # module name responding to `.to_path`
- .sqlpkg/nalgeon/crypto/crypto.so # or a filesystem path
- <%= AppExtensions.location %> # or ruby code returning a path
```
*Mike Dalessio*
* `ActiveRecord::Middleware::ShardSelector` supports granular database connection switching.

A new configuration option, `class_name:`, is introduced to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,30 @@

module ActiveRecord
module ConnectionAdapters # :nodoc:
# = Active Record SQLite3 Adapter
# = Active Record \SQLite3 Adapter
#
# The SQLite3 adapter works with the sqlite3-ruby drivers
# (available as gem from https://rubygems.org/gems/sqlite3).
# The \SQLite3 adapter works with the sqlite3[https://sparklemotion.github.io/sqlite3-ruby/]
# driver.
#
# Options:
#
# * <tt>:database</tt> - Path to the database file.
# * +:database+ (String): Filesystem path to the database file.
# * +:statement_limit+ (Integer): Maximum number of prepared statements to cache per database connection. (default: 1000)
# * +:timeout+ (Integer): Timeout in milliseconds to use when waiting for a lock. (default: no wait)
# * +:strict+ (Boolean): Enable or disable strict mode. When enabled, this will
# {disallow double-quoted string literals in SQL
# statements}[https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted].
# (default: see strict_strings_by_default)
# * +:extensions+ (Array): (<b>requires sqlite3 v2.4.0</b>) Each entry specifies a sqlite extension
# to load for this database. The entry may be a filesystem path, or the name of a class that
# responds to +.to_path+ to provide the filesystem path for the extension. See {sqlite3-ruby
# documentation}[https://sparklemotion.github.io/sqlite3-ruby/SQLite3/Database.html#class-SQLite3::Database-label-SQLite+Extensions]
# for more information.
#
# There may be other options available specific to the SQLite3 driver. Please read the
# documentation for
# {SQLite::Database.new}[https://sparklemotion.github.io/sqlite3-ruby/SQLite3/Database.html#method-c-new]
#
class SQLite3Adapter < AbstractAdapter
ADAPTER_NAME = "SQLite"

Expand Down Expand Up @@ -58,12 +74,19 @@ def dbconsole(config, options = {})

##
# :singleton-method:
# Configure the SQLite3Adapter to be used in a strict strings mode.
# This will disable double-quoted string literals, because otherwise typos can silently go unnoticed.
# For example, it is possible to create an index for a non existing column.
#
# Configure the SQLite3Adapter to be used in a "strict strings" mode. When enabled, this will
# {disallow double-quoted string literals in SQL
# statements}[https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted],
# which may prevent some typographical errors like creating an index for a non-existent
# column. The default is +false+.
#
# If you wish to enable this mode you can add the following line to your application.rb file:
#
# config.active_record.sqlite3_adapter_strict_strings_by_default = true
#
# This can also be configured on individual databases by setting the +strict:+ option.
#
class_attribute :strict_strings_by_default, default: false

NATIVE_DATABASE_TYPES = {
Expand Down Expand Up @@ -125,10 +148,16 @@ def initialize(...)
@last_affected_rows = nil
@previous_read_uncommitted = nil
@config[:strict] = ConnectionAdapters::SQLite3Adapter.strict_strings_by_default unless @config.key?(:strict)

extensions = @config.fetch(:extensions, []).map do |extension|
extension.safe_constantize || extension
end

@connection_parameters = @config.merge(
database: @config[:database].to_s,
results_as_hash: true,
default_transaction_mode: :immediate,
extensions: extensions
)
end

Expand Down
30 changes: 30 additions & 0 deletions activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ class SQLite3AdapterTest < ActiveRecord::SQLite3TestCase
class DualEncoding < ActiveRecord::Base
end

class SQLiteExtensionSpec
def self.to_path
"/path/to/sqlite3_extension"
end
end

def setup
@conn = SQLite3Adapter.new(
database: ":memory:",
Expand Down Expand Up @@ -1089,6 +1095,30 @@ def test_integer_cpk_column_returns_false_for_rowid
end
end

def test_sqlite_extensions_are_constantized_for_the_client_constructor
mock_adapter = Class.new(SQLite3Adapter) do
class << self
attr_reader :new_client_arg

def new_client(config)
@new_client_arg = config
end
end
end

conn = mock_adapter.new({
database: ":memory:",
adapter: "sqlite3",
extensions: [
"/string/literal/path",
"ActiveRecord::ConnectionAdapters::SQLite3AdapterTest::SQLiteExtensionSpec",
]
})
conn.send(:connect)

assert_equal(["/string/literal/path", SQLiteExtensionSpec], conn.class.new_client_arg[:extensions])
end

private
def assert_logged(logs)
subscriber = SQLSubscriber.new
Expand Down
17 changes: 15 additions & 2 deletions guides/source/configuring.md
Original file line number Diff line number Diff line change
Expand Up @@ -3333,7 +3333,7 @@ Now the behavior is clear, that we are only using the connection information in
#### Configuring an SQLite3 Database
Rails comes with built-in support for [SQLite3](https://www.sqlite.org), which is a lightweight serverless database application. While Rails better configures SQLite for production workloads, a busy production environment may overload SQLite. Rails defaults to using an SQLite database when creating a new project, but you can always change it later.
Rails comes with built-in support for [SQLite3](https://www.sqlite.org), which is a lightweight serverless database application. While Rails better configures SQLite for production workloads, a busy production environment may overload SQLite. Rails defaults to using an SQLite database when creating a new project because it is a zero configuration database that just works, but you can always change it later.
Here's the section of the default configuration file (`config/database.yml`) with connection information for the development environment:

Expand All @@ -3345,7 +3345,20 @@ development:
timeout: 5000
```

NOTE: Rails uses an SQLite3 database for data storage by default because it is a zero configuration database that just works. Rails also supports MySQL (including MariaDB) and PostgreSQL "out of the box", and has plugins for many database systems. If you are using a database in a production environment Rails most likely has an adapter for it.
[SQLite extensions](https://sqlite.org/loadext.html) are supported when using `sqlite3` gem v2.4.0 or later by configuring `extensions`:

``` yaml
development:
adapter: sqlite3
extensions:
- SQLean::UUID # module name responding to `.to_path`
- .sqlpkg/nalgeon/crypto/crypto.so # or a filesystem path
- <%= AppExtensions.location %> # or ruby code returning a path
```

Many useful features can be added to SQLite through extensions. You may wish to browse the [SQLite extension hub](https://sqlpkg.org/) or use gems like [`sqlpkg-ruby`](https://github.com/fractaledmind/sqlpkg-ruby) and [`sqlean-ruby`](https://github.com/flavorjones/sqlean-ruby) that simplify extension management.

Other configuration options are described in the [SQLite3Adapter documentation]( https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SQLite3Adapter.html).

#### Configuring a MySQL or MariaDB Database

Expand Down

0 comments on commit b599d4c

Please sign in to comment.