querytable

package module
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 6, 2026 License: MIT Imports: 12 Imported by: 0

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

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

View Source
const ComputedColumnsDDL = `` /* 349-byte string literal not displayed */

Variables

View Source
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

type AggCompileResult struct {
	SelectExprs []string
	GroupBySQL  string
}

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

func Compile(q WireQuery, schema Schema, startIdx int) (CompileResult, int, error)

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 ComputedExpression struct {
	Language string `json:"language"`
	Version  int    `json:"version"`
	Source   string `json:"source"`
}

type DistinctCompile

type DistinctCompile struct {
	Expr      string
	SearchSQL string // "" when search is empty
	Args      []any
}

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 FieldKind

type FieldKind int

FieldKind picks the value-coercion + operator-validation path.

const (
	FieldText FieldKind = iota
	FieldNumber
	FieldDatetime
	FieldBool
	FieldEnum
	FieldTextArray
)

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

func (o *OrderBys) UnmarshalJSON(b []byte) error

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

type SQLComputedColumnStore struct{ DB *sql.DB }

func (SQLComputedColumnStore) List

func (s SQLComputedColumnStore) List(ctx context.Context, scope, dataset string) ([]ComputedColumn, error)

func (SQLComputedColumnStore) Save

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

func LoadSchema(doc []byte) (Schema, error)

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

func (t WhereTerm) IsGroup() bool

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

func DecodeWireQuery(token string) (WireQuery, error)

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

func (q *WireQuery) UnmarshalJSON(data []byte) error

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.

func (WireQuery) Validate

func (q WireQuery) Validate() error

Validate rejects malformed query shapes. A zero Limit is allowed so an omitted value can be replaced by the caller's default; a negative value is never allowed. Values too large for the platform fail during JSON decoding.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL