Packages
exosql
0.2.15
0.2.88
0.2.87
0.2.86
0.2.85
0.2.84
0.2.83
0.2.82
0.2.81
0.2.80
0.2.79
0.2.78
0.2.77
0.2.76
0.2.75
0.2.74
0.2.73
0.2.72
0.2.71
0.2.70
0.2.69
0.2.68
0.2.67
0.2.65
0.2.64
0.2.63
0.2.62
0.2.60
0.2.59
0.2.58
0.2.57
0.2.56
0.2.55
0.2.54
0.2.53
0.2.51
0.2.50
0.2.49
0.2.48
0.2.47
0.2.46
0.2.45
0.2.44
0.2.43
0.2.42
0.2.41
0.2.40
0.2.39
0.2.38
0.2.37
0.2.36
0.2.35
0.2.33
0.2.32
0.2.31
0.2.30
0.2.29
0.2.28
0.2.26
0.2.25
0.2.24
0.2.23
0.2.22
0.2.21
0.2.20
0.2.19
0.2.18
0.2.17
0.2.16
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.1
0.1.0
Universal SQL engine for Elixir. This library implements the SQL logic to perform queries on user provided databases using a simple interface based on Foreign Data Wrappers from PostgreSQL.
Current section
Files
Jump to
Current section
Files
lib/parser.ex
require Logger
defmodule ExoSQL.Parser do
@moduledoc """
Parsed an SQL statement into a ExoSQL.Query.
The Query then needs to be planned and executed.
It also resolves partial column and table names using data from the context
and its schema functions.
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,
orderby: orderby
} = 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 = case select do
[{:all_columns}] ->
Enum.flat_map(all_tables, fn {db, table} ->
{:ok, %{ columns: columns }} = ExoSQL.schema(db, table, context)
Enum.map(columns, &{:column, {db, table, &1}})
end)
_other ->
Enum.map(select, &resolve_column(&1, all_tables, context))
end
where = if where do
resolve_column(where, from, context)
else nil end
# Resolve orderby
orderby = Enum.map(orderby, fn
{type, expr} ->
{type, resolve_column(expr, all_tables, context)}
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,
orderby: orderby
}}
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({:fn, {f, params}}, tables, context) do
params = Enum.map(params, &resolve_column(&1, tables, context))
{:fn, {f, params}}
end
def resolve_column(other, _tables, _context) do
other
end
end