Current section

Files

Jump to
exosql lib parser.ex
Raw

lib/parser.ex

require Logger
defmodule ExoSQL.Parser do
@moduledoc """
Uses leex and yecc to perform a first phase parsing, and then
convert an dprocess the structure using more context knowledge to return
a proper Query struct.
"""
defp real_parse(sql, context) do
sql = String.to_charlist(sql)
{:ok, lexed, _lines} = :sql_lexer.string(sql)
{:ok, parsed} = :sql_parser.parse(lexed)
%{select: select, from: from, where: where, groupby: groupby, join: join} = parsed
# Logger.debug(inspect parsed, pretty: true)
# first resolve all tables
# convert from to cross joins
from = Enum.map(from, &resolve_table(&1, context))
groupby = if groupby do
Enum.map(groupby, &resolve_column(&1, from, context))
else nil end
all_tables = if join != [] do
from ++ Enum.map(join, fn {_type, {table, _expr}} -> resolve_table(table, context) end)
else
from
end
# Logger.debug("All tables #{inspect all_tables}")
join = Enum.map(join, fn {type, {table, expr}} ->
{type, {
resolve_table(table, context),
resolve_column(expr, all_tables, context)
}}
end)
# the resolve all expressions as we know which tables to use
select = Enum.map(select, &resolve_column(&1, all_tables, context))
where = if where do
resolve_column(where, from, context)
else nil end
{:ok, %ExoSQL.Query{
select: select,
from: from, # all the tables it gets data from, but use only the frist and the joins.
where: where,
groupby: groupby,
join: join
}}
end
@doc """
Parses an SQL statement and returns the parsed ExoSQL struct.
"""
def parse(sql, context) do
try do
real_parse(sql, context)
catch
any -> {:error, any}
end
end
def resolve_table({:table, {nil, name}}, context) when is_binary(name) do
options = Enum.flat_map(context, fn {dbname, _db} ->
{:ok, tables} = ExoSQL.schema(dbname, context)
tables
|> Enum.filter(&(&1 == name))
|> Enum.map(&{dbname, &1})
end)
case options do
[table] -> table
l when length(l) == 0 -> throw {:not_found, name}
_other -> throw {:ambiguous_table_name, name}
end
end
def resolve_table({:table, {_db, _name} = orig} , _context), do: orig
def resolve_column({:column, {nil, nil, column}}, tables, context) do
matches = Enum.flat_map(tables, fn {db, table} ->
{:ok, table_schema} = ExoSQL.schema(db, table, context)
Enum.flat_map(table_schema.columns, fn name ->
if name == column do
[{:column, {db, table, name}}]
else
[]
end
end)
end)
case matches do
[{:column, data}] -> {:column, data}
l when length(l) == 0 -> throw {:not_found, column}
_other -> throw {:ambiguous_column_name, column}
end
end
def resolve_column({:column, {nil, table, column}}, tables, _context) do
# Logger.debug("Look for #{table}.#{column} at #{inspect tables}")
matches = Enum.flat_map(tables, fn
{db, ^table} ->
[{:column, {db, table, column}}]
_other ->
[]
end)
case matches do
[{:column, data}] -> {:column, data}
l when length(l) == 0 ->
throw {:not_found, {table, column}}
_other -> throw {:ambiguous_column_name, {table, column}}
end
end
def resolve_column({:column, _} = column, _tables, _context), do: column
def resolve_column({:op, {op, ex1, ex2}}, tables, context) do
{:op, {op, resolve_column(ex1, tables, context), resolve_column(ex2, tables, context)}}
end
def resolve_column(other, _tables, _context) do
other
end
end