Packages

Generates typed Elixir query modules from plain SQL files using Postgres inference or static metadata.

Current section

Files

Jump to
squirr_elix lib squirrelix mix_task.ex
Raw

lib/squirrelix/mix_task.ex

defmodule Squirrelix.MixTask do
@moduledoc false
alias Squirrelix.CLI
alias Squirrelix.CodegenCheckSummary
alias Squirrelix.CodegenSummary
alias Squirrelix.Error
alias Squirrelix.Metadata
alias Squirrelix.Postgres
@switches [
metadata: :string,
infer: :boolean,
url: :string,
database: :string,
hostname: :string,
username: :string,
password: :string,
port: :integer
]
@spec generate([String.t()]) :: :ok
def generate(args) do
root = File.cwd!()
opts = parse_args!(args)
with_query_source!(opts, root, fn query_source ->
root
|> Squirrelix.generate(query_source, version: version())
|> report_generate_summary()
end)
end
@spec check([String.t()]) :: :ok
def check(args) do
root = File.cwd!()
opts = parse_args!(args)
with_query_source!(opts, root, fn query_source ->
root
|> Squirrelix.check(query_source, version: version())
|> report_check_summary()
end)
end
defp with_query_source!(opts, root, callback) do
if opts[:infer] do
with_postgres_inferrer!(opts, callback)
else
callback.(load_metadata!(opts, root))
end
end
defp with_postgres_inferrer!(opts, callback) do
{:ok, _} = Application.ensure_all_started(:postgrex)
connection_options = build_connection_options(opts)
case Postgres.connect(connection_options) do
{:ok, conn} ->
try do
callback.(Postgres.inferrer(conn))
after
GenServer.stop(conn)
end
{:error, error} ->
Mix.raise("Squirrelix connection failed:\n\n#{Error.format(error)}")
end
end
# Precedence (highest first): flags → --url → DATABASE_URL → PG* → defaults.
defp build_connection_options(opts) do
case CLI.resolve_connection_options(System.get_env(), opts) do
{:ok, connection_options} -> connection_options
{:error, :invalid_url} -> Mix.raise("Invalid Postgres connection URL")
end
end
defp load_metadata!(opts, root) do
metadata_file = opts |> Keyword.get(:metadata, "squirr_elix.exs") |> Path.expand(root)
case Metadata.from_file(metadata_file, root: root) do
{:ok, metadata} ->
metadata
{:error, error} ->
Mix.raise("Could not load Squirrelix metadata:\n\n#{Error.format(error)}")
end
end
defp parse_args!(args) do
case OptionParser.parse(args, strict: @switches) do
{opts, [], []} -> opts
{_opts, extra, []} -> Mix.raise("Unexpected arguments: #{Enum.join(extra, " ")}")
{_opts, _extra, invalid} -> Mix.raise("Invalid options: #{inspect(invalid)}")
end
end
defp report_generate_summary(%CodegenSummary{status: :ok, generated_count: count}) do
Mix.shell().info("Generated #{count} #{pluralize(count, "query", "queries")}.")
:ok
end
defp report_generate_summary(%CodegenSummary{status: :empty}) do
Mix.shell().info("No SQL queries found.")
:ok
end
defp report_generate_summary(%CodegenSummary{errors: errors}) do
Mix.raise("Squirrelix generation failed:\n\n#{format_codegen_errors(errors)}")
end
defp report_check_summary(%CodegenCheckSummary{status: :ok, checked_count: count}) do
Mix.shell().info("All #{count} #{pluralize(count, "query", "queries")} current.")
:ok
end
defp report_check_summary(%CodegenCheckSummary{status: :empty}) do
Mix.shell().info("No SQL queries found.")
:ok
end
defp report_check_summary(%CodegenCheckSummary{errors: errors}) do
Mix.raise("Squirrelix check failed:\n\n#{format_codegen_errors(errors)}")
end
defp format_codegen_errors(errors) do
Enum.map_join(errors, "\n\n", fn {directory, error} ->
"#{directory}\n#{format_directory_error(error)}"
end)
end
defp format_directory_error(errors) when is_list(errors), do: Error.format_all(errors)
defp format_directory_error(%_{} = error), do: Error.format(error)
defp format_directory_error(other), do: inspect(other)
defp pluralize(1, singular, _plural), do: singular
defp pluralize(_count, _singular, plural), do: plural
defp version do
Application.spec(:squirr_elix, :vsn)
|> to_string()
end
end