mirror of https://github.com/rails/rails
Enable strict strings mode for `SQLite3Adapter`
Co-authored-by: Jean Boussier <jean.boussier@gmail.com>
This commit is contained in:
parent
962a1dd231
commit
21a6dbd313
|
@ -499,7 +499,7 @@ GEM
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
sprockets (>= 3.0.0)
|
sprockets (>= 3.0.0)
|
||||||
sqlite3 (1.4.2)
|
sqlite3 (1.4.3)
|
||||||
stackprof (0.2.17)
|
stackprof (0.2.17)
|
||||||
stimulus-rails (1.0.2)
|
stimulus-rails (1.0.2)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
|
|
|
@ -29,6 +29,27 @@
|
||||||
|
|
||||||
*Cameron Bothner and Mitch Vollebregt*
|
*Cameron Bothner and Mitch Vollebregt*
|
||||||
|
|
||||||
|
* Enable strict strings mode for `SQLite3Adapter`.
|
||||||
|
|
||||||
|
Configures SQLite with a strict strings mode, which disables double-quoted string literals.
|
||||||
|
|
||||||
|
SQLite has some quirks around double-quoted string literals.
|
||||||
|
It first tries to consider double-quoted strings as identifier names, but if they don't exist
|
||||||
|
it then considers them as string literals. Because of this, typos can silently go unnoticed.
|
||||||
|
For example, it is possible to create an index for a non existing column.
|
||||||
|
See [SQLite documentation](https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted) for more details.
|
||||||
|
|
||||||
|
If you don't want this behavior, you can disable it via:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# config/application.rb
|
||||||
|
config.active_record.sqlite3_adapter_strict_strings_by_default = false
|
||||||
|
```
|
||||||
|
|
||||||
|
Fixes #27782.
|
||||||
|
|
||||||
|
*fatkodima*, *Jean Boussier*
|
||||||
|
|
||||||
* Resolve issue where a relation cache_version could be left stale.
|
* Resolve issue where a relation cache_version could be left stale.
|
||||||
|
|
||||||
Previously, when `reset` was called on a relation object it did not reset the cache_versions
|
Previously, when `reset` was called on a relation object it did not reset the cache_versions
|
||||||
|
|
|
@ -84,11 +84,11 @@ module ActiveRecord
|
||||||
table_sql = query_value(<<-SQL, "SCHEMA")
|
table_sql = query_value(<<-SQL, "SCHEMA")
|
||||||
SELECT sql
|
SELECT sql
|
||||||
FROM sqlite_master
|
FROM sqlite_master
|
||||||
WHERE name = #{quote_table_name(table_name)} AND type = 'table'
|
WHERE name = #{quote(table_name)} AND type = 'table'
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT sql
|
SELECT sql
|
||||||
FROM sqlite_temp_master
|
FROM sqlite_temp_master
|
||||||
WHERE name = #{quote_table_name(table_name)} AND type = 'table'
|
WHERE name = #{quote(table_name)} AND type = 'table'
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
table_sql.to_s.scan(/CONSTRAINT\s+(?<name>\w+)\s+CHECK\s+\((?<expression>(:?[^()]|\(\g<expression>\))+)\)/i).map do |name, expression|
|
table_sql.to_s.scan(/CONSTRAINT\s+(?<name>\w+)\s+CHECK\s+\((?<expression>(:?[^()]|\(\g<expression>\))+)\)/i).map do |name, expression|
|
||||||
|
|
|
@ -34,7 +34,7 @@ module ActiveRecord
|
||||||
|
|
||||||
db = SQLite3::Database.new(
|
db = SQLite3::Database.new(
|
||||||
config[:database].to_s,
|
config[:database].to_s,
|
||||||
config.merge(results_as_hash: true)
|
config.merge(results_as_hash: true, strict: ConnectionAdapters::SQLite3Adapter.strict_strings_by_default)
|
||||||
)
|
)
|
||||||
|
|
||||||
ConnectionAdapters::SQLite3Adapter.new(db, logger, nil, config)
|
ConnectionAdapters::SQLite3Adapter.new(db, logger, nil, config)
|
||||||
|
@ -61,6 +61,16 @@ module ActiveRecord
|
||||||
include SQLite3::SchemaStatements
|
include SQLite3::SchemaStatements
|
||||||
include SQLite3::DatabaseStatements
|
include SQLite3::DatabaseStatements
|
||||||
|
|
||||||
|
##
|
||||||
|
# :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.
|
||||||
|
# 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
|
||||||
|
class_attribute :strict_strings_by_default, default: false
|
||||||
|
|
||||||
NATIVE_DATABASE_TYPES = {
|
NATIVE_DATABASE_TYPES = {
|
||||||
primary_key: "integer PRIMARY KEY AUTOINCREMENT NOT NULL",
|
primary_key: "integer PRIMARY KEY AUTOINCREMENT NOT NULL",
|
||||||
string: { name: "varchar" },
|
string: { name: "varchar" },
|
||||||
|
|
|
@ -220,6 +220,14 @@ To keep using the current cache store, you can turn off cache versioning entirel
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
initializer "active_record.sqlite3_adapter_strict_strings_by_default" do
|
||||||
|
if config.active_record.sqlite3_adapter_strict_strings_by_default
|
||||||
|
ActiveSupport.on_load(:active_record_sqlite3adapter) do
|
||||||
|
self.strict_strings_by_default = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
initializer "active_record.set_configs" do |app|
|
initializer "active_record.set_configs" do |app|
|
||||||
configs = app.config.active_record
|
configs = app.config.active_record
|
||||||
|
|
||||||
|
@ -246,6 +254,7 @@ To keep using the current cache store, you can turn off cache versioning entirel
|
||||||
:query_log_tags,
|
:query_log_tags,
|
||||||
:cache_query_log_tags,
|
:cache_query_log_tags,
|
||||||
:sqlite3_production_warning,
|
:sqlite3_production_warning,
|
||||||
|
:sqlite3_adapter_strict_strings_by_default,
|
||||||
:check_schema_cache_dump_version,
|
:check_schema_cache_dump_version,
|
||||||
:use_schema_cache_dump
|
:use_schema_cache_dump
|
||||||
)
|
)
|
||||||
|
|
|
@ -141,7 +141,7 @@ module ActiveRecord
|
||||||
assert_equal 2, result.columns.length
|
assert_equal 2, result.columns.length
|
||||||
assert_equal %w{ id data }, result.columns
|
assert_equal %w{ id data }, result.columns
|
||||||
|
|
||||||
@conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
|
@conn.exec_query("INSERT INTO ex (id, data) VALUES (1, 'foo')")
|
||||||
result = @conn.exec_query("SELECT id, data FROM ex")
|
result = @conn.exec_query("SELECT id, data FROM ex")
|
||||||
assert_equal 1, result.rows.length
|
assert_equal 1, result.rows.length
|
||||||
assert_equal 2, result.columns.length
|
assert_equal 2, result.columns.length
|
||||||
|
@ -152,7 +152,7 @@ module ActiveRecord
|
||||||
|
|
||||||
def test_exec_query_with_binds
|
def test_exec_query_with_binds
|
||||||
with_example_table "id int, data string" do
|
with_example_table "id int, data string" do
|
||||||
@conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
|
@conn.exec_query("INSERT INTO ex (id, data) VALUES (1, 'foo')")
|
||||||
result = @conn.exec_query(
|
result = @conn.exec_query(
|
||||||
"SELECT id, data FROM ex WHERE id = ?", nil, [Relation::QueryAttribute.new(nil, 1, Type::Value.new)])
|
"SELECT id, data FROM ex WHERE id = ?", nil, [Relation::QueryAttribute.new(nil, 1, Type::Value.new)])
|
||||||
|
|
||||||
|
@ -165,7 +165,7 @@ module ActiveRecord
|
||||||
|
|
||||||
def test_exec_query_typecasts_bind_vals
|
def test_exec_query_typecasts_bind_vals
|
||||||
with_example_table "id int, data string" do
|
with_example_table "id int, data string" do
|
||||||
@conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
|
@conn.exec_query("INSERT INTO ex (id, data) VALUES (1, 'foo')")
|
||||||
|
|
||||||
result = @conn.exec_query(
|
result = @conn.exec_query(
|
||||||
"SELECT id, data FROM ex WHERE id = ?", nil, [Relation::QueryAttribute.new("id", "1-fuu", Type::Integer.new)])
|
"SELECT id, data FROM ex WHERE id = ?", nil, [Relation::QueryAttribute.new("id", "1-fuu", Type::Integer.new)])
|
||||||
|
@ -616,6 +616,25 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_strict_strings_by_default
|
||||||
|
conn = Base.sqlite3_connection(database: ":memory:", adapter: "sqlite3")
|
||||||
|
conn.create_table :testings
|
||||||
|
|
||||||
|
assert_nothing_raised do
|
||||||
|
conn.add_index :testings, :non_existent
|
||||||
|
end
|
||||||
|
|
||||||
|
with_strict_strings_by_default do
|
||||||
|
conn = Base.sqlite3_connection(database: ":memory:", adapter: "sqlite3")
|
||||||
|
conn.create_table :testings
|
||||||
|
|
||||||
|
error = assert_raises(StandardError) do
|
||||||
|
conn.add_index :testings, :non_existent2
|
||||||
|
end
|
||||||
|
assert_match(/no such column: non_existent2/, error.message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def assert_logged(logs)
|
def assert_logged(logs)
|
||||||
subscriber = SQLSubscriber.new
|
subscriber = SQLSubscriber.new
|
||||||
|
@ -633,6 +652,13 @@ module ActiveRecord
|
||||||
SQL
|
SQL
|
||||||
super(@conn, table_name, definition, &block)
|
super(@conn, table_name, definition, &block)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def with_strict_strings_by_default
|
||||||
|
SQLite3Adapter.strict_strings_by_default = true
|
||||||
|
yield
|
||||||
|
ensure
|
||||||
|
SQLite3Adapter.strict_strings_by_default = false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -94,9 +94,11 @@ connections:
|
||||||
arunit:
|
arunit:
|
||||||
database: <%= FIXTURES_ROOT %>/fixture_database.sqlite3
|
database: <%= FIXTURES_ROOT %>/fixture_database.sqlite3
|
||||||
timeout: 5000
|
timeout: 5000
|
||||||
|
strict: true
|
||||||
arunit2:
|
arunit2:
|
||||||
database: <%= FIXTURES_ROOT %>/fixture_database_2.sqlite3
|
database: <%= FIXTURES_ROOT %>/fixture_database_2.sqlite3
|
||||||
timeout: 5000
|
timeout: 5000
|
||||||
|
strict: true
|
||||||
|
|
||||||
sqlite3_mem:
|
sqlite3_mem:
|
||||||
arunit:
|
arunit:
|
||||||
|
|
|
@ -66,6 +66,7 @@ Below are the default values associated with each target version. In cases of co
|
||||||
- [`config.active_support.default_message_verifier_serializer`](#config-active-support-default-message-verifier-serializer): `:json`
|
- [`config.active_support.default_message_verifier_serializer`](#config-active-support-default-message-verifier-serializer): `:json`
|
||||||
- [`config.action_controller.allow_deprecated_parameters_hash_equality`](#config-action-controller-allow-deprecated-parameters-hash-equality): `false`
|
- [`config.action_controller.allow_deprecated_parameters_hash_equality`](#config-action-controller-allow-deprecated-parameters-hash-equality): `false`
|
||||||
- [`config.log_file_size`](#config-log-file-size): `100.megabytes`
|
- [`config.log_file_size`](#config-log-file-size): `100.megabytes`
|
||||||
|
- [`config.active_record.sqlite3_adapter_strict_strings_by_default`](#config-active-record-sqlite3-adapter-strict-strings-by-default): `false`
|
||||||
|
|
||||||
#### Default Values for Target Version 7.0
|
#### Default Values for Target Version 7.0
|
||||||
|
|
||||||
|
@ -985,6 +986,24 @@ regular expressions.
|
||||||
|
|
||||||
Specifies if source locations of methods that call database queries should be logged below relevant queries. By default, the flag is `true` in development and `false` in all other environments.
|
Specifies if source locations of methods that call database queries should be logged below relevant queries. By default, the flag is `true` in development and `false` in all other environments.
|
||||||
|
|
||||||
|
#### `config.active_record.sqlite3_adapter_strict_strings_by_default`
|
||||||
|
|
||||||
|
Specifies whether the SQLite3Adapter should be used in a strict strings mode.
|
||||||
|
The use of a strict strings mode disables double-quoted string literals.
|
||||||
|
|
||||||
|
SQLite has some quirks around double-quoted string literals.
|
||||||
|
It first tries to consider double-quoted strings as identifier names, but if they don't exist
|
||||||
|
it then considers them as string literals. Because of this, typos can silently go unnoticed.
|
||||||
|
For example, it is possible to create an index for a non existing column.
|
||||||
|
See [SQLite documentation](https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted) for more details.
|
||||||
|
|
||||||
|
The default value depends on the `config.load_defaults` target version:
|
||||||
|
|
||||||
|
| Starting with version | The default value is |
|
||||||
|
| --------------------- | -------------------- |
|
||||||
|
| (original) | `false` |
|
||||||
|
| 7.1 | `true` |
|
||||||
|
|
||||||
#### `config.active_record.async_query_executor`
|
#### `config.active_record.async_query_executor`
|
||||||
|
|
||||||
Specifies how asynchronous queries are pooled.
|
Specifies how asynchronous queries are pooled.
|
||||||
|
|
|
@ -262,6 +262,23 @@ config.cache_store = :mem_cache_store, "cache.example.com", pool: false
|
||||||
|
|
||||||
See the [caching with Rails](https://guides.rubyonrails.org/caching_with_rails.html#connection-pool-options) guide for more information.
|
See the [caching with Rails](https://guides.rubyonrails.org/caching_with_rails.html#connection-pool-options) guide for more information.
|
||||||
|
|
||||||
|
### `SQLite3Adapter` now configured to be used in a strict strings mode
|
||||||
|
|
||||||
|
The use of a strict strings mode disables double-quoted string literals.
|
||||||
|
|
||||||
|
SQLite has some quirks around double-quoted string literals.
|
||||||
|
It first tries to consider double-quoted strings as identifier names, but if they don't exist
|
||||||
|
it then considers them as string literals. Because of this, typos can silently go unnoticed.
|
||||||
|
For example, it is possible to create an index for a non existing column.
|
||||||
|
See [SQLite documentation](https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted) for more details.
|
||||||
|
|
||||||
|
If you don't want to use `SQLite3Adapter` in a strict mode, you can disable this behavior:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# config/application.rb
|
||||||
|
config.active_record.sqlite3_adapter_strict_strings_by_default = false
|
||||||
|
```
|
||||||
|
|
||||||
Upgrading from Rails 6.1 to Rails 7.0
|
Upgrading from Rails 6.1 to Rails 7.0
|
||||||
-------------------------------------
|
-------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -284,6 +284,10 @@ module Rails
|
||||||
if respond_to?(:action_controller)
|
if respond_to?(:action_controller)
|
||||||
action_controller.allow_deprecated_parameters_hash_equality = false
|
action_controller.allow_deprecated_parameters_hash_equality = false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if respond_to?(:active_record)
|
||||||
|
active_record.sqlite3_adapter_strict_strings_by_default = true
|
||||||
|
end
|
||||||
else
|
else
|
||||||
raise "Unknown version #{target_version.to_s.inspect}"
|
raise "Unknown version #{target_version.to_s.inspect}"
|
||||||
end
|
end
|
||||||
|
|
|
@ -35,3 +35,12 @@
|
||||||
# state which matches what was committed to the database, typically the last
|
# state which matches what was committed to the database, typically the last
|
||||||
# instance to save.
|
# instance to save.
|
||||||
# Rails.application.config.active_record.run_commit_callbacks_on_first_saved_instances_in_transaction = false
|
# Rails.application.config.active_record.run_commit_callbacks_on_first_saved_instances_in_transaction = false
|
||||||
|
|
||||||
|
# Configures SQLite with a strict strings mode, which disables double-quoted string literals.
|
||||||
|
#
|
||||||
|
# SQLite has some quirks around double-quoted string literals.
|
||||||
|
# It first tries to consider double-quoted strings as identifier names, but if they don't exist
|
||||||
|
# it then considers them as string literals. Because of this, typos can silently go unnoticed.
|
||||||
|
# For example, it is possible to create an index for a non existing column.
|
||||||
|
# See https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted for more details.
|
||||||
|
# Rails.application.config.active_record.sqlite3_adapter_strict_strings_by_default = true
|
||||||
|
|
|
@ -2408,6 +2408,40 @@ module ApplicationTests
|
||||||
assert_equal false, ActiveRecord::Base.run_commit_callbacks_on_first_saved_instances_in_transaction
|
assert_equal false, ActiveRecord::Base.run_commit_callbacks_on_first_saved_instances_in_transaction
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "SQLite3Adapter.strict_strings_by_default is true by default for new apps" do
|
||||||
|
app_file "config/initializers/active_record.rb", <<~RUBY
|
||||||
|
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
|
||||||
|
RUBY
|
||||||
|
|
||||||
|
app "development"
|
||||||
|
|
||||||
|
assert_equal true, ActiveRecord::ConnectionAdapters::SQLite3Adapter.strict_strings_by_default
|
||||||
|
end
|
||||||
|
|
||||||
|
test "SQLite3Adapter.strict_strings_by_default is false by default for upgraded apps" do
|
||||||
|
remove_from_config '.*config\.load_defaults.*\n'
|
||||||
|
app_file "config/initializers/active_record.rb", <<~RUBY
|
||||||
|
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
|
||||||
|
RUBY
|
||||||
|
|
||||||
|
app "development"
|
||||||
|
|
||||||
|
assert_equal false, ActiveRecord::ConnectionAdapters::SQLite3Adapter.strict_strings_by_default
|
||||||
|
end
|
||||||
|
|
||||||
|
test "SQLite3Adapter.strict_strings_by_default can be configured via config.active_record.sqlite3_adapter_strict_strings_by_default" do
|
||||||
|
remove_from_config '.*config\.load_defaults.*\n'
|
||||||
|
add_to_config "config.active_record.sqlite3_adapter_strict_strings_by_default = true"
|
||||||
|
|
||||||
|
app_file "config/initializers/active_record.rb", <<~RUBY
|
||||||
|
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
|
||||||
|
RUBY
|
||||||
|
|
||||||
|
app "development"
|
||||||
|
|
||||||
|
assert_equal true, ActiveRecord::ConnectionAdapters::SQLite3Adapter.strict_strings_by_default
|
||||||
|
end
|
||||||
|
|
||||||
test "ActiveSupport::MessageEncryptor.use_authenticated_message_encryption is true by default for new apps" do
|
test "ActiveSupport::MessageEncryptor.use_authenticated_message_encryption is true by default for new apps" do
|
||||||
app "development"
|
app "development"
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue