# `Lotus`
[🔗](https://github.com/typhoonworks/lotus/blob/v0.16.6/lib/lotus.ex#L1)

Lotus is a lightweight Elixir library for saving and executing read-only SQL queries.

This module provides the main public API, orchestrating between:
- Storage: Query persistence and management
- Runner: SQL execution with safety checks
- Migrations: Database schema management

## Configuration

Add to your config:

    config :lotus,
      repo: MyApp.Repo,
      primary_key_type: :id,    # or :binary_id
      foreign_key_type: :id     # or :binary_id

## Usage

    # Create and save a query with variables
    {:ok, query} = Lotus.create_query(%{
      name: "Active Users",
      statement: "SELECT * FROM users WHERE active = {{is_active}}",
      variables: [
        %{name: "is_active", type: :text, label: "Is Active", default: "true"}
      ],
      search_path: "reporting, public"
    })

    # Execute a saved query
    {:ok, results} = Lotus.run_query(query)

    # Execute SQL directly (read-only)
    {:ok, results} = Lotus.run_sql("SELECT * FROM products WHERE price > $1", [100])

# `cache_opt`

```elixir
@type cache_opt() ::
  :bypass
  | :refresh
  | {:ttl_ms, non_neg_integer()}
  | {:profile, atom()}
  | {:tags, [binary()]}
```

# `opts`

```elixir
@type opts() :: [
  timeout: non_neg_integer(),
  statement_timeout_ms: non_neg_integer(),
  read_only: boolean(),
  search_path: binary() | nil,
  repo: binary() | nil,
  vars: map(),
  cache: [cache_opt()] | :bypass | :refresh | nil,
  window: window_opts(),
  filters: [Lotus.Query.Filter.t()],
  context: term()
]
```

# `window_count_mode`

```elixir
@type window_count_mode() :: :none | :exact
```

# `window_opts`

```elixir
@type window_opts() :: [
  limit: pos_integer(),
  offset: non_neg_integer(),
  count: window_count_mode()
]
```

# `can_run?`

```elixir
@spec can_run?(Lotus.Storage.Query.t(), opts()) :: boolean()
```

Checks if a query can be run with the provided variables.

Returns true if all required variables have values (either from defaults
or supplied vars), false otherwise.

## Examples

    # Query with all required variables having defaults
    Lotus.can_run?(query)
    # => true

    # Query missing required variables
    Lotus.can_run?(query)
    # => false

    # Query with runtime variable overrides
    Lotus.can_run?(query, vars: %{"user_id" => 123})
    # => true (if user_id was the missing variable)

# `child_spec`

# `create_dashboard`

Creates a new dashboard.

# `create_dashboard_card`

Creates a new card for a dashboard.

# `create_dashboard_filter`

Creates a new filter for a dashboard.

# `create_filter_mapping`

Creates a filter mapping connecting a dashboard filter to a card's query variable.

# `create_query`

Creates a new saved query.

# `create_visualization`

Creates a new visualization for a query.

# `data_repos`

Returns all configured data repositories.

# `default_data_repo`

Returns the default data repository as a {name, module} tuple.

- If there's only one data repo configured, returns it
- If multiple repos are configured and default_repo is set, returns that repo
- If multiple repos are configured without default_repo, raises an error
- If no data repos are configured, raises an error

# `delete_dashboard`

Deletes a dashboard.

# `delete_dashboard_card`

Deletes a card.

# `delete_dashboard_filter`

Deletes a filter.

# `delete_filter_mapping`

Deletes a filter mapping.

# `delete_query`

Deletes a saved query.

# `delete_visualization`

Deletes a visualization (by struct or id).

# `disable_public_sharing`

Disables public sharing for a dashboard.

# `enable_public_sharing`

Enables public sharing for a dashboard by generating a unique token.

# `get_dashboard`

Gets a single dashboard by ID. Returns nil if not found.

# `get_dashboard!`

Gets a single dashboard by ID. Raises if not found.

# `get_dashboard_by_token`

Gets a dashboard by its public sharing token.

# `get_dashboard_card`

Gets a single card by ID. Returns nil if not found.

## Options

  * `:preload` - A list of associations to preload

# `get_dashboard_card!`

Gets a single card by ID. Raises if not found.

## Options

  * `:preload` - A list of associations to preload

# `get_dashboard_filter`

Gets a single filter by ID. Returns nil if not found.

# `get_dashboard_filter!`

Gets a single filter by ID. Raises if not found.

# `get_data_repo!`

Gets a data repository by name.

Raises if the repository is not configured.

# `get_query`

Gets a single query by ID. Returns nil if not found.

# `get_query!`

Gets a single query by ID. Raises if not found.

# `get_table_schema`

Gets the schema for a specific table.

## Examples

    {:ok, schema} = Lotus.get_table_schema("primary", "users")
    {:ok, schema} = Lotus.get_table_schema("postgres", "customers", schema: "reporting")
    {:ok, schema} = Lotus.get_table_schema(MyApp.DataRepo, "products", search_path: "analytics, public")

# `get_table_stats`

Gets statistics for a specific table.

## Examples

    {:ok, stats} = Lotus.get_table_stats("primary", "users")
    {:ok, stats} = Lotus.get_table_stats("postgres", "customers", schema: "reporting")
    # Returns %{row_count: 1234}

# `list_card_filter_mappings`

Lists all filter mappings for a card.

# `list_dashboard_cards`

Lists all cards for a dashboard.

## Options

  * `:preload` - A list of associations to preload (e.g., `[:query, :filter_mappings]`)

# `list_dashboard_filters`

Lists all filters for a dashboard.

# `list_dashboards`

Lists all dashboards.

# `list_dashboards_by`

Lists dashboards with optional filtering.

## Options

  * `:search` - Search term to match against dashboard names

# `list_data_repo_names`

Lists the names of all configured data repositories.

Useful for building UI dropdowns.

# `list_queries`

Lists all saved queries.

# `list_relations`

Lists all relations (tables with schema information) in a data repository.

## Examples

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

# `list_schemas`

Lists all schemas in the given repository.

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

## Examples

    {:ok, schemas} = Lotus.list_schemas("postgres")
    # Returns ["public", "reporting", ...]

    {:ok, schemas} = Lotus.list_schemas("sqlite")
    # Returns []

# `list_tables`

Lists all tables in a data repository.

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

## Examples

    {:ok, tables} = Lotus.list_tables("postgres")
    # Returns [{"public", "users"}, {"public", "posts"}, ...]

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

    {:ok, tables} = Lotus.list_tables("sqlite")
    # Returns ["products", "orders", "order_items"]

# `list_visualizations`

Lists all visualizations for a query.

Returns visualizations ordered by position, then by id.

# `reorder_dashboard_cards`

Reorders cards in a dashboard.

# `repo`

Returns the configured Ecto repository where Lotus stores query definitions.

# `run_dashboard`

Runs all query cards in a dashboard and returns their results.

Returns a map of card IDs to their results.

## Options

  * `:filter_values` - Map of filter names to their current values
  * `:parallel` - Whether to run cards in parallel (default: true)
  * `:timeout` - Timeout per card in milliseconds (default: 30000)

# `run_dashboard_card`

Runs a single dashboard card and returns its result.

## Options

  * `:filter_values` - Map of filter names to their current values
  * `:timeout` - Query timeout in milliseconds

# `run_query`

```elixir
@spec run_query(Lotus.Storage.Query.t() | term(), opts()) ::
  {:ok, Lotus.Result.t()} | {:error, term()}
```

Run a saved query (by struct or id).

Variables in the query statement (using `{{variable_name}}` syntax) are
substituted with values from the query's default variables and any runtime
overrides provided via the `vars` option.

## Variable Resolution

Variables are resolved in this order:
1. Runtime values from `vars` option (highest priority)
2. Default values from the query's variable definitions
3. If neither exists, raises an error for missing required variable

## Examples

    # Run query with default variable values
    Lotus.run_query(query)

    # Override variables at runtime
    Lotus.run_query(query, vars: %{"min_age" => 25, "status" => "active"})

    # Run with timeout and repo options
    Lotus.run_query(query, timeout: 10_000, repo: MyApp.DataRepo)

    # Run by query ID
    Lotus.run_query(query_id, vars: %{"user_id" => 123})

## Variable Types

Variables are automatically cast based on their type definition:
- `:text` - Used as-is (strings)
- `:number` - Cast from string to integer
- `:date` - Cast from ISO8601 string to Date struct

### Windowed pagination
Pass `window: [limit: pos_integer, offset: non_neg_integer, count: :none | :exact]` to
return only a page of rows from the original query. When `count: :exact`, Lotus will
also compute `SELECT COUNT(*) FROM (original_sql)` and include `meta.total_count` in
the result. The `num_rows` field always reflects the number of rows in the returned page.

# `run_sql`

```elixir
@spec run_sql(binary(), [any()],
  read_only: boolean(),
  statement_timeout_ms: non_neg_integer(),
  timeout: non_neg_integer(),
  search_path: binary() | nil,
  repo: atom() | binary(),
  window: window_opts()
) :: {:ok, Lotus.Result.t()} | {:error, term()}
```

Run ad-hoc SQL (bypassing storage), read-only by default and sandboxed.

## Options

  * `:read_only` — when `true` (default), blocks write operations (INSERT, UPDATE,
    DELETE, DDL) at both the application and database level. Set to `false` to allow
    write queries.

## Examples

    # Run against default configured repo
    {:ok, result} = Lotus.run_sql("SELECT * FROM users")

    # Run against specific repo
    {:ok, result} = Lotus.run_sql("SELECT * FROM products", [], repo: MyApp.DataRepo)

    # With parameters
    {:ok, result} = Lotus.run_sql("SELECT * FROM users WHERE id = $1", [123])

    # With search_path for schema resolution
    {:ok, result} = Lotus.run_sql("SELECT * FROM users", [], search_path: "reporting, public")

    # Allow write queries (development use)
    {:ok, result} = Lotus.run_sql(
      "INSERT INTO notes (body) VALUES ($1)",
      ["hello"],
      read_only: false
    )

### Windowed pagination
Pass `window: [limit: pos_integer, offset: non_neg_integer, count: :none | :exact]` to
page results from the SQL. See `run_query/2` for details. The cache key automatically
incorporates the window so different pages are cached independently.

# `start_link`

# `unique_names?`

Returns whether unique query names are enforced.

# `update_dashboard`

Updates an existing dashboard.

# `update_dashboard_card`

Updates a card.

# `update_dashboard_filter`

Updates a filter.

# `update_query`

Updates an existing query.

# `update_visualization`

Updates an existing visualization.

# `validate_visualization_config`

Validates a visualization config against query results.

Checks that all referenced fields exist in the result columns and that
numeric aggregations (sum, avg) are applied only to numeric columns.

# `version`

Returns the current version of Lotus.

---

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