diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 332c07c2bc43..9abfc5799e56 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -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 diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 66e7716194ff..cdf2c3ff41c2 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 16724be1635c..83b1f4580cb4 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 83781c9856ae..21c562ac589d 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