Documentation
¶
Overview ¶
Package querytable compiles a frontend QueryState into parameterized SQL, validated against a server-defined field schema. It is the Go projection of @pythia-software/query-table-core: the wire types here mirror the TS shapes exactly so a base64url ?q= token (or a JSON request body) round-trips without translation.
Safety model: the column expression for each field comes from the schema, never from request input. An unknown field is a compile error before any SQL is built. Values reach SQL only as bound placeholders ($N). The validated operator and the schema-defined expression are the only request-influenced tokens in the SQL text.
Index ¶
- Constants
- Variables
- func NewComputedColumnsHandler(repo ComputedColumnRepository, ...) http.Handler
- type AggCompileResult
- type AggSpec
- type CompileResult
- type ComputedColumn
- type ComputedColumnRepository
- type ComputedExpression
- type DistinctCompile
- type DistinctHasNullCompile
- type FieldKind
- type FieldSpec
- type OrderBy
- type OrderBys
- type RegexExtract
- type SQLComputedColumnStore
- type SaveComputedColumnRequest
- type Schema
- type WhereClause
- type WhereTerm
- type WireQuery
Constants ¶
const ( // MaxQueryLimit is the largest row limit representable by this platform. // The package does not impose a smaller application-level row cap; consumers // and their databases own any operational limit appropriate for the dataset. MaxQueryLimit = int(^uint(0) >> 1) MaxQueryOffset = 1_000_000 MaxSelectColumns = 200 MaxWhereClauses = 100 MaxOrderByTerms = 20 MaxAggregations = 20 MaxGroupByFields = 20 )
Structural limits mirror @pythia-software/query-table-core. Compile and DecodeWireQuery both enforce them so callers are protected whether a query came from a URL token or was decoded from a JSON request body elsewhere.
const ComputedColumnsDDL = `` /* 349-byte string literal not displayed */
Variables ¶
var ErrComputedConflict = errors.New("computed definition changed; reload before saving")
Functions ¶
func NewComputedColumnsHandler ¶
func NewComputedColumnsHandler(repo ComputedColumnRepository, authorize func(*http.Request, string, bool) (string, error)) http.Handler
NewComputedColumnsHandler implements the TypeScript httpComputedColumnStore protocol. Authorize MUST validate the authenticated caller's dataset access, write permission and (for cookie-authenticated writes) CSRF protection. Return a stable tenant/user scope; it must never come directly from request input. Empty scope or nil authorization always denies access.
Types ¶
type AggCompileResult ¶
AggCompileResult holds the SQL fragments for one metric's GROUP BY query. The caller splices them into its own FROM/JOIN, sharing the rows query's WHERE so the metric covers the same filtered set (scope = whole set, no paging):
SELECT <SelectExprs joined by ", ">
FROM <caller FROM/JOIN>
[WHERE <Compile(WireQuery{Where: req.Where}, …).WhereSQL>]
[GROUP BY <GroupBySQL>]
SelectExprs is, in order: one `expr AS "g0"/"g1"/…` per group field, then the aggregate `AS "value"`, then `COUNT(*) AS "count"`. GroupBySQL lists the same group expressions (empty string ⇒ a single grand-total row, no GROUP BY).
func CompileAggregation ¶
func CompileAggregation(spec AggSpec, schema Schema) (AggCompileResult, error)
CompileAggregation validates one AggSpec against the schema allowlist and emits its SELECT + GROUP BY fragments. Like Compile, the only request-influenced tokens that reach SQL are the validated op and the schema-defined field expressions — never request input. No bound args are produced (aggregations carry no values; the shared WHERE is compiled separately via Compile).
Errors on: unknown op, unknown measure/group field, a missing measure for an op that needs one, or an op not allowed for the measure field's kind.
type AggSpec ¶
type AggSpec struct {
ID string `json:"id"`
Op string `json:"op"`
Field string `json:"field,omitempty"`
GroupBy []string `json:"groupBy,omitempty"`
}
AggSpec mirrors @pythia-software/query-table-core AggregationClause: one aggregate op over one measure column (Field; empty ⇒ COUNT(*)), broken down by zero or more group columns. Compiled by CompileAggregation; the metric panel runs one per spec over the WHERE-filtered set (no paging).
type CompileResult ¶
type CompileResult struct {
// WhereSQL is "" when no filters apply, else "(c1) AND (c2) ...". Never
// includes the WHERE keyword, so callers AND it into their own predicates.
WhereSQL string
// OrderSQL is "" to use the schema default, else
// "expr DIR NULLS x, expr2 DIR2 NULLS y, <tiebreak...>".
OrderSQL string
// SelectExprs are "<expr> AS <safe_alias>" for each requested backend field.
SelectExprs []string
// Args are the $N bound values, in placeholder order starting at startIdx.
Args []any
}
CompileResult holds the SQL fragments plus their ordered bound args.
func Compile ¶
Compile validates q against schema and emits SQL fragments. startIdx is the 1-based pgx placeholder index for the first bound value, so callers can interleave their own params; the returned int is the next free index.
Errors (never a panic) on: unknown field, an operator not allowed for a field's kind or per-field operator allowlist, a filter on a non-server-filterable field, a sort on a non-sortable field, an unknown select field, or a value that fails coercion.
type ComputedColumn ¶
type ComputedColumn struct {
ID string `json:"id"`
Label string `json:"label"`
Expression ComputedExpression `json:"expression"`
Revision string `json:"revision"`
}
type ComputedColumnRepository ¶
type ComputedColumnRepository interface {
List(ctx context.Context, scope, dataset string) ([]ComputedColumn, error)
Save(ctx context.Context, scope, dataset string, request SaveComputedColumnRequest) (ComputedColumn, error)
}
type ComputedExpression ¶
type DistinctCompile ¶
DistinctCompile holds the fragments for an autocomplete distinct-values query. The caller assembles them into its own FROM/JOIN tree:
SELECT DISTINCT <Expr> AS v FROM ... WHERE <Expr> IS NOT NULL [AND <SearchSQL>] ORDER BY v LIMIT <n+1> -- fetch one extra to compute hasMore
func CompileDistinct ¶
func CompileDistinct(field, search string, schema Schema, startIdx int) (DistinctCompile, int, error)
CompileDistinct builds the fragments to back filter-value autocomplete for a field (design feedback: every field is an autocomplete by default). The search is a literal case-insensitive substring (no wildcard injection).
type DistinctHasNullCompile ¶
type DistinctHasNullCompile struct {
IsNullExpr string
}
func CompileDistinctHasNull ¶
func CompileDistinctHasNull(field string, schema Schema) (DistinctHasNullCompile, error)
CompileDistinctHasNull builds the nullability expression for the same field-aware distinct path used by value autocomplete. It is intended for a lightweight metadata query that answers “does this field have any nulls?” without another independent field lookup path in callers.
type FieldSpec ¶
type FieldSpec struct {
Name string
Kind FieldKind
Expr string
Synthetic bool
ServerFilter bool // false → field is client-only; Compile rejects filters on it
// ArrayCaseSensitive preserves exact text-array membership keys. Default false.
ArrayCaseSensitive bool
// FilterOps is nil for the type's default matrix; a non-nil slice is the
// field's explicit operator allowlist. An empty slice disables every op.
FilterOps []string
Sortable bool
SortExpr string // expr to ORDER BY when it differs from Expr (FieldDef.sort.field → that field's Expr)
}
FieldSpec is one field's server binding. Expr is the server-defined SQL expression (from the document's bindings.postgres.expr) and is the injection boundary: it is never built from request input.
type OrderBy ¶
type OrderBy struct {
Field string `json:"field"`
Dir string `json:"dir"` // "asc" | "desc"
Nulls string `json:"nulls,omitempty"` // "first" | "last" | "" (default last)
Extract *RegexExtract `json:"extract,omitempty"`
}
OrderBy mirrors @pythia-software/query-table-core OrderByClause.
type OrderBys ¶
type OrderBys []OrderBy
OrderBys is a slice of OrderBy that also accepts a single object on decode (legacy compatibility) and a base64url `c`/`s` is handled at the WireQuery level via DecodeWireQuery.
func (*OrderBys) UnmarshalJSON ¶
UnmarshalJSON accepts either `[{...},{...}]` (current) or `{...}` (legacy).
type RegexExtract ¶
type RegexExtract struct {
Regex string `json:"regex"`
}
RegexExtract transforms a sort value before comparison. PostgreSQL's substring(text FROM regex) returns the first capture group when present and the whole match otherwise; a non-match is NULL.
type SQLComputedColumnStore ¶
func (SQLComputedColumnStore) List ¶
func (s SQLComputedColumnStore) List(ctx context.Context, scope, dataset string) ([]ComputedColumn, error)
func (SQLComputedColumnStore) Save ¶
func (s SQLComputedColumnStore) Save(ctx context.Context, scope, dataset string, request SaveComputedColumnRequest) (ComputedColumn, error)
type SaveComputedColumnRequest ¶
type SaveComputedColumnRequest struct {
Column ComputedColumn `json:"column"`
ExpectedRevision *string `json:"expectedRevision"`
}
type Schema ¶
type Schema struct {
Name string
IDField string
Fields map[string]FieldSpec
DefaultSort []OrderBy
TiebreakSort []OrderBy
}
Schema is the compiled, server-side field allowlist for one dataset.
func LoadSchema ¶
LoadSchema parses + validates a JSON schema document (the bytes of a schema/*.schema.json file) into a Schema, reading the `postgres` binding for each backend field. Fields without a postgres binding (derived / render-only) are skipped — they're never filtered, sorted, or selected on the server.
type WhereClause ¶
type WhereClause struct {
Field string `json:"field"`
Op string `json:"op"`
Value string `json:"value"`
Negated bool `json:"negated,omitempty"`
}
WhereClause mirrors @pythia-software/query-table-core WhereClause. Negated wraps the predicate in a null-exclusive NOT; it is set only for ops without a complementary operator (contains/starts_with/ends_with/includes), matching the frontend's negateClause.
type WhereTerm ¶
type WhereTerm struct {
Field string `json:"field,omitempty"`
Op string `json:"op,omitempty"`
Value string `json:"value,omitempty"`
Negated bool `json:"negated,omitempty"`
Any []WhereClause `json:"any,omitempty"`
}
WhereTerm is one conjunct of the WHERE (mirrors the TS WhereTerm = WhereClause | OrGroup). It is either a single predicate (Field/Op/Value set, Any nil) or an OR group (Any set). The two shapes are disjoint on the wire — a literal carries "field", a group carries "any" — so the default JSON (un)marshaling distinguishes them with no custom code. WHERE is the AND of these terms, i.e. conjunctive normal form.
func (WhereTerm) IsGroup ¶
IsGroup reports whether the term is an OR group rather than a single predicate.
func (WhereTerm) Literal ¶
func (t WhereTerm) Literal() WhereClause
Literal returns the predicate a non-group term represents.
func (WhereTerm) Predicates ¶
func (t WhereTerm) Predicates() []WhereClause
Predicates returns the literals the term contributes: itself for a single predicate, or its members for a group.
type WireQuery ¶
type WireQuery struct {
Select []string `json:"select,omitempty"`
Where []WhereTerm `json:"w,omitempty"`
OrderBy OrderBys `json:"o,omitempty"`
Limit int `json:"l,omitempty"`
Offset int `json:"f,omitempty"`
Aggregations []AggSpec `json:"g,omitempty"`
}
WireQuery is the server-bound subset of QueryState (the TS ServerQuery / the {s,w,o,...} `?q=` payload). View-only state (column widths) never arrives.
OrderBy unmarshals from BOTH the new array form and the legacy single-object form, so legacy links keep compiling.
func DecodeWireQuery ¶
DecodeWireQuery decodes the base64url-encoded JSON `?q=` token produced by @pythia-software/query-table-core encodeQuery. The select tuples carry widths the server ignores; only the field names are extracted. Mirrors the charset/padding fix-ups so a bookmark round-trips bit-for-bit.
func (*WireQuery) UnmarshalJSON ¶
UnmarshalJSON accepts both the readable ServerQuery property names emitted by @pythia-software/query-table-core and the compact names used inside a URL token. It validates resource limits before returning, so a normal json.Decoder is a safe API boundary even when the caller does not use DecodeWireQuery.