Skip to content

Commit

Permalink
Dump the database schema containing the current Rails version
Browse files Browse the repository at this point in the history
Since #42297, Rails now generates
datetime columns on MySQL with a default precision of 6. This means
that users upgrading to Rails 7.0 from 6.1, when loading the database
schema, would get the new precision value, which would not match the
production schema.

To avoid problems like this in the future,
Rails will now freeze `ActiveRecord::Schema` class to
be the 7.0 implementation, and will allow access to other version
using the same mechanism of `ActiveRecord::Migration`.

The schema dumper will generate the new format which will include the
Rails version and will look like this:

```
ActiveRecord::Schema[7.0].define
```

Related to #43934 and #43909.
  • Loading branch information
rafaelfranca committed Jan 28, 2022
1 parent 20846a2 commit b7f15a2
Show file tree
Hide file tree
Showing 28 changed files with 156 additions and 87 deletions.
4 changes: 2 additions & 2 deletions activerecord/lib/active_record/railties/databases.rake
Expand Up @@ -548,7 +548,7 @@ db_namespace = namespace :db do
# desc "Recreate the test database from an existent schema file (schema.rb or structure.sql, depending on `config.active_record.schema_format`)"
task load_schema: %w(db:test:purge) do
should_reconnect = ActiveRecord::Base.connection_pool.active_connection?
ActiveRecord::Schema.verbose = false
ActiveRecord::Schema::Current.verbose = false
ActiveRecord::Base.configurations.configs_for(env_name: "test").each do |db_config|
ActiveRecord::Tasks::DatabaseTasks.load_schema(db_config)
end
Expand Down Expand Up @@ -584,7 +584,7 @@ db_namespace = namespace :db do
namespace :load_schema do
task name => "db:test:purge:#{name}" do
should_reconnect = ActiveRecord::Base.connection_pool.active_connection?
ActiveRecord::Schema.verbose = false
ActiveRecord::Schema::Current.verbose = false
db_config = ActiveRecord::Base.configurations.configs_for(env_name: "test", name: name)
ActiveRecord::Tasks::DatabaseTasks.load_schema(db_config)
ensure
Expand Down
66 changes: 42 additions & 24 deletions activerecord/lib/active_record/schema.rb
Expand Up @@ -10,7 +10,7 @@ module ActiveRecord
#
# Usage:
#
# ActiveRecord::Schema.define do
# ActiveRecord::Schema[7.0].define do
# create_table :authors do |t|
# t.string :name, null: false
# end
Expand All @@ -29,33 +29,51 @@ module ActiveRecord
#
# ActiveRecord::Schema is only supported by database adapters that also
# support migrations, the two features being very similar.
class Schema < Migration::Current
# Eval the given block. All methods available to the current connection
# adapter are available within the block, so you can easily use the
# database definition DSL to build up your schema (
# {create_table}[rdoc-ref:ConnectionAdapters::SchemaStatements#create_table],
# {add_index}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_index], etc.).
#
# The +info+ hash is optional, and if given is used to define metadata
# about the current schema (currently, only the schema's version):
#
# ActiveRecord::Schema.define(version: 2038_01_19_000001) do
# ...
# end
def self.define(info = {}, &block)
new.define(info, &block)
end
class Schema < Migration[7.0]
module Definition
extend ActiveSupport::Concern

module ClassMethods
# Eval the given block. All methods available to the current connection
# adapter are available within the block, so you can easily use the
# database definition DSL to build up your schema (
# {create_table}[rdoc-ref:ConnectionAdapters::SchemaStatements#create_table],
# {add_index}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_index], etc.).
#
# The +info+ hash is optional, and if given is used to define metadata
# about the current schema (currently, only the schema's version):
#
# ActiveRecord::Schema[7.0].define(version: 2038_01_19_000001) do
# ...
# end
def define(info = {}, &block)
new.define(info, &block)
end
end

def define(info, &block) # :nodoc:
instance_eval(&block)
def define(info, &block) # :nodoc:
instance_eval(&block)

if info[:version].present?
connection.schema_migration.create_table
connection.assume_migrated_upto_version(info[:version])
if info[:version].present?
connection.schema_migration.create_table
connection.assume_migrated_upto_version(info[:version])
end

ActiveRecord::InternalMetadata.create_table
ActiveRecord::InternalMetadata[:environment] = connection.migration_context.current_environment
end
end

ActiveRecord::InternalMetadata.create_table
ActiveRecord::InternalMetadata[:environment] = connection.migration_context.current_environment
include Definition

class Current < Migration::Current
include Definition
end

def self.[](version)
Class.new(Migration::Compatibility.find(version)) do
include Definition
end
end
end
end
32 changes: 16 additions & 16 deletions activerecord/lib/active_record/schema_dumper.rb
Expand Up @@ -73,23 +73,23 @@ def define_params
@version ? "version: #{formatted_version}" : ""
end

# TODO: Fix dumper
def header(stream)
stream.puts <<HEADER
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# This file is the source Rails uses to define your schema when running `bin/rails
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(#{define_params}) do
HEADER
stream.puts <<~HEADER
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# This file is the source Rails uses to define your schema when running `bin/rails
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[#{ActiveRecord::Migration.current_version}].define(#{define_params}) do
HEADER
end

def trailer(stream)
Expand Down
Expand Up @@ -37,8 +37,28 @@ def test_has_primary_key
ActiveRecord::Base.primary_key_prefix_type = old_primary_key_prefix_type
end

def test_schema_without_version_is_the_7_0_schema
schema_class = ActiveRecord::Schema
assert schema_class < ActiveRecord::Migration[7.0]
assert_not schema_class < ActiveRecord::Migration[6.1]
assert schema_class < ActiveRecord::Schema::Definition
end

def test_current_schema_is_the_current_version_schema
schema_class = ActiveRecord::Schema::Current
assert schema_class < ActiveRecord::Migration[ActiveRecord::Migration.current_version]
assert_not schema_class < ActiveRecord::Migration[6.1]
assert schema_class < ActiveRecord::Schema::Definition
end

def test_schema_version_accessor
schema_class = ActiveRecord::Schema[6.1]
assert schema_class < ActiveRecord::Migration[6.1]
assert schema_class < ActiveRecord::Schema::Definition
end

def test_schema_define
ActiveRecord::Schema.define(version: 7) do
ActiveRecord::Schema::Current.define(version: 7) do
create_table :fruits do |t|
t.column :color, :string
t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle
Expand All @@ -57,7 +77,7 @@ def test_schema_define_with_table_name_prefix
ActiveRecord::Base.table_name_prefix = "nep_"
@schema_migration.reset_table_name
ActiveRecord::InternalMetadata.reset_table_name
ActiveRecord::Schema.define(version: 7) do
ActiveRecord::Schema::Current.define(version: 7) do
create_table :fruits do |t|
t.column :color, :string
t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle
Expand All @@ -74,7 +94,7 @@ def test_schema_define_with_table_name_prefix

def test_schema_raises_an_error_for_invalid_column_type
assert_raise NoMethodError do
ActiveRecord::Schema.define(version: 8) do
ActiveRecord::Schema::Current.define(version: 8) do
create_table :vegetables do |t|
t.unknown :color
end
Expand All @@ -97,7 +117,7 @@ def test_normalize_version
end

def test_schema_load_with_multiple_indexes_for_column_of_different_names
ActiveRecord::Schema.define do
ActiveRecord::Schema::Current.define do
create_table :multiple_indexes do |t|
t.string "foo"
t.index ["foo"], name: "multiple_indexes_foo_1"
Expand All @@ -113,7 +133,7 @@ def test_schema_load_with_multiple_indexes_for_column_of_different_names

if current_adapter?(:PostgreSQLAdapter)
def test_timestamps_with_and_without_zones
ActiveRecord::Schema.define do
ActiveRecord::Schema::Current.define do
create_table :has_timestamps do |t|
t.datetime "default_format"
t.datetime "without_time_zone"
Expand All @@ -130,7 +150,7 @@ def test_timestamps_with_and_without_zones
end

def test_timestamps_without_null_set_null_to_false_on_create_table
ActiveRecord::Schema.define do
ActiveRecord::Schema::Current.define do
create_table :has_timestamps do |t|
t.timestamps
end
Expand All @@ -141,7 +161,7 @@ def test_timestamps_without_null_set_null_to_false_on_create_table
end

def test_timestamps_without_null_set_null_to_false_on_change_table
ActiveRecord::Schema.define do
ActiveRecord::Schema::Current.define do
create_table :has_timestamps

change_table :has_timestamps do |t|
Expand All @@ -155,7 +175,7 @@ def test_timestamps_without_null_set_null_to_false_on_change_table

if ActiveRecord::Base.connection.supports_bulk_alter?
def test_timestamps_without_null_set_null_to_false_on_change_table_with_bulk
ActiveRecord::Schema.define do
ActiveRecord::Schema::Current.define do
create_table :has_timestamps

change_table :has_timestamps, bulk: true do |t|
Expand All @@ -169,7 +189,7 @@ def test_timestamps_without_null_set_null_to_false_on_change_table_with_bulk
end

def test_timestamps_without_null_set_null_to_false_on_add_timestamps
ActiveRecord::Schema.define do
ActiveRecord::Schema::Current.define do
create_table :has_timestamps
add_timestamps :has_timestamps, default: Time.now
end
Expand All @@ -180,7 +200,7 @@ def test_timestamps_without_null_set_null_to_false_on_add_timestamps

if supports_datetime_with_precision?
def test_timestamps_sets_precision_on_create_table
ActiveRecord::Schema.define do
ActiveRecord::Schema::Current.define do
create_table :has_timestamps do |t|
t.timestamps
end
Expand All @@ -191,7 +211,7 @@ def test_timestamps_sets_precision_on_create_table
end

def test_timestamps_sets_precision_on_change_table
ActiveRecord::Schema.define do
ActiveRecord::Schema::Current.define do
create_table :has_timestamps

change_table :has_timestamps do |t|
Expand All @@ -205,7 +225,7 @@ def test_timestamps_sets_precision_on_change_table

if ActiveRecord::Base.connection.supports_bulk_alter?
def test_timestamps_sets_precision_on_change_table_with_bulk
ActiveRecord::Schema.define do
ActiveRecord::Schema::Current.define do
create_table :has_timestamps

change_table :has_timestamps, bulk: true do |t|
Expand All @@ -219,7 +239,7 @@ def test_timestamps_sets_precision_on_change_table_with_bulk
end

def test_timestamps_sets_precision_on_add_timestamps
ActiveRecord::Schema.define do
ActiveRecord::Schema::Current.define do
create_table :has_timestamps
add_timestamps :has_timestamps, default: Time.now
end
Expand Down
2 changes: 1 addition & 1 deletion activerecord/test/cases/adapters/postgresql/enum_test.rb
Expand Up @@ -113,7 +113,7 @@ def test_schema_dump
def test_schema_load
original, $stdout = $stdout, StringIO.new

ActiveRecord::Schema.define do
ActiveRecord::Schema::Current.define do
create_enum :color, ["blue", "green"]

change_table :postgresql_enums do |t|
Expand Down
2 changes: 1 addition & 1 deletion activerecord/test/cases/migrator_test.rb
Expand Up @@ -226,7 +226,7 @@ def test_migrations_status_with_schema_define_in_subdirectories
schema_migration = ActiveRecord::Base.connection.schema_migration
ActiveRecord::Migrator.migrations_paths = path

ActiveRecord::Schema.define(version: 3) do
ActiveRecord::Schema::Current.define(version: 3) do
end

assert_equal [
Expand Down
5 changes: 5 additions & 0 deletions activerecord/test/cases/schema_dumper_test.rb
Expand Up @@ -38,6 +38,11 @@ def test_dump_schema_information_outputs_lexically_ordered_versions
ActiveRecord::SchemaMigration.delete_all
end

def test_schema_dump_include_migration_version
output = standard_dump
assert_match %r{ActiveRecord::Schema\[#{ActiveRecord::Migration.current_version}\]\.define}, output
end

def test_schema_dump
output = standard_dump
assert_match %r{create_table "accounts"}, output
Expand Down
2 changes: 1 addition & 1 deletion activerecord/test/schema/mysql2_specific_schema.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true

ActiveRecord::Schema.define do
ActiveRecord::Schema::Current.define do
if supports_datetime_with_precision?
create_table :datetime_defaults, force: true do |t|
t.datetime :modified_datetime, precision: nil, default: -> { "CURRENT_TIMESTAMP" }
Expand Down
2 changes: 1 addition & 1 deletion activerecord/test/schema/oracle_specific_schema.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true

ActiveRecord::Schema.define do
ActiveRecord::Schema::Current.define do
execute "drop table test_oracle_defaults" rescue nil
execute "drop sequence test_oracle_defaults_seq" rescue nil
execute "drop sequence companies_nonstd_seq" rescue nil
Expand Down
2 changes: 1 addition & 1 deletion activerecord/test/schema/postgresql_specific_schema.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true

ActiveRecord::Schema.define do
ActiveRecord::Schema::Current.define do
enable_extension!("uuid-ossp", ActiveRecord::Base.connection)
enable_extension!("pgcrypto", ActiveRecord::Base.connection) if ActiveRecord::Base.connection.supports_pgcrypto_uuid?

Expand Down
2 changes: 1 addition & 1 deletion activerecord/test/schema/schema.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true

ActiveRecord::Schema.define do
ActiveRecord::Schema::Current.define do
# ------------------------------------------------------------------- #
# #
# Please keep these create table statements in alphabetical order #
Expand Down
2 changes: 1 addition & 1 deletion activerecord/test/schema/sqlite_specific_schema.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true

ActiveRecord::Schema.define do
ActiveRecord::Schema::Current.define do
create_table :defaults, force: true do |t|
t.date :modified_date, default: -> { "CURRENT_DATE" }
t.date :fixed_date, default: "2004-01-01"
Expand Down
2 changes: 1 addition & 1 deletion guides/bug_report_templates/action_mailbox_gem.rb
Expand Up @@ -45,7 +45,7 @@ class TestApp < Rails::Application
require ActiveStorage::Engine.root.join("db/migrate/20170806125915_create_active_storage_tables.rb").to_s
require ActionMailbox::Engine.root.join("db/migrate/20180917164000_create_action_mailbox_tables.rb").to_s

ActiveRecord::Schema.define do
ActiveRecord::Schema::Current.define do
CreateActiveStorageTables.new.change
CreateActionMailboxTables.new.change
end
Expand Down
2 changes: 1 addition & 1 deletion guides/bug_report_templates/action_mailbox_main.rb
Expand Up @@ -44,7 +44,7 @@ class TestApp < Rails::Application
require ActiveStorage::Engine.root.join("db/migrate/20170806125915_create_active_storage_tables.rb").to_s
require ActionMailbox::Engine.root.join("db/migrate/20180917164000_create_action_mailbox_tables.rb").to_s

ActiveRecord::Schema.define do
ActiveRecord::Schema::Current.define do
CreateActiveStorageTables.new.change
CreateActionMailboxTables.new.change
end
Expand Down
2 changes: 1 addition & 1 deletion guides/bug_report_templates/active_record_gem.rb
Expand Up @@ -20,7 +20,7 @@
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Schema.define do
ActiveRecord::Schema::Current.define do
create_table :posts, force: true do |t|
end

Expand Down
2 changes: 1 addition & 1 deletion guides/bug_report_templates/active_record_main.rb
Expand Up @@ -19,7 +19,7 @@
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Schema.define do
ActiveRecord::Schema::Current.define do
create_table :posts, force: true do |t|
end

Expand Down
Expand Up @@ -20,7 +20,7 @@
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Schema.define do
ActiveRecord::Schema::Current.define do
create_table :payments, force: true do |t|
t.decimal :amount, precision: 10, scale: 0, default: 0, null: false
end
Expand Down

0 comments on commit b7f15a2

Please sign in to comment.