InfluxElixir.Client.Local.SQLParser (InfluxElixir v0.1.35)

Copy Markdown View Source

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.

Pure functions only: no ETS, no connection state.

Summary

Types

Plain aggregates; :stddev/:var are the sample forms, as in InfluxDB.

CAST(expr AS INTEGER | DOUBLE | VARCHAR) targets (and their synonyms).

:asc / :desc (nulls last / first), or a direction with explicit NULLS placement.

An arithmetic expression inside an aggregate: a field reference, a numeric literal, or a binary operation over two expressions.

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).

A time comparand: nanoseconds since the epoch, or now() plus an offset in nanoseconds, resolved when the query runs.

A WHERE conjunction is a list of nodes: a predicate, an {:or, branches} node whose branches are conjunctions, or a {:not, conjunction} node.

Functions

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>.

Substitutes $name placeholders with SQL literals built from params.

The first $name placeholder left in sql after substitution, or nil. String literals are ignored: a substituted value that happens to contain $ is text.

Types

aggregate()

@type aggregate() ::
  :avg
  | :sum
  | :count
  | :min
  | :max
  | :median
  | :stddev
  | :stddev_pop
  | :var
  | :var_pop

Plain aggregates; :stddev/:var are the sample forms, as in InfluxDB.

cast_type()

@type cast_type() :: :integer | :float | :string

CAST(expr AS INTEGER | DOUBLE | VARCHAR) targets (and their synonyms).

direction()

@type direction() :: :asc | :desc | {:asc | :desc, :nulls_first | :nulls_last}

:asc / :desc (nulls last / first), or a direction with explicit NULLS placement.

expr()

@type expr() ::
  {:field, binary()}
  | {:lit, number() | binary()}
  | {:op, :+ | :- | :* | :/ | :rem, expr(), expr()}
  | {:neg, expr()}
  | {:cast, expr(), cast_type()}

An arithmetic expression inside an aggregate: a field reference, a numeric literal, or a binary operation over two expressions.

operand()

@type operand() :: binary() | {:expr, expr()}

A WHERE operand: a column name, or an arithmetic expression over columns and literals (price <= med * 3).

order_by()

@type order_by() :: [{binary() | {:expr, expr()}, direction()}]

ORDER BY terms in order; a target is time, a column, an output alias or an expression.

parsed_query()

@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: pos_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
}

One SELECT. ctes holds the WITH name AS (...) queries that precede it, in order; measurement may name one of them.

projection()

@type projection() :: {binary() | expr(), binary()}

A projected column: {source, output} where source is a column name or an arithmetic expr/0 ((bid + ask) / 2 AS mid).

select_column()

@type select_column() ::
  {:time_bucket, binary()}
  | {:aggregate, aggregate(), expr(), binary()}
  | {:count_star, binary()}
  | {:count_distinct, binary(), binary()}
  | {:ordered_aggregate, :first | :last, binary(), binary(), binary()}
  | {:selector, :first | :last | :min | :max, binary(), binary(),
     :value | :time | :struct, binary()}
  | {:grouping_column, binary(), binary()}
  | {:constant, term(), binary()}

time_value()

@type time_value() :: integer() | {:now, integer()}

A time comparand: nanoseconds since the epoch, or now() plus an offset in nanoseconds, resolved when the query runs.

where_clause()

@type where_clause() :: {where_op(), binary(), term()}

where_node()

@type where_node() ::
  where_clause() | {:or, [[where_node()]]} | {:not, [where_node()]}

A WHERE conjunction is a list of nodes: a predicate, an {:or, branches} node whose branches are conjunctions, or a {:not, conjunction} node.

where_op()

@type where_op() ::
  :eq
  | :gt
  | :lt
  | :gte
  | :lte
  | :ne
  | :in
  | :not_in
  | :is_null
  | :is_not_null
  | :between
  | :not_between
  | :like
  | :not_like

Functions

parse_select(sql, opts \\ [])

@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.

parse_where(rest)

@spec parse_where(binary()) :: {:ok, [where_node()]} | {:error, map()}

Parses the WHERE ... clause (if any) out of the text after FROM <table>.

resolve_params(sql, params)

@spec resolve_params(binary(), map()) :: binary()

Substitutes $name placeholders with SQL literals built from params.

unbound_placeholder(sql)

@spec unbound_placeholder(binary()) :: binary() | nil

The first $name placeholder left in sql after substitution, or nil. String literals are ignored: a substituted value that happens to contain $ is text.