InfluxElixir.Client.Local.InfluxQL (InfluxElixir v0.1.40)

Copy Markdown View Source

The InfluxQL SELECT subset InfluxElixir.Client.Local answers, shaped the way InfluxDB 3 answers it (verified against the engine; see docs/design/2026-09-23_local-influxql.md).

InfluxQL is not SQL with other keywords. Every row carries "iox::measurement" and "time"; rows come back in time order; a row with no selected field value is dropped; an unknown column or measurement is an empty result, not an error; aggregates are named after the function (mean, count, ...) and put time at the lower bound the WHERE gives time (the epoch when it gives none), except a lone selector (MAX, MIN, FIRST, LAST), which returns its point's time and tags; LIMIT and OFFSET apply per GROUP BY series.

This module is pure. parse/1 turns the statement into a query map; run/4 shapes rows the caller has already filtered with the statement's WHERE clause (Client.Local runs it through its SQL engine) and put in time order.

WHERE follows InfluxQL, not SQL (where_plan/2): a missing tag is the empty string (host != 'a' keeps points without host, host = '' finds them), =~ /re/ and !~ /re/ are unanchored matches on tags and false on fields, </> on a tag is false, durations (now() - 30m) are intervals, double-quoted identifiers are exact, and NOT is the engine's parse error. Compared with time, a bare integer or a duration is an offset from the epoch in nanoseconds (time >= 2, time > 0s, time > 1s - 999999999ns), != and <> are the engine's planning error, and a float is refused. The engine pulls time comparisons out of the whole WHERE as if they were joined by AND, so one inside an OR is refused by name. SHOW TAG VALUES [FROM m] WITH KEY = | != | =~ | !~ | IN (...) [WHERE ...] lists values as the engine does, over the last 24 hours unless the WHERE bounds time (parse_show_tag_values/1).

A reserved word (reserved?/1) is no bare identifier: the engine's parse errors for it, and for a select list, FROM, WHERE, GROUP BY, ORDER BY, LIMIT or a statement after a ; that stops short, are positioned in the text as sent and read with the statement's own offset. A field is compared with a literal by the types of the two (b = 1 is false for every row, u > -1 wraps the -1, an integer past the signed range compares as unsigned), LIMIT and OFFSET beyond the signed 64-bit range are the planning error and beyond the unsigned a parse error.

Refused by name, rather than answered wrongly: GROUP BY time(...) (the engine fills every empty bucket), fill(), INTO, SLIMIT/SOFFSET, subqueries, GROUP BY *, functions other than MEAN SUM COUNT MIN MAX FIRST LAST, F(*) other than COUNT(*), plain columns beside anything but a single selector, arithmetic in the select list, several measurements in FROM, sub-second durations in now() - ..., LIMIT / OFFSET on SHOW TAG VALUES, an unsigned field compared with a string, a string field or a tag compared with an integer past the signed range, a field compared with a constant the double cannot fold, a bare non-boolean field inside AND / OR, DISTINCT, and a statement after a ; that the double does not read. A keyword inside a quoted string, quoted identifier or regular expression is not one: WHERE k = 'into' is answered.

Summary

Types

A bound on time: nanoseconds since the epoch, or an offset from the query's now().

The type of a field, as the engine plans it.

A select item: every column, a column, or a function of a column.

Which tag keys a SHOW TAG VALUES lists.

A parsed SELECT.

A WHERE as the caller's SQL, with the lower bounds its time comparisons give.

Functions

The engine's planning error for a LIMIT or OFFSET beyond the signed 64-bit range (LIMIT first), or :ok. It is raised only for a measurement that exists.

Whether a tag key is one a key_filter/0 lists.

Whether a WHERE names time: then it, not the default window, bounds the rows.

Parses an InfluxQL SELECT. Returns {:error, message} for syntax the engine rejects and for the constructs listed in the moduledoc.

Parses SHOW TAG VALUES [FROM m] WITH KEY = k | != k | =~ /re/ | !~ /re/ | IN (k, ...) [WHERE ...], or nil when the statement is not one. LIMIT and OFFSET are refused by name: the engine applies them per measurement in an order the double does not reproduce.

Whether a bare word is one InfluxQL reserves (any case).

Shapes rows — the measurement's points already filtered by the statement's WHERE, as SQL row maps with "time", in time order — into the engine's answer. tags names the measurement's tag columns; everything else but time is a field.

Rewrites an InfluxQL WHERE into the caller's SQL, given the measurement's tag columns, with the column names the WHERE mentions and the lower bounds it puts on time: time >= x and time = x give x, time > x gives x + 1, upper bounds give none. An aggregate over a lower bound is stamped with the greatest of them (run/4). types maps each field to its type: a comparison of a field with a literal follows the engine's rules for the two types (see the section on typed comparisons). deferred is the engine's error for a bare field as the whole condition, which it raises after it has checked LIMIT and OFFSET. {:error, message} for what the double refuses by name; {:error, {:engine, body}} for what the engine itself answers with a 400.

Types

bound()

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

A bound on time: nanoseconds since the epoch, or an offset from the query's now().

field_type()

@type field_type() :: :integer | :unsigned | :float | :string | :boolean

The type of a field, as the engine plans it.

item()

@type item() ::
  :star
  | {:column, binary(), binary()}
  | {:aggregate, binary(), binary() | :star, binary() | nil}

A select item: every column, a column, or a function of a column.

key_filter()

@type key_filter() ::
  {:eq, binary()}
  | {:ne, binary()}
  | {:in, [binary()]}
  | {:regex, Regex.t(), boolean()}

Which tag keys a SHOW TAG VALUES lists.

query()

@type query() :: %{
  items: [item()],
  measurement: binary(),
  where: binary() | nil,
  group_by: [binary()],
  descending: boolean(),
  limit: non_neg_integer() | nil,
  offset: non_neg_integer()
}

A parsed SELECT.

where_plan()

@type where_plan() :: %{
  sql: binary(),
  lowers: [bound()],
  idents: MapSet.t(binary()),
  deferred: binary() | nil
}

A WHERE as the caller's SQL, with the lower bounds its time comparisons give.

Functions

check_window(map)

@spec check_window(query()) :: :ok | {:error, {:engine, binary()}}

The engine's planning error for a LIMIT or OFFSET beyond the signed 64-bit range (LIMIT first), or :ok. It is raised only for a measurement that exists.

key_listed?(key, arg)

@spec key_listed?(binary(), key_filter()) :: boolean()

Whether a tag key is one a key_filter/0 lists.

mentions_time?(where)

@spec mentions_time?(binary() | nil) :: boolean()

Whether a WHERE names time: then it, not the default window, bounds the rows.

parse(statement)

@spec parse(binary()) :: {:ok, query()} | {:error, binary() | {:engine, binary()}}

Parses an InfluxQL SELECT. Returns {:error, message} for syntax the engine rejects and for the constructs listed in the moduledoc.

parse_show_tag_values(statement)

@spec parse_show_tag_values(binary()) ::
  nil
  | {:ok,
     %{measurement: binary() | nil, keys: key_filter(), where: binary() | nil}}
  | {:error, binary()}

Parses SHOW TAG VALUES [FROM m] WITH KEY = k | != k | =~ /re/ | !~ /re/ | IN (k, ...) [WHERE ...], or nil when the statement is not one. LIMIT and OFFSET are refused by name: the engine applies them per measurement in an order the double does not reproduce.

reserved?(word)

@spec reserved?(binary()) :: boolean()

Whether a bare word is one InfluxQL reserves (any case).

run(query, rows, tags, opts \\ [])

@spec run(query(), [map()], MapSet.t(binary()), keyword()) :: [map()]

Shapes rows — the measurement's points already filtered by the statement's WHERE, as SQL row maps with "time", in time order — into the engine's answer. tags names the measurement's tag columns; everything else but time is a field.

Options:

  • :lower - the lower bound (nanoseconds) the WHERE gives time; an aggregate row is stamped with it, and with the epoch without one (verified: WHERE time >= 2 answers mean at 2 ns, WHERE time < 3 at the epoch; with GROUP BY every series carries it)
  • :fields - the measurement's field names, when LIMIT or OFFSET need them (they are read from the rows otherwise)

where_plan(where, tags, types \\ %{})

@spec where_plan(binary(), MapSet.t(binary()), %{required(binary()) => field_type()}) ::
  {:ok, where_plan()} | {:error, binary() | {:engine, binary()}}

Rewrites an InfluxQL WHERE into the caller's SQL, given the measurement's tag columns, with the column names the WHERE mentions and the lower bounds it puts on time: time >= x and time = x give x, time > x gives x + 1, upper bounds give none. An aggregate over a lower bound is stamped with the greatest of them (run/4). types maps each field to its type: a comparison of a field with a literal follows the engine's rules for the two types (see the section on typed comparisons). deferred is the engine's error for a bare field as the whole condition, which it raises after it has checked LIMIT and OFFSET. {:error, message} for what the double refuses by name; {:error, {:engine, body}} for what the engine itself answers with a 400.