Documentation
¶
Overview ¶
Package planner compiles the validated GQL AST to parameterized PostgreSQL SQL against the search_index union view (SPEC-04 §6, §8).
M9 (landed): the three-branch MR-6 projection (witness | any-union | asset-all), the mode filter (ModeService default / ModeAsset DISTINCT ON asset_id), tokenized/substring/element-substring matching (==, IN, CONTAINS-ILIKE on strings, CONTAINS @> ARRAY on lists), IN_CIDR (ip::inet <<= cidr), IN_DOMAIN (domain = v OR domain ILIKE '%.' || v), sort rules (NULLS LAST/FIRST per direction; row_id ASC tiebreaker always appended), cursor pagination (disjunctive strict-advancement predicate correct for any primary direction + ASC row_id tiebreaker), limit/timeout wiring (Plan.Timeout carried as a Go duration and applied separately via Plan.ApplyStatementTimeoutSQL — PG's SET LOCAL does not accept a $N placeholder, so the timeout value never appears literally in any SQL text, AC-2.8), parameterized SQL only ($N placeholders, never interpolated user values, AC-2.8), and the explain output (Plan.Explain: mode, branch set, limit, timeout, sort, cursor, projection, SQL, args).
The three branches classify a compiled Plan for EXPLAIN output:
- witness: a flat per-row predicate evaluated directly against search_index (the common case — port == 443, http.status == 200);
- any-union: an ANY(host){ body } block compiled to a correlated EXISTS subquery against a fresh alias of search_index (the host's services) — the outer row matches if any inner service row of the same asset_id satisfies body;
- any-service-inline: an ANY(service){ body } block folded into the outer predicate (search_index is already per-service);
- asset-all: mode=asset wraps the projection in a WITH page AS (…) DISTINCT ON (asset_id) subquery preferring last_seen DESC, so the result is one row per asset.
A single Plan can carry several branches at once (e.g. a witness predicate ANDed with an any-union, run in mode=asset).
Layering: the planner is a pure compiler. It imports internal/gql/ast only (plus the Go standard library). It does NOT import pgx or any execution package — Plan.ApplyStatementTimeoutSQL returns the parameterized SQL the M10 SearchService runs; the planner itself executes nothing. The planner never imports any collector or egress package (ADR-0010 Search ≠ Scan; AC-3.1 import-graph test).
Index ¶
Constants ¶
const ( // CodePlanEmpty is returned when Compile is called with a nil root. CodePlanEmpty = "GQL_E_PLAN_EMPTY" // CodePlanSortField is returned when the requested sort field is not // in the sortable registry (or is a list/bool field, which cannot // sort meaningfully). CodePlanSortField = "GQL_E_PLAN_SORT_FIELD" // CodePlanSortDir is returned when the sort direction is not one of // "asc" / "desc" (case-insensitive). CodePlanSortDir = "GQL_E_PLAN_SORT_DIR" // CodePlanCursor is returned when the cursor cannot be decoded or // does not match the current sort field/direction. CodePlanCursor = "GQL_E_PLAN_CURSOR" // CodePlanLimit is returned when the requested limit is out of range. CodePlanLimit = "GQL_E_PLAN_LIMIT" // CodePlanTimeout is returned when the requested timeout is out of // range (must be > 0 and <= the 30s hard cap, PD-1). CodePlanTimeout = "GQL_E_PLAN_TIMEOUT" // CodePlanMode is returned when the mode is not ModeService or // ModeAsset. CodePlanMode = "GQL_E_PLAN_MODE" // CodePlanProjection is returned when a requested projection field // is not a known search_index column. CodePlanProjection = "GQL_E_PLAN_PROJECTION" )
Stable planner error codes.
const ( DefaultLimit = 100 MaxLimit = 1000 MinTimeout = 100 * time.Millisecond MaxTimeout = 30 * time.Second )
Defaults and hard caps (PD-1, SPEC-03 §3).
Variables ¶
This section is empty.
Functions ¶
func SortedFieldNames ¶
func SortedFieldNames() []string
SortedFieldNames returns the sortable field set, sorted (used by the search command's help text).
Types ¶
type Branch ¶
type Branch struct {
Witness bool // a flat per-row predicate (no quantifier)
AnyUnion bool // an ANY(host){…} block compiled to an EXISTS subquery
AssetAll bool // mode=asset DISTINCT ON (asset_id) projection
AnyService bool // an ANY(service){…} block compiled inline (folded to witness)
}
Branch classifies the compiled Plan for EXPLAIN output. A single Plan can carry several branches (e.g., a flat predicate wrapped in mode=asset AND containing an ANY(host){…} subquery).
func CompileWhere ¶
CompileWhere compiles a validated AST into ONLY the WHERE clause over search_index plus its positional args — no SELECT, ORDER BY, or LIMIT. It is the facet engine's entry point (internal/search/facets.go): facet aggregates must filter over exactly the rows the search would return, so they reuse the planner's WHERE compilation verbatim (same operator lowering, same alias qualification, same literal handling). The returned SQL is safe to embed in `... FROM search_index WHERE (<where>) ...`.
type CursorPayload ¶
type CursorPayload struct {
SortField string
RowID int64
// RowKind completes the row key: search_index.row_id is the
// service id on 'service' rows and the ASSET id on 'asset-only'
// rows — two independent id sequences that can collide numerically,
// so (row_kind, row_id) is the true unique row key and the
// tiebreaker must advance over BOTH.
RowKind string
Value string // stringified sort-key value at the cursor row
// Null is true when the cursor row's sort value was SQL NULL (the
// value is ""). NULL rows sort at the ASC-tail / DESC-head (NULLS
// LAST / NULLS FIRST), so the advancement predicate needs the
// NULL-aware forms in renderCursorPredicate.
Null bool
}
CursorPayload is the decoded form of the opaque pagination cursor.
func DecodeCursor ¶
func DecodeCursor(s string) (*CursorPayload, error)
DecodeCursor parses an opaque base64 cursor.
func (CursorPayload) Encode ¶
func (c CursorPayload) Encode() string
Encode renders an opaque base64 cursor from a cursor payload.
type Direction ¶
type Direction int
Direction is a sort direction.
func ParseDirection ¶
ParseDirection parses "asc" / "desc" (case-insensitive).
type Mode ¶
type Mode int
Mode is the search mode (PD-1; default service).
type Options ¶
type Options struct {
Mode Mode
Sort []SortSpec // empty = default (last_seen DESC, row_id ASC)
Limit int // 0 = server default (DefaultLimit)
Timeout time.Duration
Cursor string // opaque base64 from a prior Result.NextCursor
Fields []string
}
Options controls the compiled Plan.
type Plan ¶
type Plan struct {
SQL string
Args []any
Branch Branch
Mode Mode
Sort []SortSpec
Limit int
Timeout time.Duration
// CursorDecoded holds the (sort_key, row_id) decoded from
// Options.Cursor, for EXPLAIN. Nil if no cursor was supplied.
CursorDecoded *CursorPayload
// ProjectionFields is the field set the SQL projects (after mode
// collapse if ModeAsset). Empty = default = all search_index cols.
ProjectionFields []string
}
Plan is the compiled output.
func (*Plan) ApplyStatementTimeoutSQL ¶
ApplyStatementTimeoutSQL returns a parameterized SQL statement that applies Plan.Timeout as a transaction-local statement_timeout when executed in the same transaction as Plan.SQL. M10's SearchService uses this so the timeout value never appears literally in any SQL text (AC-2.8): the value is bound through the args slice.
The returned (sql, args) pair is `SELECT set_config('statement_timeout', $1::text, true)` with the timeout in milliseconds as a string. PG's `SET LOCAL` does not accept a $N placeholder, so set_config() is the parameterized form. Returns ("", nil) when the plan has no timeout.