Packages

Mix task to generate Ecto migrations from SQL schema file

Current section

Files

Jump to
ecto_extract_migrations lib ecto_extract_migrations.ex
Raw

lib/ecto_extract_migrations.ex

defmodule EctoExtractMigrations do
@moduledoc """
The main entry point is lib/mix/tasks/ecto_extract_migrations.ex.
This module mainly has common library functions.
"""
@app :ecto_extract_migrations
require EEx
@template_execute_sql """
execute(
\"\"\"
<%= Regex.replace(~r/^/m, sql, " ") %>
\"\"\")
"""
@template_execute_sql_updown """
execute(
\"\"\"
<%= Regex.replace(~r/^/m, up_sql, " ") %>
\"\"\",
\"\"\"
<%= Regex.replace(~r/^/m, down_sql, " ") %>
\"\"\"
)
"""
# defmodule ParseError do
# defexception message: "default message"
# end
@doc "Parse line from SQL file (Stream.transform function)"
@spec parse({binary, integer}, nil | {module, binary}) :: {list, nil | {module, binary}}
# First line of SQL statement
def parse({line, line_num}, nil) do
module_parse({line, line_num})
end
# In multi-line SQL statement
def parse({line, line_num}, {module, state}) do
# Mix.shell().info("#{line_num}> #{line} #{inspect state}")
case module.parse(line, state) do
{:ok, value} ->
# Parsing succeeded
sql = state.sql <> line
{[%{module: module, type: module.type(), line_num: line_num, sql: sql, data: value}], nil}
{:continue, new_state} ->
# Keep reading lines
# This assumes that we will ultimately succeed, probably overly optimistic.
# The alternative is to stop when e.g. we hit a line ending with ";"
{[], {module, new_state}}
{:error, _reason} ->
{[], nil}
end
end
@spec module_parse({binary, integer}) :: {list, nil | {module, map}}
def module_parse(value) do
modules = [
EctoExtractMigrations.Commands.Whitespace,
EctoExtractMigrations.Commands.Comment,
EctoExtractMigrations.Commands.CreateExtension,
EctoExtractMigrations.Commands.CreateSchema,
EctoExtractMigrations.Commands.CreateIndex,
EctoExtractMigrations.Commands.CreateTrigger,
EctoExtractMigrations.Commands.CreateFunction,
EctoExtractMigrations.Commands.AlterTable,
EctoExtractMigrations.Commands.AlterSequence,
EctoExtractMigrations.Commands.CreateTable,
EctoExtractMigrations.Commands.CreateSequence,
EctoExtractMigrations.Commands.CreateType,
EctoExtractMigrations.Commands.CreateView,
]
module_parse(value, modules)
end
@spec module_parse({binary, integer}, list(module)) :: {list, nil | {module, map}}
def module_parse({line, line_num}, []) do
# No parser matched line
Mix.shell().info("UNKNOWN #{line_num}> #{String.trim_trailing(line)}")
{[], nil}
end
def module_parse({line, line_num}, [module | rest]) do
case module.match(line) do
{:ok, value} ->
# Parsing succeeded
{[%{module: module, type: module.type(), line_num: line_num, sql: line, data: value}], nil}
{:continue, state} ->
# Matched multi-line statement, keep going
{[], {module, state}}
{:error, _reason} ->
# Try next parser
module_parse({line, line_num}, rest)
end
end
EEx.function_from_string(:def, :eval_template_execute_sql, @template_execute_sql, [:sql])
EEx.function_from_string(:def, :eval_template_execute_sql, @template_execute_sql_updown, [:up_sql, :down_sql])
@doc "Expand template file to path under priv/templates"
@spec template_path(Path.t()) :: Path.t()
def template_path(file) do
template_dir = Application.app_dir(@app, ["priv", "templates"])
Path.join(template_dir, file)
end
@doc "Evaluate template file with bindings"
@spec eval_template_file(Path.t(), Keyword.t()) :: {:ok, binary} | {:error, term}
def eval_template_file(template_file, bindings \\ []) do
path = template_path(template_file)
{:ok, EEx.eval_file(path, bindings, trim: true)}
rescue
e ->
{:error, {:template, e}}
end
@doc "Evaluate template file with bindings"
@spec eval_template(Path.t(), Keyword.t()) :: {:ok, binary} | {:error, term}
def eval_template(template_file, bindings \\ []) do
{:ok, EEx.eval_file(template_file, bindings, trim: true)}
rescue
e ->
{:error, {:template, e}}
end
@doc "Convert SQL name to Elixir module name"
@spec sql_name_to_module(binary | list(binary)) :: binary
def sql_name_to_module(name) when is_binary(name), do: Macro.camelize(name)
def sql_name_to_module(["public", name]), do: Macro.camelize(name)
def sql_name_to_module([schema, name]) do
"#{Macro.camelize(schema)}.#{Macro.camelize(name)}"
end
@doc "Convert schema qualified name in list format to binary"
@spec object_name(binary | list(binary)) :: binary
def object_name(name) when is_binary(name), do: name
def object_name(["public", name]), do: name
def object_name([schema, name]), do: "#{schema}.#{name}"
@doc "Convert NimbleParsec result tuple into simple ok/error tuple"
@spec parsec_result(tuple) :: {:ok, term} | {:error, term}
def parsec_result(result) do
case result do
{:ok, [acc], "", _, _line, _offset} ->
{:ok, acc}
{:ok, _, rest, _, _line, _offset} ->
{:error, "could not parse: " <> rest}
{:error, reason, _rest, _context, _line, _offset} ->
{:error, reason}
end
end
end