defmodule Squirrelix.Error.CannotReadFile do @moduledoc """ Error returned when a file cannot be read. """ @enforce_keys [:file, :reason] defstruct [:file, :reason] @type t :: %__MODULE__{file: String.t(), reason: File.posix()} end defmodule Squirrelix.Error.QueryFileHasInvalidName do @moduledoc """ Error returned when a SQL filename cannot become an Elixir function name. """ @enforce_keys [:file, :reason] defstruct [:file, :reason, :suggested_name] @type reason :: :empty | {:invalid_grapheme, non_neg_integer(), String.t()} @type t :: %__MODULE__{file: String.t(), reason: reason(), suggested_name: String.t() | nil} end defmodule Squirrelix.Error.CannotOverwriteFile do @moduledoc """ Error returned when a generated file would overwrite human-written code. """ @enforce_keys [:file] defstruct [:file] @type t :: %__MODULE__{file: String.t()} end defmodule Squirrelix.Error.CannotWriteFile do @moduledoc """ Error returned when generated content cannot be written. """ @enforce_keys [:file, :reason] defstruct [:file, :reason] @type t :: %__MODULE__{file: String.t(), reason: File.posix()} end defmodule Squirrelix.Error.OutdatedFile do @moduledoc """ Error returned when an existing generated file differs from expected output. """ @enforce_keys [:file] defstruct [:file] @type t :: %__MODULE__{file: String.t()} end defmodule Squirrelix.Error.DuplicateReturnColumns do @moduledoc """ Error returned when a query result contains duplicate column names. """ @enforce_keys [:file, :starting_line, :content, :names] defstruct [:file, :starting_line, :content, :names] @type t :: %__MODULE__{ file: String.t(), starting_line: pos_integer(), content: String.t(), names: [String.t()] } end defmodule Squirrelix.Error.QueryHasInvalidColumn do @moduledoc """ Error returned when a query result column name cannot become an Elixir identifier. """ @enforce_keys [:file, :starting_line, :content, :column_name, :reason] defstruct [:file, :starting_line, :content, :column_name, :reason, :suggested_name] @type reason :: :empty | {:invalid_grapheme, non_neg_integer(), String.t()} @type t :: %__MODULE__{ file: String.t(), starting_line: pos_integer(), content: String.t(), column_name: String.t(), reason: reason(), suggested_name: String.t() | nil } end defmodule Squirrelix.Error.MissingQueryMetadata do @moduledoc """ Error returned when no parameter and return metadata is available for a query. """ @enforce_keys [:file] defstruct [:file] @type t :: %__MODULE__{file: String.t()} end defmodule Squirrelix.Error.MissingQueryMetadataField do @moduledoc """ Error returned when query metadata is missing a required field. """ @enforce_keys [:file, :field] defstruct [:file, :field] @type t :: %__MODULE__{file: String.t(), field: :params | :returns} end defmodule Squirrelix.Error.InvalidQueryMetadataFile do @moduledoc """ Error returned when a query metadata file cannot be evaluated to a metadata map. """ @enforce_keys [:file, :reason] defstruct [:file, :reason] @type t :: %__MODULE__{file: String.t(), reason: term()} end defmodule Squirrelix.Error.UnsupportedPostgresType do @moduledoc """ Error returned when a Postgres type has no Elixir mapping. Composite types (`create type ... as (...)`) and some built-ins such as `point` are intentionally unsupported; `hint` carries actionable guidance when available (see the Types guide). """ @enforce_keys [:name] defstruct [:name, :hint] @type t :: %__MODULE__{name: String.t(), hint: String.t() | nil} end defmodule Squirrelix.Error.QueryHasInvalidEnum do @moduledoc """ Error returned when a Postgres enum has no variants. """ @enforce_keys [:file, :starting_line, :content, :enum_name, :reason] defstruct [:file, :starting_line, :content, :enum_name, :reason] @type reason :: :no_variants @type t :: %__MODULE__{ file: String.t(), starting_line: pos_integer(), content: String.t(), enum_name: String.t(), reason: reason() } end defmodule Squirrelix.Error.PostgresSyntaxError do @moduledoc """ Error returned when Postgres rejects query syntax during inference. """ @enforce_keys [:file, :starting_line, :content, :message] defstruct [:file, :starting_line, :content, :message, :position] @type t :: %__MODULE__{ file: String.t(), starting_line: pos_integer(), content: String.t(), message: String.t(), position: pos_integer() | nil } end defmodule Squirrelix.Error.MissingPostgresTable do @moduledoc """ Error returned when query inference references a table Postgres cannot find. """ @enforce_keys [:file, :starting_line, :content, :message] defstruct [:file, :starting_line, :content, :message, :table, :position] @type t :: %__MODULE__{ file: String.t(), starting_line: pos_integer(), content: String.t(), message: String.t(), table: String.t() | nil, position: pos_integer() | nil } end defmodule Squirrelix.Error.MissingPostgresColumn do @moduledoc """ Error returned when query inference references a column Postgres cannot find. """ @enforce_keys [:file, :starting_line, :content, :message] defstruct [:file, :starting_line, :content, :message, :column, :position] @type t :: %__MODULE__{ file: String.t(), starting_line: pos_integer(), content: String.t(), message: String.t(), column: String.t() | nil, position: pos_integer() | nil } end defmodule Squirrelix.Error.MissingPostgresConstraint do @moduledoc """ Error returned when query inference references a constraint Postgres cannot find. """ @enforce_keys [:file, :starting_line, :content, :message] defstruct [:file, :starting_line, :content, :message, :constraint, :table, :position] @type t :: %__MODULE__{ file: String.t(), starting_line: pos_integer(), content: String.t(), message: String.t(), constraint: String.t() | nil, table: String.t() | nil, position: pos_integer() | nil } end defmodule Squirrelix.Error.PostgresInferenceError do @moduledoc """ Error returned when Postgres rejects query inference for an unclassified reason. """ @enforce_keys [:file, :starting_line, :content, :message] defstruct [:file, :starting_line, :content, :message, :code, :position] @type t :: %__MODULE__{ file: String.t(), starting_line: pos_integer(), content: String.t(), message: String.t(), code: atom() | nil, position: pos_integer() | nil } end defmodule Squirrelix.Error.PostgresConnectionTimeout do @moduledoc """ Error returned when establishing a Postgres connection times out during `--infer`. """ @enforce_keys [:host, :port, :timeout_seconds] defstruct [:host, :port, :timeout_seconds] @type t :: %__MODULE__{ host: String.t(), port: non_neg_integer(), timeout_seconds: non_neg_integer() } end defmodule Squirrelix.Error.CannotConnectToPostgres do @moduledoc """ Error returned when Squirrelix cannot connect to Postgres for inference. Covers TCP failures (refused, unreachable, closed) and authorization / catalog problems. Connection timeouts use `Squirrelix.Error.PostgresConnectionTimeout`. """ @enforce_keys [:host, :port, :reason] defstruct [:host, :port, :reason, :user, :database, :message, :detail] @type reason :: :refused | :unreachable | :closed | :nxdomain | :invalid_authorization | :invalid_password | :invalid_catalog | :unknown @type t :: %__MODULE__{ host: String.t(), port: non_neg_integer(), reason: reason(), user: String.t() | nil, database: String.t() | nil, message: String.t() | nil, detail: term() | nil } end defmodule Squirrelix.Error do @moduledoc """ Formatting helpers for structured Squirrelix errors. Error **structs** under `Squirrelix.Error.*` are part of the supported public API — pattern-match them in tooling that inspects `CodegenSummary` / `CodegenCheckSummary` errors. Use `format/1` and `format_all/1` to render the same actionable text Mix tasks print. User-facing failures (codegen, check, metadata, connection) format through `format/1` with titles and actionable hints where possible. Connection failures and timeouts during `--infer` are classified with `connection_error/2` into `CannotConnectToPostgres` or `PostgresConnectionTimeout`. """ alias Squirrelix.ConnectionOptions alias Squirrelix.Error.CannotConnectToPostgres alias Squirrelix.Error.CannotOverwriteFile alias Squirrelix.Error.CannotReadFile alias Squirrelix.Error.CannotWriteFile alias Squirrelix.Error.DuplicateReturnColumns alias Squirrelix.Error.InvalidQueryMetadataFile alias Squirrelix.Error.MissingPostgresColumn alias Squirrelix.Error.MissingPostgresConstraint alias Squirrelix.Error.MissingPostgresTable alias Squirrelix.Error.MissingQueryMetadata alias Squirrelix.Error.MissingQueryMetadataField alias Squirrelix.Error.OutdatedFile alias Squirrelix.Error.PostgresConnectionTimeout alias Squirrelix.Error.PostgresInferenceError alias Squirrelix.Error.PostgresSyntaxError alias Squirrelix.Error.QueryFileHasInvalidName alias Squirrelix.Error.QueryHasInvalidColumn alias Squirrelix.Error.QueryHasInvalidEnum alias Squirrelix.Error.UnsupportedPostgresType alias Squirrelix.Query @doc false @spec normalize(struct()) :: struct() def normalize(%PostgresInferenceError{code: :undefined_object, message: message} = error) when is_binary(message) do if constraint_error_message?(message) do %MissingPostgresConstraint{ file: error.file, starting_line: error.starting_line, content: error.content, message: message, constraint: constraint_name(message), table: constraint_table(message), position: error.position } else error end end def normalize(error), do: error @doc false @spec connection_error(term(), ConnectionOptions.t()) :: CannotConnectToPostgres.t() | PostgresConnectionTimeout.t() def connection_error(reason, %ConnectionOptions{} = opts) do case classify_connection_reason(reason) do :timeout -> %PostgresConnectionTimeout{ host: opts.host, port: opts.port, timeout_seconds: opts.timeout_seconds } {kind, message} when kind in [:invalid_authorization, :invalid_password, :invalid_catalog] -> %CannotConnectToPostgres{ host: opts.host, port: opts.port, user: opts.user, database: opts.database, reason: kind, message: message } {kind, detail} when kind in [:refused, :unreachable, :closed, :nxdomain] -> %CannotConnectToPostgres{ host: opts.host, port: opts.port, user: opts.user, database: opts.database, reason: kind, detail: detail } {:unknown, detail} -> %CannotConnectToPostgres{ host: opts.host, port: opts.port, user: opts.user, database: opts.database, reason: :unknown, detail: detail, message: connection_detail_message(detail) } end end @doc false @spec attach_query(struct(), Query.t()) :: struct() def attach_query(%{__struct__: module} = error, %Query{} = query) do if query_context_fields?(module) do %{error | file: query.file, starting_line: query.starting_line, content: query.content} else error end end @doc """ Formats a structured error for display (same text Mix tasks print). """ @spec format(struct()) :: String.t() def format(error), do: error |> normalize() |> do_format() @doc """ Formats a list of structured errors, separated by blank lines. """ @spec format_all([struct()]) :: String.t() def format_all(errors) when is_list(errors) do Enum.map_join(errors, "\n\n", &format/1) end @doc false @spec postgresql_code(struct()) :: String.t() | nil def postgresql_code(%PostgresSyntaxError{}), do: "42601" def postgresql_code(%MissingPostgresTable{}), do: "42P01" def postgresql_code(%MissingPostgresColumn{}), do: "42703" def postgresql_code(%MissingPostgresConstraint{}), do: "42704" def postgresql_code(%PostgresInferenceError{code: nil}), do: nil def postgresql_code(%PostgresInferenceError{code: code}) when is_atom(code) do Map.get( %{ syntax_error: "42601", undefined_table: "42P01", undefined_column: "42703", undefined_object: "42704", feature_not_supported: "0A000" }, code, code |> Atom.to_string() |> String.upcase() ) end def postgresql_code(_error), do: nil defp query_context_fields?(module) do module |> Module.split() |> List.last() |> then(fn name -> name in [ "PostgresSyntaxError", "MissingPostgresTable", "MissingPostgresColumn", "MissingPostgresConstraint", "PostgresInferenceError", "DuplicateReturnColumns", "QueryHasInvalidEnum", "QueryHasInvalidColumn" ] end) end defp do_format(%QueryFileHasInvalidName{} = error), do: format_query_file_name_error(error) defp do_format(%DuplicateReturnColumns{} = error), do: format_duplicate_return_columns_error(error) defp do_format(%QueryHasInvalidColumn{} = error), do: format_invalid_column_error(error) defp do_format(%QueryHasInvalidEnum{} = error), do: format_invalid_enum_error(error) defp do_format(%UnsupportedPostgresType{} = error), do: format_unsupported_type_error(error) defp do_format(%OutdatedFile{} = error), do: format_outdated_file_error(error) defp do_format(%CannotOverwriteFile{} = error), do: format_cannot_overwrite_file_error(error) defp do_format(%CannotReadFile{} = error), do: format_cannot_read_file_error(error) defp do_format(%CannotWriteFile{} = error), do: format_cannot_write_file_error(error) defp do_format(%MissingQueryMetadata{} = error), do: format_missing_query_metadata_error(error) defp do_format(%MissingQueryMetadataField{} = error), do: format_missing_query_metadata_field_error(error) defp do_format(%InvalidQueryMetadataFile{} = error), do: format_invalid_query_metadata_file_error(error) defp do_format(%PostgresConnectionTimeout{} = error), do: format_connection_timeout_error(error) defp do_format(%CannotConnectToPostgres{} = error), do: format_cannot_connect_error(error) defp do_format(error) do if postgres_query_error?(error) do format_postgres_query_error(error) else "Error: #{inspect(error)}" end end defp postgres_query_error?(%PostgresSyntaxError{}), do: true defp postgres_query_error?(%MissingPostgresTable{}), do: true defp postgres_query_error?(%MissingPostgresColumn{}), do: true defp postgres_query_error?(%MissingPostgresConstraint{}), do: true defp postgres_query_error?(%PostgresInferenceError{}), do: true defp postgres_query_error?(_error), do: false defp format_postgres_query_error(error) do title = case postgresql_code(error) do nil -> "Invalid query" code -> "Invalid query [#{code}]" end [ "Error: #{title}", "", code_block(error), additional_message(error) ] |> Enum.reject(&(&1 == "")) |> Enum.join("\n") end defp format_query_file_name_error(%QueryFileHasInvalidName{} = error) do lines = [ "Error: Query file with invalid name", "", "File #{error.file} doesn't have a valid name.", "The name of a file is used to generate a corresponding Elixir function, so it should be a valid Elixir identifier.", identifier_reason_message(error.reason), "Hint: A file name must start with a lowercase letter and can only contain lowercase letters, numbers and underscores." ] lines = case error.suggested_name do nil -> lines suggested_name -> lines ++ ["Maybe try renaming it to `#{suggested_name}`?"] end Enum.join(lines, "\n") end defp format_duplicate_return_columns_error(%DuplicateReturnColumns{} = error) do pretty_names = Enum.map_join(error.names, ", ", &"`#{&1}`") label = case error.names do [_] -> "name" _ -> "names" end [ "Error: Duplicate names", "", code_block(error), "This query returns multiple values sharing the same #{label}: #{pretty_names}." ] |> Enum.join("\n") end defp format_invalid_column_error(%QueryHasInvalidColumn{reason: :empty} = error) do [ "Error: Column with empty name", "", code_block(error), "A column returned by this query has the empty string as a name, all columns should have a valid Elixir identifier as name." ] |> Enum.join("\n") end defp format_invalid_column_error(%QueryHasInvalidColumn{} = error) do message = case error.suggested_name do nil -> "This is not a valid Elixir identifier" suggested_name -> "This is not a valid Elixir identifier, maybe try `#{suggested_name}`?" end [ "Error: Column with invalid name", "", code_block(error, {error.column_name, message}), "Hint: A column name must start with a lowercase letter and can only contain lowercase letters, numbers and underscores." ] |> Enum.join("\n") end defp format_invalid_enum_error(%QueryHasInvalidEnum{} = error) do reason_message = case error.reason do :no_variants -> "it has no variants." end [ "Error: Query with invalid enum", "", code_block(error), "One of the values in this query is the `#{error.enum_name}` enum, but it cannot be turned into a generated type because #{reason_message}" ] |> Enum.join("\n") end defp format_unsupported_type_error(%UnsupportedPostgresType{} = error) do lines = [ "Error: Unsupported type", "", "One of the rows returned by this query has type `#{error.name}` which cannot currently generate code for." ] lines = case error.hint do nil -> lines hint -> lines ++ ["Hint: #{hint}"] end Enum.join(lines, "\n") end defp format_outdated_file_error(%OutdatedFile{} = error) do [ "Error: Outdated file", "", "It looks like #{error.file} is outdated, try running `mix squirrelix.gen` to generate a new up to date version." ] |> Enum.join("\n") end defp format_cannot_overwrite_file_error(%CannotOverwriteFile{} = error) do [ "Error: Cannot overwrite file", "", "It looks like #{error.file} already exists and was not generated by Squirrelix, it cannot be overwritten!", "Hint: Rename the file and run `mix squirrelix.gen` again." ] |> Enum.join("\n") end defp format_cannot_read_file_error(%CannotReadFile{} = error) do [ "Error: Cannot read file", "", "I couldn't read #{error.file} because of the following error: #{posix_reason(error.reason)}." ] |> Enum.join("\n") end defp format_cannot_write_file_error(%CannotWriteFile{} = error) do [ "Error: Cannot write to file", "", "I couldn't write to #{error.file} because of the following error: #{posix_reason(error.reason)}." ] |> Enum.join("\n") end defp format_missing_query_metadata_error(%MissingQueryMetadata{} = error) do [ "Error: Missing query metadata", "", "I couldn't find type metadata for #{error.file}.", "Hint: Add an entry to `squirr_elix.exs`, or run with `--infer` to infer types from Postgres." ] |> Enum.join("\n") end defp format_missing_query_metadata_field_error(%MissingQueryMetadataField{} = error) do [ "Error: Incomplete query metadata", "", "The metadata for #{error.file} is missing the `#{error.field}` field.", "Hint: Each metadata entry needs both `params:` and `returns:` lists." ] |> Enum.join("\n") end defp format_invalid_query_metadata_file_error(%InvalidQueryMetadataFile{} = error) do [ "Error: Invalid query metadata file", "", "I couldn't load query metadata from #{error.file}: #{metadata_file_reason(error.reason)}.", "Hint: The metadata file must evaluate to a map of query paths to `%{params: ..., returns: ...}` entries. Prefer `--write-metadata` after a successful `--infer` pass." ] |> Enum.join("\n") end defp format_connection_timeout_error(%PostgresConnectionTimeout{} = error) do [ "Error: Connection timed out", "", "I couldn't connect to `#{error.host}` at port #{error.port} because the connection timed out after #{error.timeout_seconds} seconds.", "Hint: Increase `PGCONNECT_TIMEOUT` or the URL `connect_timeout` parameter, check that `#{error.host}:#{error.port}` is reachable, or use a metadata file (`squirr_elix.exs`) instead of `--infer`." ] |> Enum.join("\n") end defp format_cannot_connect_error(%CannotConnectToPostgres{reason: reason} = error) when reason in [:invalid_authorization, :invalid_password] do lines = [ "Error: Cannot authenticate with Postgres", "", authentication_message(error) ] lines = case error.message do nil -> lines message -> lines ++ [message] end (lines ++ [ "Hint: Check `PGUSER`, `PGPASSWORD`, and `PGDATABASE` (or `--username` / `--password` / `--database`), or use a metadata file (`squirr_elix.exs`) instead of `--infer`." ]) |> Enum.join("\n") end defp format_cannot_connect_error(%CannotConnectToPostgres{reason: :invalid_catalog} = error) do lines = [ "Error: Cannot connect to Postgres", "", "I couldn't connect to database `#{error.database}`." ] lines = case error.message do nil -> lines message -> lines ++ [message] end (lines ++ [ "Hint: Set `PGDATABASE` or `--database` to an existing database, or use a metadata file (`squirr_elix.exs`) instead of `--infer`." ]) |> Enum.join("\n") end defp format_cannot_connect_error(%CannotConnectToPostgres{} = error) do [ "Error: Cannot connect to Postgres", "", tcp_connection_message(error), "Hint: Check `PGHOST` and `PGPORT` (or `--hostname` / `--port`), ensure Postgres is running, or use a metadata file (`squirr_elix.exs`) instead of `--infer`." ] |> Enum.join("\n") end defp authentication_message(%CannotConnectToPostgres{reason: :invalid_password} = error) do "Invalid password for user `#{error.user}`." end defp authentication_message(%CannotConnectToPostgres{} = error) do "I couldn't connect to database `#{error.database}` with user `#{error.user}`." end defp tcp_connection_message(%CannotConnectToPostgres{reason: :refused} = error) do "I couldn't connect to the database because `#{error.host}` refused the connection to port #{error.port}." end defp tcp_connection_message(%CannotConnectToPostgres{reason: :closed} = error) do "I couldn't connect to the database because `#{error.host}` closed the connection to port #{error.port}." end defp tcp_connection_message(%CannotConnectToPostgres{reason: :unreachable} = error) do "I couldn't connect to the database because `#{error.host}` is unreachable." end defp tcp_connection_message(%CannotConnectToPostgres{reason: :nxdomain} = error) do "I couldn't connect to the database because host `#{error.host}` could not be resolved." end defp tcp_connection_message(%CannotConnectToPostgres{} = error) do detail = case connection_detail_message(error.detail) || error.message do nil -> "an unexpected connection error" message -> message end "I couldn't connect to the database at `#{error.host}` port #{error.port}: #{detail}." end defp classify_connection_reason(%DBConnection.ConnectionError{message: message}) when is_binary(message) do cond do String.contains?(message, "timeout") -> :timeout String.contains?(message, "econnrefused") or String.contains?(message, "connection refused") -> {:refused, message} String.contains?(message, "ehostunreach") or String.contains?(message, "unreachable") -> {:unreachable, message} String.contains?(message, "nxdomain") -> {:nxdomain, message} String.contains?(message, "closed") -> {:closed, message} true -> {:unknown, message} end end defp classify_connection_reason(%Postgrex.Error{postgres: postgres}) when is_map(postgres) do message = Map.get(postgres, :message) case Map.get(postgres, :code) do :invalid_password -> {:invalid_password, message} :invalid_authorization_specification -> {:invalid_authorization, message} :invalid_catalog_name -> {:invalid_catalog, message} _other -> {:unknown, postgres} end end defp classify_connection_reason(:timeout), do: :timeout defp classify_connection_reason(:econnrefused), do: {:refused, :econnrefused} defp classify_connection_reason(:ehostunreach), do: {:unreachable, :ehostunreach} defp classify_connection_reason(:nxdomain), do: {:nxdomain, :nxdomain} defp classify_connection_reason(:closed), do: {:closed, :closed} defp classify_connection_reason(other), do: {:unknown, other} defp connection_detail_message(nil), do: nil defp connection_detail_message(message) when is_binary(message), do: message defp connection_detail_message(other), do: inspect(other) defp posix_reason(reason) when is_atom(reason) do reason |> :file.format_error() |> IO.chardata_to_string() end defp posix_reason(reason), do: inspect(reason) defp metadata_file_reason(:not_a_map), do: "it did not evaluate to a map" defp metadata_file_reason(reason) when is_exception(reason), do: Exception.message(reason) defp metadata_file_reason(reason) when is_binary(reason), do: reason defp metadata_file_reason(reason), do: inspect(reason) defp code_block( %{file: file, content: content, starting_line: starting_line} = error, pointer \\ nil ) do pointer = case pointer do {column_name, message} -> pointer_for({column_name, message}, content) nil -> pointer_for(error) end lines = String.split(content, "\n") width = byte_size(Integer.to_string(starting_line + length(lines) - 1)) + 2 padding = String.duplicate(" ", width + 1) header = [ padding <> "╭─ #{file}", padding <> "│" ] body = lines |> Enum.with_index(starting_line) |> Enum.flat_map(fn {line, line_number} -> prefix = String.pad_leading("#{line_number} │ ", width) if pointer != nil and elem(pointer, 0) == line_number do {from, message} = elem(pointer, 1) caret = String.duplicate(" ", from) <> "┬" underline = String.duplicate(" ", from) <> "╰─ " <> message [ prefix <> line, prefix <> caret, prefix <> underline ] else [prefix <> line] end end) footer = [padding <> "┆"] Enum.join(header ++ body ++ footer, "\n") end defp additional_message(%PostgresInferenceError{message: message}), do: message defp additional_message(%MissingPostgresConstraint{message: message, position: nil}), do: message defp additional_message(%{message: message, position: nil}), do: message defp additional_message(_error), do: "" defp pointer_for({column_name, message}, content) when is_binary(column_name) and is_binary(message) and is_binary(content) do case find_name_span(column_name, content) do {line_number, column} -> {line_number, {column, message}} :error -> nil end end defp pointer_for(%{position: position, message: message, content: content}) when is_integer(position) and is_binary(message) and is_binary(content) do {line_number, column} = position_to_line_column(content, position) {line_number, {column, message}} end defp pointer_for(_error), do: nil defp find_name_span(name, content) do case :binary.match(content, name) do {offset, _length} -> offset_to_line_column(content, offset) :nomatch -> :error end end defp offset_to_line_column(content, offset) do prefix = binary_part(content, 0, offset) parts = String.split(prefix, "\n") {length(parts), String.length(List.last(parts))} end defp identifier_reason_message(:empty), do: "Reason: the file name is empty" defp identifier_reason_message({:invalid_grapheme, 0, grapheme}), do: "Reason: cannot start with #{inspect(grapheme)}" defp identifier_reason_message({:invalid_grapheme, position, grapheme}), do: "Reason: invalid character #{inspect(grapheme)} at position #{position}" defp position_to_line_column(content, position) do prefix = String.slice(content, 0, position - 1) parts = String.split(prefix, "\n") {length(parts), String.length(List.last(parts))} end defp constraint_error_message?(message) do String.starts_with?(message, "constraint ") and String.contains?(message, " does not exist") end defp constraint_name(message) do case Regex.run(~r/constraint "([^"]+)"/, message) do [_match, name] -> name nil -> nil end end defp constraint_table(message) do case Regex.run(~r/for table "([^"]+)"/, message) do [_match, name] -> name nil -> nil end end end