GithubHelp home page GithubHelp logo

artur-sulej / excellent_migrations Goto Github PK

View Code? Open in Web Editor NEW
210.0 7.0 21.0 100 KB

An Elixir tool for checking safety of database migrations.

License: MIT License

Elixir 100.00%
elixir static-analysis code-analysis credo ecto migrations ast

excellent_migrations's Introduction

Excellent Migrations

CI Tests Module Version Hex Docs Total Download License Last Updated

Detect potentially dangerous or destructive operations in your database migrations.

Installation

The package can be installed by adding :excellent_migrations to your list of dependencies in mix.exs:

def deps do
  [
    {:excellent_migrations, "~> 0.1", only: [:dev, :test], runtime: false}
  ]
end

Documentation

Documentation is available on Hexdocs.

How It Works

This tool analyzes code (AST) of migration files. You don't have to edit or include any additional code in your migration files, except for occasionally adding a config comment for assuring safety.

How to use it

There are multiple ways to integrate with Excellent Migrations.

Credo check

Excellent Migrations provide custom, ready-to-use check for Credo.

Add ExcellentMigrations.CredoCheck.MigrationsSafety to your .credo file:

%{
  configs: [
    %{
      # …
      checks: [
        # …
        {ExcellentMigrations.CredoCheck.MigrationsSafety, []}
      ]
    }
  ]
}

Example credo warnings:

  Warnings - please take a look
┃
┃ [W] ↗ Raw SQL used
┃       apps/cookbook/priv/repo/migrations/20211024133700_create_recipes.exs:13 #(Cookbook.Repo.Migrations.CreateRecipes.up)
┃ [W] ↗ Index added not concurrently
┃       apps/cookbook/priv/repo/migrations/20211024133705_create_index_on_veggies.exs:37 #(Cookbook.Repo.Migrations.CreateIndexOnVeggies.up)

mix task

mix excellent_migrations.check_safety

This mix task analyzes migrations and logs a warning for each danger detected.

migration task

mix excellent_migrations.migrate

Running this task will first analyze migrations. If no dangers are detected it will proceed and run mix ecto.migrate. If there are any, it will log errors and stop.

Code

You can also use it in code. To do so, you need to get source code and AST of your migration file, e.g. via File.read!/1 and Code.string_to_quoted/2. Then pass them to ExcellentMigrations.DangersDetector.detect_dangers(ast). It will return a keyword list containing danger types and lines where they were detected.

Checks

Potentially dangerous operations:

Postgres-specific checks:

Best practices:

You can also disable specific checks.

Removing a column

If Ecto is still configured to read a column in any running instances of the application, then queries will fail when loading data into your structs. This can happen in multi-node deployments or if you start the application before running migrations.

BAD ❌

# Without a code change to the Ecto Schema

def change do
  alter table("recipes") do
    remove :no_longer_needed_column
  end
end

GOOD ✅

Safety can be assured if the application code is first updated to remove references to the column so it's no longer loaded or queried. Then, the column can safely be removed from the table.

  1. Deploy code change to remove references to the field.
  2. Deploy migration change to remove the column.

First deployment:

# First deploy, in the Ecto schema

defmodule Cookbook.Recipe do
  schema "recipes" do
-   column :no_longer_needed_column, :text
  end
end

Second deployment:

def change do
  alter table("recipes") do
    remove :no_longer_needed_column
  end
end

Adding a column with a default value

Adding a column with a default value to an existing table may cause the table to be rewritten. During this time, reads and writes are blocked in Postgres, and writes are blocked in MySQL and MariaDB.

BAD ❌

Note: This becomes safe in:

  • Postgres 11+
  • MySQL 8.0.12+
  • MariaDB 10.3.2+
def change do
  alter table("recipes") do
    add :favourite, :boolean, default: false
    # This took 10 minutes for 100 million rows with no fkeys,

    # Obtained an AccessExclusiveLock on the table, which blocks reads and
    # writes.
  end
end

GOOD ✅

Add the column first, then alter it to include the default.

First migration:

def change do
  alter table("recipes") do
    add :favourite, :boolean
    # This took 0.27 milliseconds for 100 million rows with no fkeys,
  end
end

Second migration:

def change do
  alter table("recipes") do
    modify :favourite, :boolean, default: false
    # This took 0.28 milliseconds for 100 million rows with no fkeys,
  end
end

Schema change to read the new column:

schema "recipes" do
+ field :favourite, :boolean, default: false
end

Column with volatile default

If the default value is volatile (e.g., clock_timestamp(), uuid_generate_v4(), random()) each row will need to be updated with the value calculated at the time ALTER TABLE is executed.

BAD ❌

Adding volatile default to column:

def change do
  alter table(:recipes) do
    modify(:identifier, :uuid, default: fragment("uuid_generate_v4()"))
  end
end

Adding column with volatile default:

def change do
  alter table(:recipes) do
    add(:identifier, :uuid, default: fragment("uuid_generate_v4()"))
  end
end

GOOD ✅

To avoid a potentially lengthy update operation, particularly if you intend to fill the column with mostly nondefault values anyway, it may be preferable to:

  1. add the column with no default
  2. insert the correct values using UPDATE query
  3. only then add any desired default

Also creating a new table with column with volatile default is safe, because it does not contain any records.


Backfilling data

Ecto creates a transaction around each migration, and backfilling in the same transaction that alters a table keeps the table locked for the duration of the backfill. Also, running a single query to update data can cause issues for large tables.

BAD ❌

defmodule Cookbook.BackfillRecipes do
  use Ecto.Migration
  import Ecto.Query

  def change do
    alter table("recipes") do
      add :new_data, :text
    end

    flush()

    Cookbook.Recipe
    |> where(new_data: nil)
    |> Cookbook.Repo.update_all(set: [new_data: "some data"])
  end
end

GOOD ✅

There are several different strategies to perform safe backfilling. This article explains them in great details.


Changing the type of a column

Changing the type of a column may cause the table to be rewritten. During this time, reads and writes are blocked in Postgres, and writes are blocked in MySQL and MariaDB.

BAD ❌

Safe in Postgres:

  • increasing length on varchar or removing the limit
  • changing varchar to text
  • changing text to varchar with no length limit
  • Postgres 9.2+ - increasing precision (NOTE: not scale) of decimal or numeric columns. eg, increasing 8,2 to 10,2 is safe. Increasing 8,2 to 8,4 is not safe.
  • Postgres 9.2+ - changing decimal or numeric to be unconstrained
  • Postgres 12+ - changing timestamp to timestamptz when session TZ is UTC

Safe in MySQL/MariaDB:

  • increasing length of varchar from < 255 up to 255.
  • increasing length of varchar from > 255 up to max.
def change do
  alter table("recipes") do
    modify :my_column, :boolean, from: :text
  end
end

GOOD ✅

Take a phased approach:

  1. Create a new column
  2. In application code, write to both columns
  3. Backfill data from old column to new column
  4. In application code, move reads from old column to the new column
  5. In application code, remove old column from Ecto schemas.
  6. Drop the old column.

Renaming a column

Ask yourself: "Do I really need to rename a column?". Probably not, but if you must, read on and be aware it requires time and effort.

If Ecto is configured to read a column in any running instances of the application, then queries will fail when loading data into your structs. This can happen in multi-node deployments or if you start the application before running migrations.

There is a shortcut: Don't rename the database column, and instead rename the schema's field name and configure it to point to the database column.

BAD ❌

# In your schema
schema "recipes" do
  field :summary, :text
end


# In your migration
def change do
  rename table("recipes"), :title, to: :summary
end

The time between your migration running and your application getting the new code may encounter trouble.

GOOD ✅

Strategy 1

Rename the field in the schema only, and configure it to point to the database column and keep the database column the same. Ensure all calling code relying on the old field name is also updated to reference the new field name.

defmodule Cookbook.Recipe do
  use Ecto.Schema

  schema "recipes" do
    field :author, :string
    field :preparation_minutes, :integer, source: :prep_min
  end
end
## Update references in other parts of the codebase:
   recipe = Repo.get(Recipe, "my_id")
-  recipe.prep_min
+  recipe.preparation_minutes

Strategy 2

Take a phased approach:

  1. Create a new column
  2. In application code, write to both columns
  3. Backfill data from old column to new column
  4. In application code, move reads from old column to the new column
  5. In application code, remove old column from Ecto schemas.
  6. Drop the old column.

Renaming a table

Ask yourself: "Do I really need to rename a table?". Probably not, but if you must, read on and be aware it requires time and effort.

If Ecto is still configured to read a table in any running instances of the application, then queries will fail when loading data into your structs. This can happen in multi-node deployments or if you start the application before running migrations.

There is a shortcut: rename the schema only, and do not change the underlying database table name.

BAD ❌

def change do
  rename table("recipes"), to: table("dish_algorithms")
end

GOOD ✅

Strategy 1

Rename the schema only and all calling code, and don’t rename the table:

- defmodule Cookbook.Recipe do
+ defmodule Cookbook.DishAlgorithm do
  use Ecto.Schema

  schema "dish_algorithms" do
    field :author, :string
    field :preparation_minutes, :integer
  end
end

# and in calling code:
- recipe = Cookbook.Repo.get(Cookbook.Recipe, "my_id")
+ dish_algorithm = Cookbook.Repo.get(Cookbook.DishAlgorithm, "my_id")

Strategy 2

Take a phased approach:

  1. Create the new table. This should include creating new constraints (checks and foreign keys) that mimic behavior of the old table.
  2. In application code, write to both tables, continuing to read from the old table.
  3. Backfill data from old table to new table
  4. In application code, move reads from old table to the new table
  5. In application code, remove the old table from Ecto schemas.
  6. Drop the old table.

Adding a check constraint

Adding a check constraint blocks reads and writes to the table in Postgres, and blocks writes in MySQL/MariaDB while every row is checked.

BAD ❌

def change do
  create constraint("ingredients", :price_must_be_positive, check: "price > 0")
  # Creating the constraint with validate: true (the default when unspecified)
  # will perform a full table scan and acquires a lock preventing updates
end

GOOD ✅

There are two operations occurring:

  1. Creating a new constraint for new or updating records
  2. Validating the new constraint for existing records

If these commands are happening at the same time, it obtains a lock on the table as it validates the entire table and fully scans the table. To avoid this full table scan, we can separate the operations.

In one migration:

def change do
  create constraint("ingredients", :price_must_be_positive, check: "price > 0", validate: false)
  # Setting validate: false will prevent a full table scan, and therefore
  # commits immediately.
end

In the next migration:

def change do
  execute "ALTER TABLE ingredients VALIDATE CONSTRAINT price_must_be_positive", ""
  # Acquires SHARE UPDATE EXCLUSIVE lock, which allows updates to continue
end

These can be in the same deployment, but ensure there are 2 separate migrations.


Setting NOT NULL on an existing column

Setting NOT NULL on an existing column blocks reads and writes while every row is checked. Just like the Adding a check constraint scenario, there are two operations occurring:

  1. Creating a new constraint for new or updating records
  2. Validating the new constraint for existing records

To avoid the full table scan, we can separate these two operations.

BAD ❌

def change do
  alter table("recipes") do
    modify :favourite, :boolean, null: false
  end
end

GOOD ✅

Add a check constraint without validating it, backfill data to satiate the constraint and then validate it. This will be functionally equivalent.

In the first migration:

# Deployment 1
def change do
  create constraint("recipes", :favourite_not_null, check: "favourite IS NOT NULL", validate: false)
end

This will enforce the constraint in all new rows, but not care about existing rows until that row is updated.

You'll likely need a data migration at this point to ensure that the constraint is satisfied.

Then, in the next deployment's migration, we'll enforce the constraint on all rows:

# Deployment 2
def change do
  execute "ALTER TABLE recipes VALIDATE CONSTRAINT favourite_not_null", ""
end

If you're using Postgres 12+, you can add the NOT NULL to the column after validating the constraint. From the Postgres 12 docs:

SET NOT NULL may only be applied to a column provided none of the records in the table contain a NULL value for the column. Ordinarily this is checked during the ALTER TABLE by scanning the entire table; however, if a valid CHECK constraint is found which proves no NULL can exist, then the table scan is skipped.

# **Postgres 12+ only**

def change do
  execute "ALTER TABLE recipes VALIDATE CONSTRAINT favourite_not_null", ""

  alter table("recipes") do
    modify :favourite, :boolean, null: false
  end

  drop constraint("recipes", :favourite_not_null)
end

If your constraint fails, then you should consider backfilling data first to cover the gaps in your desired data integrity, then revisit validating the constraint.


Executing SQL directly

Excellent Migrations can’t ensure safety for raw SQL statements. Make really sure that what you’re doing is safe, then use:

defmodule Cookbook.ExecuteRawSql do
  # excellent_migrations:safety-assured-for-this-file raw_sql_executed

  def change do
    execute("...")
  end
end

Adding an index non-concurrently

Creating an index will block both reads and writes.

BAD ❌

def change do
  create index("recipes", [:slug])

  # This obtains a ShareLock on "recipes" which will block writes to the table
end

GOOD ✅

With Postgres, instead create the index concurrently which does not block reads. You will need to disable the database transactions to use CONCURRENTLY, and since Ecto obtains migration locks through database transactions this also implies that competing nodes may attempt to try to run the same migration (eg, in a multi-node Kubernetes environment that runs migrations before startup). Therefore, some nodes will fail startup for a variety of reasons.

@disable_ddl_transaction true
@disable_migration_lock true

def change do
  create index("recipes", [:slug], concurrently: true)
end

The migration may still take a while to run, but reads and updates to rows will continue to work. For example, for 100,000,000 rows it took 165 seconds to add run the migration, but SELECTS and UPDATES could occur while it was running.

Do not have other changes in the same migration; only create the index concurrently and separate other changes to later migrations.


Adding an index concurrently without disabling lock or transaction

Concurrently indexes need to set both @disable_ddl_transaction and @disable_migration_lock to true. See more:

BAD ❌

defmodule Cookbook.AddIndex do
  def change do
    create index(:recipes, [:cookbook_id, :cuisine], concurrently: true)
  end
end

GOOD ✅

defmodule Cookbook.AddIndex do
  @disable_ddl_transaction true
  @disable_migration_lock true

  def change do
    create index(:recipes, [:cookbook_id, :cuisine], concurrently: true)
  end
end

Adding a reference

Adding a foreign key blocks writes on both tables.

BAD ❌

def change do
  alter table("recipes") do
    add :cookbook_id, references("cookbooks")
  end
end

GOOD ✅

In the first migration

def change do
  alter table("recipes") do
    add :cookbook_id, references("cookbooks", validate: false)
  end
end

In the second migration

def change do
  execute "ALTER TABLE recipes VALIDATE CONSTRAINT cookbook_id_fkey", ""
end

These migrations can be in the same deployment, but make sure they are separate migrations.


Adding a json column

In Postgres, there is no equality operator for the json column type, which can cause errors for existing SELECT DISTINCT queries in your application.

BAD ❌

def change do
  alter table("recipes") do
    add :extra_data, :json
  end
end

GOOD ✅

Use jsonb instead. Some say it’s like "json" but "better."

def change do
  alter table("recipes") do
    add :extra_data, :jsonb
  end
end

Keeping non-unique indexes to three columns or less

BAD ❌

Adding a non-unique index with more than three columns rarely improves performance.

defmodule Cookbook.AddIndexOnIngredients do
  def change do
    create index(:recipes, [:a, :b, :c, :d], concurrently: true)
  end
end

GOOD ✅

Instead, start an index with columns that narrow down the results the most.

defmodule Cookbook.AddIndexOnIngredients do
  def change do
    create index(:recipes, [:b, :d], concurrently: true)
  end
end

For Postgres, be sure to add them concurrently.


Assuring safety

To mark an operation in a migration as safe use config comment. It will be ignored during analysis.

There are two config comments available:

  • excellent_migrations:safety-assured-for-next-line <operation_type>
  • excellent_migrations:safety-assured-for-this-file <operation_type>

Ignoring checks for given line:

defmodule Cookbook.AddTypeToRecipesWithDefault do
  def change do
    alter table(:recipes) do
      # excellent_migrations:safety-assured-for-next-line column_added_with_default
      add(:type, :string, default: "dessert")
    end
  end
end

Ignoring checks for the whole file:

defmodule Cookbook.AddTypeToRecipesWithDefault do
  # excellent_migrations:safety-assured-for-this-file column_added_with_default

  def change do
    alter table(:recipes) do
      add(:type, :string, default: "dessert")
    end
  end
end

Possible operation types are:

  • check_constraint_added
  • column_added_with_default
  • column_reference_added
  • column_removed
  • column_renamed
  • column_type_changed
  • column_volatile_default
  • index_concurrently_without_disable_ddl_transaction
  • index_concurrently_without_disable_migration_lock
  • index_not_concurrently
  • json_column_added
  • many_columns_index
  • not_null_added
  • operation_delete
  • operation_insert
  • operation_update
  • raw_sql_executed
  • table_dropped
  • table_renamed

Disable checks

Ignore specific dangers for all migration checks with:

config :excellent_migrations, skip_checks: [:raw_sql_executed, :not_null_added]

Existing migrations

To skip analyzing migrations that were created before adding this package, set timestamp from the last migration in start_after in config:

config :excellent_migrations, start_after: "20191026080101"

Similar tools & resources

Contributing

Everyone is encouraged and welcome to help improve this project. Here are a few ways you can help:

Copyright and License

Copyright (c) 2021 Artur Sulej

This work is free. You can redistribute it and/or modify it under the terms of the MIT License. See the LICENSE.md file for more details.

excellent_migrations's People

Contributors

andriiklymchuk avatar artur-sulej avatar bismark avatar bluehatbrit avatar hiagomeels avatar kianmeng avatar pyzlnar avatar ryvasquez avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

excellent_migrations's Issues

Danger detected incorrectly for repos that use `migration_lock: :pg_advisory_lock`

My repo is configured to use the pg advisory lock strategy for locking migrations across nodes, which does not use a database transaction, and when I assure ExcellentMigrations that it's ok to not use the migration lock, and I'm creating an index concurrently, it still complains about not disabling the ddl transaction even though I have.

I traced it to this case statement that has a catchall clause to add the DDL Transaction danger, which seems incorrect.

Here's an example setup:

config :my_app, MyApp.Repo,
  migration_lock: :pg_advisory_lock
defmodule MyApp.Repo.Migrations.MyMigration do
  use Ecto.Migration
  @disable_ddl_transaction true

  # excellent_migrations:safety-assured-for-this-file index_concurrently_without_disable_migration_lock

  def change do
    create_if_not_exists index("foo", [:bar], concurrently: true)
  end
end
my_app ❯ mix credo
Checking 2682 source files (this might take a while) ...

  Warnings - please take a look
┃ 
┃ [W] ↗ Index concurrently without disable ddl transaction
┃       priv/repo/migrations/20240125215410_my_migration.exs:6 #(MyApp.Repo.Migrations.MyMigration.change)

Safety of rolling back non-concurrent drops

excellent_migrations won't complain about things like these:

def change do
  drop_if_exists index(:my_table, [:my_column])
end

Even though rolling back such a migration will recreate the index non-concurrently.

Changing this to:

def change do
  drop_if_exists index(:my_table, [:my_column], concurrently: true)
end

Will recreate the index concurrently when rolling back.

Since dropping an index by itself will acquire an ACCESS EXCLUSIVE lock on the table, I'm thinking maybe we should enforce the dropping of indexes to be concurrent to err on the side of caution.

Curious what others think of this.

Architecture of detectors

It's a good idea to tackle complexity of finding unsafe operations – to make them easier to read, understand, maintain and develop.
There two aspects:

  • there are many checks
  • some checks are compound – they need to check conditions on multiple lines (e.g. index added concurrently and lock disabled)

Possible approach is to make AstParser return relevant parts of code (not yet detected unsafe operations). Then add another step to the process that would decide about dangers (based on that data from AstParser).
I like this approach, because it would decompose the logic into separate, independent steps with different responsibilities.

The question is: will this approach be suitable for all cases, including detecting irreversible migrations.

We've already had some discussion about it with @hiagomeels here.

Feature Request: Configurable migrations paths

Currently when called by credo it's not possible to specify the migration path because it's hard-coded in relevant_file?/2:

  def relevant_file?(path, start_after) do
    !String.starts_with?(path, ["deps/", "_build/"]) &&
      !String.contains?(path, ["/deps/", "/_build/"]) &&
      String.contains?(path, "migrations/") &&
      migration_timestamp(path) > start_after
  end

This causes an issue because I'm using data migrations (similarly to how it's spelled out here: https://fly.io/phoenix-files/backfilling-data/) that are stored in priv/data_migrations/ and I'd like excellent migrations to ignore them because I do plan to use Repo operations within them (and they have a whole host of other considerations).

FunctionClauseError

** (FunctionClauseError) no function clause matching in ExcellentMigrations.MessageGenerator.build_message/1    
    
    The following arguments were given to ExcellentMigrations.MessageGenerator.build_message/1:
    
        # 1
        :column_reference_added
    
    Attempted function clauses (showing 10 out of 14):
    
        def build_message(%{type: type, path: path, line: line})
        def build_message(:raw_sql_executed)
        def build_message(:index_not_concurrently)
        def build_message(:many_columns_index)
        def build_message(:column_added_with_default)
        def build_message(:column_removed)
        def build_message(:table_dropped)
        def build_message(:table_renamed)
        def build_message(:column_renamed)
        def build_message(:not_null_added)
        ...
        (4 clauses not shown)
    
    (excellent_migrations 0.1.1) lib/message_generator.ex:4: ExcellentMigrations.MessageGenerator.build_message/1
    (excellent_migrations 0.1.1) lib/message_generator.ex:5: ExcellentMigrations.MessageGenerator.build_message/1
    (excellent_migrations 0.1.1) lib/mix/tasks/check_safety.ex:23: anonymous fn/1 in Mix.Tasks.ExcellentMigrations.CheckSafety.run/1
    (elixir 1.13.0-rc.0) lib/enum.ex:937: Enum."-each/2-lists^foreach/1-0-"/2
    (excellent_migrations 0.1.1) lib/mix/tasks/check_safety.ex:21: Mix.Tasks.ExcellentMigrations.CheckSafety.run/1
    (mix 1.13.0-rc.0) lib/mix/task.ex:397: anonymous fn/3 in Mix.Task.run_task/3
    (mix 1.13.0-rc.0) lib/mix/cli.ex:84: Mix.CLI.run_task/2

I'm actually not sure which of my migrations triggered this, but the whole lot of them are at: https://github.com/pmarreck/mpnetwork/tree/yolo/priv/repo/migrations

Non-zero status is not returned when safety checks fail

Problem

I noticed that despite the implementation, the Mix task does not appear to return a non-zero exit status when dangers are found. Here's an example:

» mix excellent_migrations.check_safety; echo $status

09:08:12.638 [warning] Index not concurrently in priv/repo/migrations/20230103220859_testing-stuff.exs:5
0

Note: Echo'd status is 0.

Solution

I'm not certain why this is an issue, but I'm guessing that perhaps System.stop(1) isn't behaving as expected with Mix? Perhaps there's another way to return non-zero status from Mix, such as raising an error after logging dangers, https://hexdocs.pm/mix/Mix.html#raise/2.

Check is incorrect for index

based on recent change in Ecto SQL 3.9.x , it seems that it s not working correctly anymore this check. They added an advisory pg lock. per https://hexdocs.pm/ecto_sql/Ecto.Migration.html#index/3-adding-dropping-indexes-concurrently

I tried creating a migration that has a config of advisory lock but the check complained incorrectly until I added the following

@disable_migration_lock true

to my migration. This is wrong because the point of the advisory lock is to avoid removing all completely the lock?

  use Ecto.Migration

  @disable_ddl_transaction true

  def change do
    create table(:mytable) do
      add(:some_field, :uuid, null: false)

      timestamps()
    end

    create(unique_index(:mytable, [:some_field], concurrently: true))
end
I get an a credo error with "Index concurrently without disable ddl transaction" 

so then I have to fix it by added @disable_migration_lock true but based on the way it s supposed to work, I should not have to turn it off at all

I would be happy to help but not sure how this works

Avoid compilation warnings when migrating with safety_assured

Hi, thanks for the library! I'm currently integrating it into a project.

Is there a way to add a @safety_assured annotation without causing a compilation warning? Currently I get warnings like this:

$ mix ecto.migrate
warning: module attribute @safety_assured was set but never used
  priv/repo/migrations/20211105021748_not_null_fields.exs:3

Although to fix this, the approach for marking safety assured might need to be changed.

Maybe instead of:

@safety_assured [:not_null_added, :column_type_changed]

It could be written as:

ExcellentMigrations.safety_assured([:not_null_added, :column_type_changed])

Release a new version?

👋 Can you release a new version that includes the fix for proper exit codes? We've been using 0.1.6 and inadvertently letting unsafe migrations pass our build checks.

Thank you!

Non-concurrent index detection doesn't work with pipelines

The following code will not be flagged when checked against excellent_migrations. This is because the AST parser expects a different structure: https://github.com/Artur-Sulej/excellent_migrations/blob/master/lib/ast_parser.ex#L40

defmodule ExcellentMigrations.CreateIndexPipeline do
  def up do
    :dumplings |> index([:dough]) |> create()
    :dumplings |> index([:dough]) |> create_if_not_exists()
  end

  def down do
    :dumplings |> index([:dough]) |> drop()
    :dumplings |> index([:dough]) |> drop_if_exists()
  end
end

Is there a reason why we can't just look at all calls of index and unique_index whether inside create or not?

Feature request: Provide database and version to reduce false positives

If we require/allow the user to provide the name of their database and it's version then we could skip raising warnings when we know that the warning is not valid for that database and version combination. For example in the description of the "Adding a column with a default value" check we have the note:

Note: This becomes safe in:

  • Postgres 11+
  • MySQL 8.0.12+
  • MariaDB 10.3.2+

If we provided either :postgres, :mysql, or :maria_db along with the version as part of the configuration of excellent_migrations then we could skip raising these warnings in the cases when we know they're safe.

Add more example migrations & tests for them

Let's keep cooking theme in tables/columns names for consistency and fun 🧑‍🍳
I believe it's a great introductory task, if you want to contribute (but not the only one).

You can catch me on Elixir slack @Artur Sulej for a chat about details.

running excellent_migrations breaks on this migration

Here's the migration it breaks on:

https://github.com/pmarreck/mpnetwork/blob/yolo/priv/repo/migrations/20171016152948_add_additional_indexes_to_attachments_table.exs

Here's the error I see:

bash>> mix excellent_migrations.check_safety

==> excellent_migrations
Compiling 9 files (.ex)
Generated excellent_migrations app

14:07:05.637 [error] Task #PID<0.478.0> started from #PID<0.93.0> terminating
** (Protocol.UndefinedError) protocol Enumerable not implemented for :primary of type Atom
    (elixir 1.13.0-rc.0) lib/enum.ex:1: Enumerable.impl_for!/1
    (elixir 1.13.0-rc.0) lib/enum.ex:155: Enumerable.count/1
    (elixir 1.13.0-rc.0) lib/enum.ex:656: Enum.count/1
    (excellent_migrations 0.1.0) lib/parser.ex:63: ExcellentMigrations.Parser.detect_many_columns_index/1
    (excellent_migrations 0.1.0) lib/parser.ex:21: ExcellentMigrations.Parser.detect_dangers/1
    (excellent_migrations 0.1.0) lib/parser.ex:12: anonymous fn/3 in ExcellentMigrations.Parser.traverse_ast/2
    (stdlib 3.16.1) lists.erl:1358: :lists.mapfoldl/3
    (elixir 1.13.0-rc.0) lib/macro.ex:448: Macro.do_traverse/4
Function: &:erlang.apply/2
    Args: [#Function<1.59565726/1 in ExcellentMigrations.Runner.check_migrations/1>, ["priv/repo/migrations/20171016152948_add_additional_indexes_to_attachments_table.exs"]]
** (EXIT from #PID<0.93.0>) an exception was raised:
    ** (Protocol.UndefinedError) protocol Enumerable not implemented for :primary of type Atom
        (elixir 1.13.0-rc.0) lib/enum.ex:1: Enumerable.impl_for!/1
        (elixir 1.13.0-rc.0) lib/enum.ex:155: Enumerable.count/1
        (elixir 1.13.0-rc.0) lib/enum.ex:656: Enum.count/1
        (excellent_migrations 0.1.0) lib/parser.ex:63: ExcellentMigrations.Parser.detect_many_columns_index/1
        (excellent_migrations 0.1.0) lib/parser.ex:21: ExcellentMigrations.Parser.detect_dangers/1
        (excellent_migrations 0.1.0) lib/parser.ex:12: anonymous fn/3 in ExcellentMigrations.Parser.traverse_ast/2
        (stdlib 3.16.1) lists.erl:1358: :lists.mapfoldl/3
        (elixir 1.13.0-rc.0) lib/macro.ex:448: Macro.do_traverse/4

Using latest elixir and erlang, and a recent phoenix (1.5).

This is an old-ish migration so it's entirely possible that even though it runs (it does), it might be syntactically out-of-date

Incorporate "Safe Ecto Migrations" Content Into Credo Checks Where Possible

Prominent Elixir dev @dbernheisel has talked about how to do safe Ecto migrations in a couple places online:

On a recent episode of the Thinking Elixir podcast, he talks again about safe migrations (the topic starts at 7:49 and ends at 18:46).

At 15:07 another host asks if David is planning on adapting his writings/recipes as credo checks. David says he probably won't and cites excellent_migrations as a good existing tool for this (that's how I ended up here 👋).

My question: As far as y'all can tell, do the credo checks in this repo cover most of the cases that David outlines in the above series/repo? I know some of what David suggests is hyper-specific to your own application, but it'd at least be nice if the "easy stuff" was covered in the credo checks!

Thanks!

index_not_concurrently should verify that DDL transactions/migrations lock are disabled

Hello 👋

First, thanks for working on this library. It seems helpful to enforce good practices while you do data migrations. In particular, I'm interested in the index_not_concurrently check. On my quick review, it seems to detect the lack of the concurrently option in your index., but apparently, the check doesn't verify for:

@disable_ddl_transaction true
@disable_migration_lock true

Both module attributes are recommended when you add or drop an index concurrently, from the Ecto docs we have:

PostgreSQL supports adding/dropping indexes concurrently (see the docs). However, this feature does not work well with the transactions used by Ecto to guarantee integrity during migrations.

Therefore, to migrate indexes concurrently, you need to set both @disable_ddl_transaction and @disable_migration_lock to true:

Disabling DDL transactions removes the guarantee that all of the changes in the migration will happen at once. Disabling the migration lock removes the guarantee only a single node will run a given migration if multiple nodes are attempting to migrate at the same time.

Since running migrations outside a transaction and without locks can be dangerous, consider performing very few operations in migrations that add concurrent indexes. We recommend to run migrations with concurrent indexes in isolation and disable those features only temporarily.

Also, based on the last paragraph, the calls that should be allowed while you have disabled the DDL transactions and the migration lock are create/drop index(...., concurrently: true), but I don't know if that's feasible with this check.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.