defmodule QuackDB.Meta.Table do @moduledoc "A row from DuckDB's table listing pragmas." defstruct [:database, :schema, :name, :column_names, :column_types, :temporary] @type t :: %__MODULE__{ database: String.t() | nil, schema: String.t() | nil, name: String.t() | nil, column_names: [String.t()] | nil, column_types: [String.t()] | nil, temporary: boolean() | nil } end defmodule QuackDB.Meta.Database do @moduledoc "A row from DuckDB's `PRAGMA database_list`." defstruct [:seq, :name, :file] @type t :: %__MODULE__{seq: integer() | nil, name: String.t() | nil, file: String.t() | nil} end defmodule QuackDB.Meta.Column do @moduledoc "A row from DuckDB's `pragma_table_info` table function." defstruct [:cid, :name, :type, :notnull, :dflt_value, :pk] @type t :: %__MODULE__{ cid: integer() | nil, name: String.t() | nil, type: String.t() | nil, notnull: boolean() | nil, dflt_value: String.t() | nil, pk: boolean() | nil } end defmodule QuackDB.Meta do @moduledoc """ DuckDB catalog and metadata helpers. These functions wrap DuckDB's logical metadata pragmas and table functions. They accept either a QuackDB connection or a QuackDB-backed Ecto repo. Table arguments may be schema modules, atoms, strings, or `{prefix, source}` tuples. """ alias QuackDB.Meta.Column alias QuackDB.Meta.Database alias QuackDB.Meta.Table @type source :: module() | atom() | String.t() | {atom() | String.t(), atom() | String.t()} @doc "Lists tables visible to the current DuckDB connection." @spec tables(DBConnection.conn() | module(), keyword()) :: {:ok, [Table.t()]} | {:error, Exception.t()} def tables(connection, options \\ []) do expanded? = Keyword.get(options, :expanded, false) query_options = Keyword.delete(options, :expanded) statement = if expanded?, do: "PRAGMA show_tables_expanded", else: "PRAGMA show_tables" with {:ok, result} <- QuackDB.query(connection, statement, [], query_options) do {:ok, table_rows(result, expanded?)} end end @doc "Lists tables visible to the current DuckDB connection, raising on errors." @spec tables!(DBConnection.conn() | module(), keyword()) :: [Table.t()] def tables!(connection, options \\ []) do case tables(connection, options) do {:ok, tables} -> tables {:error, error} -> raise error end end @doc "Lists attached DuckDB databases." @spec databases(DBConnection.conn() | module(), keyword()) :: {:ok, [Database.t()]} | {:error, Exception.t()} def databases(connection, options \\ []) do with {:ok, result} <- QuackDB.query(connection, "PRAGMA database_list", [], options) do {:ok, QuackDB.ResultMapper.rows_to_structs(result, Database)} end end @doc "Lists attached DuckDB databases, raising on errors." @spec databases!(DBConnection.conn() | module(), keyword()) :: [Database.t()] def databases!(connection, options \\ []) do case databases(connection, options) do {:ok, databases} -> databases {:error, error} -> raise error end end @doc "Returns logical column metadata for a table." @spec table_info(DBConnection.conn() | module(), source(), keyword()) :: {:ok, [Column.t()]} | {:error, Exception.t()} def table_info(connection, source, options \\ []) do statement = QuackDB.SQL.call(:pragma_table_info, [QuackDB.SourceRef.name(source)]) with {:ok, result} <- QuackDB.query(connection, statement, [], options) do {:ok, QuackDB.ResultMapper.rows_to_structs(result, Column)} end end @doc "Returns logical column metadata for a table, raising on errors." @spec table_info!(DBConnection.conn() | module(), source(), keyword()) :: [Column.t()] def table_info!(connection, source, options \\ []) do case table_info(connection, source, options) do {:ok, columns} -> columns {:error, error} -> raise error end end @doc "Returns primary-key columns for a table." @spec primary_keys(DBConnection.conn() | module(), source(), keyword()) :: {:ok, [Column.t()]} | {:error, Exception.t()} def primary_keys(connection, source, options \\ []) do with {:ok, columns} <- table_info(connection, source, options) do {:ok, Enum.filter(columns, & &1.pk)} end end @doc "Returns primary-key columns for a table, raising on errors." @spec primary_keys!(DBConnection.conn() | module(), source(), keyword()) :: [Column.t()] def primary_keys!(connection, source, options \\ []) do case primary_keys(connection, source, options) do {:ok, columns} -> columns {:error, error} -> raise error end end @doc "Returns column defaults for a table as a map keyed by column name." @spec column_defaults(DBConnection.conn() | module(), source(), keyword()) :: {:ok, %{String.t() => String.t()}} | {:error, Exception.t()} def column_defaults(connection, source, options \\ []) do with {:ok, columns} <- table_info(connection, source, options) do defaults = columns |> Enum.reject(&is_nil(&1.dflt_value)) |> Map.new(&{&1.name, &1.dflt_value}) {:ok, defaults} end end @doc "Returns column defaults for a table, raising on errors." @spec column_defaults!(DBConnection.conn() | module(), source(), keyword()) :: %{ String.t() => String.t() } def column_defaults!(connection, source, options \\ []) do case column_defaults(connection, source, options) do {:ok, defaults} -> defaults {:error, error} -> raise error end end defp table_rows(result, true), do: QuackDB.ResultMapper.rows_to_structs(result, Table) defp table_rows(%QuackDB.Result{columns: ["name"], rows: rows}, false) when is_list(rows) do Enum.map(rows, fn [name] -> %Table{name: name} end) end defp table_rows(result, false), do: QuackDB.ResultMapper.rows_to_structs(result, Table) end