Packages
exosql
0.2.85
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/builtins.ex
require Logger
require Integer
defmodule ExoSQL.Builtins do
@moduledoc """
Builtin functions.
There are two categories, normal functions and aggregate functions. Aggregate
functions receive as first parameter a ExoSQL.Result with a full table,
and the rest of parameters are the function calling parameters, unsolved.
These expressions must be first simplified with
`ExoSQL.Expr.simplify` and then executed on the rows with
`ExoSQL.Expr.run_expr`.
"""
import ExoSQL.Utils, only: [to_number: 1, to_float: 1]
@functions %{
"bool" => {ExoSQL.Builtins, :bool},
"lower" => {ExoSQL.Builtins, :lower},
"upper" => {ExoSQL.Builtins, :upper},
"split" => {ExoSQL.Builtins, :split},
"join" => {ExoSQL.Builtins, :join},
"to_string" => {ExoSQL.Builtins, :to_string_},
"to_datetime" => {ExoSQL.Builtins, :to_datetime},
"to_timestamp" => {ExoSQL.Builtins, :to_timestamp},
"to_number" => {ExoSQL.Utils, :to_number!},
"substr" => {ExoSQL.Builtins, :substr},
"now" => {ExoSQL.Builtins, :now},
"strftime" => {ExoSQL.Builtins, :strftime},
"format" => {ExoSQL.Builtins, :format},
"debug" => {ExoSQL.Builtins, :debug},
"width_bucket" => {ExoSQL.Builtins, :width_bucket},
"generate_series" => {ExoSQL.Builtins, :generate_series},
"urlparse" => {ExoSQL.Builtins, :urlparse},
"jp" => {ExoSQL.Builtins, :jp},
"json" => {ExoSQL.Builtins, :json},
"unnest" => {ExoSQL.Builtins, :unnest},
"regex" => {ExoSQL.Builtins, :regex},
"regex_all" => {ExoSQL.Builtins, :regex_all},
"random" => {ExoSQL.Builtins, :random},
"randint" => {ExoSQL.Builtins, :randint},
"range" => {ExoSQL.Builtins, :range},
"greatest" => {ExoSQL.Builtins, :greatest},
"lowest" => {ExoSQL.Builtins, :lowest},
"coalesce" => {ExoSQL.Builtins, :coalesce},
"nullif" => {ExoSQL.Builtins, :nullif},
"datediff" => {ExoSQL.DateTime, :datediff},
## Math
"round" => {ExoSQL.Builtins, :round},
"trunc" => {ExoSQL.Builtins, :trunc},
"floor" => {ExoSQL.Builtins, :floor},
"ceil" => {ExoSQL.Builtins, :ceil},
"power" => {ExoSQL.Builtins, :power},
"sqrt" => {ExoSQL.Builtins, :sqrt},
"log" => {ExoSQL.Builtins, :log},
"ln" => {ExoSQL.Builtins, :ln},
"abs" => {ExoSQL.Builtins, :abs},
"mod" => {ExoSQL.Builtins, :mod},
"sign" => {ExoSQL.Builtins, :sign},
## Aggregates
"count" => {ExoSQL.Builtins, :count},
"sum" => {ExoSQL.Builtins, :sum},
"avg" => {ExoSQL.Builtins, :avg},
"max" => {ExoSQL.Builtins, :max_},
"min" => {ExoSQL.Builtins, :min_}
}
def call_function({mod, fun, name}, params) do
try do
apply(mod, fun, params)
rescue
_excp ->
# Logger.debug("Exception #{inspect _excp}: #{inspect {{mod, fun}, params}}")
throw({:function, {name, params}})
end
end
def call_function(name, params) do
case @functions[name] do
nil ->
raise {:unknown_function, name}
{mod, fun} ->
try do
apply(mod, fun, params)
rescue
_excp ->
# Logger.debug("Exception #{inspect(_excp)}: #{inspect({{mod, fun}, params})}")
throw({:function, {name, params}})
end
end
end
def cant_simplify(f) do
is_aggregate(f) or f in ["random", "randint", "debug"]
end
def is_projectable(f) do
f in ["unnest", "generate_series"]
end
def round(n) do
{:ok, n} = to_float(n)
Kernel.round(n)
end
def round(n, 0) do
{:ok, n} = to_float(n)
Kernel.round(n)
end
def round(n, "0") do
{:ok, n} = to_float(n)
Kernel.round(n)
end
def round(n, r) do
{:ok, n} = to_float(n)
{:ok, r} = to_number(r)
Float.round(n, r)
end
def power(nil, _), do: nil
def power(_, nil), do: nil
def power(_, 0), do: 1
# To allow big power. From https://stackoverflow.com/questions/32024156/how-do-i-raise-a-number-to-a-power-in-elixir#32024157
def power(x, n) when Integer.is_odd(n) do
{:ok, x} = to_number(x)
{:ok, n} = to_number(n)
x * :math.pow(x, n - 1)
end
def power(x, n) do
{:ok, x} = to_number(x)
{:ok, n} = to_number(n)
result = :math.pow(x, n / 2)
result * result
end
def sqrt(nil), do: nil
def sqrt(n) do
{:ok, n} = to_number(n)
:math.sqrt(n)
end
def log(nil), do: nil
def log(n) do
{:ok, n} = to_number(n)
:math.log10(n)
end
def ln(nil), do: nil
def ln(n) do
{:ok, n} = to_number(n)
:math.log(n)
end
def abs(nil), do: nil
def abs(n) do
{:ok, n} = to_number(n)
:erlang.abs(n)
end
def mod(nil, _), do: nil
def mod(_, nil), do: nil
def mod(n, m) do
{:ok, n} = to_number(n)
{:ok, m} = to_number(m)
:math.fmod(n, m)
end
def sign(nil), do: nil
def sign(n) do
{:ok, n} = to_number(n)
cond do
n < 0 -> -1
n == 0 -> 0
true -> 1
end
end
def random(), do: :rand.uniform()
def randint(max_) do
:rand.uniform(max_ - 1)
end
def randint(min_, max_) do
:rand.uniform(max_ - min_ - 1) + min_ - 1
end
def bool(nil), do: false
def bool(0), do: false
def bool(""), do: false
def bool(false), do: false
def bool(_), do: true
def lower(nil), do: nil
def lower({:range, {a, _b}}), do: a
def lower(s), do: String.downcase(s)
def upper(nil), do: nil
def upper({:range, {_a, b}}), do: b
def upper(s), do: String.upcase(s)
def to_string_(%DateTime{} = d), do: DateTime.to_iso8601(d)
def to_string_(%{} = d) do
{:ok, e} = Poison.encode(d)
e
end
def to_string_(s), do: to_string(s)
def now(), do: Timex.local()
def now(tz), do: Timex.now(tz)
def to_datetime(nil), do: nil
def to_datetime(other), do: ExoSQL.DateTime.to_datetime(other)
def to_datetime(other, mod), do: ExoSQL.DateTime.to_datetime(other, mod)
def to_timestamp(%DateTime{} = d), do: DateTime.to_unix(d)
def substr(nil, _skip, _len) do
""
end
def substr(str, skip, len) do
# force string
str = to_string_(str)
{:ok, skip} = to_number(skip)
{:ok, len} = to_number(len)
if len < 0 do
String.slice(str, skip, max(0, String.length(str) + len - skip))
else
String.slice(str, skip, len)
end
end
def substr(str, skip) do
# A upper limit on what to return, should be enought
substr(str, skip, 10_000)
end
def join(str, sep \\ ",") do
Enum.join(str, sep)
end
def split(nil, _sep), do: []
def split(str, sep) do
String.split(str, sep)
end
def split(str) do
String.split(str, [", ", ",", " "])
end
@doc ~S"""
Convert datetime to string.
If no format is given, it is as to_string, which returns the ISO 8601.
Format allows all substitutions from
[Timex.format](https://hexdocs.pm/timex/Timex.Format.DateTime.Formatters.Strftime.html),
for example:
%d day of month: 00
%H hour: 00-24
%m month: 01-12
%M minute: 00-59
%s seconds since 1970-01-01
%S seconds: 00-59
%Y year: 0000-9999
%i ISO 8601 format
%V Week number
%% %
"""
def strftime(%DateTime{} = d), do: to_string_(d)
def strftime(%DateTime{} = d, format), do: ExoSQL.DateTime.strftime(d, format)
def strftime(other, format), do: strftime(to_datetime(other), format)
@doc ~S"""
sprintf style formatting.
Known interpolations:
%d - Integer
%f - Float, 2 digits
%.Nf - Float N digits
%k - integer with k, M sufix
%.k - float with k, M sufix, uses float part
"""
def format(str), do: ExoSQL.Format.format(str, [])
def format(str, args) when is_list(args) do
ExoSQL.Format.format(str, args)
end
@doc ~S"""
Very simple sprintf formatter. Knows this formats:
* %%
* %s
* %d
* %f (only two decimals)
* %.{ndec}f
"""
def format(str, arg1), do: format(str, [arg1])
def format(str, arg1, arg2), do: format(str, [arg1, arg2])
def format(str, arg1, arg2, arg3), do: format(str, [arg1, arg2, arg3])
def format(str, arg1, arg2, arg3, arg4), do: format(str, [arg1, arg2, arg3, arg4])
def format(str, arg1, arg2, arg3, arg4, arg5), do: format(str, [arg1, arg2, arg3, arg4, arg5])
def format(str, arg1, arg2, arg3, arg4, arg5, arg6),
do: format(str, [arg1, arg2, arg3, arg4, arg5, arg6])
def format(str, arg1, arg2, arg3, arg4, arg5, arg6, arg7),
do: format(str, [arg1, arg2, arg3, arg4, arg5, arg6, arg7])
def format(str, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8),
do: format(str, [arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8])
def format(str, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9),
do: format(str, [arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9])
@doc ~S"""
Print some value to the log
"""
def debug(str) do
Logger.debug("SQL DEBUG: #{inspect(str)}")
str
end
@doc ~S"""
Returns to which bucket it belongs.
Only numbers, but datetimes can be transformed to unix datetime.
"""
def width_bucket(n, start_, end_, nbuckets) do
import ExoSQL.Utils, only: [to_float!: 1, to_number!: 1]
n = to_float!(n)
start_ = to_float!(start_)
end_ = to_float!(end_)
nbuckets = to_number!(nbuckets)
bucket = (n - start_) * nbuckets / (end_ - start_)
bucket = bucket |> Kernel.round()
cond do
bucket < 0 -> 0
bucket >= nbuckets -> nbuckets - 1
true -> bucket
end
end
@doc ~S"""
Performs a regex match
May return a list of groups, or a dict with named groups, depending on
the regex.
As an optional third parameter it performs a jp query.
Returns NULL if no match (which is falsy, so can be used for expressions)
"""
def regex(str, regexs) do
# slow. Should have been precompiled (simplify)
regex_real(str, regexs)
end
def regex(str, regexs, query) do
jp(regex_real(str, regexs), query)
end
def regex_real(str, {regex, captures}) do
if captures do
Regex.named_captures(regex, str)
else
Regex.run(regex, str)
end
end
def regex_real(str, regexs) when is_binary(regexs) do
captures = String.contains?(regexs, "(?<")
regex = Regex.compile!(regexs)
regex_real(str, {regex, captures})
end
@doc ~S"""
Performs a regex scan
Returns all the matches, if groups are used, then its a list of groups matching.
As an optional third parameter it performs a jp query.
Returns NULL if no match (which is falsy, so can be used for expressions)
"""
def regex_all(str, regexs) do
# slow. Should have been precompiled (simplify)
regex_all_real(str, regexs)
end
def regex_all(str, regexs, query) do
regex_all_real(str, regexs)
|> Enum.map(&jp(&1, query))
end
def regex_all_real(str, regexs) when is_binary(regexs) do
regex = Regex.compile!(regexs)
regex_all_real(str, regex)
end
def regex_all_real(str, regex) do
Regex.scan(regex, str)
end
@doc ~S"""
Generates a table with the series of numbers as given. Use for histograms
without holes.
"""
def generate_series(end_), do: generate_series(1, end_, 1)
def generate_series(start_, end_), do: generate_series(start_, end_, 1)
def generate_series(%DateTime{} = start_, %DateTime{} = end_, days) when is_number(days) do
generate_series(start_, end_, "#{days}D")
end
def generate_series(%DateTime{} = start_, %DateTime{} = end_, mod) when is_binary(mod) do
duration =
case ExoSQL.DateTime.Duration.parse(mod) do
{:error, other} ->
throw({:error, other})
%ExoSQL.DateTime.Duration{seconds: 0, days: 0, months: 0, years: 0} ->
throw({:error, :invalid_duration})
{:ok, other} ->
other
end
cmp =
if ExoSQL.DateTime.Duration.is_negative(duration) do
:lt
else
:gt
end
rows =
ExoSQL.Utils.generate(start_, fn value ->
cmpr = DateTime.compare(value, end_)
if cmpr == cmp do
:halt
else
next = ExoSQL.DateTime.Duration.datetime_add(value, duration)
{[value], next}
end
end)
%ExoSQL.Result{
columns: [{:tmp, :tmp, "generate_series"}],
rows: rows
}
end
def generate_series(start_, end_, step)
when is_number(start_) and is_number(end_) and is_number(step) do
if step == 0 do
raise ArgumentError, "Step invalid. Will never reach end."
end
if step < 0 and start_ < end_ do
raise ArgumentError, "Start, end and step invalid. Will never reach end."
end
if step > 0 and start_ > end_ do
raise ArgumentError, "Start, end and step invalid. Will never reach end."
end
%ExoSQL.Result{
columns: [{:tmp, :tmp, "generate_series"}],
rows: generate_series_range(start_, end_, step)
}
end
def generate_series(start_, end_, step) do
import ExoSQL.Utils, only: [to_number!: 1, to_number: 1]
# there are two options: numbers or dates. Check if I can convert the start_ to a number
# and if so, do the generate_series for numbers
case to_number(start_) do
{:ok, start_} ->
generate_series(start_, to_number!(end_), to_number!(step))
# maybe a date
{:error, _} ->
generate_series(to_datetime(start_), to_datetime(end_), step)
end
end
defp generate_series_range(current, stop, step) do
cond do
step > 0 and current > stop ->
[]
step < 0 and current < stop ->
[]
true ->
[[current] | generate_series_range(current + step, stop, step)]
end
end
@doc ~S"""
Parses an URL and return some part of it.
If not what is provided, returns a JSON object with:
* host
* port
* scheme
* path
* query
* user
If what is passed, it performs a JSON Pointer search (jp function).
It must receive a url with scheme://server or the result may not be well
formed.
For example, for emails, just use "email://connect@serverboards.io" or
similar.
"""
def urlparse(url), do: urlparse(url, nil)
def urlparse(nil, what), do: urlparse("", what)
def urlparse(url, what) do
parsed = URI.parse(url)
query =
case parsed.query do
nil -> nil
q -> URI.decode_query(q)
end
json = %{
"host" => parsed.host,
"port" => parsed.port,
"scheme" => parsed.scheme,
"path" => parsed.path,
"query" => query,
"user" => parsed.userinfo,
"domain" => get_domain(parsed.host)
}
if what do
jp(json, what)
else
json
end
end
@doc ~S"""
Gets the domain from the domain name.
This means "google" from "www.google.com" or "google" from "www.google.co.uk"
The algorithm disposes the tld (.uk) and the skips unwanted names (.co).
Returns the first thats rest, or a default that is originally the full domain
name or then each disposed part.
"""
def get_domain(nil), do: nil
def get_domain(hostname) do
[_tld | rparts] = hostname |> String.split(".") |> Enum.reverse()
# always remove last part
get_domainr(rparts, hostname)
end
# list of strings that are never domains.
defp get_domainr([head | rest], candidate) do
nodomains = ~w(com org net www co)
if head in nodomains do
get_domainr(rest, candidate)
else
head
end
end
defp get_domainr([], candidate), do: candidate
@doc ~S"""
Performs a JSON Pointer search on JSON data.
It just uses / to separate keys.
"""
def jp(nil, _), do: nil
def jp(json, idx) when is_list(json) and is_number(idx), do: Enum.at(json, idx)
def jp(json, str) when is_binary(str), do: jp(json, String.split(str, "/"))
def jp(json, [head | rest]) when is_list(json) do
n = ExoSQL.Utils.to_number!(head)
jp(Enum.at(json, n), rest)
end
def jp(json, ["" | rest]), do: jp(json, rest)
def jp(json, [head | rest]), do: jp(Map.get(json, head, nil), rest)
def jp(json, []), do: json
@doc ~S"""
Convert from a string to a JSON object
"""
def json(nil), do: nil
def json(str) when is_binary(str) do
Poison.decode!(str)
end
def json(js) when is_map(js), do: js
def json(arr) when is_list(arr), do: arr
@doc ~S"""
Extracts some keys from each value on an array and returns the array of
those values
"""
def unnest(array) do
array = json(array) || []
%ExoSQL.Result{
columns: [{:tmp, :tmp, "unnest"}],
rows: Enum.map(array, &[&1])
}
end
def unnest(array, cols) when is_list(cols) do
array = json(array) || []
rows =
Enum.map(array, fn row ->
Enum.map(cols, &Map.get(row, &1))
end)
columns = Enum.map(cols, &{:tmp, :tmp, &1})
%ExoSQL.Result{
columns: columns,
rows: rows
}
end
def unnest(array, col1), do: unnest(array, [col1])
def unnest(array, col1, col2), do: unnest(array, [col1, col2])
def unnest(array, col1, col2, col3), do: unnest(array, [col1, col2, col3])
def unnest(array, col1, col2, col3, col4), do: unnest(array, [col1, col2, col3, col4])
def unnest(array, col1, col2, col3, col4, col5),
do: unnest(array, [col1, col2, col3, col4, col5])
def unnest(array, col1, col2, col3, col4, col5, col5),
do: unnest(array, [col1, col2, col3, col4, col5, col5])
def unnest(array, col1, col2, col3, col4, col5, col5, col6),
do: unnest(array, [col1, col2, col3, col4, col5, col5, col6])
def unnest(array, col1, col2, col3, col4, col5, col5, col6, col7),
do: unnest(array, [col1, col2, col3, col4, col5, col5, col6, col7])
def unnest(array, col1, col2, col3, col4, col5, col5, col6, col7, col8),
do: unnest(array, [col1, col2, col3, col4, col5, col5, col6, col7, col8])
@doc ~S"""
Creates a range, which can later be used in:
* `IN` -- Subset / element contains
* `*` -- Interesection -> nil if no intersection, the intersected range if any.
"""
def range(a, b), do: {:range, {a, b}}
@doc ~S"""
Get the greatest of arguments
"""
def greatest(a, nil), do: a
def greatest(nil, b), do: b
def greatest(a, b) do
if a > b do
a
else
b
end
end
def greatest(a, b, c), do: Enum.reduce([a, b, c], nil, &greatest/2)
def greatest(a, b, c, d), do: Enum.reduce([a, b, c, d], nil, &greatest/2)
def greatest(a, b, c, d, e), do: Enum.reduce([a, b, c, d, e], nil, &greatest/2)
@doc ~S"""
Get the least of arguments.
Like min, for not for aggregations.
"""
def least(a, nil), do: a
def least(nil, b), do: b
def least(a, b) do
if a < b do
a
else
b
end
end
def least(a, b, c), do: Enum.reduce([a, b, c], nil, &least/2)
def least(a, b, c, d), do: Enum.reduce([a, b, c, d], nil, &least/2)
def least(a, b, c, d, e), do: Enum.reduce([a, b, c, d, e], nil, &least/2)
@doc ~S"""
Returns the first not NULL
"""
def coalesce(a, b), do: Enum.find([a, b], &(&1 != nil))
def coalesce(a, b, c), do: Enum.find([a, b, c], &(&1 != nil))
def coalesce(a, b, c, d), do: Enum.find([a, b, c, d], &(&1 != nil))
def coalesce(a, b, c, d, e), do: Enum.find([a, b, c, d, e], &(&1 != nil))
@doc ~S"""
Returns NULL if both equal, first argument if not.
"""
def nullif(a, a), do: nil
def nullif(a, _), do: a
def floor(n) when is_float(n), do: trunc(Float.floor(n))
def floor(n) when is_number(n), do: n
def floor(n), do: floor(ExoSQL.Utils.to_number!(n))
def ceil(n) when is_float(n), do: trunc(Float.ceil(n))
def ceil(n) when is_number(n), do: n
def ceil(n), do: ceil(ExoSQL.Utils.to_number!(n))
### Aggregate functions
def is_aggregate(x) do
x in ["count", "avg", "sum", "max", "min"]
end
def count(data, {:lit, '*'}) do
Enum.count(data.rows)
end
def count(data, {:distinct, expr}) do
expr = ExoSQL.Expr.simplify(expr, %{columns: data.columns})
Enum.reduce(data.rows, MapSet.new(), fn row, acc ->
case ExoSQL.Expr.run_expr(expr, %{row: row}) do
nil -> acc
val -> MapSet.put(acc, val)
end
end)
|> Enum.count()
end
def count(data, expr) do
expr = ExoSQL.Expr.simplify(expr, %{columns: data.columns})
Enum.reduce(data.rows, 0, fn row, acc ->
case ExoSQL.Expr.run_expr(expr, %{row: row}) do
nil -> acc
_other -> 1 + acc
end
end)
end
def avg(data, expr) do
# Logger.debug("Avg of #{inspect data} by #{inspect expr}")
if data.columns == [] do
nil
else
sum(data, expr) / count(data, {:lit, '*'})
end
end
def sum(data, expr) do
# Logger.debug("Sum of #{inspect data} by #{inspect expr}")
expr = ExoSQL.Expr.simplify(expr, %{columns: data.columns})
# Logger.debug("Simplified expression #{inspect expr}")
Enum.reduce(data.rows, 0, fn row, acc ->
n = ExoSQL.Expr.run_expr(expr, %{row: row})
n =
case ExoSQL.Utils.to_number(n) do
{:ok, n} -> n
{:error, nil} -> 0
end
acc + n
end)
end
def max_(data, expr) do
expr = ExoSQL.Expr.simplify(expr, %{columns: data.columns})
Enum.reduce(data.rows, nil, fn row, acc ->
n = ExoSQL.Expr.run_expr(expr, %{row: row})
{acc, n} = ExoSQL.Expr.match_types(acc, n)
if ExoSQL.Expr.is_greater(acc, n) do
acc
else
n
end
end)
end
def min_(data, expr) do
expr = ExoSQL.Expr.simplify(expr, %{columns: data.columns})
Enum.reduce(data.rows, nil, fn row, acc ->
n = ExoSQL.Expr.run_expr(expr, %{row: row})
{acc, n} = ExoSQL.Expr.match_types(acc, n)
res =
if acc != nil and ExoSQL.Expr.is_greater(n, acc) do
acc
else
n
end
res
end)
end
## Simplications.
# Precompile regex
def simplify("format", [{:lit, format} | rest]) when is_binary(format) do
compiled = ExoSQL.Format.compile_format(format)
# Logger.debug("Simplify format: #{inspect compiled}")
simplify("format", [{:lit, compiled} | rest])
end
def simplify("regex", [str, {:lit, regexs}]) when is_binary(regexs) do
regex = Regex.compile!(regexs)
captures = String.contains?(regexs, "(?<")
simplify("regex", [str, {:lit, regex}, {:lit, captures}])
end
def simplify("regex", [str, {:lit, regexs}, {:lit, query}]) when is_binary(regexs) do
regex = Regex.compile!(regexs)
captures = String.contains?(regexs, "(?<")
# this way jq can be simplified too
params = [
simplify("regex", [str, {:lit, regex}, {:lit, captures}]),
{:lit, query}
]
simplify("jp", params)
end
def simplify("jp", [json, {:lit, path}]) when is_binary(path) do
# Logger.debug("JP #{inspect json}")
simplify("jp", [json, {:lit, String.split(path, "/")}])
end
def simplify("random", []), do: {:fn, {{ExoSQL.Builtins, :random, "random"}, []}}
def simplify("randint", params), do: {:fn, {{ExoSQL.Builtins, :randint, "randint"}, params}}
# default: convert to {mod fun name} tuple
def simplify(name, params) when is_binary(name) do
# Logger.debug("Simplify #{inspect name} #{inspect params}")
if not is_aggregate(name) and Enum.all?(params, &is_lit(&1)) do
# Logger.debug("All is literal for #{inspect {name, params}}.. just do it once")
params = Enum.map(params, fn {:lit, n} -> n end)
ret = ExoSQL.Builtins.call_function(name, params)
{:lit, ret}
else
case @functions[name] do
nil ->
throw({:unknown_function, name})
{mod, fun} ->
{:fn, {{mod, fun, name}, params}}
end
end
end
def simplify(modfun, params), do: {:fn, {modfun, params}}
def is_lit({:lit, '*'}), do: false
def is_lit({:lit, _n}), do: true
def is_lit(_), do: false
end