Documentation
¶
Index ¶
Constants ¶
const DefaultMaxRows = 10000
DefaultMaxRows is the fallback result LIMIT applied when no explicit LIMIT is specified and no policy MaxRows is set — preventing an unbounded read. It is the default value of the operator-facing query.default_max_rows config knob (passed into Build), and the guard Build falls back to when that knob is misconfigured to 0.
Variables ¶
var ErrColumnsAndSelectAll = errors.New("columns and select_all are mutually exclusive")
ErrColumnsAndSelectAll is returned when a query sets both an explicit Columns list and SelectAll — an ambiguous request the handler maps to 400. Pick one: name the columns, or ask for all of them.
var ErrEmptyProjection = errors.New("query selects no columns")
ErrEmptyProjection is returned by Build when a query selects nothing — no columns, no aggregations, and SelectAll is false. That is a request for no data, so the handler returns an empty result (HTTP 200 []) rather than building an invalid SELECT with no projection. Omitting columns is therefore safe-by- default: you get nothing unless you name columns or set SelectAll.
var ErrNoReadableColumns = errors.New("no columns readable for role")
ErrNoReadableColumns is returned by Build for a SelectAll read when the role may select the table but its column allowlist permits no columns at all. Rather than fall back to a bare SELECT * — the fail-open behind #223 — Build refuses the query (→ 403). This is distinct from ErrEmptyProjection: the caller explicitly asked for all columns and is entitled to none. Mirrors the stream path, where a fully column-restricted role receives an empty event.
Functions ¶
func SafeDecodeNATS ¶
SafeDecodeNATS reverses the NATS-safe encoding back to the raw ClickHouse table name.
func SafeEncodeNATS ¶
SafeEncodeNATS converts any ClickHouse table name into a safe, single NATS token. It preserves alphanumerics and underscores, but percent-encodes everything else.
Types ¶
type Aggregation ¶
type Aggregation struct {
Fn string `json:"fn"` // count, sum, avg, min, max, countDistinct, etc.
Column string `json:"column"` // "*" for count(*)
Alias string `json:"alias"` // result column name
}
Aggregation represents an aggregation function call.
type BuildResult ¶
BuildResult holds the generated SQL and bound parameters.
func Build ¶
func Build(table string, q *StructuredQuery, schema *discovery.TableSchema, perms *policy.ResolvedPermissions, bucketSeconds, defaultMaxRows int) (*BuildResult, error)
Build converts a StructuredQuery into parameterized ClickHouse SQL.
Every column the query references — in the projection, an aggregation argument, a filter, group_by, order_by, or time_range — is validated against the schema (it must be a real, discovered column) AND authorized against perms, the caller's resolved column permissions. perms may be nil, which means "no policy" — every column is allowed (used by callers that gate access elsewhere, and by tests). Centralizing the authorization here, at the one place that already enumerates every column reference, is what keeps a denied column from slipping through any single clause (#223). perms also carries the role's row-level-security predicate and max_rows cap, which Build emits as part of the WHERE and LIMIT clauses it assembles — never spliced into the rendered SQL afterward (#322).
Every identifier that reaches the SQL — columns, the table, aggregation aliases — is backtick-quoted via chsql.QuoteIdent, so the builder accepts any name ClickHouse accepts while remaining injection-safe. Values stay positional `?` parameters bound by the driver.
Projection rules: SelectAll requests every readable column (expanded to the role's allow/deny set); an explicit Columns list projects exactly those (where "*" is a literal column name, not a wildcard); the two are mutually exclusive. A query with neither, and no aggregations, selects nothing → ErrEmptyProjection.
type Columns ¶
type Columns []string
Columns is the list of row columns a query projects. It accepts either a JSON array of strings (`["a","b"]`) or a single JSON string (`"a"`, treated as `["a"]`) so a hand-written query meaning one column can drop the brackets.
An omitted field, null, "", or [] all decode to an empty list — a request for no columns. Column names must be strings: a non-string scalar (number, bool), an object, or a non-string array element is rejected rather than coerced. An explicit empty-string element (`[""]`) is rejected too — that is a malformed column name, distinct from the "no columns" forms above (a whitespace name like `[" "]` is a legal, if unusual, ClickHouse column and is kept).
func (*Columns) UnmarshalJSON ¶
UnmarshalJSON implements the string-or-array decoding described on Columns.
type Filter ¶
type Filter struct {
Column string `json:"column"`
Op string `json:"op"` // eq, neq, gt, gte, lt, lte, in, like
Value any `json:"value"` // scalar or array (for "in")
}
Filter represents a WHERE condition.
type ForbiddenAggregationError ¶
type ForbiddenAggregationError struct {
Fn string
}
ForbiddenAggregationError reports that a query used an aggregation function the role's policy denies (allowed_aggregations / denied_aggregations).
func (*ForbiddenAggregationError) Error ¶
func (e *ForbiddenAggregationError) Error() string
type ForbiddenColumnError ¶
type ForbiddenColumnError struct {
Column string
}
ForbiddenColumnError reports that a query referenced a column the role's allowlist denies. It can surface from any clause — projection, aggregation argument, filter, group_by, order_by, or time_range — because Build authorizes every column reference, not just the SELECT list.
func (*ForbiddenColumnError) Error ¶
func (e *ForbiddenColumnError) Error() string
type OrderClause ¶
type OrderClause struct {
Column string `json:"column"`
Dir string `json:"dir"` // "asc" or "desc"
}
OrderClause specifies sort order.
type StructuredQuery ¶
type StructuredQuery struct {
// Columns is the explicit list of row columns to project. Empty (omitted, [],
// "", or null) means "no row columns" — combined with no aggregations and no
// SelectAll, the query returns nothing (see SelectAll). "*" here is a literal
// column name, not a wildcard.
Columns Columns `json:"columns,omitempty"`
// SelectAll requests every column the caller's role may read (the all-columns
// wildcard, expanded to the role's allowed/denied columns). It is mutually
// exclusive with a non-empty Columns. This is the only way to get a full-row
// read — omitting Columns does NOT select all, so a hidden column can never
// leak by simply leaving Columns out.
SelectAll bool `json:"select_all,omitempty"`
Aggregations []Aggregation `json:"aggregations,omitempty"`
Filters []Filter `json:"filters,omitempty"`
GroupBy []string `json:"group_by,omitempty"`
OrderBy []OrderClause `json:"order_by,omitempty"`
Limit int `json:"limit,omitempty"`
TimeRange *TimeRange `json:"time_range,omitempty"`
}
StructuredQuery represents a type-safe query AST that gets translated to SQL.
type TimeRange ¶
type TimeRange struct {
Column string `json:"column"` // timestamp column name
Since string `json:"since"` // RFC3339 or relative (e.g. "1h", "30m", "7d", "2w")
Until string `json:"until"` // RFC3339, relative (e.g. "1h"), or empty (=now)
}
TimeRange constrains a query to a time window.