Lotus (Lotus v0.14.0)

Copy Markdown View Source

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])

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 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.

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 an existing query.

Updates an existing visualization.

Validates a visualization config against query results.

Returns the current version of Lotus.

Types

cache_opt()

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

opts()

@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()
]

window_count_mode()

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

window_opts()

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

Functions

can_run?(query, opts \\ [])

@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(opts)

create_dashboard(attrs)

Creates a new dashboard.

create_dashboard_card(dashboard_or_id, attrs)

Creates a new card for a dashboard.

create_dashboard_filter(dashboard_or_id, attrs)

Creates a new filter for a dashboard.

create_filter_mapping(card, filter, variable_name, opts \\ [])

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

create_query(attrs)

Creates a new saved query.

create_visualization(query_or_id, attrs)

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(dashboard)

Deletes a dashboard.

delete_dashboard_card(card_or_id)

Deletes a card.

delete_dashboard_filter(filter_or_id)

Deletes a filter.

delete_filter_mapping(mapping_or_id)

Deletes a filter mapping.

delete_query(query)

Deletes a saved query.

delete_visualization(viz_or_id)

Deletes a visualization (by struct or id).

disable_public_sharing(dashboard)

Disables public sharing for a dashboard.

enable_public_sharing(dashboard)

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

get_dashboard(id)

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

get_dashboard!(id)

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

get_dashboard_by_token(token)

Gets a dashboard by its public sharing token.

get_dashboard_card(id, opts \\ [])

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

Options

  • :preload - A list of associations to preload

get_dashboard_card!(id, opts \\ [])

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

Options

  • :preload - A list of associations to preload

get_dashboard_filter(id)

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

get_dashboard_filter!(id)

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

get_data_repo!(name)

Gets a data repository by name.

Raises if the repository is not configured.

get_query(id)

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

get_query!(id)

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

get_table_schema(repo_or_name, table_name, opts \\ [])

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(repo_or_name, table_name, opts \\ [])

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(card_or_id)

Lists all filter mappings for a card.

list_dashboard_cards(dashboard_or_id, opts \\ [])

Lists all cards for a dashboard.

Options

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

list_dashboard_filters(dashboard_or_id)

Lists all filters for a dashboard.

list_dashboards()

Lists all dashboards.

list_dashboards_by(opts)

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(repo_or_name, opts \\ [])

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(repo_or_name, opts \\ [])

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(repo_or_name, opts \\ [])

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(query_or_id)

Lists all visualizations for a query.

Returns visualizations ordered by position, then by id.

reorder_dashboard_cards(dashboard_or_id, card_ids)

Reorders cards in a dashboard.

repo()

Returns the configured Ecto repository where Lotus stores query definitions.

run_dashboard(dashboard_or_id, opts \\ [])

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(card_or_id, opts \\ [])

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(query_or_id, opts \\ [])

@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(sql, params \\ [], opts \\ [])

@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.

start_link(opts \\ [])

unique_names?()

Returns whether unique query names are enforced.

update_dashboard(dashboard, attrs)

Updates an existing dashboard.

update_dashboard_card(card, attrs)

Updates a card.

update_dashboard_filter(filter, attrs)

Updates a filter.

update_query(query, attrs)

Updates an existing query.

update_visualization(viz, attrs)

Updates an existing visualization.

validate_visualization_config(config, result)

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.