Lotus (Lotus v0.9.2)

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

Returns all configured data repositories.

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

Deletes a saved query.

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

Returns the configured Ecto repository where Lotus stores query definitions.

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

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_query(attrs)

Creates a new saved 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_query(query)

Deletes a saved query.

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_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"]

repo()

Returns the configured Ecto repository where Lotus stores query definitions.

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_query(query, attrs)

Updates an existing query.

version()

Returns the current version of Lotus.