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