Skip to content

Instantly share code, notes, and snippets.

@stuartjohnpage
Created November 19, 2025 20:29
Show Gist options
  • Select an option

  • Save stuartjohnpage/5ae7c4e032e79cbc0949f34a4052ad14 to your computer and use it in GitHub Desktop.

Select an option

Save stuartjohnpage/5ae7c4e032e79cbc0949f34a4052ad14 to your computer and use it in GitHub Desktop.
A complete example of a delta-sharing predicate parser using Nimble Parsec
defmodule Revelry.DeltaSharing.PredicateParser do
@moduledoc """
Parser for Delta Sharing predicate expressions using NimbleParsec.
Parses SQL-like filter predicates such as:
- `"project_id = 123"`
- `"status != 'closed'"`
- `"created_at >= '2024-01-01'"`
- `"price > 99.99"`
Returns tuples in the format `{operator, column_name, value}` where:
- operator: `:eq`, `:neq`, `:gt`, `:lt`, `:gte`, `:lte`
- column_name: string
- value: parsed value (integer, float, boolean, nil, or string)
"""
import NimbleParsec
whitespace = ascii_string([?\s, ?\t], min: 0)
column_name =
[?a..?z, ?A..?Z, ?0..?9, ?_, ?.]
|> ascii_string(min: 1)
|> unwrap_and_tag(:column)
operator =
[
">=" |> string() |> replace(:gte),
"<=" |> string() |> replace(:lte),
"!=" |> string() |> replace(:neq),
"=" |> string() |> replace(:eq),
">" |> string() |> replace(:gt),
"<" |> string() |> replace(:lt)
]
|> choice()
|> unwrap_and_tag(:op)
quoted_string =
choice([
"'"
|> string()
|> ignore()
|> repeat(
choice([
"\\'" |> string() |> replace(?'),
"\\\"" |> string() |> replace(?"),
utf8_char(not: ?')
])
)
|> ignore(string("'"))
|> reduce({List, :to_string, []}),
"\""
|> string()
|> ignore()
|> repeat(
choice([
"\\\"" |> string() |> replace(?"),
"\\'" |> string() |> replace(?'),
utf8_char(not: ?")
])
)
|> ignore(string("\""))
|> reduce({List, :to_string, []})
])
number =
[?-, ?+]
|> ascii_char()
|> optional()
|> ascii_string([?0..?9], min: 1)
|> optional(
"."
|> string()
|> ascii_string([?0..?9], min: 1)
)
|> reduce(:parse_number)
literal =
choice([
"true" |> string() |> replace(true),
"TRUE" |> string() |> replace(true),
"false" |> string() |> replace(false),
"FALSE" |> string() |> replace(false),
"null" |> string() |> replace(nil),
"NULL" |> string() |> replace(nil)
])
value =
[
quoted_string,
literal,
number
]
|> choice()
|> unwrap_and_tag(:value)
predicate =
whitespace
|> concat(column_name)
|> concat(whitespace)
|> concat(operator)
|> concat(whitespace)
|> concat(value)
|> concat(whitespace)
|> eos()
|> reduce(:build_predicate)
defparsec(:predicate, predicate)
defp parse_number(parts) do
number_string =
Enum.map_join(parts, fn
?- -> "-"
?+ -> "+"
part when is_binary(part) -> part
end)
case Integer.parse(number_string) do
{value, ""} ->
value
{_value, _remainder} ->
{value, ""} = Float.parse(number_string)
value
end
end
defp build_predicate(parts) do
column = Keyword.get(parts, :column)
op = Keyword.get(parts, :op)
value = Keyword.get(parts, :value)
{op, column, value}
end
@doc """
Parse a predicate string into a tuple.
Returns `{:ok, {operator, column, value}}` on success,
or `{:error, reason}` on failure.
## Examples
iex> parse_predicate("project_id = 123")
{:ok, {:eq, "project_id", 123}}
iex> parse_predicate("status != 'closed'")
{:ok, {:neq, "status", "closed"}}
iex> parse_predicate("price >= 99.99")
{:ok, {:gte, "price", 99.99}}
iex> parse_predicate("active = true")
{:ok, {:eq, "active", true}}
"""
def parse_predicate(predicate_string) when is_binary(predicate_string) do
case predicate(predicate_string) do
{:ok, [result], "", _, _, _} ->
{:ok, result}
{:error, reason, _rest, _context, {line, col}, _byte_offset} ->
{:error, "parse error at line #{line}, column #{col}: #{reason}"}
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment