SQL parser for InfluxElixir.Client.Local.
Recognises the SQL subset documented on InfluxElixir.Client.Local and
produces a parsed_query/0 for the executor. It is deliberately strict:
anything the real InfluxDB v3 engine would reject — or that the double
cannot execute faithfully — is refused with a Client.Local:-prefixed 400
so a query cannot pass tests here and fail in production.
This module cuts a statement into its parts and hands each to the module that reads it:
InfluxElixir.Client.Local.SQLLexer— comments, statements and unterminated literals, before anything elseInfluxElixir.Client.Local.SQLIdentifiers— case foldingInfluxElixir.Client.Local.SQLSelect— an aggregate select listInfluxElixir.Client.Local.SQLExpr— arithmetic expressionsInfluxElixir.Client.Local.SQLWhere— theWHEREclauseInfluxElixir.Client.Local.SQLClauses—GROUP BYandORDER BYInfluxElixir.Client.Local.SQLLimit—LIMITandOFFSETInfluxElixir.Client.Local.SQLTime—timecomparands
A $name placeholder is read as a value wherever a value may stand and is
kept as a node; InfluxElixir.Client.Local.SQLBind replaces the nodes with
the values the engine would read for them, so a parameter is data and never
SQL text.
Pure functions only: no ETS, no connection state.
Summary
Types
Plain aggregates; :stddev/:var are the sample forms, as in InfluxDB.
CAST(expr AS type) targets: an integer width, DOUBLE or text.
:asc / :desc (nulls last / first), or a direction with explicit NULLS placement.
An arithmetic expression; see InfluxElixir.Client.Local.SQLExpr.t/0.
A WHERE operand: a column name, or an arithmetic expression over columns
and literals (price <= med * 3).
ORDER BY terms in order; a target is time, a column, an output alias or an expression.
One SELECT. ctes holds the WITH name AS (...) queries that precede
it, in order; measurement may name one of them.
A projected column: {source, output} where source is a column name or
an arithmetic expr/0 ((bid + ask) / 2 AS mid).
One column of an aggregate or grouped select list.
A time comparand: nanoseconds since the epoch, now() plus an offset in
nanoseconds (resolved when the query runs), null, or a string the optimizer
cannot read (raised once the type checks have passed).
A predicate: operator, left operand, right side.
A WHERE conjunction is a list of nodes: a predicate, an {:or, branches}
node whose branches are conjunctions, or a {:not, conjunction} node.
The comparison, set and pattern operators of a predicate.
Functions
Binds a parsed query's $name placeholders to params; see
InfluxElixir.Client.Local.SQLBind.bind/2.
Parses a statement — an optional WITH list of non-recursive CTEs followed
by one SELECT — into a parsed_query/0.
Parses the WHERE ... clause (if any) out of the text after FROM <table>,
for a statement that binds no parameters (DELETE, InfluxQL). A $name
in it is the engine's unbound-placeholder error, and so is a time string
the optimizer cannot read.
Types
@type aggregate() :: InfluxElixir.Client.Local.SQLSelect.aggregate()
Plain aggregates; :stddev/:var are the sample forms, as in InfluxDB.
@type cast_type() :: InfluxElixir.Client.Local.SQLExpr.cast_type()
CAST(expr AS type) targets: an integer width, DOUBLE or text.
@type direction() :: InfluxElixir.Client.Local.SQLClauses.direction()
:asc / :desc (nulls last / first), or a direction with explicit NULLS placement.
@type expr() :: InfluxElixir.Client.Local.SQLExpr.t()
An arithmetic expression; see InfluxElixir.Client.Local.SQLExpr.t/0.
@type operand() :: InfluxElixir.Client.Local.SQLWhere.operand()
A WHERE operand: a column name, or an arithmetic expression over columns
and literals (price <= med * 3).
@type order_by() :: InfluxElixir.Client.Local.SQLClauses.order_by()
ORDER BY terms in order; a target is time, a column, an output alias or an expression.
@type parsed_query() :: %{ measurement: binary(), where: [where_node()], order_by: order_by(), limit: non_neg_integer() | nil, offset: non_neg_integer() | nil, group_by_interval: non_neg_integer() | nil, group_by_columns: [binary()] | nil, select_columns: [select_column()] | nil, distinct_columns: [binary()] | nil, distinct_on: [binary()] | nil, projection_columns: [projection()] | nil, ctes: [{binary(), parsed_query()}], cross_join: {binary(), [binary()]} | nil, qualifier: binary(), qualified: %{required(binary()) => binary()}, plan_error: InfluxElixir.Client.Local.SQLError.t() | nil, limit_error: InfluxElixir.Client.Local.SQLError.t() | nil }
One SELECT. ctes holds the WITH name AS (...) queries that precede
it, in order; measurement may name one of them.
A projected column: {source, output} where source is a column name or
an arithmetic expr/0 ((bid + ask) / 2 AS mid).
@type select_column() :: InfluxElixir.Client.Local.SQLSelect.column()
One column of an aggregate or grouped select list.
@type time_value() :: integer() | {:now, integer()} | nil | {:invalid_time, InfluxElixir.Client.Local.SQLError.t()}
A time comparand: nanoseconds since the epoch, now() plus an offset in
nanoseconds (resolved when the query runs), null, or a string the optimizer
cannot read (raised once the type checks have passed).
@type where_clause() :: InfluxElixir.Client.Local.SQLWhere.clause()
A predicate: operator, left operand, right side.
@type where_node() :: InfluxElixir.Client.Local.SQLWhere.node_t()
A WHERE conjunction is a list of nodes: a predicate, an {:or, branches}
node whose branches are conjunctions, or a {:not, conjunction} node.
@type where_op() :: InfluxElixir.Client.Local.SQLWhere.op()
The comparison, set and pattern operators of a predicate.
Functions
@spec bind(parsed_query(), %{required(binary()) => term()}) :: {:ok, parsed_query()} | {:error, map()}
Binds a parsed query's $name placeholders to params; see
InfluxElixir.Client.Local.SQLBind.bind/2.
@spec parse_select( binary(), keyword() ) :: {:ok, parsed_query()} | {:error, term()}
Parses a statement — an optional WITH list of non-recursive CTEs followed
by one SELECT — into a parsed_query/0.
Identifiers follow DataFusion's rules (see
InfluxElixir.Client.Local.SQLIdentifiers) unless identifiers: :exact
is given — for SQL the double writes itself, from InfluxQL, whose
identifiers are case-sensitive.
@spec parse_where(binary()) :: {:ok, [where_node()]} | {:error, map()}
Parses the WHERE ... clause (if any) out of the text after FROM <table>,
for a statement that binds no parameters (DELETE, InfluxQL). A $name
in it is the engine's unbound-placeholder error, and so is a time string
the optimizer cannot read.