Enable strict strings mode for `SQLite3Adapter`

Co-authored-by: Jean Boussier <jean.boussier@gmail.com>
This commit is contained in:
fatkodima 2022-06-14 02:40:03 +03:00
parent 962a1dd231
commit 21a6dbd313
12 changed files with 158 additions and 7 deletions

View File

@ -499,7 +499,7 @@ GEM
actionpack (>= 5.2)
activesupport (>= 5.2)
sprockets (>= 3.0.0)
sqlite3 (1.4.2)
sqlite3 (1.4.3)
stackprof (0.2.17)
stimulus-rails (1.0.2)
railties (>= 6.0.0)

View File

@ -29,6 +29,27 @@
*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.
Previously, when `reset` was called on a relation object it did not reset the cache_versions

View File

@ -84,11 +84,11 @@ module ActiveRecord
table_sql = query_value(<<-SQL, "SCHEMA")
SELECT sql
FROM sqlite_master
WHERE name = #{quote_table_name(table_name)} AND type = 'table'
WHERE name = #{quote(table_name)} AND type = 'table'
UNION ALL
SELECT sql
FROM sqlite_temp_master
WHERE name = #{quote_table_name(table_name)} AND type = 'table'
WHERE name = #{quote(table_name)} AND type = 'table'
SQL
table_sql.to_s.scan(/CONSTRAINT\s+(?<name>\w+)\s+CHECK\s+\((?<expression>(:?[^()]|\(\g<expression>\))+)\)/i).map do |name, expression|

View File

@ -34,7 +34,7 @@ module ActiveRecord
db = SQLite3::Database.new(
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)
@ -61,6 +61,16 @@ module ActiveRecord
include SQLite3::SchemaStatements
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 = {
primary_key: "integer PRIMARY KEY AUTOINCREMENT NOT NULL",
string: { name: "varchar" },

View File

@ -220,6 +220,14 @@ To keep using the current cache store, you can turn off cache versioning entirel
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|
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,
:cache_query_log_tags,
:sqlite3_production_warning,
:sqlite3_adapter_strict_strings_by_default,
:check_schema_cache_dump_version,
:use_schema_cache_dump
)

View File

@ -141,7 +141,7 @@ module ActiveRecord
assert_equal 2, result.columns.length
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")
assert_equal 1, result.rows.length
assert_equal 2, result.columns.length
@ -152,7 +152,7 @@ module ActiveRecord
def test_exec_query_with_binds
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(
"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
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(
"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
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
def assert_logged(logs)
subscriber = SQLSubscriber.new
@ -633,6 +652,13 @@ module ActiveRecord
SQL
super(@conn, table_name, definition, &block)
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

View File

@ -94,9 +94,11 @@ connections:
arunit:
database: <%= FIXTURES_ROOT %>/fixture_database.sqlite3
timeout: 5000
strict: true
arunit2:
database: <%= FIXTURES_ROOT %>/fixture_database_2.sqlite3
timeout: 5000
strict: true
sqlite3_mem:
arunit:

View File

@ -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.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.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
@ -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.
#### `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`
Specifies how asynchronous queries are pooled.

View File

@ -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.
### `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
-------------------------------------

View File

@ -284,6 +284,10 @@ module Rails
if respond_to?(:action_controller)
action_controller.allow_deprecated_parameters_hash_equality = false
end
if respond_to?(:active_record)
active_record.sqlite3_adapter_strict_strings_by_default = true
end
else
raise "Unknown version #{target_version.to_s.inspect}"
end

View File

@ -35,3 +35,12 @@
# state which matches what was committed to the database, typically the last
# instance to save.
# 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

View File

@ -2408,6 +2408,40 @@ module ApplicationTests
assert_equal false, ActiveRecord::Base.run_commit_callbacks_on_first_saved_instances_in_transaction
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
app "development"