From b8cc89e9ff4b48b35683c47ba5b8b5af87ea304d Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Wed, 4 Dec 2024 11:41:06 -0500 Subject: [PATCH] Support loading SQLite3 extensions with `config/database.yml` 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. --- .../connection_adapters/sqlite3_adapter.rb | 43 ++++++++++++++++--- .../adapters/sqlite3/sqlite3_adapter_test.rb | 30 +++++++++++++ guides/source/configuring.md | 17 +++++++- 3 files changed, 82 insertions(+), 8 deletions(-) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 66e7716194ff0..cdf2c3ff41c23 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -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: # - # * :database - 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): (requires sqlite3 v2.4.0) 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" @@ -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 = { @@ -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 diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 16724be1635ce..83b1f4580cb49 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -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:", @@ -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 diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 83781c9856ae7..21c562ac589d5 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -3335,6 +3335,8 @@ Now the behavior is clear, that we are only using the connection information in 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. +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. + Here's the section of the default configuration file (`config/database.yml`) with connection information for the development environment: ```yaml @@ -3345,7 +3347,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