# `Lotus.Schema`
[🔗](https://github.com/elixir-lotus/lotus/blob/v1.0.0/lib/lotus/schema.ex#L1)

Schema introspection functionality for Lotus.

Provides functions to list schemas, tables and inspect table schemas across
different database adapters (PostgreSQL, MySQL, SQLite, etc.).

## Visibility Filtering

All schema and table listing functions automatically apply visibility rules
configured in your application:

- **Schema visibility** filters which schemas are accessible
- **Table visibility** filters which tables within allowed schemas are accessible
- **Built-in security** automatically blocks system schemas and tables

Schema visibility takes precedence - if a schema is denied, all tables within it
are blocked regardless of table-level rules.

## Middleware and Caching

Every discovery call fires two middleware events (see `Lotus.Middleware`):

1. The kind-specific `:after_list_*` event with a kind-specific payload.
2. The unified `:after_discover` event with `%{kind:, source:, result:, context:}`.

Both events run **outside** the cache callback — only the raw, visibility-filtered
adapter result is cached. Context-sensitive middleware (e.g. per-tenant table
filtering) is therefore safe to use without poisoning the cache, at the cost of
running the middleware pipeline on every call.

`Lotus.Visibility.Resolver` callbacks receive `(source_name, scope)`. When `scope`
is `nil` (the default), cache keys are identical to pre-scope versions. When
non-nil, a digest of the scope is appended to the cache key so different scopes
produce independent cached entries. Keep scope low-cardinality for good cache
hit rates.

A resolver that reads runtime context (e.g. the process dictionary) instead of
using the `scope` argument will cache incorrectly — place context-dependent
logic in middleware or use `scope` to key the cache.

## Database-Specific Behavior

- **PostgreSQL**: Returns namespaced `{schema, table}` tuples
- **MySQL**: Returns `{database, table}` tuples (schemas = databases in MySQL)
- **SQLite**: Returns table names as strings (schema-less)

# `describe_table`

```elixir
@spec describe_table(module() | String.t(), String.t(), keyword()) ::
  {:ok, [map()]} | {:error, term()}
```

Describes a specific table (column definitions).

Returns a list of column definitions with their types and constraints.

## Options

- `:schema` - Look for table in specific namespace
- `:schemas` - Search for table in multiple namespaces (first match wins)
- `:search_path` - Use PostgreSQL search_path to resolve table location
- `:cache` - Cache options (profile, ttl_ms, etc.)
- `:context` - Opaque value passed to the `:after_describe_table` and
  `:after_discover` middleware events (see `Lotus.Middleware`)
- `:scope` - Opaque value passed to the visibility resolver and hashed
  into the cache key. Different scopes produce independent cached entries.

## Examples

    {:ok, columns} = Lotus.Schema.describe_table(MyApp.Repo, "users")
    # Column definitions for public.users

    {:ok, columns} = Lotus.Schema.describe_table("postgres", "customers", schema: "reporting")
    # Column definitions for reporting.customers

    {:ok, columns} = Lotus.Schema.describe_table("postgres", "customers", search_path: "reporting, public")
    # Finds customers table using search_path resolution

# `get_table_stats`

```elixir
@spec get_table_stats(module() | String.t(), String.t(), keyword()) ::
  {:ok, map()} | {:error, binary()}
```

Gets basic statistics about a table.

Returns information like row count and table size.

## Options

- `:schema` - Look for table in specific schema
- `:schemas` - Search for table in multiple schemas (first match wins)
- `:search_path` - Use PostgreSQL search_path to resolve table location
- `:scope` - Opaque value passed to the visibility resolver and hashed into
  the cache key
- `:cache` - Cache options (profile, ttl_ms, etc.)

## Examples

    {:ok, stats} = Lotus.Schema.get_table_stats(MyApp.Repo, "users")
    # Returns: %{row_count: 1234}

    {:ok, stats} = Lotus.Schema.get_table_stats("postgres", "customers", schema: "reporting")
    # Gets stats for reporting.customers

Sources whose adapter implements `c:Lotus.Source.Adapter.table_stats/3`
answer from that callback and may return additional keys alongside
`:row_count`. Everything else falls back to `SELECT COUNT(*)`.

# `list_relations`

```elixir
@spec list_relations(
  module() | String.t(),
  keyword()
) :: {:ok, [{String.t() | nil, String.t()}]} | {:error, term()}
```

Lists all relations (tables with namespace information) in the given repository.

Similar to list_tables/2 but returns {schema, table} tuples instead of just table names.
Useful for UIs that need to display the full namespace-qualified list.

## Options

- `:schema` - Search in specific schema
- `:schemas` - Search in multiple schemas
- `:search_path` - Use PostgreSQL search_path
- `:include_views` - Include views in results (default: false)
- `:cache` - Cache options (profile, ttl_ms, etc.)
- `:context` - Opaque value passed to the `:after_list_relations` and
  `:after_discover` middleware events (see `Lotus.Middleware`)
- `:scope` - Opaque value passed to the visibility resolver and hashed
  into the cache key. Different scopes produce independent cached entries.

## Examples

    {:ok, relations} = Lotus.Schema.list_relations("postgres", search_path: "reporting, public")
    # Returns [{"reporting", "customers"}, {"reporting", "orders"}, {"public", "users"}, ...]

# `list_schemas`

```elixir
@spec list_schemas(
  module() | String.t(),
  keyword()
) :: {:ok, [String.t()]} | {:error, term()}
```

Lists all visible schemas in the given repository.

Returns a list of schema names filtered by visibility rules. For databases
without schemas (like SQLite), returns an empty list.

**Note**: Results are automatically filtered by schema visibility rules.
System schemas (like `pg_catalog`) are always blocked for security.

## Options

- `:cache` - Cache options (profile, ttl_ms, etc.)
- `:context` - Opaque value passed to the `:after_list_schemas` and
  `:after_discover` middleware events (see `Lotus.Middleware`)
- `:scope` - Opaque value passed to the visibility resolver and hashed
  into the cache key. Different scopes produce independent cached entries.

## Examples

    {:ok, schemas} = Lotus.Schema.list_schemas(MyApp.Repo)
    # PostgreSQL: ["public", "reporting", ...]  (filtered by visibility)

    {:ok, schemas} = Lotus.Schema.list_schemas("mysql")
    # MySQL: ["app_production", "analytics_db", ...]  (databases = schemas)

    {:ok, schemas} = Lotus.Schema.list_schemas("sqlite")
    # SQLite: []  (schema-less database)

# `list_tables`

```elixir
@spec list_tables(
  module() | String.t(),
  keyword()
) :: {:ok, [{String.t(), String.t()}] | [String.t()]} | {:error, term()}
```

Lists all visible tables in the given repository.

For databases with schemas (like PostgreSQL), returns {schema, table} tuples.
For databases without schemas (like SQLite), returns just table names as strings.

**Note**: Results are automatically filtered by visibility rules:

1. Schema visibility is checked first - denied schemas block all their tables
2. Table visibility is then applied to tables in allowed schemas
3. System tables are always blocked for security

## Options

- `:schema` - Search in specific schema (e.g., `schema: "reporting"`)
- `:schemas` - Search in multiple schemas (e.g., `schemas: ["reporting", "public"]`)
- `:search_path` - Use PostgreSQL search_path (e.g., `search_path: "reporting, public"`)
- `:include_views` - Include views in results (default: false)
- `:cache` - Cache options (profile, ttl_ms, etc.)
- `:context` - Opaque value passed to the `:after_list_tables` and
  `:after_discover` middleware events (see `Lotus.Middleware`)
- `:scope` - Opaque value passed to the visibility resolver and hashed
  into the cache key. Different scopes produce independent cached entries.

## Examples

    {:ok, tables} = Lotus.Schema.list_tables(MyApp.Repo)
    # PostgreSQL: [{"public", "users"}, {"public", "posts"}, ...]  (filtered by visibility)

    {:ok, tables} = Lotus.Schema.list_tables("postgres", search_path: "reporting, public")
    # PostgreSQL: [{"reporting", "customers"}, {"reporting", "orders"}, {"public", "users"}, ...]

    {:ok, tables} = Lotus.Schema.list_tables("mysql")
    # MySQL: [{"app_db", "users"}, {"analytics_db", "reports"}, ...]  (databases = schemas)

    {:ok, tables} = Lotus.Schema.list_tables("sqlite")
    # SQLite: ["products", "orders", "order_items"]  (schema-less)

---

*Consult [api-reference.md](api-reference.md) for complete listing*
