Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Up & Down Again: A Migration's Tale

Up & Down Again: A Migration's Tale

You run rake db:migrate and rake db:schema:load regularly, but what do they actually do? How does rake db:rollback automatically reverse migrations and why can't it reverse all of them? How can you teach these tasks new tricks to support additional database constructs?

We'll answer all of this and more as we explore the world of schema management in Rails. You will leave this talk with a deep understanding of how Rails manages schema, a better idea of its pitfalls, and ready to bend it to your will.

Derek Prior

April 19, 2018
Tweet

More Decks by Derek Prior

Other Decks in Programming

Transcript

  1. Up & Down Again
    A Migration's Tale

    View Slide

  2. Derek Prior
    @derekprior

    View Slide

  3. View Slide

  4. Schema
    Management
    @derekprior

    View Slide

  5. 0.10.1
    @derekprior

    View Slide

  6. Agenda
    @derekprior

    View Slide

  7. Agenda
    → Anatomy of a Migration
    @derekprior

    View Slide

  8. Agenda
    → Anatomy of a Migration
    → Applying and Reverting a Migration
    @derekprior

    View Slide

  9. Agenda
    → Anatomy of a Migration
    → Applying and Reverting a Migration
    → Schema Dumping
    @derekprior

    View Slide

  10. Agenda
    → Anatomy of a Migration
    → Applying and Reverting a Migration
    → Schema Dumping
    → Shortcomings and Extensions
    @derekprior

    View Slide

  11. Anatomy of a
    Migration
    @derekprior

    View Slide

  12. class CreatePosts < ActiveRecord::Migration[5.2]
    def change
    create_table :posts do |t|
    t.belongs_to :user, null: false, index: true, foreign_key: true
    t.string :title, null: false, index: { unique: true }
    t.text :body, null: false
    t.timestamps null: false, index: true
    end
    end
    end
    @derekprior

    View Slide

  13. class CreatePosts < ActiveRecord::Migration[5.2]
    def change
    create_table :posts do |t|
    t.belongs_to :user, null: false, index: true, foreign_key: true
    t.string :title, null: false, index: { unique: true }
    t.text :body, null: false
    t.timestamps null: false, index: true
    end
    end
    end
    @derekprior

    View Slide

  14. ActiveRecord::Migration
    def self.[](version)
    Compatibility.find(version)
    end
    module Compatibility
    def self.find(version)
    name = "V#{version.tr('.', '_')}"
    # Some error handling...
    const_get(name)
    end
    end
    @derekprior

    View Slide

  15. ActiveRecord::Migration
    def self.[](version)
    Compatibility.find(version)
    end
    module Compatibility
    def self.find(version)
    name = "V#{version.tr('.', '_')}"
    # Some error handling...
    const_get(name)
    end
    end
    @derekprior

    View Slide

  16. ActiveRecord::Migration[5.2]
    @derekprior

    View Slide

  17. ActiveRecord::Migration::Compatibility::V5_2
    @derekprior

    View Slide

  18. Compatibility::V5_2
    module ActiveRecord
    class Migration
    module Compatibility
    class V5_2 < Migration
    end
    end
    end
    end
    @derekprior

    View Slide

  19. Prevent Subclassing Migration
    module ActiveRecord
    class Migration
    def self.inherited(subclass)
    super
    if subclass.superclass == Migration
    raise StandardError, "Directly inheriting ... is not supported. "
    end
    end
    end
    end
    @derekprior

    View Slide

  20. class CreatePosts < ActiveRecord::Migration[5.1]
    def change
    create_table :posts do |t|
    t.belongs_to :user, null: false, index: true, foreign_key: true
    t.string :title, null: false, index: { unique: true }
    t.text :body, null: false
    t.timestamps null: false, index: true
    end
    end
    end
    @derekprior

    View Slide

  21. Compatibility::V5_1
    class V5_1 < V5_2
    def change_column(table_name, column_name, type, options = {})
    if adapter_name == "PostgreSQL"
    #...
    else
    super
    end
    end
    def create_table(table_name, options = {})
    if adapter_name == "Mysql2"
    #...
    else
    super
    end
    end
    end
    @derekprior

    View Slide

  22. Compatibility::V5_1
    class V5_1 < V5_2
    def change_column(table_name, column_name, type, options = {})
    if adapter_name == "PostgreSQL"
    #...
    else
    super
    end
    end
    def create_table(table_name, options = {})
    if adapter_name == "Mysql2"
    #...
    else
    super
    end
    end
    end
    @derekprior

    View Slide

  23. Compatibility::V5_1
    class V5_1 < V5_2
    def change_column(table_name, column_name, type, options = {})
    if adapter_name == "PostgreSQL"
    #...
    else
    super
    end
    end
    def create_table(table_name, options = {})
    if adapter_name == "Mysql2"
    #...
    else
    super
    end
    end
    end
    @derekprior

    View Slide

  24. Stable
    Migrations
    @derekprior

    View Slide

  25. !
    @derekprior

    View Slide

  26. class CreatePosts < ActiveRecord::Migration[5.2]
    def change
    create_table :posts do |t|
    t.belongs_to :user, null: false, index: true, foreign_key: true
    t.string :title, null: false, index: { unique: true }
    t.text :body, null: false
    t.timestamps null: false, index: true
    end
    end
    end
    @derekprior

    View Slide

  27. class CreatePosts < ActiveRecord::Migration[5.2]
    def change
    create_table :posts do |t|
    t.belongs_to :user, null: false, index: true, foreign_key: true
    t.string :title, null: false, index: { unique: true }
    t.text :body, null: false
    t.timestamps null: false, index: true
    end
    end
    end
    @derekprior

    View Slide

  28. Schema
    Statements
    @derekprior

    View Slide

  29. ActiveRecord::ConnectionAdapters::SchemaStatements
    @derekprior

    View Slide

  30. create_table :posts do |t|
    t.belongs_to :user, null: false, index: true, foreign_key: true
    t.string :title, null: false, index: { unique: true }
    t.text :body, null: false
    t.timestamps null: false, index: true
    end
    @derekprior

    View Slide

  31. create_table :posts do |t|
    t.belongs_to :user, null: false, index: true, foreign_key: true
    t.string :title, null: false, index: { unique: true }
    t.text :body, null: false
    t.timestamps null: false, index: true
    end
    @derekprior

    View Slide

  32. Jobs To Be Done
    @derekprior

    View Slide

  33. Jobs To Be Done
    → Adds non nullable user_id to CREATE TABLE
    @derekprior

    View Slide

  34. Jobs To Be Done
    → Adds non nullable user_id to CREATE TABLE
    → Adds an index to the user_id column
    @derekprior

    View Slide

  35. Jobs To Be Done
    → Adds non nullable user_id to CREATE TABLE
    → Adds an index to the user_id column
    → Adds a foreign key to the users table
    @derekprior

    View Slide

  36. class CreatePosts < ActiveRecord::Migration[5.2]
    def change
    create_table :posts do |t|
    t.belongs_to :user, null: false, index: true, foreign_key: true
    t.string :title, null: false, index: { unique: true }
    t.text :body, null: false
    t.timestamps null: false, index: true
    end
    end
    end
    @derekprior

    View Slide

  37. Postgres
    CREATE TABLE "posts" (
    "id" bigserial primary key,
    "title" character varying NOT NULL,
    "body" text NOT NULL,
    "user_id" bigint NOT NULL,
    "created_at" timestamp NOT NULL,
    "updated_at" timestamp NOT NULL,
    CONSTRAINT "fk_rails_5b5ddfd518"
    FOREIGN KEY ("user_id")
    REFERENCES "users" ("id")
    );
    CREATE UNIQUE INDEX "index_posts_on_title" ON "posts" ("title");
    CREATE INDEX "index_posts_on_user_id" ON "posts" ("user_id");
    CREATE INDEX "index_posts_on_created_at" ON "posts" ("created_at");
    CREATE INDEX "index_posts_on_updated_at" ON "posts" ("updated_at");
    @derekprior

    View Slide

  38. MySQL
    CREATE TABLE `posts` (
    `id` bigint NOT NULL AUTO_INCREMENT PRIMARY KEY,
    `title` varchar(255) NOT NULL,
    `body` text NOT NULL,
    `user_id` bigint NOT NULL,
    `created_at` datetime NOT NULL,
    `updated_at` datetime NOT NULL,
    UNIQUE INDEX `index_posts_on_title` (`title`),
    INDEX `index_posts_on_user_id` (`user_id`),
    INDEX `index_posts_on_created_at` (`created_at`),
    INDEX `index_posts_on_updated_at` (`updated_at`),
    CONSTRAINT `fk_rails_5b5ddfd518`
    FOREIGN KEY (`user_id`)
    REFERENCES `users` (`id`)
    );
    @derekprior

    View Slide

  39. Adatper
    Pattern
    @derekprior

    View Slide

  40. Adapter Pattern
    "Convert the interface of a class into another
    interface clients expect. Adapter lets classes work
    together that couldn't otherwise because of
    incompatible interfaces."
    -- Design Patterns (Gang of Four)
    @derekprior

    View Slide

  41. External Dependencies
    @derekprior

    View Slide

  42. External Dependencies
    → Postgres
    @derekprior

    View Slide

  43. External Dependencies
    → Postgres
    → MySQL
    @derekprior

    View Slide

  44. External Dependencies
    → Postgres
    → MySQL
    → SQLite
    @derekprior

    View Slide

  45. External Dependencies
    → Postgres
    → MySQL
    → SQLite
    → ...
    @derekprior

    View Slide

  46. Connection Adapters
    @derekprior

    View Slide

  47. Connection Adapters
    → AbstractAdapter
    @derekprior

    View Slide

  48. Connection Adapters
    → AbstractAdapter
    → PostgreSQLAdapter
    @derekprior

    View Slide

  49. Connection Adapters
    → AbstractAdapter
    → PostgreSQLAdapter
    → Mysql2Adapter
    @derekprior

    View Slide

  50. Connection Adapters
    → AbstractAdapter
    → PostgreSQLAdapter
    → Mysql2Adapter
    → SQLite3Adapter
    @derekprior

    View Slide

  51. Connection Adapters
    → AbstractAdapter
    → PostgreSQLAdapter
    → Mysql2Adapter
    → SQLite3Adapter
    → ...
    @derekprior

    View Slide

  52. Connection Adapter Responsibilities
    @derekprior

    View Slide

  53. Connection Adapter Responsibilities
    → Schema inspection
    @derekprior

    View Slide

  54. Connection Adapter Responsibilities
    → Schema inspection
    → Schema statements
    @derekprior

    View Slide

  55. Connection Adapter Responsibilities
    → Schema inspection
    → Schema statements
    → Mapping types
    @derekprior

    View Slide

  56. Connection Adapter Responsibilities
    → Schema inspection
    → Schema statements
    → Mapping types
    → Cataloging capabilities
    @derekprior

    View Slide

  57. Connection Adapter Responsibilities
    → Schema inspection
    → Schema statements
    → Mapping types
    → Cataloging capabilities
    → Quoting
    @derekprior

    View Slide

  58. Agenda
    → Anatomy of a Migration
    → Applying and Reverting a Migration
    → Schema Dumping
    → Shortcomings and Extensions
    @derekprior

    View Slide

  59. Applying a
    Migration
    @derekprior

    View Slide

  60. rails db:migrate
    @derekprior

    View Slide

  61. Which
    Migrations
    Will Run?
    @derekprior

    View Slide

  62. 20180327012835_create_posts.rb
    @derekprior

    View Slide

  63. What's in a Filename?
    20180327012835 create_posts
    version name
    @derekprior

    View Slide

  64. SELECT "schema_migrations"."version"
    FROM "schema_migrations"
    ORDER BY "schema_migrations"."version" ASC
    @derekprior

    View Slide

  65. INSERT INTO "schema_migrations" ("version")
    VALUES ($1) RETURNING "version"
    [["version", "20180327012835"]]
    @derekprior

    View Slide

  66. rails db:migrate:status
    @derekprior

    View Slide

  67. rails db:migrate:status
    Status Migration ID Migration Name
    up 20180327005927 Create users
    down 20180327012835 Create posts
    @derekprior

    View Slide

  68. rails db:migrate:status
    Status Migration ID Migration Name
    up 20180327005927 Create users
    up 20180327012835 ** NO FILE **
    @derekprior

    View Slide

  69. Rolling Back a
    Migration
    @derekprior

    View Slide

  70. With a down method
    def up
    add_column :users, :name, :string
    remove_column :users, :first_name
    remove_column :users, :last_name
    end
    def down
    add_column :users, :first_name, :string
    add_column :users, :last_name, :string
    remove_column :users: name
    end
    @derekprior

    View Slide

  71. With a change method
    def change
    add_column :users, :name, :string
    remove_column :users, :first_name
    remove_column :users, :last_name
    end
    @derekprior

    View Slide

  72. Down For
    Free
    @derekprior

    View Slide

  73. ActiveRecord::Migration::CommandRecorder
    def initialize(delegate = nil)
    @commands = []
    end
    def record(*command, &block)
    @commands << (command << block)
    end
    @derekprior

    View Slide

  74. ActiveRecord::Migration::CommandRecorder
    def create_table(*args, &block)
    record(:create_table, args, block)
    end
    def add_index(*args, &block)
    record(:add_index, args, block)
    end
    @derekprior

    View Slide

  75. ActiveRecord::Migration::CommandRecorder
    def invert_create_table(args, &block)
    [:drop_table, args, block]
    end
    def invert_add_index(args, &block)
    [:remove_index, args, block]
    end
    def invert_rename_table(args)
    [:rename_table, args.reverse]
    end
    @derekprior

    View Slide

  76. Is This Migration Reversible?
    def change
    add_column :users, :name, :string
    remove_column :users, :first_name
    remove_column :users, :last_name
    end
    @derekprior

    View Slide

  77. ActiveRecord::IrreversibleMigration
    remove_column is only reversible if given a type
    @derekprior

    View Slide

  78. def change
    add_column :users, :name, :string
    remove_column :users, :first_name
    remove_column :users, :last_name
    end
    @derekprior

    View Slide

  79. def change
    add_column :users, :name, :string
    remove_column :users, :first_name, :string
    remove_column :users, :last_name, :string
    end
    @derekprior

    View Slide

  80. def change
    add_column :users, :name, :string
    remove_column :users, :first_name, :string
    remove_column :users, :last_name, :string
    end
    @derekprior

    View Slide

  81. What Did We Record?
    [
    [:add_column, [:users, :name, :string]],
    [:remove_column, [:users, :first_name, :string]],
    [:remove_column, [:users, :last_name, :string]],
    ]
    @derekprior

    View Slide

  82. Inverted
    [
    [:add_column, [:users, :first_name, :string]
    [:add_column, [:users, :last_name, :string]
    [:remove_column, [:users, :name]],
    ]
    @derekprior

    View Slide

  83. !
    @derekprior

    View Slide

  84. !
    @derekprior

    View Slide

  85. What if a removed column originally had...
    @derekprior

    View Slide

  86. What if a removed column originally had...
    → A null: false constraint?
    @derekprior

    View Slide

  87. What if a removed column originally had...
    → A null: false constraint?
    → A default value?
    @derekprior

    View Slide

  88. What if a removed column originally had...
    → A null: false constraint?
    → A default value?
    → A foreign key?
    @derekprior

    View Slide

  89. It's Not Magic
    @derekprior

    View Slide

  90. Agenda
    → Anatomy of a Migration
    → Applying and Reverting a Migration
    → Schema Dumping
    → Shortcomings and Extensions
    @derekprior

    View Slide

  91. # This file is auto-generated from the current state of the database.
    # ...
    ActiveRecord::Schema.define(version: 2018_03_27_012835) do
    enable_extension "plpgsql"
    create_table "posts", force: :cascade do |t|
    t.string "title", null: false
    #...
    end
    create_table "users", force: :cascade do |t|
    t.string "email", null: false
    #...
    end
    add_foreign_key "posts", "users"
    end
    @derekprior

    View Slide

  92. How is it
    Generated?
    @derekprior

    View Slide

  93. ActiveRecord::SchemaDumper
    module ActiveRecord
    class SchemaDumper
    def self.dump(connection, stream = STDOUT)
    connection.create_schema_dumper.dump(stream)
    end
    def dump(stream)
    header(stream)
    extensions(stream)
    tables(stream)
    trailer(stream)
    end
    end
    end
    @derekprior

    View Slide

  94. module ActiveRecord
    class SchemaDumper
    def self.dump(connection, stream = STDOUT)
    connection.create_schema_dumper.dump(stream)
    end
    def dump(stream)
    header(stream)
    extensions(stream)
    tables(stream)
    trailer(stream)
    end
    end
    end
    @derekprior

    View Slide

  95. module ActiveRecord
    class SchemaDumper
    def self.dump(connection, stream = STDOUT)
    connection.create_schema_dumper.dump(stream)
    end
    def dump(stream)
    header(stream)
    extensions(stream)
    tables(stream)
    trailer(stream)
    end
    end
    end
    @derekprior

    View Slide

  96. # This file is auto-generated from the current state of the database.
    # ...
    ActiveRecord::Schema.define(version: 2018_03_27_012835) do
    enable_extension "plpgsql"
    create_table "posts", force: :cascade do |t|
    t.string "title", null: false
    #...
    end
    create_table "users", force: :cascade do |t|
    t.string "email", null: false
    #...
    end
    add_foreign_key "posts", "users"
    end
    @derekprior

    View Slide

  97. # This file is auto-generated from the current state of the database.
    # ...
    ActiveRecord::Schema.define(version: 2018_03_27_012835) do
    enable_extension "plpgsql"
    create_table "posts", force: :cascade do |t|
    t.string "title", null: false
    #...
    end
    create_table "users", force: :cascade do |t|
    t.string "email", null: false
    #...
    end
    add_foreign_key "posts", "users"
    end
    @derekprior

    View Slide

  98. # This file is auto-generated from the current state of the database.
    # ...
    ActiveRecord::Schema.define(version: 2018_03_27_012835) do
    enable_extension "plpgsql"
    create_table "posts", force: :cascade do |t|
    t.string "title", null: false
    #...
    end
    create_table "users", force: :cascade do |t|
    t.string "email", null: false
    #...
    end
    add_foreign_key "posts", "users"
    end
    @derekprior

    View Slide

  99. # This file is auto-generated from the current state of the database.
    # ...
    ActiveRecord::Schema.define(version: 2018_03_27_012835) do
    enable_extension "plpgsql"
    create_table "posts", force: :cascade do |t|
    t.string "title", null: false
    #...
    end
    create_table "users", force: :cascade do |t|
    t.string "email", null: false
    #...
    end
    add_foreign_key "posts", "users"
    end
    @derekprior

    View Slide

  100. # This file is auto-generated from the current state of the database.
    # ...
    ActiveRecord::Schema.define(version: 2018_03_27_012835) do
    enable_extension "plpgsql"
    create_table "posts", force: :cascade do |t|
    t.string "title", null: false
    #...
    end
    create_table "users", force: :cascade do |t|
    t.string "email", null: false
    #...
    end
    add_foreign_key "posts", "users"
    end
    @derekprior

    View Slide

  101. # This file is auto-generated from the current state of the database.
    # ...
    ActiveRecord::Schema.define(version: 2018_03_27_012835) do
    enable_extension "plpgsql"
    create_table "posts", force: :cascade do |t|
    t.string "title", null: false
    #...
    end
    create_table "users", force: :cascade do |t|
    t.string "email", null: false
    #...
    end
    add_foreign_key "posts", "users"
    end
    @derekprior

    View Slide

  102. Justifies Its Existence
    # 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.
    #
    # Note that this schema.rb definition is the authoritative source for your
    # database schema. If you need to create the application database on another
    # system, you should be using db:schema:load, not running all the migrations
    # from scratch. The latter is a flawed and unsustainable approach (the more migrations
    # you'll amass, the slower it'll run and the greater likelihood for issues).
    #
    # It's strongly recommended that you check this file into your version control system.
    @derekprior

    View Slide

  103. "... the authoritative
    source for your database
    schema"
    @derekprior

    View Slide

  104. !
    @derekprior

    View Slide

  105. "... you should be using
    db:schema:load, not
    running all migrations
    from scratch"
    @derekprior

    View Slide

  106. "the latter is a flawed and
    unsustainable approach"
    @derekprior

    View Slide

  107. "The more migrations you
    amass, the slower it'll run
    and the greater
    likelihood for issues"
    @derekprior

    View Slide

  108. !
    @derekprior

    View Slide

  109. !
    @derekprior

    View Slide


  110. @derekprior

    View Slide

  111. "greater likelihood for
    issues..."
    @derekprior

    View Slide

  112. Causes of Migration Rot
    @derekprior

    View Slide

  113. Causes of Migration Rot
    → Changing schema statements over time
    @derekprior

    View Slide

  114. Causes of Migration Rot
    → Changing schema statements over time
    → External dependencies in migrations
    @derekprior

    View Slide

  115. Causes of Migration Rot
    → Changing schema statements over time
    → External dependencies in migrations
    @derekprior

    View Slide

  116. External Dependencies in Migrations
    class CombineNameFields < ActiveRecord::Migration[5.2]
    def change
    add_column :users, :name, :string
    reversible do |dir|
    dir.up do
    User.find_each { |u| u.update(name: "#{u.first_name} #{u.last_name}") }
    end
    end
    remove_column :users, :first_name, :string
    remove_column :users, :last_name, :string
    end
    end
    @derekprior

    View Slide

  117. External Dependencies in Migrations
    class CombineNameFields < ActiveRecord::Migration[5.2]
    def change
    add_column :users, :name, :string
    reversible do |dir|
    dir.up do
    User.find_each { |u| u.update(name: "#{u.first_name} #{u.last_name}") }
    end
    end
    remove_column :users, :first_name, :string
    remove_column :users, :last_name, :string
    end
    end
    @derekprior

    View Slide

  118. External Dependencies in Migrations
    class CombineNameFields < ActiveRecord::Migration[5.2]
    def change
    add_column :users, :name, :string
    execute "UPDATE users SET name = CONCAT(first_name, ' ', last_name)"
    remove_column :users, :first_name, :string
    remove_column :users, :last_name, :string
    end
    end
    @derekprior

    View Slide

  119. Mind Your
    Dependencies
    @derekprior

    View Slide

  120. Agenda
    → Anatomy of a Migration
    → Applying and Reverting a Migration
    → Schema Dumping
    → Shortcomings and Extensions
    @derekprior

    View Slide

  121. Shortcomings
    @derekprior

    View Slide

  122. Shortcomings
    → Migration rot
    @derekprior

    View Slide

  123. Shortcomings
    → Migration rot
    → Further removed from SQL
    @derekprior

    View Slide

  124. Shortcomings
    → Migration rot
    → Further removed from SQL
    → Support for a limited subset of features
    @derekprior

    View Slide

  125. Foreign Keys
    @derekprior

    View Slide

  126. Rails 4.2
    @derekprior

    View Slide

  127. Expression
    Indexes
    @derekprior

    View Slide

  128. Rails 5.0
    @derekprior

    View Slide

  129. Functions
    @derekprior

    View Slide

  130. !
    @derekprior

    View Slide

  131. Triggers
    @derekprior

    View Slide


  132. @derekprior

    View Slide

  133. Views
    @derekprior

    View Slide

  134. !
    @derekprior

    View Slide

  135. execute "WHATEVER SQL YOU WANT"
    @derekprior

    View Slide

  136. config.active_record.schema_format = :sql
    @derekprior

    View Slide

  137. Extending
    Migrations
    @derekprior

    View Slide

  138. No Official API
    @derekprior

    View Slide

  139. !
    @derekprior

    View Slide

  140. !
    @derekprior

    View Slide

  141. Database
    Views
    @derekprior

    View Slide

  142. Requirements
    @derekprior

    View Slide

  143. Requirements
    → Schema Statements
    @derekprior

    View Slide

  144. Requirements
    → Schema Statements
    → Command Recorder
    @derekprior

    View Slide

  145. Requirements
    → Schema Statements
    → Command Recorder
    → Schema Dumper
    @derekprior

    View Slide

  146. Schema Statements
    module Scenic
    module Statements
    def create_view(version:, materialized: false)
    end
    def drop_view(version:, revert_to_version: nil, materialized: false)
    end
    def update_view(name, version:, revert_to_version: nil, materialized: false)
    end
    end
    end
    @derekprior

    View Slide

  147. Requirements
    → Schema Statements
    → Command Recorder
    → Schema Dumper
    @derekprior

    View Slide

  148. Command Recorder
    module Scenic
    module CommandRecorder
    def create_view(*args)
    record(:create_view, args)
    end
    def drop_view(*args)
    record(:drop_view, args)
    end
    def update_view(*args)
    record(:update_view, args)
    end
    end
    end
    @derekprior

    View Slide

  149. Command Recorder
    module Scenic
    module CommandRecorder
    def invert_create_view(args)
    [:drop_view, args]
    end
    def invert_drop_view(args)
    perform_scenic_inversion(:create_view, args)
    end
    def invert_update_view(args)
    perform_scenic_inversion(:update_view, args)
    end
    end
    end
    @derekprior

    View Slide

  150. Command Recorder
    module Scenic
    module CommandRecorder
    def perform_scenic_inversion(method, args)
    scenic_args = StatementArguments.new(args)
    if scenic_args.revert_to_version.nil?
    message = "#{method} is reversible only if given a revert_to_version"
    raise ActiveRecord::IrreversibleMigration, message
    end
    [method, scenic_args.invert_version.to_a]
    end
    end
    end
    @derekprior

    View Slide

  151. Requirements
    → Schema Statements
    → Command Recorder
    → Schema Dumper
    @derekprior

    View Slide

  152. Schema Dumper
    module Scenic
    module SchemaDumper
    def tables(stream)
    super
    views(stream)
    end
    def views
    # ...
    end
    end
    end
    @derekprior

    View Slide

  153. Schema Dumper
    module Scenic
    module SchemaDumper
    def tables(stream)
    super
    views(stream)
    end
    def views
    # ...
    end
    end
    end
    @derekprior

    View Slide

  154. Wiring It All Up
    module Scenic
    class Railtie < Rails::Railtie
    initializer "scenic.load" do
    ActiveSupport.on_load :active_record do
    Scenic.load
    end
    end
    end
    def self.load
    ActiveRecord::ConnectionAdapters::AbstractAdapter.include Scenic::Statements
    ActiveRecord::Migration::CommandRecorder.include Scenic::CommandRecorder
    ActiveRecord::SchemaDumper.prepend Scenic::SchemaDumper
    end
    end
    @derekprior

    View Slide

  155. Wiring It All Up
    module Scenic
    class Railtie < Rails::Railtie
    initializer "scenic.load" do
    ActiveSupport.on_load :active_record do
    Scenic.load
    end
    end
    end
    def self.load
    ActiveRecord::ConnectionAdapters::AbstractAdapter.include Scenic::Statements
    ActiveRecord::Migration::CommandRecorder.include Scenic::CommandRecorder
    ActiveRecord::SchemaDumper.prepend Scenic::SchemaDumper
    end
    end
    @derekprior

    View Slide

  156. !
    @derekprior

    View Slide

  157. It's ALIVE!
    class UpdateSearchResults < ActiveRecord::Migration[5.2]
    def change
    update_view :search_results, version: 3, revert_to_version: 2
    end
    end
    @derekprior

    View Slide

  158. !
    @derekprior

    View Slide

  159. !
    @derekprior

    View Slide

  160. !
    @derekprior

    View Slide

  161. What if we did none of this?
    -- Some guy on a podcast
    @derekprior

    View Slide

  162. SQL
    Migrations
    @derekprior

    View Slide

  163. SQL Migrations
    db/migrate
    ├── 20180327005927_create_users
    │ ├── down.sql
    │ └── up.sql
    └── 20180327012835_create_posts
    ├── down.sql
    └── up.sql
    @derekprior

    View Slide

  164. @derekprior

    View Slide

  165. Pareto
    Principle
    @derekprior

    View Slide

  166. for many events, roughly
    80% of the effects come
    from 20% of the causes
    @derekprior

    View Slide

  167. Higher Level
    Abstraction
    @derekprior

    View Slide

  168. Ruby
    Migrations
    @derekprior

    View Slide

  169. !
    @derekprior

    View Slide

  170. !
    Maybe learn to love structure.sql

    View Slide

  171. Thank You
    Derek Prior
    → twitter: @derekprior
    → email: [email protected]
    → podcast: http://bikeshed.fm

    View Slide