Skip to content

Instantly share code, notes, and snippets.

@hsavit1
Forked from sasa1977/sql_parser.exs
Created June 5, 2020 01:57
Show Gist options
  • Save hsavit1/ddbd9f3d169cd995a4792eed210db1e7 to your computer and use it in GitHub Desktop.
Save hsavit1/ddbd9f3d169cd995a4792eed210db1e7 to your computer and use it in GitHub Desktop.

Revisions

  1. @sasa1977 sasa1977 created this gist Oct 13, 2019.
    161 changes: 161 additions & 0 deletions sql_parser.exs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,161 @@
    defmodule SqlParser do
    def run() do
    input = "select col1 from (
    select col2, col3 from (
    select col4, col5, col6 from some_table
    )
    )
    "
    IO.puts("input: #{inspect(input)}\n")
    IO.inspect(parse(input))
    end

    defp parse(input) do
    parser = select_statement()
    parser.(input)
    end

    defp select_statement() do
    sequence([
    keyword(:select),
    columns(),
    keyword(:from),
    choice([token(identifier()), subquery()])
    ])
    |> map(fn [_, columns, _, from] ->
    %{
    statement: :select,
    columns: columns,
    from: from
    }
    end)
    end

    defp subquery() do
    sequence([
    token(char(?()),
    lazy(fn -> select_statement() end),
    token(char(?)))
    ])
    |> map(fn [_, select_statement, _] -> select_statement end)
    end

    defp lazy(combinator) do
    fn input ->
    parser = combinator.()
    parser.(input)
    end
    end

    defp keyword(expected) do
    identifier()
    |> token()
    |> satisfy(fn identifier ->
    String.upcase(identifier) == String.upcase(to_string(expected))
    end)
    |> map(fn _ -> expected end)
    end

    defp columns(), do: separated_list(token(identifier()), token(char(?,)))

    defp separated_list(element_parser, separator_parser) do
    sequence([
    element_parser,
    many(sequence([separator_parser, element_parser]))
    ])
    |> map(fn [first_element, rest] ->
    other_elements = Enum.map(rest, fn [_, element] -> element end)
    [first_element | other_elements]
    end)
    end

    defp token(parser) do
    sequence([
    many(choice([char(?\s), char(?\n)])),
    parser,
    many(choice([char(?\s), char(?\n)]))
    ])
    |> map(fn [_lw, term, _tw] -> term end)
    end

    defp sequence(parsers) do
    fn input ->
    case parsers do
    [] ->
    {:ok, [], input}

    [first_parser | other_parsers] ->
    with {:ok, first_term, rest} <- first_parser.(input),
    {:ok, other_terms, rest} <- sequence(other_parsers).(rest),
    do: {:ok, [first_term | other_terms], rest}
    end
    end
    end

    defp map(parser, mapper) do
    fn input ->
    with {:ok, term, rest} <- parser.(input),
    do: {:ok, mapper.(term), rest}
    end
    end

    defp identifier() do
    many(identifier_char())
    |> satisfy(fn chars -> chars != [] end)
    |> map(fn chars -> to_string(chars) end)
    end

    defp many(parser) do
    fn input ->
    case parser.(input) do
    {:error, _reason} ->
    {:ok, [], input}

    {:ok, first_term, rest} ->
    {:ok, other_terms, rest} = many(parser).(rest)
    {:ok, [first_term | other_terms], rest}
    end
    end
    end

    defp identifier_char(), do: choice([ascii_letter(), char(?_), digit()])

    defp choice(parsers) do
    fn input ->
    case parsers do
    [] ->
    {:error, "no parser suceeded"}

    [first_parser | other_parsers] ->
    with {:error, _reason} <- first_parser.(input),
    do: choice(other_parsers).(input)
    end
    end
    end

    defp digit(), do: satisfy(char(), fn char -> char in ?0..?9 end)
    defp ascii_letter(), do: satisfy(char(), fn char -> char in ?A..?Z or char in ?a..?z end)

    defp char(expected), do: satisfy(char(), fn char -> char == expected end)

    defp satisfy(parser, acceptor) do
    fn input ->
    with {:ok, term, rest} <- parser.(input) do
    if acceptor.(term),
    do: {:ok, term, rest},
    else: {:error, "term rejected"}
    end
    end
    end

    defp char() do
    fn input ->
    case input do
    "" -> {:error, "unexpected end of input"}
    <<char::utf8, rest::binary>> -> {:ok, char, rest}
    end
    end
    end
    end

    SqlParser.run()