DSQLEX

A SQL-like DSL (Domain Specific Language) for evaluating dynamic calculations in Elixir.

DSQLEX allows users to define calculation expressions using familiar SQL syntax, which can be stored, validated, and evaluated at runtime against structured data.

Features

Installation

Add dsqlex to your list of dependencies in mix.exs:

def deps do
  [
    {:dsqlex, "~> 0.1.0"}
  ]
end

Quick Start

# Define your data context
context = %{
  "price" => Decimal.new("500.00"),
  "quantity" => Decimal.new("4"),
  "category" => "B",
  "rate" => Decimal.new("5.00")
}

# Evaluate a simple expression (SELECT is optional)
{:ok, result} = Dsqlex.eval("price / rate", context)
# => {:ok, Decimal.new("100")}

# Evaluate a conditional expression
{:ok, result} = Dsqlex.eval("""
  CASE 
    WHEN category = 'A' THEN quantity 
    WHEN category != 'A' THEN (price / rate) 
  END
""", context)
# => {:ok, Decimal.new("100")}

API

Dsqlex.eval(expression, context)

Evaluates an expression against a context and returns the result. The SELECT keyword is optional.

{:ok, result} = Dsqlex.eval("x * 2", %{"x" => Decimal.new("50")})
# => {:ok, Decimal.new("100")}

{:error, reason} = Dsqlex.eval("unknown_field", %{})
# => {:error, "Unknown field: unknown_field"}

Dsqlex.parse(expression)

Parses an expression and returns the AST without evaluating. Useful for validating expressions before storing them in a database.

{:ok, ast} = Dsqlex.parse("x / y")
# => {:ok, {:select, {:binary_op, :divide, {:identifier, "x"}, {:identifier, "y"}}}}

{:ok, ast} = Dsqlex.parse("1 + 2 + 3")
# => {:ok, {:select, {:binary_op, :plus, {:binary_op, :plus, {:number, "1"}, {:number, "2"}}, {:number, "3"}}}}


{:error, reason} = Dsqlex.parse("1 + 2 * 3")
# => {:error, "Ambiguous expression: mixing +/- and *// requires parentheses"}

Dsqlex.tokenize(expression)

Tokenizes an expression without parsing. Useful for debugging.

{:ok, tokens} = Dsqlex.tokenize("1 + 2")
# => {:ok, [number: "1", operator: :plus, number: "2"]}

Supported Syntax

Literals

Type Examples
Numbers 42, 3.14, 0.5
Strings 'hello', 'world'
Booleans TRUE, FALSE
Null NULL

Comments

Style Description
-- ... SQL line comment, runs to end of line
# ... MySQL-style line comment, runs to end of line
/* ... */ Block comment, may span multiple lines

Comments are stripped at the lexer stage and never reach the parser, so they may appear anywhere whitespace is allowed and may contain any characters (including non-ASCII text) in their bodies.

status_id NOT IN (
  1,  -- pending review
  2,  -- archived – soft-deleted
  3   -- naïve test
)

Arithmetic Operators

Operator Description Example
+ Addition SELECT a + b
- Subtraction SELECT a - b
* Multiplication SELECT a * b
/ Division SELECT a / b

Comparison Operators

Operator Description Example
= Equal SELECT a = b
!= Not equal SELECT a != b
< Less than SELECT a < b
> Greater than SELECT a > b
<= Less than or equal SELECT a <= b
>= Greater than or equal SELECT a >= b

Logical Operators

Operator Description Example
AND Logical AND SELECT a = 1 AND b = 2
OR Logical OR SELECT a = 1 OR b = 2

Note: You can chain the same operator (a AND b AND c), but mixing AND/OR requires parentheses.

CASE/WHEN

CASE 
  WHEN condition1 THEN result1
  WHEN condition2 THEN result2
  ELSE default_result
END

Functions

Function Description Example
ROUND(value, precision) Round to decimal places ROUND(x, 2)
COALESCE(a, b, ...) Return first non-null COALESCE(a, b, 0)
UPPER(string) Convert to uppercase UPPER(x)
LOWER(string) Convert to lowercase LOWER(x)
ABS(number) Absolute value ABS(x)
CONCAT(a, b, ...) Concatenate strings CONCAT(a, ' ', b)

The Parentheses Rule

To eliminate ambiguity and ensure calculation correctness, DSQLEX requires parentheses when mixing operator groups:

# ✅ Valid - single operation
"SELECT a + b"
"SELECT a = 1"

# ✅ Valid - chaining the same operator group
"SELECT a + b + c"        # additive chain, left-associative: (a+b)+c
"SELECT a - b + c"        # additive chain
"SELECT a * b / c"        # multiplicative chain, left-associative: (a*b)/c

# ✅ Valid - parentheses make intent clear
"SELECT (a + b) * c"
"SELECT (a = 1 AND b = 2) OR c = 3"

# ✅ Valid - same logical operator can chain
"SELECT a = 1 AND b = 2 AND c = 3"
"SELECT a = 1 OR b = 2 OR c = 3"

# ❌ Invalid - mixing operator groups requires parentheses
"SELECT a + b * c"        # mixing additive and multiplicative
"SELECT a = 1 AND b = 2 OR c = 3"  # mixing AND/OR

This design choice prioritizes correctness over convenience — mixed-precedence expressions must use parentheses to make the intended order of operations explicit.

Examples

Conditional Selection

expression = """
  CASE 
    WHEN category = 'A' THEN x 
    WHEN category != 'A' THEN (y / z) 
  END
"""

context = %{
  "x" => Decimal.new("100.00"),
  "y" => Decimal.new("500.00"),
  "category" => "B",
  "z" => Decimal.new("5.00")
}

{:ok, result} = Dsqlex.eval(expression, context)
# => {:ok, Decimal.new("100")}

Tiered Calculation

expression = """
  CASE 
    WHEN x > 1000 THEN ROUND(x * 0.02, 2)
    WHEN x > 100 THEN ROUND(x * 0.03, 2)
    ELSE ROUND(x * 0.05, 2)
  END
"""

{:ok, result} = Dsqlex.eval(expression, %{"x" => Decimal.new("500")})
# => {:ok, Decimal.new("15.00")}

Null Handling

expression = "ROUND(COALESCE(x, 0) + y, 2)"

context = %{
  "x" => nil,
  "y" => Decimal.new("99.99")
}

{:ok, result} = Dsqlex.eval(expression, context)
# => {:ok, Decimal.new("99.99")}

Conditional Text

expression = """
  CASE 
    WHEN status = 'active' THEN UPPER(label)
    ELSE 'INACTIVE'
  END
"""

{:ok, result} = Dsqlex.eval(expression, %{"status" => "active", "label" => "hello world"})
# => {:ok, "HELLO WORLD"}

Architecture

DSQLEX uses a classic three-stage pipeline:

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   String    │ ──► │   Tokens    │ ──► │     AST     │ ──► Result
│             │     │             │     │             │
│  "SELECT    │     │ [{:keyword, │     │ {:select,   │
│   a + b"    │     │   :select}, │     │  {:binary_  │
│             │     │  ...]       │     │   op, ...}} │
└─────────────┘     └─────────────┘     └─────────────┘
     Lexer              Parser            Evaluator

Testing

mix test

The test suite includes 162 tests covering:

License

MIT