defmodule Dsqlex.Parser do
# Arithmetic operators (split by precedence group)
@additive_ops [:plus, :minus]
@multiplicative_ops [:multiply, :divide]
# Comparison operators
@comparison_ops [:eq, :neq, :lt, :gt, :lte, :gte]
def parse(tokens) do
case parse_select(tokens) do
{:ok, ast, []} -> {:ok, ast}
{:ok, _ast, remaining} -> {:error, "Unexpected tokens: #{inspect(remaining)}"}
error -> error
end
end
# SELECT is optional - if present, consume it; if not, just parse the expression
defp parse_select([{:keyword, :select} | rest]) do
case parse_expression(rest) do
{:ok, expr, rest} -> {:ok, {:select, expr}, rest}
error -> error
end
end
defp parse_select(tokens) do
# No SELECT keyword - parse expression directly
case parse_expression(tokens) do
{:ok, expr, rest} -> {:ok, {:select, expr}, rest}
error -> error
end
end
# Expression: starts at the lowest precedence (logical)
defp parse_expression(tokens), do: parse_logical(tokens)
# ============================================================
# LEVEL 1: Logical AND/OR (lowest precedence)
# - Operates on comparison expressions
# - Same-op chaining allowed (a AND b AND c)
# - Mixing AND/OR requires parentheses
# ============================================================
defp parse_logical(tokens) do
with {:ok, left, rest} <- parse_comparison(tokens) do
maybe_parse_logical_op(left, rest)
end
end
defp maybe_parse_logical_op(left, [{:keyword, :and} | rest]) do
with {:ok, right, rest} <- parse_comparison(rest) do
parse_and_chain({:binary_op, :and, left, right}, rest)
end
end
defp maybe_parse_logical_op(left, [{:keyword, :or} | rest]) do
with {:ok, right, rest} <- parse_comparison(rest) do
parse_or_chain({:binary_op, :or, left, right}, rest)
end
end
defp maybe_parse_logical_op(left, rest), do: {:ok, left, rest}
# Continue AND chain
defp parse_and_chain(left, [{:keyword, :and} | rest]) do
with {:ok, right, rest} <- parse_comparison(rest) do
parse_and_chain({:binary_op, :and, left, right}, rest)
end
end
defp parse_and_chain(_left, [{:keyword, :or} | _]) do
{:error, "Ambiguous expression: mixing AND/OR requires parentheses"}
end
defp parse_and_chain(left, rest), do: {:ok, left, rest}
# Continue OR chain
defp parse_or_chain(left, [{:keyword, :or} | rest]) do
with {:ok, right, rest} <- parse_comparison(rest) do
parse_or_chain({:binary_op, :or, left, right}, rest)
end
end
defp parse_or_chain(_left, [{:keyword, :and} | _]) do
{:error, "Ambiguous expression: mixing AND/OR requires parentheses"}
end
defp parse_or_chain(left, rest), do: {:ok, left, rest}
# ============================================================
# LEVEL 2: Comparison operators
# - Operates on arithmetic expressions
# - Only ONE comparison allowed (no chaining)
# ============================================================
defp parse_comparison(tokens) do
with {:ok, left, rest} <- parse_arithmetic(tokens) do
maybe_parse_comparison_op(left, rest)
end
end
defp maybe_parse_comparison_op(left, [{:operator, op} | rest]) when op in @comparison_ops do
with {:ok, right, rest} <- parse_arithmetic(rest) do
# Check for chained comparisons (not allowed)
case rest do
[{:operator, op2} | _] when op2 in @comparison_ops ->
{:error, "Cannot chain comparison operators. Use parentheses."}
_ ->
{:ok, {:binary_op, op, left, right}, rest}
end
end
end
# expr IN (val1, val2, ...)
defp maybe_parse_comparison_op(left, [{:keyword, :in}, {:lparen} | rest]) do
with {:ok, items, rest} <- parse_in_list(rest) do
case rest do
[{:rparen} | rest] -> {:ok, {:in, left, items}, rest}
_ -> {:error, "Expected closing parenthesis ')' after IN list"}
end
end
end
# expr NOT IN (val1, val2, ...)
defp maybe_parse_comparison_op(left, [{:keyword, :not}, {:keyword, :in}, {:lparen} | rest]) do
with {:ok, items, rest} <- parse_in_list(rest) do
case rest do
[{:rparen} | rest] -> {:ok, {:not_in, left, items}, rest}
_ -> {:error, "Expected closing parenthesis ')' after NOT IN list"}
end
end
end
# expr IS NOT NULL / TRUE / FALSE
defp maybe_parse_comparison_op(left, [{:keyword, :is}, {:keyword, :not}, {:keyword, kw} | rest])
when kw in [:null, :true, :false] do
literal = case kw do
:null -> {:null}
:true -> {:boolean, true}
:false -> {:boolean, false}
end
{:ok, {:binary_op, :neq, left, literal}, rest}
end
# expr IS NULL / TRUE / FALSE
defp maybe_parse_comparison_op(left, [{:keyword, :is}, {:keyword, kw} | rest])
when kw in [:null, :true, :false] do
literal = case kw do
:null -> {:null}
:true -> {:boolean, true}
:false -> {:boolean, false}
end
{:ok, {:binary_op, :eq, left, literal}, rest}
end
# expr NOT LIKE pattern
defp maybe_parse_comparison_op(left, [{:keyword, :not}, {:keyword, :like} | rest]) do
with {:ok, pattern, rest} <- parse_primary(rest) do
{:ok, {:not_like, left, pattern}, rest}
end
end
# expr LIKE pattern
defp maybe_parse_comparison_op(left, [{:keyword, :like} | rest]) do
with {:ok, pattern, rest} <- parse_primary(rest) do
{:ok, {:like, left, pattern}, rest}
end
end
defp maybe_parse_comparison_op(left, rest), do: {:ok, left, rest}
# ============================================================
# LEVEL 3: Arithmetic operators
# - Operates on primaries
# - Same-group chaining allowed (a + b - c, a * b / c)
# - Mixing additive (+/-) and multiplicative (*//) requires parentheses
# ============================================================
defp parse_arithmetic(tokens) do
with {:ok, left, rest} <- parse_primary(tokens) do
maybe_parse_arithmetic_op(left, rest)
end
end
defp maybe_parse_arithmetic_op(left, [{:operator, op} | rest]) when op in @additive_ops do
with {:ok, right, rest} <- parse_primary(rest) do
parse_additive_chain({:binary_op, op, left, right}, rest)
end
end
defp maybe_parse_arithmetic_op(left, [{:operator, op} | rest]) when op in @multiplicative_ops do
with {:ok, right, rest} <- parse_primary(rest) do
parse_multiplicative_chain({:binary_op, op, left, right}, rest)
end
end
defp maybe_parse_arithmetic_op(left, rest), do: {:ok, left, rest}
# Continue additive chain (+/-)
defp parse_additive_chain(left, [{:operator, op} | rest]) when op in @additive_ops do
with {:ok, right, rest} <- parse_primary(rest) do
parse_additive_chain({:binary_op, op, left, right}, rest)
end
end
defp parse_additive_chain(_left, [{:operator, op} | _]) when op in @multiplicative_ops do
{:error, "Ambiguous expression: mixing +/- and *// requires parentheses"}
end
defp parse_additive_chain(left, rest), do: {:ok, left, rest}
# Continue multiplicative chain (*//)
defp parse_multiplicative_chain(left, [{:operator, op} | rest]) when op in @multiplicative_ops do
with {:ok, right, rest} <- parse_primary(rest) do
parse_multiplicative_chain({:binary_op, op, left, right}, rest)
end
end
defp parse_multiplicative_chain(_left, [{:operator, op} | _]) when op in @additive_ops do
{:error, "Ambiguous expression: mixing +/- and *// requires parentheses"}
end
defp parse_multiplicative_chain(left, rest), do: {:ok, left, rest}
# ============================================================
# LEVEL 4: Primary expressions (highest precedence)
# ============================================================
defp parse_primary([{:number, n} | rest]), do: {:ok, {:number, n}, rest}
defp parse_primary([{:string, s} | rest]), do: {:ok, {:string, s}, rest}
defp parse_primary([{:identifier, name} | rest]), do: {:ok, {:identifier, name}, rest}
defp parse_primary([{:keyword, :null} | rest]), do: {:ok, {:null}, rest}
defp parse_primary([{:keyword, :true} | rest]), do: {:ok, {:boolean, true}, rest}
defp parse_primary([{:keyword, :false} | rest]), do: {:ok, {:boolean, false}, rest}
# Parenthesized expression - resets to top level (logical)
defp parse_primary([{:lparen} | rest]) do
with {:ok, expr, rest} <- parse_expression(rest) do
case rest do
[{:rparen} | rest] -> {:ok, expr, rest}
_ -> {:error, "Expected closing parenthesis ')'"}
end
end
end
# CASE WHEN ... THEN ... [WHEN ... THEN ...] [ELSE ...] END
defp parse_primary([{:keyword, :case} | rest]) do
with {:ok, when_clauses, rest} <- parse_when_clauses(rest),
{:ok, else_clause, rest} <- parse_else_clause(rest),
{:ok, rest} <- expect_keyword(:end, rest) do
{:ok, {:case_expr, when_clauses, else_clause}, rest}
end
end
# Function call: FUNCTION_NAME(arg1, arg2, ...)
defp parse_primary([{:function, name}, {:lparen} | rest]) do
with {:ok, args, rest} <- parse_function_args(rest) do
case rest do
[{:rparen} | rest] -> {:ok, {:call, name, args}, rest}
_ -> {:error, "Expected closing parenthesis ')' after function arguments"}
end
end
end
defp parse_primary(tokens), do: {:error, "Unexpected token: #{inspect(tokens)}"}
# ============================================================
# IN list helpers
# ============================================================
# Parse comma-separated values inside IN (...)
defp parse_in_list([{:rparen} | _] = tokens), do: {:ok, [], tokens}
defp parse_in_list(tokens) do
with {:ok, first, rest} <- parse_primary(tokens) do
parse_more_in_items([first], rest)
end
end
defp parse_more_in_items(items, [{:comma} | rest]) do
with {:ok, item, rest} <- parse_primary(rest) do
parse_more_in_items(items ++ [item], rest)
end
end
defp parse_more_in_items(items, rest), do: {:ok, items, rest}
# ============================================================
# Function call helpers
# ============================================================
# Parse comma-separated arguments (can be empty)
defp parse_function_args([{:rparen} | _] = tokens), do: {:ok, [], tokens}
defp parse_function_args(tokens) do
with {:ok, first_arg, rest} <- parse_expression(tokens) do
parse_more_args([first_arg], rest)
end
end
defp parse_more_args(args, [{:comma} | rest]) do
with {:ok, arg, rest} <- parse_expression(rest) do
parse_more_args(args ++ [arg], rest)
end
end
defp parse_more_args(args, rest), do: {:ok, args, rest}
# ============================================================
# CASE/WHEN helpers
# ============================================================
# Parse one or more WHEN clauses
defp parse_when_clauses(tokens) do
case parse_when_clause(tokens) do
{:ok, clause, rest} ->
parse_more_when_clauses([clause], rest)
{:error, _} = error ->
error
end
end
defp parse_more_when_clauses(clauses, [{:keyword, :when} | _] = tokens) do
case parse_when_clause(tokens) do
{:ok, clause, rest} ->
parse_more_when_clauses(clauses ++ [clause], rest)
{:error, _} = error ->
error
end
end
defp parse_more_when_clauses(clauses, rest), do: {:ok, clauses, rest}
# Parse single: WHEN condition THEN result
defp parse_when_clause([{:keyword, :when} | rest]) do
with {:ok, condition, rest} <- parse_expression(rest),
{:ok, rest} <- expect_keyword(:then, rest),
{:ok, result, rest} <- parse_expression(rest) do
{:ok, {:when, condition, result}, rest}
end
end
defp parse_when_clause(_tokens), do: {:error, "Expected WHEN clause"}
# Parse optional: ELSE result
defp parse_else_clause([{:keyword, :else} | rest]) do
parse_expression(rest)
end
defp parse_else_clause(rest), do: {:ok, nil, rest}
# Helper to expect a specific keyword
defp expect_keyword(expected, [{:keyword, actual} | rest]) when expected == actual do
{:ok, rest}
end
defp expect_keyword(expected, tokens) do
{:error, "Expected #{String.upcase(to_string(expected))}, got: #{inspect(Enum.take(tokens, 1))}"}
end
end