Created
November 19, 2025 20:29
-
-
Save stuartjohnpage/5ae7c4e032e79cbc0949f34a4052ad14 to your computer and use it in GitHub Desktop.
A complete example of a delta-sharing predicate parser using Nimble Parsec
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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