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_idUsage
# 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])
Summary
Functions
Checks if a query can be run with the provided variables.
Creates a new dashboard.
Creates a new card for a dashboard.
Creates a new filter for a dashboard.
Creates a filter mapping connecting a dashboard filter to a card's query variable.
Creates a new saved query.
Creates a new visualization for a query.
Returns all configured data repositories.
Returns the default data repository as a {name, module} tuple.
Deletes a dashboard.
Deletes a card.
Deletes a filter.
Deletes a filter mapping.
Deletes a saved query.
Deletes a visualization (by struct or id).
Disables public sharing for a dashboard.
Enables public sharing for a dashboard by generating a unique token.
Gets a single dashboard by ID. Returns nil if not found.
Gets a single dashboard by ID. Raises if not found.
Gets a dashboard by its public sharing token.
Gets a single card by ID. Returns nil if not found.
Gets a single card by ID. Raises if not found.
Gets a single filter by ID. Returns nil if not found.
Gets a single filter by ID. Raises if not found.
Gets a data repository by name.
Gets a single query by ID. Returns nil if not found.
Gets a single query by ID. Raises if not found.
Gets the schema for a specific table.
Gets statistics for a specific table.
Lists all filter mappings for a card.
Lists all cards for a dashboard.
Lists all filters for a dashboard.
Lists all dashboards.
Lists dashboards with optional filtering.
Lists the names of all configured data repositories.
Lists all saved queries.
Lists all relations (tables with schema information) in a data repository.
Lists all schemas in the given repository.
Lists all tables in a data repository.
Lists all visualizations for a query.
Reorders cards in a dashboard.
Returns the configured Ecto repository where Lotus stores query definitions.
Runs all query cards in a dashboard and returns their results.
Runs a single dashboard card and returns its result.
Run a saved query (by struct or id).
Run ad-hoc SQL (bypassing storage), still read-only and sandboxed.
Returns whether unique query names are enforced.
Updates an existing dashboard.
Updates a card.
Updates a filter.
Updates an existing query.
Updates an existing visualization.
Validates a visualization config against query results.
Returns the current version of Lotus.
Types
@type cache_opt() :: :bypass | :refresh | {:ttl_ms, non_neg_integer()} | {:profile, atom()} | {:tags, [binary()]}
@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() ]
@type window_count_mode() :: :none | :exact
@type window_opts() :: [ limit: pos_integer(), offset: non_neg_integer(), count: window_count_mode() ]
Functions
@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)
Creates a new dashboard.
Creates a new card for a dashboard.
Creates a new filter for a dashboard.
Creates a filter mapping connecting a dashboard filter to a card's query variable.
Creates a new saved query.
Creates a new visualization for a query.
Returns all configured data repositories.
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
Deletes a dashboard.
Deletes a card.
Deletes a filter.
Deletes a filter mapping.
Deletes a saved query.
Deletes a visualization (by struct or id).
Disables public sharing for a dashboard.
Enables public sharing for a dashboard by generating a unique token.
Gets a single dashboard by ID. Returns nil if not found.
Gets a single dashboard by ID. Raises if not found.
Gets a dashboard by its public sharing token.
Gets a single card by ID. Returns nil if not found.
Options
:preload- A list of associations to preload
Gets a single card by ID. Raises if not found.
Options
:preload- A list of associations to preload
Gets a single filter by ID. Returns nil if not found.
Gets a single filter by ID. Raises if not found.
Gets a data repository by name.
Raises if the repository is not configured.
Gets a single query by ID. Returns nil if not found.
Gets a single query by ID. Raises if not found.
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")
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}
Lists all filter mappings for a card.
Lists all cards for a dashboard.
Options
:preload- A list of associations to preload (e.g.,[:query, :filter_mappings])
Lists all filters for a dashboard.
Lists all dashboards.
Lists dashboards with optional filtering.
Options
:search- Search term to match against dashboard names
Lists the names of all configured data repositories.
Useful for building UI dropdowns.
Lists all saved queries.
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"}, ...]
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 []
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"]
Lists all visualizations for a query.
Returns visualizations ordered by position, then by id.
Reorders cards in a dashboard.
Returns the configured Ecto repository where Lotus stores query definitions.
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)
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
@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:
- Runtime values from
varsoption (highest priority) - Default values from the query's variable definitions
- 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.
@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), still read-only and sandboxed.
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")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.
Returns whether unique query names are enforced.
Updates an existing dashboard.
Updates a card.
Updates a filter.
Updates an existing query.
Updates an existing visualization.
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.
Returns the current version of Lotus.