Current section
Files
Jump to
Current section
Files
lib/ecto_db_scanner.ex
defmodule EctoDBScanner do
@moduledoc """
A PostgreSQL database scanner that discovers structure, maps types, and detects enums.
"""
@default_pool_size 5
@scan_opts [
:analyze,
:detect_enums,
:enum_detection_timeout,
:enum_detection_max_concurrency
]
@doc """
Scans a PostgreSQL database and returns its structure.
Accepts connection params as a keyword list:
EctoDBScanner.scan(
hostname: "localhost",
username: "postgres",
password: "postgres",
database: "my_db",
port: 5432
)
## Options
* `:analyze` (boolean, default `true`) — Runs `ANALYZE` against the target
database before scanning so planner statistics (notably
`pg_class.reltuples`, used for row counts and enum detection sampling)
are fresh. On large databases this may take a while; set to `false` if
stats are already current or the connecting role lacks permission.
* `:detect_enums` (boolean, default `true`) — Runs the cardinality-based
heuristic that samples string columns to detect enum-like sets of
values. Set to `false` to skip detection on databases where even
`TABLESAMPLE`-bounded sampling is too slow; PostgreSQL `ENUM`-typed
columns are still detected via catalog lookup.
* `:enum_detection_timeout` (integer, default `60_000`) — Per-column
sampling timeout in milliseconds for heuristic enum detection. A column
that exceeds it is silently dropped from the results, the rest of the
scan continues.
* `:enum_detection_max_concurrency` (integer) — Number of columns to
sample concurrently. Defaults to `pool_size - 1` (minimum 1) so the
connection pool is not saturated by the scan itself.
All other options are passed through to the underlying repo connection.
Returns `{:ok, %EctoDBScanner.Result.Database{}}` or `{:error, reason}`.
"""
def scan(opts) when is_list(opts) do
{scan_opts, repo_opts} = Keyword.split(opts, @scan_opts)
repo_config =
Keyword.merge(
[pool_size: @default_pool_size],
repo_opts
)
{:ok, repo_pid} = EctoDBScanner.Repo.start_link(repo_config)
try do
if Keyword.get(scan_opts, :analyze, true) do
Ecto.Adapters.SQL.query!(EctoDBScanner.Repo, "ANALYZE", [], timeout: :infinity)
end
reactor_options = %{
detect_enums: Keyword.get(scan_opts, :detect_enums, true),
enum_detection_timeout: Keyword.get(scan_opts, :enum_detection_timeout),
enum_detection_max_concurrency: Keyword.get(scan_opts, :enum_detection_max_concurrency)
}
Reactor.run(EctoDBScanner.Scanner, %{
repo: EctoDBScanner.Repo,
options: reactor_options
})
after
Supervisor.stop(repo_pid)
end
end
end