defmodule Dsqlex do
@moduledoc """
DSQLEX - A SQL-like DSL for evaluating calculations in Elixir.
## Usage
# Define a calculation expression (SELECT is optional)
expression = "CASE WHEN category = 'A' THEN x ELSE (y / z) END"
# Create a context with your data
context = %{
"x" => Decimal.new("100.00"),
"y" => Decimal.new("500.00"),
"category" => "B",
"z" => Decimal.new("5.00")
}
# Evaluate!
{:ok, result} = Dsqlex.eval(expression, context)
# => {:ok, Decimal.new("100")}
## Supported Features
- **Arithmetic:** `+`, `-`, `*`, `/`
- **Comparison:** `=`, `!=`, `<`, `>`, `<=`, `>=`
- **Logical:** `AND`, `OR` (same-operator chaining allowed)
- **Control flow:** `CASE WHEN ... THEN ... ELSE ... END`
- **Functions:** `ROUND()`, `COALESCE()`, `UPPER()`, `LOWER()`, `ABS()`, `CONCAT()`
- **Literals:** Numbers, strings, booleans, NULL
## Parentheses Rule
To avoid ambiguity, expressions that mix operator groups require parentheses:
# Valid
"1 + 2"
"1 + 2 + 3"
"(1 + 2) * 3"
"a = 1 AND b = 2 AND c = 3"
# Invalid (ambiguous)
"1 + 2 * 3"
"a = 1 AND b = 2 OR c = 3"
"""
alias Dsqlex.{Lexer, Parser, Evaluator}
@doc """
Evaluates an expression against a context.
The `SELECT` keyword is optional.
## Parameters
- `expression` - A string containing the expression
- `context` - A map of field names (strings) to values
## Returns
- `{:ok, result}` - The computed result
- `{:error, reason}` - If lexing, parsing, or evaluation fails
## Examples
iex> Dsqlex.eval("1 + 2", %{})
{:ok, Decimal.new("3")}
iex> Dsqlex.eval("x * 2", %{"x" => Decimal.new("50")})
{:ok, Decimal.new("100")}
"""
def eval(expression, context, opts \\ []) when is_binary(expression) and is_map(context) do
with {:ok, tokens} <- Lexer.tokenize(expression),
{:ok, ast} <- Parser.parse(tokens),
{:ok, result} <- Evaluator.evaluate(ast, context, opts) do
{:ok, result}
end
end
@doc """
Parses an expression and returns the AST without evaluating.
Useful for validating expressions before storing them.
The `SELECT` keyword is optional.
## Examples
iex> Dsqlex.parse("x / y")
{:ok, {:select, {:binary_op, :divide, {:identifier, "x"}, {:identifier, "y"}}}}
"""
def parse(expression) when is_binary(expression) do
with {:ok, tokens} <- Lexer.tokenize(expression) do
Parser.parse(tokens)
end
end
@doc """
Tokenizes an expression without parsing.
Useful for debugging or inspecting the lexer output.
"""
def tokenize(expression) when is_binary(expression) do
Lexer.tokenize(expression)
end
end