├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── config └── config.exs ├── lib ├── ecto │ └── migration │ │ ├── auto.ex │ │ ├── auto │ │ ├── field.ex │ │ └── index.ex │ │ └── system_table.ex └── ecto_migrate.ex ├── mix.exs └── test ├── ecto_migrate_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | mix.lock 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | elixir: 3 | - 1.2.6 4 | - 1.3.2 5 | otp_release: 6 | - 18.3 7 | - 19.0 8 | services: mysql 9 | sudo: false 10 | script: 11 | - MIX_ENV=mysql mix test 12 | - MIX_ENV=pg mix test 13 | notifications: 14 | recipients: 15 | - elixmoon@gmail.com 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.6.3 2 | 3 | * Enhancements 4 | * add migrated? to check is a table migrated or not 5 | 6 | ## v0.6.2 7 | 8 | * Enhancements 9 | * use ecto ~> 1.0.0 10 | 11 | ## v0.6.1 12 | 13 | * Enhancements 14 | * use ecto ~> 0.16.0 15 | 16 | ## v0.6.0 17 | 18 | * Backwards incompatible changes 19 | * as ecto => 0.15.0 renames some fields in Ecto.Association.Has from `assoc` to `related` 20 | 21 | ## v0.5.0 22 | 23 | * Backwards incompatible changes 24 | * as ecto => 0.14.0 generate __schema__(:type, field) instead of __schema__(:field, field) 25 | 26 | ## v0.4.1 27 | 28 | * Enhancements 29 | * use ecto ~> 0.13.0 30 | 31 | ## v0.4.0 32 | 33 | * Enhancements 34 | * allow defining more sources for the same model 35 | 36 | * Backwards incompatible changes 37 | * use insert! update! as supported in ecto > 0.12.0, it is no more compatible with ecto < 0.12.0 38 | 39 | ## v0.3.2 40 | 41 | * Enhancements 42 | * add more space for meta information 43 | * update meta information after migration run 44 | 45 | ## v0.3.1 46 | 47 | * bump for using ecto 0.12.0-rc 48 | 49 | ## v0.3.0 50 | 51 | * Backwards incompatible changes 52 | * use BIGINT type for primitive type integer 53 | 54 | ## v0.2.1 55 | 56 | * Enhancements 57 | * Use try rescue for custom types, for the cases, where module may not be loaded(as in development) 58 | 59 | ## v0.2.0 60 | 61 | * Backwards incompatible changes 62 | * types will be saved in native form, that changes from Ecto types doesn't try run migration 63 | 64 | ## v0.1.1 65 | 66 | * Enhancements 67 | * Do not use `@derive [Access]` for structs 68 | 69 | * Bug fixes 70 | * Allow setting of options for `belongs_to`-defined fields too 71 | 72 | ## v0.1.0 73 | 74 | * Initial release 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Travelping 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Ecto Migrate [![Build Status](https://travis-ci.org/xerions/ecto_migrate.svg)](https://travis-ci.org/xerions/ecto_migrate) 2 | ============ 3 | 4 | Ecto migrate brings automatic migrations to ecto. Instead of defining and writting manuall diffing 5 | from actual model and old model. The `ecto_migrate` do it for you. It save actual represantation of 6 | a model model in database and checks, if actual model have the same format as saved in database. 7 | 8 | To test, use EctoIt (is depended on it for tests purposes): 9 | 10 | ``` 11 | iex -S mix 12 | ``` 13 | 14 | After, it should be possible: 15 | 16 | ```elixir 17 | :application.start(:ecto_it) 18 | alias EctoIt.Repo 19 | 20 | import Ecto.Query 21 | 22 | defmodule Weather do # is for later at now 23 | use Ecto.Model 24 | 25 | schema "weather" do 26 | field :city 27 | field :temp_lo, :integer 28 | field :temp_hi, :integer 29 | field :prcp, :float, default: 0.0 30 | end 31 | end 32 | 33 | Ecto.Migration.Auto.migrate(Repo, Weather) 34 | 35 | %Weather{city: "Berlin", temp_lo: 20, temp_hi: 25} |> Repo.insert 36 | Repo.all(from w in Weather, where: w.city == "Berlin") 37 | 38 | ``` 39 | 40 | Lets redefine the same model in a shell and migrate it 41 | 42 | ```elixir 43 | 44 | defmodule Weather do # is for later at now 45 | use Ecto.Model 46 | 47 | schema "weather" do 48 | field :city 49 | field :temp_lo, :integer 50 | field :temp_hi, :integer 51 | field :prcp, :float, default: 0.0 52 | field :wind, :float, default: 0.0 53 | end 54 | end 55 | 56 | Ecto.Migration.Auto.migrate(Repo, Weather) 57 | Repo.all(from w in Weather, where: w.city == "Berlin") 58 | 59 | ``` 60 | 61 | Lets use references 62 | 63 | ```elixir 64 | 65 | defmodule Post do 66 | use Ecto.Model 67 | 68 | schema "posts" do 69 | field :title, :string 70 | field :public, :boolean, default: true 71 | field :visits, :integer 72 | has_many :comments, Comment 73 | end 74 | end 75 | 76 | defmodule Comment do 77 | use Ecto.Model 78 | 79 | schema "comments" do 80 | field :text, :string 81 | belongs_to :post, Post 82 | end 83 | end 84 | 85 | Ecto.Migration.Auto.migrate(Repo, Post) 86 | Ecto.Migration.Auto.migrate(Repo, Comment) 87 | 88 | ``` 89 | 90 | `ecto_migrate` also provides additional `migrate/3` API. For using with custom source defined models. Example: 91 | 92 | ```elixir 93 | defmodule Taggable do 94 | use Ecto.Model 95 | 96 | schema "this is not a valid schema name and it will never be used" do 97 | field :tag_id, :integer 98 | end 99 | end 100 | 101 | defmodule MyModel do 102 | use Ecto.Model 103 | schema "my_model" do 104 | field :a, :string 105 | has_many :my_model_tags, {"my_model_tags", Taggable}, [foreign_key: :tag_id] 106 | end 107 | end 108 | ``` 109 | 110 | Now we can migrate `my_model_tags` table with: 111 | 112 | ```elixir 113 | Ecto.Migration.Auto.migrate(Repo, MyModel) 114 | Ecto.Migration.Auto.migrate(Repo, Taggable, [for: MyModel]) 115 | ``` 116 | 117 | It will generate and migrate `my_model_tags` table to the database which will be associated with `my_model` table. 118 | 119 | Indexes 120 | ------- 121 | 122 | `ecto_migrate` has support of indexes: 123 | 124 | ```elixir 125 | defmodule Weather do # is for later at now 126 | use Ecto.Model 127 | use Ecto.Migration.Auto.Index 128 | 129 | index(:city, unique: true) 130 | index(:prcp) 131 | schema "weather" do 132 | field :city 133 | field :temp_lo, :integer 134 | field :temp_hi, :integer 135 | field :prcp, :float, default: 0.0 136 | end 137 | end 138 | ``` 139 | 140 | If you do not want to use DSL for defining indexes, macro index doing no more, as generate function: 141 | 142 | ```elixir 143 | defmodule Weather do # is for later at now 144 | use Ecto.Model 145 | 146 | schema "weather" do 147 | field :city 148 | field :temp_lo, :integer 149 | field :temp_hi, :integer 150 | field :prcp, :float, default: 0.0 151 | end 152 | 153 | def __indexes__ do 154 | [{[:city], [unique: true]}, 155 | {[:prpc], []}] 156 | end 157 | end 158 | ``` 159 | 160 | Extra attribute options 161 | ----------------------- 162 | 163 | ```elixir 164 | defmodule Weather do # is for later at now 165 | use Ecto.Model 166 | use Ecto.Migration.Auto.Index 167 | 168 | schema "weather" do 169 | field :city 170 | field :temp_lo, :integer 171 | field :temp_hi, :integer 172 | field :prcp, :float, default: 0.0 173 | end 174 | 175 | def __attribute_option__(:city), do: [size: 40] 176 | def __attribute_option__(_), do: [] 177 | end 178 | ``` 179 | 180 | Possibility to have more sources 181 | -------------------------------- 182 | 183 | If the same model used by different sources, it is possible to define callback for it 184 | 185 | ```elixir 186 | defmodule Weather do # is for later at now 187 | use Ecto.Model 188 | use Ecto.Migration.Auto.Index 189 | 190 | schema "weather" do 191 | field :city 192 | field :temp_lo, :integer 193 | field :temp_hi, :integer 194 | field :prcp, :float, default: 0.0 195 | end 196 | 197 | def __sources__, do: ["weather", "history_weather"] 198 | end 199 | ``` 200 | 201 | Upgrades in 0.3.x versions 202 | -------------------------- 203 | 204 | If you have installed version before 0.3.2, use 0.3.2 or 0.3.3 for upgrading the table, after that it is possible to 205 | upgrade higher versions. 206 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for third- 9 | # party users, it should be done in your mix.exs file. 10 | 11 | # Sample configuration: 12 | # 13 | # config :logger, 14 | # level: :info 15 | # 16 | # config :logger, :console, 17 | # format: "$date $time [$level] $metadata$message\n", 18 | # metadata: [:user_id] 19 | 20 | # It is also possible to import configuration files, relative to this 21 | # directory. For example, you can emulate configuration per environment 22 | # by uncommenting the line below and defining dev.exs, test.exs and such. 23 | # Configuration from the imported file will override the ones defined 24 | # here (which is why it is important to import them last). 25 | # 26 | # import_config "#{Mix.env}.exs" 27 | -------------------------------------------------------------------------------- /lib/ecto/migration/auto.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Migration.Auto do 2 | @moduledoc """ 3 | This module provide function for doing automigration of models. 4 | 5 | ## Examples 6 | 7 | ### Configuration for Repo, only for iex try taste, please use supervisor in your application 8 | 9 | :application.set_env(:example, Repo, [adapter: Ecto.Adapters.MySQL, database: "example", username: "root"]) 10 | defmodule Repo, do: (use Ecto.Repo, otp_app: :example) 11 | Repo.start_link 12 | 13 | ### The same example for postgres 14 | 15 | :application.set_env(:example, Repo, [adapter: Ecto.Adapters.Postgres, database: "example", username: "postgres"]) 16 | defmodule Repo, do: (use Ecto.Repo, otp_app: :example) 17 | Repo.start_link 18 | 19 | ### Usage 20 | 21 | import Ecto.Query 22 | 23 | defmodule Weather do # is for later at now 24 | use Ecto.Model 25 | 26 | schema "weather" do 27 | field :city 28 | field :temp_lo, :integer 29 | field :temp_hi, :integer 30 | field :prcp, :float, default: 0.0 31 | end 32 | end 33 | 34 | Ecto.Migration.Auto.migrate(Repo, Weather) 35 | 36 | %Weather{city: "Berlin", temp_lo: 20, temp_hi: 25} |> Repo.insert 37 | Repo.all(from w in Weather, where: w.city == "Berlin") 38 | 39 | """ 40 | import Ecto.Query 41 | alias Ecto.Migration.Auto.Index 42 | alias Ecto.Migration.Auto.Field 43 | alias Ecto.Migration.SystemTable 44 | 45 | @doc """ 46 | Runs an up migration on the given repository for the given model. 47 | 48 | ## Options 49 | 50 | * :for - used for models, which are used as custom source 51 | 52 | ## Examples 53 | 54 | iex> Ecto.Migration.Auto.migrate(Repo, Model) 55 | 56 | iex> Ecto.Migration.Auto.migrate(Repo, Tag, for: Model) 57 | """ 58 | def migrate(repo, module, opts \\ []) do 59 | ensure_exists(repo) 60 | for tablename <- sources(module) do 61 | {related_field, tablename} = get_tablename(module, tablename, opts) 62 | tableatom = tablename |> String.to_atom 63 | for_opts = {related_field, opts[:for]} 64 | 65 | {fields_changes, relateds} = repo.get(SystemTable, tablename) |> Field.check(tableatom, module, for_opts) 66 | index_changes = (from s in SystemTable.Index, where: ^tablename == s.tablename) |> repo.all |> Index.check(tableatom, module) 67 | 68 | if migration_module = check_gen(tableatom, module, fields_changes, index_changes, opts) do 69 | Ecto.Migrator.up(repo, random, migration_module) 70 | Field.update_meta(repo, module, tablename, relateds) # May be in transaction? 71 | Index.update_meta(repo, module, tablename, index_changes) 72 | end 73 | end 74 | end 75 | 76 | def sources(module) do 77 | tablename = module.__schema__(:source) 78 | case function_exported?(module, :__sources__, 0) do 79 | true -> module.__sources__() 80 | false -> [tablename] 81 | end 82 | end 83 | 84 | def migrated?(repo, model) do 85 | tablename = model.__schema__(:source) 86 | try do 87 | query = from t in Ecto.Migration.SystemTable, select: t, where: t.tablename == ^tablename 88 | case repo.all(query) do 89 | [] -> false 90 | [%Ecto.Migration.SystemTable{tablename: _tablename}] -> true 91 | end 92 | catch _, _ -> 93 | false 94 | end 95 | end 96 | 97 | defp get_tablename(_module, tablename, []) do 98 | {nil, tablename} 99 | end 100 | defp get_tablename(module, _, [for: mod]) do 101 | %Ecto.Association.Has{related_key: related_key, queryable: {tablename, _}} = find_related_field(module, mod) 102 | {related_key, tablename} 103 | end 104 | 105 | defp find_related_field(module, mod) do 106 | (mod.__schema__(:associations) 107 | |> Stream.map(&mod.__schema__(:association, &1)) 108 | |> Enum.find(&related_mod?(&1, module))) || raise(ArgumentError, message: "association in #{m2s(mod)} for #{m2s(module)} not found") 109 | end 110 | 111 | defp related_mod?(%Ecto.Association.Has{related: mod, queryable: {_, _}}, mod), do: true 112 | defp related_mod?(_, _), do: false 113 | 114 | defp check_gen(_tablename, _module, {false, []}, {[], []}, _opts), do: nil 115 | defp check_gen(tablename, module, {create?, changed_fields}, {create_indexes, delete_indexes}, opts) do 116 | migration_module = migration_module(module, opts) 117 | up = gen_up(module, tablename, create?, changed_fields, create_indexes, delete_indexes) 118 | quote do 119 | defmodule unquote(migration_module) do 120 | use Ecto.Migration 121 | def up do 122 | unquote(up) 123 | end 124 | def down do 125 | drop table(unquote tablename) 126 | end 127 | end 128 | end |> Code.eval_quoted 129 | migration_module 130 | end 131 | 132 | defp gen_up(module, tablename, create?, changed_fields, create_indexes, delete_indexes) do 133 | up_table = gen_up_table(module, tablename, create?, changed_fields) 134 | up_indexes = gen_up_indexes(create_indexes, delete_indexes) 135 | quote do 136 | unquote(up_table) 137 | unquote(up_indexes) 138 | end 139 | end 140 | 141 | defp gen_up_table(module, tablename, true, changed_fields) do 142 | key? = module.__schema__(:primary_key) == [:id] 143 | quote do 144 | create table(unquote(tablename), primary_key: unquote(key?)) do 145 | unquote(changed_fields) 146 | end 147 | end 148 | end 149 | 150 | defp gen_up_table(_module, tablename, false, changed_fields) do 151 | quote do 152 | alter table(unquote(tablename)) do 153 | unquote(changed_fields) 154 | end 155 | end 156 | end 157 | 158 | defp gen_up_indexes(create_indexes, delete_indexes) do 159 | quote do 160 | unquote(delete_indexes) 161 | unquote(create_indexes) 162 | end 163 | end 164 | 165 | @migration_tables [{SystemTable.Index, SystemTable.Index.Migration}, {SystemTable, SystemTable.Migration}] 166 | defp ensure_exists(repo) do 167 | for {model, migration} <- @migration_tables do 168 | ensure_exists(repo, model, migration) 169 | end 170 | end 171 | 172 | defp ensure_exists(repo, model, migration) do 173 | try do 174 | repo.get(model, "test") 175 | catch 176 | _, _ -> 177 | Ecto.Migrator.up(repo, random, migration) 178 | end 179 | end 180 | 181 | defp random, do: :crypto.rand_uniform(0, 1099511627775) 182 | 183 | defp migration_module(module, []), do: Module.concat(module, Migration) 184 | defp migration_module(module, [for: mod]), do: Module.concat([module, mod, Migration]) 185 | 186 | defp m2s(mod) do 187 | case to_string(mod) do 188 | "Elixir." <> modstring -> modstring 189 | modstring -> modstring 190 | end 191 | end 192 | end 193 | -------------------------------------------------------------------------------- /lib/ecto/migration/auto/field.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Migration.Auto.Field do 2 | alias Ecto.Migration.SystemTable 3 | 4 | @doc """ 5 | Update meta information in repository 6 | """ 7 | def update_meta(repo, module, tablename, relateds) do 8 | metainfo = module.__schema__(:fields) |> Stream.map(&field_to_meta(&1, module, relateds)) |> Enum.join(",") 9 | updated_info = %SystemTable{tablename: tablename, metainfo: metainfo} 10 | if repo.get(SystemTable, tablename) do 11 | repo.update!(updated_info) 12 | else 13 | repo.insert!(updated_info) 14 | end 15 | end 16 | 17 | defp field_to_meta(field, module, relateds) do 18 | case List.keyfind(relateds, field, 0) do 19 | nil -> 20 | (field |> Atom.to_string) <> ":" <> (module.__schema__(:type, field) |> type |> Atom.to_string) 21 | {_, _, related_table} -> 22 | (field |> Atom.to_string) <> ":" <> (related_table |> Atom.to_string) 23 | end 24 | end 25 | 26 | @doc """ 27 | Check database fields with actual models fields and gives 28 | """ 29 | def check(old_fields, _tablename, module, for_opts) do 30 | relateds = associations(module) 31 | metainfo = old_keys(old_fields) 32 | new_fields = module.__schema__(:fields) 33 | 34 | add_fields = add(module, new_fields, metainfo, relateds, for_opts) 35 | remove_fields = remove(new_fields, metainfo) 36 | 37 | {{old_fields == nil, remove_fields ++ add_fields}, relateds} 38 | end 39 | 40 | defp old_keys(nil), do: [] 41 | defp old_keys(%{metainfo: fields_string}) do 42 | fields_string 43 | |> String.split(",") 44 | |> Stream.map(&String.split(&1, ":")) 45 | |> Enum.map(fn(field_type) -> field_type |> Enum.map(&String.to_atom/1) |> List.to_tuple end) 46 | end 47 | 48 | defp associations(module) do 49 | module.__schema__(:associations) |> Enum.flat_map(fn(association) -> 50 | case module.__schema__(:association, association) do 51 | %Ecto.Association.BelongsTo{owner_key: field, related: related_module} -> 52 | [{field, related_module, related_module.__schema__(:source) |> String.to_atom}] 53 | _ -> 54 | [] 55 | end 56 | end) 57 | end 58 | 59 | defp add(module, all_fields, fields_in_db, relateds, {related_key, related_mod}) when is_list(all_fields) do 60 | for name <- all_fields, name != :id do 61 | case List.keyfind(relateds, name, 0) do 62 | nil -> 63 | unless name == related_key do 64 | type = type(module.__schema__(:type, name)) 65 | opts = get_attribute_opts(module, name) 66 | add(name, type, fields_in_db, quote do: [unquote(type), unquote(opts)]) 67 | else 68 | association_table = related_mod.__schema__(:source) |> String.to_atom 69 | add(name, association_table, fields_in_db, quote do: [Ecto.Migration.references(unquote(association_table))]) 70 | end 71 | {related_field_name, _mod, association_table} -> 72 | opts = get_attribute_opts(module, related_field_name) 73 | add(name, association_table, fields_in_db, quote do: [Ecto.Migration.references(unquote(association_table)), unquote(opts)]) 74 | end 75 | end |> List.flatten 76 | end 77 | 78 | defp add(name, type, fields_in_db, quoted_type) do 79 | case List.keyfind(fields_in_db, name, 0) do 80 | nil -> 81 | quote do: add(unquote(name), unquote_splicing(quoted_type)) 82 | {_maybe_new_name, new_type} -> 83 | unless new_type == type do 84 | quote do: modify(unquote(name), unquote_splicing(quoted_type)) 85 | else 86 | [] 87 | end 88 | end 89 | end 90 | 91 | defp remove(all_fields, fields_in_db) do 92 | fields_in_db 93 | |> Stream.filter(fn({name, _}) -> not Enum.member?(all_fields, name) end) 94 | |> Enum.map(fn({name, _}) -> quote do: remove(unquote(name)) end) 95 | end 96 | 97 | def type(data) do 98 | try do 99 | data.type() 100 | rescue 101 | UndefinedFunctionError -> better_db_type(data) 102 | end 103 | end 104 | 105 | defp better_db_type(:integer), do: :BIGINT 106 | defp better_db_type(type), do: type 107 | 108 | defp get_attribute_opts(module, name) do 109 | case function_exported?(module, :__attribute_option__, 1) do 110 | true -> module.__attribute_option__(name) 111 | _ -> [] 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/ecto/migration/auto/index.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Migration.Auto.Index do 2 | import Ecto.Query 3 | alias Ecto.Migration.SystemTable.Index 4 | 5 | defmacro __using__(_opts) do 6 | quote do 7 | import Ecto.Migration.Auto.Index 8 | @indexes [] 9 | @before_compile Ecto.Migration.Auto.Index 10 | end 11 | end 12 | 13 | defmacro index(columns, opts \\ []) do 14 | quote do 15 | @indexes [{unquote(List.wrap(columns)), unquote(opts)} | @indexes] 16 | end 17 | end 18 | 19 | defmacro __before_compile__(_env) do 20 | quote do 21 | def __indexes__ do 22 | @indexes 23 | end 24 | end 25 | end 26 | 27 | @doc """ 28 | Update meta information in repository 29 | """ 30 | def update_meta(_repo, _module, _tablename, {[], []}), do: nil 31 | def update_meta(repo, module, tablename, _) do 32 | (from s in Index, where: s.tablename == ^tablename) |> repo.delete_all 33 | for {columns, opts} <- all(module) do 34 | repo.insert!(Map.merge(%Index{tablename: tablename, index: Enum.join(columns, ",")}, :maps.from_list(opts))) 35 | end 36 | end 37 | 38 | @doc """ 39 | Check database indexes with actual models indexes and gives the difference 40 | """ 41 | def check(old_indexes, tablename, module) do 42 | new_indexes = all(module) |> Enum.map(&merge_default/1) 43 | old_indexes = old_indexes |> Enum.map(&transform_from_db/1) 44 | 45 | create_indexes = (new_indexes -- old_indexes) |> create(tablename) 46 | delete_indexes = (old_indexes -- new_indexes) |> delete(tablename) 47 | 48 | {create_indexes, delete_indexes} 49 | end 50 | 51 | defp merge_default({index, opts}) do 52 | {index, Keyword.merge(default_index_opts, opts) |> List.keysort(0)} 53 | end 54 | 55 | defp transform_from_db(index) do 56 | columns = String.split(index.index, ",") |> Enum.map(&String.to_atom(&1)) 57 | {columns, get_opts(index)} 58 | end 59 | 60 | defp create(create_indexes, tablename) do 61 | quoted_indexes(create_indexes, tablename) |> Enum.map(fn(index) -> (quote do: create unquote(index)) end) 62 | end 63 | 64 | defp delete(delete_indexes, tablename) do 65 | quoted_indexes(delete_indexes, tablename) |> Enum.map(fn(index) -> (quote do: drop unquote(index)) end) 66 | end 67 | 68 | defp quoted_indexes(indexes, tablename) do 69 | for {fields, opts} <- indexes do 70 | quote do: index(unquote(tablename), unquote(fields), unquote(opts)) 71 | end 72 | end 73 | 74 | def all(module) do 75 | case :erlang.function_exported(module, :__indexes__, 0) do 76 | true -> 77 | module.__indexes__() 78 | _ -> 79 | [] 80 | end 81 | end 82 | 83 | defp get_opts(index) do 84 | # in sort order 85 | [{:concurrently, index.concurrently}, {:name, index.name}, {:unique, index.unique}, {:using, index.using}] 86 | end 87 | 88 | defp default_index_opts do 89 | [concurrently: nil, name: nil, unique: nil, using: nil] 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/ecto/migration/system_table.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.Migration.SystemTable.Index.Migration do 2 | use Ecto.Migration 3 | def up do 4 | create table(:ecto_auto_migration_index) do 5 | add :tablename, :string 6 | add :index, :string 7 | add :name, :string 8 | add :concurrently, :string 9 | add :unique, :boolean 10 | add :using, :string 11 | end 12 | end 13 | end 14 | 15 | defmodule Ecto.Migration.SystemTable.Index do 16 | use Ecto.Model 17 | @primary_key {:tablename, :string, []} 18 | schema "ecto_auto_migration_index" do 19 | field :index, :string 20 | field :name, :string 21 | field :concurrently, :boolean 22 | field :unique, :boolean 23 | field :using, :string 24 | end 25 | end 26 | 27 | defmodule Ecto.Migration.SystemTable.Migration do 28 | use Ecto.Migration 29 | def up do 30 | create table(:ecto_auto_migration) do 31 | add :tablename, :string 32 | add :metainfo, :string, size: 2040 33 | end 34 | end 35 | end 36 | 37 | defmodule Ecto.Migration.SystemTable do 38 | use Ecto.Model 39 | @primary_key {:tablename, :string, []} 40 | schema "ecto_auto_migration" do 41 | field :metainfo, :string 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/ecto_migrate.ex: -------------------------------------------------------------------------------- 1 | defmodule EctoMigrate do 2 | end 3 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule EctoMigrate.Mixfile do 2 | use Mix.Project 3 | @version "0.6.3" 4 | @github "https://github.com/xerions/ecto_migrate" 5 | 6 | def project do 7 | [app: :ecto_migrate, 8 | version: @version, 9 | 10 | description: description, 11 | package: package, 12 | 13 | # Docs 14 | name: "Ecto Auto Migrate", 15 | docs: [source_ref: "v#{@version}", 16 | source_url: @github], 17 | deps: deps] 18 | end 19 | 20 | defp description do 21 | """ 22 | Ecto auto migration library. It allows to generate and run migrations for initial 23 | and update migrations. 24 | """ 25 | end 26 | 27 | defp package do 28 | [maintainers: ["Dmitry Russ(Aleksandrov)", "Yury Gargay"], 29 | licenses: ["Apache 2.0"], 30 | links: %{"GitHub" => @github}] 31 | end 32 | 33 | # Configuration for the OTP application 34 | # 35 | # Type `mix help compile.app` for more information 36 | def application do 37 | [applications: [:logger]] 38 | end 39 | 40 | # Dependencies can be Hex packages: 41 | # 42 | # {:mydep, "~> 0.3.0"} 43 | # 44 | # Or git/path repositories: 45 | # 46 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 47 | # 48 | # Type `mix help deps` for more examples and options 49 | defp deps do 50 | [{:postgrex, ">= 0.0.0", optional: true}, 51 | {:mariaex, ">= 0.0.0", optional: true}, 52 | {:ecto, "~> 1.0.0"}, 53 | {:ecto_it, "~> 0.2.0", optional: true}] 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/ecto_migrate_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TestModel do 2 | use Ecto.Schema 3 | use Ecto.Migration.Auto.Index 4 | 5 | index(:l, using: "hash") 6 | index(:f) 7 | schema "ecto_migrate_test_table" do 8 | field :f, :string 9 | field :i, :integer 10 | field :l, :boolean 11 | end 12 | end 13 | 14 | defmodule Ecto.Taggable do 15 | use Ecto.Model 16 | use Ecto.Migration.Auto.Index 17 | 18 | index(:tag_id, using: "hash") 19 | schema "this is not a valid schema name and it will never be used" do 20 | field :name, :string 21 | field :model, :string 22 | field :tag_id, :integer 23 | end 24 | end 25 | 26 | defmodule MyModel do 27 | use Ecto.Model 28 | schema "my_model" do 29 | field :a, :string 30 | field :b, :integer 31 | field :c, Ecto.DateTime 32 | has_many :my_model_tags, {"my_model_tags", Ecto.Taggable}, [foreign_key: :tag_id] 33 | end 34 | 35 | def __sources__, do: ["my_model", "my_model_2"] 36 | 37 | end 38 | 39 | defmodule EctoMigrateTest do 40 | use ExUnit.Case 41 | import Ecto.Query 42 | alias EctoIt.Repo 43 | 44 | setup do 45 | :ok = :application.start(:ecto_it) 46 | on_exit fn -> :application.stop(:ecto_it) end 47 | end 48 | 49 | test "ecto_migrate with tags test" do 50 | Ecto.Migration.Auto.migrate(EctoIt.Repo, TestModel) 51 | query = from t in Ecto.Migration.SystemTable, select: t 52 | [result] = Repo.all(query) 53 | assert result.metainfo == "id:id,f:string,i:BIGINT,l:boolean" 54 | assert result.tablename == "ecto_migrate_test_table" 55 | 56 | Ecto.Migration.Auto.migrate(Repo, MyModel) 57 | Ecto.Migration.Auto.migrate(Repo, Ecto.Taggable, [for: MyModel]) 58 | 59 | Repo.insert!(%MyModel{a: "foo"}) 60 | Repo.insert!(%MyModel{a: "bar"}) 61 | %MyModel{a: "foo"} |> Ecto.Model.put_source("my_model_2") |> Repo.insert! 62 | 63 | model = %MyModel{} 64 | new_tag = Ecto.Model.build(model, :my_model_tags) 65 | new_tag = %{new_tag | tag_id: 2, name: "test_tag", model: MyModel |> to_string} 66 | EctoIt.Repo.insert!(new_tag) 67 | 68 | query = from c in MyModel, where: c.id == 2, preload: [:my_model_tags] 69 | [result] = EctoIt.Repo.all(query) 70 | [tags] = result.my_model_tags 71 | 72 | assert tags.id == 1 73 | assert tags.model == "Elixir.MyModel" 74 | assert tags.name == "test_tag" 75 | 76 | assert true == Ecto.Migration.Auto.migrated?(Repo, TestModel) 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------