sql

package
v0.18.52 Latest Latest
Warning

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

Go to latest
Published: Sep 6, 2026 License: AGPL-3.0 Imports: 10 Imported by: 0

Documentation

Overview

Package sql provides SQL parsing using a custom recursive descent parser.

Index

Constants

View Source
const UnnamedOutputColumn = "?column?"

UnnamedOutputColumn is the name PostgreSQL gives an output column that has no natural one: an operator expression, a literal, a predicate, a scalar subquery over an unnamed item. It is spelled exactly as PostgreSQL spells it, question marks included, because it is what a client reads out of RowDescription and what a query has to quote to refer to the column.

Variables

This section is empty.

Functions

func AppendRowLimit added in v0.18.33

func AppendRowLimit(sql string, info *SelectInfo, n int) string

AppendRowLimit returns sql bounded to at most n rows, when a LIMIT can be appended to it without changing what it means.

It exists because two of the four subquery constructs do not want a SET. EXISTS asks whether there is A row and a scalar subquery is an ERROR past ONE, so reading the whole result to answer either is work nobody asked for — and on the DML door it was a WRONG REFUSAL: that door bounds what a subquery may return (54000, `WADJET_IN_SET_MAX`), so `DELETE FROM t WHERE EXISTS (SELECT 1 FROM big)` refused at the ten thousandth row of a question one row answers, and a scalar subquery past the bound reported 54000 where this engine's own rule is 21000. Bounding the READ puts each construct back inside the rule it is judged by: IN keeps the bound because IN really does need the set.

The append is declined — sql is returned unchanged — when the query already bounds itself with LIMIT or OFFSET, or is a UNION, because in those spellings a trailing LIMIT is not an append: it is either a syntax error or a second, differently-scoped bound. Those keep whatever behaviour they had.

info must be the parse of sql. Callers that hold one (the correlated evaluators parse once at compile time and rebuild per row) pass it; callers that hold only text use WithRowLimit.

func CheckSingleStatement added in v0.18.23

func CheckSingleStatement(sql string) error

CheckSingleStatement refuses a string that carries more than one statement, the way PostgreSQL refuses one at an entry point that answers with a single result.

THE ORDER IS PART OF THE ANSWER, and it is measured against 17.11 rather than remembered. PostgreSQL parses the whole string first, so

INSERT INTO t (id) VALUES (1); INSERT INTO t (id) VALUES (2)
    -> 42601  cannot insert multiple commands into a prepared statement
INSERT INTO t (id) VALUES (1); ZZZ NOT SQL
    -> 42601  syntax error at or near "ZZZ"

A syntax error anywhere in the string outranks the multi-command refusal, and reporting the multi-command message for the second one would tell a client its SQL was fine when it was not. Both carry 42601.

A single statement costs nothing here: the string is not parsed twice, because the caller is about to parse it anyway.

func CoerceBooleanLiterals added in v0.18.30

func CoerceBooleanLiterals(info *SelectInfo) error

CoerceBooleanLiterals resolves an UNKNOWN-typed literal used as a truth value, which is the one shape a boolean context accepts that is not already a boolean (#599).

PostgreSQL types a bare quoted literal from the context it meets, so a boolean context runs it through the boolean input function: measured live on postgres:17-alpine, `SELECT 1 WHERE 'true'`, `'yes'`, `'t'` and `' 1 '` all SUCCEED, `WHERE NULL` succeeds and returns nothing, and `WHERE 'abc'` is 22P02 `invalid input syntax for type boolean: "abc"` — NOT the 42804 a typed non-boolean gets.

Wadjet answered 0 rows for all four, because nothing typed the literal at all: `expr.FilterPredicate`'s generic arm takes a failed `v.(bool)` assertion for FALSE. So this runs at parse time, where the literal can simply BECOME the boolean it names.

The grammar is PostgreSQL's `parse_bool_with_len`, the same one `CAST(<string> AS BOOLEAN)` already follows (ADR-0012): case-insensitive, C whitespace trimmed, any non-empty PREFIX of "true"/"false"/"yes"/"no", plus "on"/"off" and the single characters "1" and "0". `'tr'` and `'fals'` ARE values; `'o'` alone is not, because it cannot choose between "on" and "off".

func ExprIdentity added in v0.18.6

func ExprIdentity(n Node) string

ExprIdentity renders an expression in the canonical form that answers "are these the same expression?".

Three spelling differences are erased, and only these three:

  • PARENTHESES. `(g + 1)`, `((g) + 1)` and `g + 1` are one identity. The parse tree already records the grouping, so a ParenNode carries no information the tree does not.
  • IDENTIFIER CASE. `G + 1` and `g + 1` are one identity, the way PostgreSQL folds an unquoted identifier.
  • WHITESPACE. `g+1` and `g + 1` are one identity, which the AST rendering already gave.

Nothing else is erased. Associativity in particular is NOT: `g - 1 - 2` is `(g - 1) - 2` and `g - (1 - 2)` is itself, and the two identities differ, because the two expressions differ. That is why the rendering is fully parenthesised at every infix node rather than relying on String()'s precedence-free output — dropping a ParenNode and printing `a * b + c` for `a * (b + c)` would make two DIFFERENT expressions one identity, which is the wrong answer in the more dangerous direction.

func ExprIdentityUnqualified added in v0.18.21

func ExprIdentityUnqualified(n Node) string

ExprIdentityUnqualified is ExprIdentity with TABLE QUALIFIERS erased as well, so `typemx.g + 1` and `g + 1` are one identity (#738).

It is a SEPARATE function and not a fourth rule in ExprIdentity, because erasing a qualifier needs a SCOPE that this file does not have: `a.x` and `b.x` over a join are two expressions and `t.x` and `x` in a single-relation block are one. Only a caller holding the block's FROM list can tell them apart, and exactly one does — physical.groupCheck, which uses this when the block has ONE source and ExprIdentity when it has more.

Erasing it unconditionally would make two different expressions one identity, which is the failure this file's header calls "the wrong answer in the more dangerous direction".

func FoldIdent added in v0.18.30

func FoldIdent(s string) string

FoldIdent is the case fold PostgreSQL applies to an UNQUOTED identifier: ASCII A-Z becomes a-z and NOTHING else changes.

The ASCII restriction is PostgreSQL's own, measured on postgres:17-alpine with a UTF8 server encoding: `CREATE TABLE t (Ä int)` stores the column as `Ä`, and `SELECT 1 AS Ä` publishes `Ä`. strings.ToLower would fold it to `ä` and invent a name no PostgreSQL client would expect, so the fold is written out byte by byte rather than borrowed from unicode.

func GroupKeyName added in v0.18.6

func GroupKeyName(n Node) string

GroupKeyName is the column name a GROUP BY term's value is published under by the aggregate, on both execution paths.

A bare column reference keeps its own name with any delimiters stripped, so `GROUP BY "g + 1"` names the column `g + 1` and not the four-token string `"g + 1"` the worker's hash aggregate cannot find in a batch (#725). Anything else — a computed key — is named by its own rendered text with redundant outer parentheses removed, so `GROUP BY (g + 1)` and `GROUP BY g + 1` publish ONE name and a SELECT item spelled either way resolves to it.

Case is PRESERVED: this is a name a batch column is matched against by bytes, so the name must be what the value is actually published under. ExprIdentity, which is only ever compared, is the one that folds case.

func HasTopLevelReturning added in v0.18.5

func HasTopLevelReturning(sql string) bool

HasTopLevelReturning reports whether a DML statement writes a RETURNING clause outside any parentheses.

RETURNING is not a lexer keyword, so every clause that collects raw text swallowed it and then answered differently: bare `DELETE FROM t RETURNING *` took it as the table's ALIAS, `DELETE ... WHERE id = 1 RETURNING *` fed it to the WHERE's complete-parse and called legal SQL a syntax error, and `INSERT ... RETURNING id` dropped it in silence and reported INSERT 1. One check over the whole statement gives all four doors the same answer: it is a legal statement whose feature this server has not implemented, so 0A000 (#686 R2-4).

An unquoted RETURNING is always the clause — PostgreSQL reserves the word, so a column of that name must be double-quoted, and a quoted identifier is not matched here.

func HasTopLevelWhereToken added in v0.18.5

func HasTopLevelWhereToken(sql string) bool

HasTopLevelWhereToken reports whether sql spells a WHERE keyword outside any parentheses.

It exists for one caller: the backstop in wadjet.BuildDMLPredicate, which has to tell "this DELETE is unconditional because it was written that way" from "this DELETE is unconditional because the parser dropped its WHERE". The two are indistinguishable in DMLTarget.WhereSQL, and the second one empties tables (#686).

The lexer decides, not strings.Contains: a WHERE inside a string literal ('WHERE') or a quoted identifier ("where") is not a clause, and a WHERE belonging to a SUBQUERY (`SET n = (SELECT ... WHERE ...)`) is not this statement's clause either — hence the depth counter.

func IsAggregate

func IsAggregate(name string) bool

IsAggregate returns true if the function name is a known aggregate.

func LeadingKeyword added in v0.18.20

func LeadingKeyword(sql string) string

LeadingKeyword returns a statement's first keyword, uppercased, with leading whitespace and comments skipped — or "" when the statement has no keyword.

It exists because a TEXT PREFIX is not a classifier. pgwire decided "is this DML" with `strings.HasPrefix(upper, "INSERT ")` — a literal space — so `INSERT\nINTO t …`, `UPDATE\tt SET …` and `/* hint */ UPDATE …` all missed the branch. Multi-line SQL is what every ORM and JDBC prepared statement emits and a `/* … */` prefix is what sqlcommenter prepends, so the miss was the common case, not the corner: those statements fell through to the QUERY path, which meant Describe EXECUTED the write and Execute ran it AGAIN — a duplicated row reported as `SELECT 1` (review B3).

The lexer already knows what a comment and a quoted identifier are, so this asks it rather than re-deciding.

func NormalizeIdentRef

func NormalizeIdentRef(s string) string

NormalizeIdentRef strips the delimiters from an identifier reference so that it reads as the plain column name the execution layer matches against batch schemas: `"id.orig_h"` → id.orig_h, `"my tbl"."c"` → my tbl.c. Strings that are not a bare identifier reference (expressions, function calls, literals) are returned unchanged.

func OuterColumnCandidates

func OuterColumnCandidates(subquerySQL string) []string

OuterColumnCandidates returns the column names a subquery may read from the query that ENCLOSES it: every reference it cannot resolve against its own FROM clause. Subqueries nested inside it are walked too, so a correlation two levels down is reported at the top.

It exists for column pruning. A column a correlated subquery reads is a column the outer query NEEDS, even when it appears nowhere in the outer SELECT list or WHERE clause — and the pruning walk had no case for a subquery node at all, so it never saw one. The outer batch then carried no such column, readOuterValues substituted NULL for it, every comparison against that NULL was UNKNOWN, and the query answered 0 rows with no indication anything had gone wrong (issue #347).

It is deliberately over-inclusive where it cannot be sure. An unqualified name is reported even though the subquery's own FROM may well supply it, because deciding that needs a catalog this package does not have. Naming a column the outer relation does not have costs nothing — the caller filters candidates against the scan's own schema (sanitizeScanNeeds) — while missing one it does have is the wrong answer above. A reference qualified by one of the subquery's own tables or aliases is the one case it can rule out, and does.

A subquery that does not parse yields no candidates: the expression compiler parses the same text and declines to build a correlated evaluator for it, and the runtime guard in readOuterValues fails loudly if one is built anyway.

func OutputColumnName added in v0.18.43

func OutputColumnName(col SelectColumn) string

OutputColumnName is the name PostgreSQL publishes an unaliased SELECT item under — its `FigureColname`, decided from the parsed AST and never from the expression's TEXT.

The whole rule, measured on PostgreSQL 17 over 49 spellings (#732):

g, t.g, (g)                       → g            (the column)
(c_row).b                         → b            (the FIELD)
abs(g), count(*), sum(g) OVER ()  → abs, count, sum   (the function)
CASE …, COALESCE, NULLIF, GREATEST → case, coalesce, …
EXISTS (…)                        → exists
ARRAY[…]                          → array
EXTRACT(YEAR FROM d)              → extract
CAST(g AS int), g::int            → g            (the ARGUMENT)
CAST('2020-01-01' AS date)        → date         (the TYPE, only when the
                                                  argument has no name)
(SELECT g FROM … LIMIT 1)         → g            (the subquery's column)
g + 1, -g, 1, 'abc', g IS NULL,
g = 1, g BETWEEN 1 AND 2,
g IN (1,2), g || 'x', (SELECT 1)  → ?column?

A CAST is the one PostgreSQL gets asked about most and the one most often guessed wrong: it is named after its ARGUMENT, and reaches for the type only when the argument itself is unnamed. The brief for arc E3 said "a CAST → the TYPE" and the measurement said otherwise.

Several `?column?` in one SELECT list is legal and is what PostgreSQL does: `SELECT g + 1, g + 2` returns two columns of that name. Output slots have identity by POSITION (#556/#557), so a duplicate published name is not a collision.

It returns "" for a STAR, which has no single name.

func ParseBoolText added in v0.18.30

func ParseBoolText(s string) (bool, bool)

ParseBoolText is PostgreSQL's boolean input function, `parse_bool_with_len`. Exported because two doors need the SAME grammar and a second copy is how they drift: a boolean CONTEXT here, and CAST(<string> AS BOOLEAN) in the expression compiler.

func QuoteIdent

func QuoteIdent(name string) string

QuoteIdent renders an identifier so that re-parsing it yields the same identifier. Names the lexer would otherwise re-read as something else — embedded dots (a flat JSON column such as id.orig_h), spaces, other punctuation, a leading digit, an ASCII UPPER-CASE letter (which the lexer folds, #731), or a keyword spelling — come back double-quoted with any interior quote doubled. Names that already lex as a single unquoted identifier are returned unchanged, so printed SQL for ordinary columns is byte-identical to what it was before delimited identifiers existed.

func RebuildSQL

func RebuildSQL(info *SelectInfo, rewrittenWhere Node) string

RebuildSQL reconstructs a full SELECT SQL string from a SelectInfo, using the provided expression as the WHERE clause instead of the original. This is used by the correlated subquery evaluator to substitute outer values.

func RefuseReservedSlotName added in v0.18.6

func RefuseReservedSlotName(name, where string) error

RefuseReservedSlotName returns the 42939 (reserved_name) refusal for a name a user is CREATING that collides with a hidden slot, or nil when it does not. where names the site, so the message says which door saw it.

Call it where a name is MINTED BY THE USER: a SELECT alias, a derived table's or a CTE's output names, and the DDL and ingest doors (CREATE TABLE, the CreateTable API, an Ingester's schema, an INSERT column list that creates a column).

Do NOT call it on a table's STORED columns at read time. Reading is not minting: the column already exists, some binary wrote it, and refusing it makes the table unreadable by every query — including the `SELECT *` that would show the user what is in it — while CREATE TABLE, the Go API and INSERT all still succeeded, so the trap closed behind the user with DROP TABLE the only exit. A stored collision is handled by renumbering the PLANNER's slot instead (renameCollidingSlots), so the two coexist.

func RefuseReservedSlotNames added in v0.18.6

func RefuseReservedSlotNames(names []string, where string) error

RefuseReservedSlotNames refuses the first colliding name in names, in sorted order so the message does not depend on map iteration.

func RefuseUnsupportedStatement added in v0.18.42

func RefuseUnsupportedStatement(pq *ParsedQuery) error

RefuseUnsupportedStatement is the refusal a door owes a parsed statement no handler of that door accepts. It returns nil for a statement that IS a query, so a door calls it once, at the point it dispatches by statement type, immediately after its own handlers have had their turn.

#860: `ALTER TABLE` — and `CREATE VIEW`, `DROP VIEW`, and on the HTTP door the alert and snapshot statements — parse into a ParsedQuery whose type no branch handles, so every door fell through to ExtractSelect and reported `no SELECT info in parsed query`: an internal invariant's wording, with no SQLSTATE on the HTTP door and the blanket 42000 through pgwire. A client cannot branch on that, and the message names nothing it wrote.

The disposition is 0A000 (feature_not_supported), which is already this engine's class for a shape it parses and cannot execute (`RETURNING is not supported`, above), and the message names the STATEMENT. It is raised here rather than inside ExtractSelect because a better message from ExtractSelect would still be produced by planner code, one call site at a time, for a statement no planner should ever have been handed.

func ReservedSlotFamily added in v0.18.6

func ReservedSlotFamily(name string) string

ReservedSlotFamily returns the slot prefix name collides with, or "".

The comparison is case-insensitive because column resolution is: a user who writes `__WIN_0` reaches the same slot every consumer looks up.

func RevertGroupByAliasesShadowedByInput added in v0.18.16

func RevertGroupByAliasesShadowedByInput(info *SelectInfo, provides func(bare string) bool)

RevertGroupByAliasesShadowedByInput applies PostgreSQL's precedence for a bare GROUP BY name: an INPUT COLUMN wins over a SELECT alias.

The parser substitutes such a name with the alias's defining expression unconditionally, and its own doc comment claimed the opposite ("a table column with the same name keeps precedence over the alias") — protocol item 9's exact failure mode, a record describing intended behaviour as present behaviour. There is no precedence check in the parser and there cannot be one: it has no schema and no scope. So the substitution is provisional and this undoes it, called from the layer that knows what the FROM sources provide.

provides reports whether one of this block's own sources carries the bare name. It must answer only where it is CERTAIN: an unenumerable source (a table function, a SELECT *, a table absent from the catalog) has to answer false, which keeps the substitution and the pre-#739 answer.

The wrong-answer shape this closes: `SELECT h AS g, COUNT(*) FROM gcov GROUP BY g, h` grouped by (h, h) and answered 2 rows where PostgreSQL — which groups by (g, h) — answers 6. Both engines answered, and they answered different numbers.

func SetAlertIntervalFloorForTest

func SetAlertIntervalFloorForTest(d time.Duration) func()

SetAlertIntervalFloorForTest lowers the CREATE ALERT interval floor for the duration of a test. Call the returned function (typically with defer) to restore the production floor.

func SlotName added in v0.18.6

func SlotName(family SlotFamily, n int) string

SlotName is the Nth slot of a family: `SlotName(SlotWindowOutput, 0)` is `__win_0`.

Minting a slot through this rather than through a local fmt.Sprintf is what keeps the reservation and the names in one place — a family whose names are built somewhere else is a family the refusal below does not really cover.

func SplitIdentRef

func SplitIdentRef(s string) (qualifier, name string, ok bool)

SplitIdentRef interprets s as an identifier reference and returns its optional qualifier and its name. Quoting is honoured, so the qualifier split happens only at a dot the lexer actually produced:

l_orderkey     → ("", "l_orderkey")
o.o_custkey    → ("o", "o_custkey")
"id.orig_h"    → ("", "id.orig_h")   — one delimited name, not qualified
"my tbl"."c"   → ("my tbl", "c")

ok is false for anything that is not a bare identifier reference (a function call, an arithmetic expression, a literal, a multi-level path); callers then fall back to their own string handling.

func SplitStatements added in v0.18.23

func SplitStatements(sql string) []string

SplitStatements splits a SQL string into the statements a client wrote, separated by TOP-LEVEL semicolons.

It exists because PostgreSQL's simple query protocol carries a whole script in one message and runs it as a SEQUENCE, one CommandComplete per statement. Everything here that reached a semicolon before this did `strings.TrimRight(sql, ";")` and nothing else, so a two-statement string was ONE statement to every parser below it — and what happened to the tail then depended on which sub-parser consumed it. `INSERT …; INSERT …` ran the first and silently dropped the second, and `INSERT …; ZZZ NOT SQL` ran the INSERT and silently ignored the garbage, where PostgreSQL runs neither (#711).

THE LEXER DECIDES, not strings.Split. A semicolon is a separator only where it is a semicolon TOKEN at paren depth zero, so

UPDATE t SET name = 'a;b' WHERE id = 1     -- one statement
SELECT "a;b" FROM t                        -- one statement
SELECT 1 -- ; not a separator
SELECT $$a;b$$                             -- one statement
SELECT /* ; */ 1                           -- one statement

all stay whole: the lexer reads string literals, dollar-quoted strings, double-quoted identifiers and both comment forms, so none of them can emit a TokenSemicolon.

The DEPTH counter is the same guard HasTopLevelWhereToken uses. A semicolon inside parentheses cannot separate statements in any legal SQL, and treating one as a separator would cut a statement in half and report the halves' errors instead of the statement's. When the parens never balance — the input is malformed — depth never returns to zero, nothing is split, and the whole string goes to Parse, which is where a malformed statement's error belongs.

A lex ERROR (an unterminated string literal, say) stops the scan and the unconsumed remainder is returned as one final piece, for the same reason: the error is the statement's, and Parse is what reports it.

A piece with NO STATEMENT IN IT is dropped, and that means whitespace, semicolons AND COMMENTS. `SELECT 1; -- trailing comment` is one statement in PostgreSQL and was one statement here before this function existed; treating the comment as a second statement made that string — and `…; /* banner */`, and a stray `--` from an editor, and the `-- query tag` every ORM appends — a two-statement string that every one-statement door then refused with 42601. Trimming whitespace alone is not enough for the same reason `strings.Split` is not enough one paragraph up: only the lexer knows what a comment is. A piece whose first token is EOF holds nothing to run.

An input with no statement at all yields nil, which callers read as PostgreSQL's EmptyQuery — and PostgreSQL answers a comment-only query string with exactly that.

func WindowOutputName added in v0.18.6

func WindowOutputName(col SelectColumn) string

WindowOutputName is the name a SELECT-list window column is published under.

The alias where the query gave one, and otherwise the FUNCTION's name, which is what PostgreSQL 17 calls it:

SELECT SUM(a) OVER () FROM t              -- PostgreSQL: "sum"
SELECT ROW_NUMBER() OVER (ORDER BY id)…   -- PostgreSQL: "row_number"

FIVE places name this column and they have to agree: the logical builder's projection (which decides what the operator emits), the embedded API's deriveColumns (the single-process result schema), the binder's blockOutputs (a derived table's namespace), and the two positional-ORDER-BY resolvers. They did not: an unaliased window was `sum(a) OVER (...)` in four of them and the empty string in the fifth, so the projection published nothing, the result schema asked for the text, and `ORDER BY 1` rewrote to a name with parentheses in it that no sort key could resolve.

It lives here rather than in the logical package because the parser cannot import the planner, and the positional resolvers are in the parser.

func WithRowLimit added in v0.18.33

func WithRowLimit(sql string, n int) string

WithRowLimit is AppendRowLimit for a caller that holds only the subquery text. A statement that does not parse is returned unchanged: this function decides how much to READ, never what is legal, and the compiler and the runner both raise on their own for text they cannot use.

Types

type AlterAlertInfo

type AlterAlertInfo struct {
	Name   string
	Enable bool // true = ENABLE, false = DISABLE
}

AlterAlertInfo holds details for ALTER ALERT ... ENABLE|DISABLE.

type AlterTableInfo

type AlterTableInfo struct {
	Table         string
	Action        string // "ADD COLUMN", "DROP COLUMN", "RENAME COLUMN"
	ColumnName    string
	NewColumnName string // for RENAME COLUMN
	ColumnType    string // for ADD COLUMN
	Nullable      bool   // for ADD COLUMN (default true)
}

AlterTableInfo holds details for an ALTER TABLE statement.

type AnalyzeTableInfo

type AnalyzeTableInfo struct {
	Name string
}

AnalyzeTableInfo holds details for an ANALYZE TABLE statement.

type AndNode

type AndNode struct {
	Left  Node
	Right Node
}

AndNode is a logical AND.

func (*AndNode) String

func (a *AndNode) String() string

type AnyAllExpr

type AnyAllExpr struct {
	Left     Node
	Op       string // =, !=, <, <=, >, >=
	Modifier string // "ANY", "ALL", "SOME"
	Values   []Node // value list or single SubqueryNode
}

AnyAllExpr is expr op ANY/ALL/SOME (subquery or values).

func (*AnyAllExpr) String

func (a *AnyAllExpr) String() string

type ArrayLitNode

type ArrayLitNode struct {
	Elements []Node
}

ArrayLitNode is ARRAY[expr, expr, ...].

func (*ArrayLitNode) String

func (a *ArrayLitNode) String() string

type BetweenExpr

type BetweenExpr struct {
	Left Node
	Not  bool
	Low  Node
	High Node
}

BetweenExpr is expr [NOT] BETWEEN low AND high.

func (*BetweenExpr) String

func (b *BetweenExpr) String() string

type BinaryOp

type BinaryOp struct {
	Left  Node
	Op    string // +, -, *, /, %, ||
	Right Node
}

BinaryOp is a binary arithmetic/string expression.

func (*BinaryOp) String

func (b *BinaryOp) String() string

type CTEDef

type CTEDef struct {
	Name      string   // CTE name (lowercased for matching)
	SQL       string   // the CTE body SQL (the SELECT inside the parentheses)
	Columns   []string // optional column name list
	Recursive bool     // WITH RECURSIVE
	// contains filtered or unexported fields
}

CTEDef represents a Common Table Expression definition.

func (*CTEDef) BodySelect added in v0.18.35

func (c *CTEDef) BodySelect() (*SelectInfo, error)

BodySelect returns the parsed SELECT body of a CTE definition, memoized on the definition, for the same reason SubSelect memoizes a derived table's.

type CaseNode

type CaseNode struct {
	Subject Node         // nil for searched CASE
	Whens   []WhenClause // at least one
	Else    Node         // nil if no ELSE
}

CaseNode is a CASE expression.

func (*CaseNode) String

func (c *CaseNode) String() string

type CastNode

type CastNode struct {
	Inner    Node
	TypeName string
}

CastNode is CAST(expr AS type).

func (*CastNode) String

func (c *CastNode) String() string

type CmpExpr

type CmpExpr struct {
	Left  Node
	Op    string // =, !=, <, <=, >, >=
	Right Node
}

CmpExpr is a comparison expression.

func (*CmpExpr) String

func (c *CmpExpr) String() string

type ColRef

type ColRef struct {
	Table  string
	Column string
	// Slot marks a reference the PLANNER planted at a hidden slot, as
	// opposed to one the user wrote. Nothing a query can contain sets it:
	// the parser never does, and re-parsing a rendered expression loses it,
	// because it is provenance and not spelling.
	//
	// It exists because the two are otherwise indistinguishable. A table may
	// legitimately store a column called `__win_0` (ADR-0025 rule 1: reading
	// such a table is never refused), and then `SUM(plain) OVER () + 0` has
	// a `__win_0` reference the nested-window rewrite planted BESIDE a
	// `__win_0` the scan really emits. Moving the planner's slot past the
	// stored column has to move the first and must not touch the second
	// (#750, #694).
	Slot bool
}

ColRef is a column reference, optionally qualified (table.column).

func ColumnRefs added in v0.18.5

func ColumnRefs(n Node) ([]*ColRef, error)

ColumnRefs returns every column reference in an expression tree.

It exists for the callers that must resolve names against a schema BEFORE executing — the DML doors, which do not go through the planner and so never had a name-resolution step at all. `UPDATE t SET n = 1 WHERE nosuchcol = 1` answered "UPDATE 0" because the reference evaluated to NULL on every row, where PostgreSQL raises 42703 (#678).

A node type it does not know is an ERROR, never a silent skip. That is the whole reason it lives beside the AST it walks: a node added to ast.go without a case here fails loudly at the one call site that depends on completeness, instead of quietly letting an unresolvable column through. The three nodes that carry RAW SQL rather than a parsed subtree (SubqueryNode, ExistsNode, WindowFuncNode) are refused for the same reason — their columns are not visible from here.

func ColumnRefsOutsideSubqueries added in v0.18.33

func ColumnRefsOutsideSubqueries(n Node) ([]*ColRef, error)

ColumnRefsOutsideSubqueries is ColumnRefs with SubqueryNode and ExistsNode treated as OPAQUE leaves rather than refused.

A DML predicate containing a subquery is compiled with a real subquery runner and an outer scope (#688), so the names INSIDE the subquery are resolved by the subquery's own planning and the names outside it are this door's to check. Refusing the whole tree because one operand is a subquery is what made `DELETE FROM t WHERE id IN (SELECT id FROM s)` a 0A000.

WindowFuncNode stays refused: a window function is not legal in a WHERE clause on any door, and letting one through here would only move the failure later.

func (*ColRef) String

func (c *ColRef) String() string

type ColumnDef

type ColumnDef struct {
	Name     string
	Type     string
	Nullable bool // true by default; NOT NULL sets it to false
}

ColumnDef defines a column in a CREATE TABLE statement.

type CreateAlertInfo

type CreateAlertInfo struct {
	Name       string
	QueryText  string        // raw SELECT text, re-parsed at eval time
	Interval   time.Duration // validated >= 10s at parse time
	WebhookURL string        // "" if no webhook sink
	Headers    map[string]string
	InsertInto string // "" if no table sink; at least one sink required
}

CreateAlertInfo holds details for a CREATE ALERT statement.

type CreateFunctionInfo

type CreateFunctionInfo struct {
	Name    string
	Params  []string
	Body    string
	Replace bool // CREATE OR REPLACE
	Locked  bool // WITH LOCK
}

CreateFunctionInfo holds details for a CREATE FUNCTION statement.

type CreateSnapshotInfo

type CreateSnapshotInfo struct{}

CreateSnapshotInfo is the AST for a CREATE SNAPSHOT statement. Empty in v1 — statement takes no arguments.

type CreateTableInfo

type CreateTableInfo struct {
	Name          string
	Columns       []ColumnDef
	PartitionKeys []string
}

CreateTableInfo holds details for a CREATE TABLE statement.

type CreateViewInfo

type CreateViewInfo struct {
	Name    string
	SQL     string // the view definition SQL
	Replace bool   // CREATE OR REPLACE VIEW
}

CreateViewInfo holds details for a CREATE VIEW statement.

type DMLTarget added in v0.18.5

type DMLTarget struct {
	Table     string // table name, as the statement spelled it
	Qualifier string // schema/catalog qualifier ("public"), "" when unqualified
	// Alias is the `[AS] a` the statement gave, "" when it gave none. When it
	// is set it HIDES the table name: PostgreSQL answers `DELETE FROM pr AS a
	// WHERE pr.id = 1` with 42P01, not with a delete.
	Alias string
	// WhereSQL is the raw WHERE clause. "" means the statement had NO WHERE
	// at all — which is a legal unconditional statement, and is why StmtSQL
	// is carried beside it: a clause the parser dropped looks identical here.
	WhereSQL string
	// StmtSQL is the whole statement, trimmed. The backstop re-lexes it to
	// ask whether a WHERE keyword the parsed clause does not account for was
	// written.
	StmtSQL string
}

DMLTarget is the relation a DELETE or an UPDATE names, together with the statement text that named it.

It is one type shared by both because both doors (the embedded API and the HTTP server) resolve the two identically, and because the STATEMENT TEXT has to travel with the clause for the empty-predicate backstop in wadjet.BuildDMLPredicate to have anything to check (#686).

type DeleteInfo

type DeleteInfo struct {
	DMLTarget
}

DeleteInfo holds details for a DELETE statement.

type DescribeInfo

type DescribeInfo struct {
	TableName string
}

DescribeInfo holds details for a DESCRIBE/SHOW COLUMNS statement.

type DropAlertInfo

type DropAlertInfo struct {
	Name     string
	IfExists bool
}

DropAlertInfo holds details for a DROP ALERT statement.

type DropFunctionInfo

type DropFunctionInfo struct {
	Name     string
	IfExists bool
}

DropFunctionInfo holds details for a DROP FUNCTION statement.

type DropTableInfo

type DropTableInfo struct {
	Name     string
	IfExists bool
}

DropTableInfo holds details for a DROP TABLE statement.

type DropViewInfo

type DropViewInfo struct {
	Name     string
	IfExists bool
}

DropViewInfo holds details for a DROP VIEW statement.

type ExistsNode

type ExistsNode struct {
	Not bool
	SQL string
}

ExistsNode is [NOT] EXISTS (SELECT ...).

func (*ExistsNode) String

func (e *ExistsNode) String() string

type ExplainInfo

type ExplainInfo struct {
	Verbose  bool
	Analyze  bool
	InnerSQL string
	// InnerType is the statement type EXPLAIN was asked about. EXPLAIN
	// carries the inner statement's SelectInfo and nothing else, so without
	// this a door cannot say WHICH statement it cannot explain.
	InnerType QueryType
}

ExplainInfo holds details for an EXPLAIN statement.

type FrameBound

type FrameBound struct {
	Type   FrameBoundType
	Offset Node // nil for UNBOUNDED/CURRENT ROW
}

FrameBound describes one end of a window frame.

type FrameBoundType

type FrameBoundType int

FrameBoundType identifies the type of a frame bound.

const (
	BoundUnboundedPreceding FrameBoundType = iota
	BoundPreceding
	BoundCurrentRow
	BoundFollowing
	BoundUnboundedFollowing
)

type FrameMode

type FrameMode int

FrameMode identifies ROWS vs RANGE.

const (
	FrameRows FrameMode = iota
	FrameRange
)

type FuncCallNode

type FuncCallNode struct {
	Name     string
	Args     []Node
	Distinct bool // COUNT(DISTINCT col)
	Star     bool // COUNT(*)
	// OutputLabel is the name PostgreSQL publishes this call under when the
	// SELECT list wrote no alias, for the calls the parser REWRITES into a
	// different function: `EXTRACT(YEAR FROM d)` becomes `year(d)` here and
	// PostgreSQL still labels the column `extract`. Empty means Name is the
	// label, which is the ordinary case (OutputColumnName, #732).
	OutputLabel string
}

FuncCallNode is a function call expression.

func FindAllAggregates

func FindAllAggregates(node Node) []*FuncCallNode

FindAllAggregates walks an expression tree and returns all aggregate function calls found. For multi-aggregate expressions like MAX(x) - MIN(x), this returns both aggregates.

func FindNestedAggregate

func FindNestedAggregate(node Node) *FuncCallNode

FindNestedAggregate walks an expression tree and returns the first aggregate function call found, or nil if none exists. This detects aggregates nested inside binary expressions like SUM(x) * 0.0001.

func (*FuncCallNode) String

func (f *FuncCallNode) String() string

type InExpr

type InExpr struct {
	Left   Node
	Not    bool
	Values []Node
}

InExpr is expr [NOT] IN (values...) or expr [NOT] IN (SELECT ...).

func (*InExpr) String

func (e *InExpr) String() string

type InsertInfo

type InsertInfo struct {
	Table   string     // table name
	Columns []string   // target column names (empty = all columns)
	Values  [][]string // rows of value expressions
}

InsertInfo holds details for an INSERT statement.

type IntervalLit

type IntervalLit struct {
	Value int
	Unit  string // "day", "month", "year", "hour", "minute", "second"
}

IntervalLit represents INTERVAL 'N' DAY or INTERVAL 'N days' expressions.

func (*IntervalLit) String

func (i *IntervalLit) String() string

type IsExpr

type IsExpr struct {
	Left  Node
	Not   bool
	Check string // "null", "true", "false"
}

IsExpr is expr IS [NOT] NULL/TRUE/FALSE.

func (*IsExpr) String

func (e *IsExpr) String() string

type JoinInfo

type JoinInfo struct {
	Type          string // join, left join, right join, full outer join, cross join
	LeftTable     string
	RightTable    string
	RightAlias    string
	RightTableRef *TableRef // full right-side table ref (includes function info)
	Condition     string
	CondExpr      Node
	// Using is the column list of a `JOIN ... USING (a, b)`, lower-cased, in
	// the order written. The join CONDITION is desugared into Condition /
	// CondExpr at parse time (`<left>.a = <right>.a AND ...`), because that
	// half needs no catalog; the list is kept because the OUTPUT half does —
	// USING merges the joined column into ONE output column under `SELECT *`,
	// and deciding which columns a star stands for is a catalog question
	// (#655).
	Using   []string
	Lateral bool // LATERAL join — right side can reference left side columns
	// FromItem is the index into SelectInfo.Tables of the comma-separated
	// FROM item this join EXTENDS. A FROM list is a list of items and an
	// explicit JOIN belongs to the item it follows, but the parser flattens
	// items into Tables and joins into Joins, which loses that association:
	// `FROM a JOIN b ON …, c` and `FROM a, b JOIN c ON …` produce two-entry
	// Tables and a one-entry Joins that are otherwise indistinguishable.
	// The builder needs it to attach each join to the right item — folding
	// the comma tables in first instead planned the former as `(a × c) ⋈ b`,
	// which buries a real cross product under the equi-join (#593) and
	// leaves the WHERE equality straddling that join's two sides, where the
	// key pair resolves to nothing and the query answers zero rows (#594).
	// Non-decreasing across Joins, since Tables only grows as parsing
	// advances. Zero for a hand-built JoinInfo, which attaches it to the
	// first item — the same tree the single-item case has always produced.
	FromItem int
}

JoinInfo describes a JOIN clause.

type LikeExpr

type LikeExpr struct {
	Left    Node
	Not     bool
	Pattern Node
}

LikeExpr is expr [NOT] LIKE pattern.

func (*LikeExpr) String

func (l *LikeExpr) String() string

type Lit

type Lit struct {
	Value string
	Kind  LiteralKind
}

Lit is a literal value (string, number, bool, null).

func (*Lit) String

func (l *Lit) String() string

type LiteralKind

type LiteralKind int

LiteralKind identifies the kind of literal value.

const (
	LitString LiteralKind = iota
	LitNumber
	LitBool
	LitNull
)

type LiteralPlaceholder

type LiteralPlaceholder struct {
	Name string
}

LiteralPlaceholder marks a deferred literal whose value is computed at stage-dispatch time from the output of a prerequisite stage. The physical planner inserts these when a filter expression's subquery references a CTE whose distributed-pipeline output would diverge from single-process evaluation; the native-DAG coordinator rewrites the serialized filter expression by string-replacing ":<Name>" with the concrete literal before dispatching the task. String renders as ":<Name>" so any code path that accidentally serializes the expression before substitution produces an unambiguous syntax error instead of silently coercing.

func (*LiteralPlaceholder) String

func (l *LiteralPlaceholder) String() string

type MergeInfo

type MergeInfo struct {
	Target          string // target table name
	TargetQualifier string // schema/catalog qualifier, "" when unqualified
	TargetAlias     string
	Source          string // source table/subquery
	SourceAlias     string
	OnCondition     string            // MERGE ON condition
	WhenClauses     []MergeWhenClause // WHEN MATCHED / NOT MATCHED clauses
}

MergeInfo holds details for a MERGE statement.

type MergeWhenClause

type MergeWhenClause struct {
	Matched   bool   // true = WHEN MATCHED, false = WHEN NOT MATCHED
	Condition string // optional AND condition
	Action    string // "UPDATE", "DELETE", "INSERT"
	SQL       string // raw SET/VALUES clause
}

MergeWhenClause represents a WHEN clause in a MERGE statement.

type Node

type Node interface {
	String() string
	// contains filtered or unexported methods
}

Node is the base interface for all SQL expression AST nodes. Every concrete node type must implement nodeTag (a marker method) and String.

func ParseExpression

func ParseExpression(sql string) (Node, error)

ParseExpression parses a single expression from a SQL string. Used for standalone expression parsing (e.g., UDF bodies, WHERE clauses).

It stops where the expression grammar stops and does NOT require that the whole string was consumed, so a caller holding text that must be an expression IN FULL has to use ParseExpressionComplete instead.

func ParseExpressionComplete added in v0.18.5

func ParseExpressionComplete(sql string) (Node, error)

ParseExpressionComplete parses one expression and refuses text left over after it.

ParseExpression stops at the first token the expression grammar cannot use and reports success, which for a DML WHERE clause is not a truncated expression but silent data loss: `id > 0 AND name @@ 'zzz'` parses to `id > 0`, and a DELETE carrying it removes every row the SURVIVING PREFIX matches. The conjunct that was dropped is the one that would have NARROWED it. Three spellings found this way — an unsupported operator (@@, #), PostgreSQL's ISNULL suffix, and a stray token after a parenthesised term — each emptied a table the full predicate matched in part or not at all (#686 review).

A WHERE this server cannot read in full is a WHERE whose meaning it does not know, and running the part it did read is the worst available answer (ADR-0019, correctness-fix protocol item 8).

func ReplaceAggregate

func ReplaceAggregate(node Node, aggName string) Node

ReplaceAggregate replaces the first aggregate function call in the expression tree with a ColRef pointing to the aggregate output column name.

func ReplaceAllAggregates

func ReplaceAllAggregates(node Node, replacements map[string]string) Node

ReplaceAllAggregates replaces all aggregate function calls in the expression tree with ColRef nodes. The replacements map maps lowercase aggregate expression strings (e.g., "sum(rx_bytes)") to output column names.

func ReplaceGroupKeyRefs added in v0.18.6

func ReplaceGroupKeyRefs(node Node, keys map[string]string) Node

ReplaceGroupKeyRefs rewrites every subexpression that IS one of an aggregate's GROUP BY keys into a reference to the column the aggregate publishes that key under. keys maps ExprIdentity to published name.

This is what makes a HAVING over a computed group key mean anything. Above the aggregate the input columns are gone and only the key's own output column carries the value, so `HAVING g + 1 > 2` written as arithmetic over `g` evaluated to UNKNOWN on every row — and a filter admits only TRUE, so the query returned no rows at all where PostgreSQL returns five (#720).

The walk is TOP-DOWN and stops at the first whole-term match, so the LARGEST expression that is a key is the one replaced: over `GROUP BY g + 1`, the predicate `g + 1 > 2` becomes `"g + 1" > 2` rather than descending to a `g` the aggregate does not emit.

It never enters an aggregate call. Inside one, `SUM(g + 1)`, the expression is evaluated over the aggregate's INPUT rows, where `g` is exactly the column that does exist; replacing it with the grouped output would compute something else entirely.

func ReplaceWindowFuncs added in v0.18.4

func ReplaceWindowFuncs(node Node, replacements map[*WindowFuncNode]string) Node

ReplaceWindowFuncs replaces each window function call named in replacements with a ColRef to its precomputed output column, leaving the surrounding expression intact. Nodes are matched by pointer identity, not by rendered text, because WindowFuncNode.String() collapses the OVER clause and two distinct windows would otherwise collide. It is the window analogue of ReplaceAllAggregates: after the builder has extracted a nested window into a NodeWindow output column, this rewrites SUM(x) OVER (...) + 1 into __win_0 + 1 so the ordinary projection compiler evaluates the outer expression over the window's result.

func RewriteExpr added in v0.18.6

func RewriteExpr(node Node, fn func(Node) (Node, bool)) Node

RewriteExpr rebuilds an expression, offering every node to fn TOP-DOWN. fn returns (replacement, true) to substitute a node and stop descending into it, or (nil, false) to leave it and have its children visited.

It never enters an aggregate call: inside one the expression is evaluated over the aggregate's INPUT rows, which is a different namespace from the grouped output every caller of this is rewriting for.

A node kind it does not know is returned as it stands rather than guessed at — the conservative answer, and the one every caller relied on before this walk was shared.

func RewriteOuterRefs

func RewriteOuterRefs(node Node, outerTables map[string]bool, vals map[string]any) Node

RewriteOuterRefs returns a deep copy of the AST with correlated ColRef nodes replaced by literal values from vals. Keys in vals are "table.column" (lowercased).

func RewriteUnqualifiedOuterRefs

func RewriteUnqualifiedOuterRefs(node Node, unqualOuter map[string]string, vals map[string]any) Node

RewriteUnqualifiedOuterRefs replaces unqualified column references that were detected as outer refs (via column mapping). unqualOuter maps column names (lowercased) to their resolved table. vals contains "table.column" → value.

func Unparen added in v0.18.6

func Unparen(n Node) Node

Unparen strips redundant outer parentheses from an expression. `(g)` is `g`; `(a) + (b)` is unchanged, because its parentheses are not outer.

type NotNode

type NotNode struct {
	Inner Node
}

NotNode is a logical NOT.

func (*NotNode) String

func (n *NotNode) String() string

type OrNode

type OrNode struct {
	Left  Node
	Right Node
}

OrNode is a logical OR.

func (*OrNode) String

func (o *OrNode) String() string

type OrderByItem

type OrderByItem struct {
	Column string
	// Expr is the parsed form of Column. A sort term that is not a plain
	// column reference — `year(d)`, `-id`, `a + b`, an ordinal — can only be
	// honoured by evaluating it, so the logical builder needs the tree, not
	// just its text (#320). Nil when the item was built without parsing.
	Expr       Node
	Desc       bool
	NullsFirst *bool // nil = default, true = NULLS FIRST, false = NULLS LAST
	// Ordinal is the 1-based select-list POSITION this term was written as,
	// or 0 when it was written as a name or an expression.
	//
	// It survives resolvePositionalRefs' rewrite because a NAME is not an
	// address once two output columns share one: `SELECT n_name AS u,
	// n_regionkey AS u FROM nation ORDER BY 2, 1` sorted by column ONE on
	// every arm, since every resolver below binds the first column carrying
	// `u`. The position is known exactly at the rewrite and was thrown away
	// there (#557).
	Ordinal int
}

OrderByItem describes an ORDER BY element.

type OuterRef

type OuterRef struct {
	Table  string // outer table alias (lowercased)
	Column string // column name (lowercased)
}

OuterRef represents a correlated column reference to an outer query scope.

func DanglingTableRefs

func DanglingTableRefs(subquerySQL string) []OuterRef

DanglingTableRefs reports the qualified column references in subquerySQL whose table identifier is not defined by any FROM clause within the subquery itself, at any nesting depth. A non-empty result means the subquery is not self-contained: executed standalone, the dangling reference resolves to no column and evaluates NULL, which is how a mis-deferred correlated scalar silently answered 0 on the stage DAG (#359). Unlike FindCorrelatedRefs it needs no outer table list, so it can guard a site that has lost the outer scope. Only qualified references are visible to it — unqualified correlation needs the scoped analysis.

func FindCorrelatedRefs

func FindCorrelatedRefs(subquerySQL string, outerTables map[string]bool) ([]OuterRef, error)

FindCorrelatedRefs parses a subquery SQL string and returns any column references that refer to tables in outerTables but not to tables defined within the subquery itself. An empty result means the subquery is uncorrelated.

func FindCorrelatedRefsWithColumns

func FindCorrelatedRefsWithColumns(subquerySQL string, outerTables map[string]bool, outerCols map[string]string) ([]OuterRef, error)

FindCorrelatedRefsWithColumns is like FindCorrelatedRefs but also accepts a column-to-table mapping for resolving unqualified column references.

func FindCorrelatedRefsWithScope

func FindCorrelatedRefsWithScope(subquerySQL string, outerTables map[string]bool, outerCols map[string]string, innerCols TableColumns) ([]OuterRef, error)

FindCorrelatedRefsWithScope is FindCorrelatedRefsWithColumns plus the subquery's own column namespace, supplied by innerCols. With it, an unqualified name that the subquery's FROM supplies is resolved there and is not reported as correlated — the actual SQL rule. Without it the analysis falls back to comparing table identifiers, which only rejects the outer reference when the outer table's identifier happens to be spelled the same as one of the subquery's tables (issue #334).

type ParenNode

type ParenNode struct {
	Inner Node
}

ParenNode wraps a parenthesized expression.

func (*ParenNode) String

func (p *ParenNode) String() string

type ParsedQuery

type ParsedQuery struct {
	Type           QueryType
	TableName      string
	SQL            string
	Explain        *ExplainInfo
	Describe       *DescribeInfo
	CreateFunction *CreateFunctionInfo
	DropFunction   *DropFunctionInfo
	CreateTable    *CreateTableInfo
	DropTable      *DropTableInfo
	AnalyzeTable   *AnalyzeTableInfo
	CreateView     *CreateViewInfo
	DropView       *DropViewInfo
	AlterTable     *AlterTableInfo
	Merge          *MergeInfo
	Update         *UpdateInfo
	Delete         *DeleteInfo
	Insert         *InsertInfo
	CreateAlert    *CreateAlertInfo
	DropAlert      *DropAlertInfo
	AlterAlert     *AlterAlertInfo
	CreateSnapshot *CreateSnapshotInfo
	Windows        []WindowSpec // extracted window function specs
	CTEs           []CTEDef     // extracted CTE definitions
	SelectInfo     *SelectInfo  // parsed SELECT info (replaces AST)
}

ParsedQuery represents a parsed SQL query.

func Parse

func Parse(sql string) (*ParsedQuery, error)

Parse parses ONE SQL statement into a ParsedQuery.

A string carrying SEVERAL statements is refused here, which is what makes this function the one-statement entry point every one-statement door needs: `wadjet.DB.Execute`, `wadjet.DB.Query`, the HTTP door and the CLI all reach it, and all of them return exactly one result. Only the pgwire SIMPLE query protocol runs a sequence, and it splits the string with SplitStatements before it gets here (#711). See CheckSingleStatement for what "refused" means and why the order matters.

type QueryType

type QueryType int

QueryType identifies the kind of SQL statement.

const (
	QuerySelect QueryType = iota
	QueryExplain
	QueryDescribe
	QueryCreateFunction
	QueryDropFunction
	QueryShowFunctions
	QueryCreateTable
	QueryDropTable
	QueryAnalyzeTable
	QueryShowTables
	QueryUpdate
	QueryDelete
	QueryInsert
	QueryCreateView
	QueryDropView
	QueryAlterTable
	QueryMerge
	QueryCreateAlert
	QueryDropAlert
	QueryAlterAlert
	QueryCreateSnapshot
	QueryUnsupported
)

func (QueryType) StatementName added in v0.18.42

func (t QueryType) StatementName() string

StatementName is the SQL keyword phrase a QueryType stands for, in the spelling a client wrote. It is what a refusal names, so it has to be the statement and not the internal constant.

func (QueryType) String added in v0.18.42

func (t QueryType) String() string

String makes a QueryType printable as the statement it names, so a log line or a %v in an internal error reads as SQL rather than as an integer.

type SelectColumn

type SelectColumn struct {
	Expr        string
	Alias       string
	Star        bool
	IsAgg       bool
	AggFunc     string
	AggArg      string
	AggArgExpr  Node        // AST for aggregate argument expression
	AggArgs     []Node      // EVERY argument, AggArgExpr included (#353)
	AggDistinct bool        // COUNT(DISTINCT col)
	IsWindow    bool        // true if this is a window function
	WindowSpec  *WindowSpec // window function details
	ColumnRef   string
	TableRef    string
	ASTExpr     Node // our AST expression node
	// PublishedName is PostgreSQL's name for this output column when the
	// SELECT list wrote no alias — its `FigureColname` (#732). It is stamped
	// at PARSE time, on the item AS WRITTEN, because the planner REWRITES
	// items: the LATERAL empty-input restoration turns `s.item_count` into
	// `COALESCE(s.item_count, 0)`, and deriving the name from the rewritten
	// AST published `coalesce` for a column PostgreSQL calls `item_count`.
	// Empty for a synthetically built column, where OutputColumnName derives
	// it from the AST instead.
	PublishedName string
}

SelectColumn describes a column in a SELECT clause.

type SelectInfo

type SelectInfo struct {
	Tables       []TableRef
	Joins        []JoinInfo
	Columns      []SelectColumn
	Where        string
	WhereExpr    Node
	GroupBy      []string
	GroupByExprs []Node     // AST for GROUP BY expressions (parallel to GroupBy)
	GroupingSets [][]string // GROUPING SETS / CUBE / ROLLUP (nil = simple GROUP BY)
	// GroupByAliasOrigin records, per GROUP BY entry, the bare name the
	// parser SUBSTITUTED a SELECT alias's expression for — "" where it did
	// not. It exists because the substitution is PROVISIONAL: PostgreSQL
	// resolves a bare GROUP BY name against the INPUT COLUMNS FIRST and only
	// then against an output alias, and the parser has no schema, so it
	// cannot know which. The scope layer can (physical.colScope), and
	// RevertGroupByAliasesShadowedByInput undoes the entries the input
	// really provides.
	//
	// Recorded rather than decided-later-from-scratch so that an entry point
	// with no catalog — and therefore no scope — keeps exactly the answer it
	// had before the rule existed, instead of losing the substitution
	// entirely (#739).
	GroupByAliasOrigin []string
	Having             string
	HavingExpr         Node
	Distinct           bool
	Qualify            string
	QualifyExpr        Node
	OrderBy            []OrderByItem
	Limit              string
	Offset             string
	Windows            []WindowSpec // window function specs extracted during pre-parse
	CTEs               []CTEDef     // CTE definitions extracted during pre-parse
	Union              *UnionInfo   // non-nil if this is a UNION query
}

SelectInfo contains extracted information from a SELECT statement.

func ExtractSelect

func ExtractSelect(pq *ParsedQuery) (*SelectInfo, error)

ExtractSelect returns the SelectInfo from a parsed query.

type SetClause

type SetClause struct {
	Column string
	Value  string // raw expression text
}

SetClause represents a single SET column = value assignment.

type SetOp

type SetOp string

SetOp identifies the type of set operation.

const (
	SetOpUnion     SetOp = "UNION"
	SetOpIntersect SetOp = "INTERSECT"
	SetOpExcept    SetOp = "EXCEPT"
)

type SlotAllocator added in v0.18.6

type SlotAllocator struct {
	// contains filtered or unexported fields
}

SlotAllocator hands out fresh hidden-slot names for ONE query scope.

It exists because `SlotName` is a pure namer and nothing ALLOCATED. Two independent authors then wrote the same bug against it within a day: a slot search that excluded the names already in scope but not the slots it had itself already issued.

  • The window renamer, moving a slot past a stored `__win_0` column, took the first name not in the STORED set — `__win_1`, which the query's SECOND window already held. Both wrote `__win_1` and the by-name projection handed window #2 window #1's value. Silent, single-process path only, so a two-path divergence as well as a wrong number.
  • The group-key minting, materializing two computed GROUP BY keys, skipped names in scope but not slots issued to earlier keys of the same aggregate. Two keys landed in one column and twelve groups collapsed to three. Silent.

One shape, two authors, because the shared API let each of them write their own search. This is the only way a slot may be obtained; `SlotName` remains for rendering a known index and for tests.

Not safe for concurrent use: an allocator belongs to one query scope, which is planned on one goroutine.

func NewSlotAllocator added in v0.18.6

func NewSlotAllocator(inScope ...string) *SlotAllocator

NewSlotAllocator returns an allocator for a query scope, seeded with the names that already exist in it. Seeding is case-insensitive, because column resolution is.

func (*SlotAllocator) Issued added in v0.18.6

func (a *SlotAllocator) Issued() []string

Issued lists the slots this allocator handed out, in order.

func (*SlotAllocator) Next added in v0.18.6

func (a *SlotAllocator) Next(family SlotFamily) (string, bool)

Next returns the next unused slot of a family and records it as used.

It excludes BOTH the seeded names and every slot this allocator has already issued, which is the whole of its contract.

ok is false only when the family is EXHAUSTED — no free index below the search bound — which needs a scope holding a million names of one family and has no known SQL. A caller that gets false must leave the plan as it was rather than invent a name: an allocator that cannot allocate is a reason to decline an optimization, never a reason to reuse a slot.

Terminates: the family's cursor advances by one per candidate and never rewinds, `taken` grows by at most one per successful call, and the loop is bounded.

func (*SlotAllocator) Seed added in v0.18.6

func (a *SlotAllocator) Seed(names ...string)

Seed adds more names to the scope. Safe to call after allocation has begun — a name seeded late is excluded from every LATER allocation, and the names already issued stay issued.

type SlotFamily added in v0.18.6

type SlotFamily string

SlotFamily names one kind of hidden slot. The value is the name prefix, so a family constant and its reservation cannot drift apart.

const (
	SlotWindowOutput SlotFamily = "__win_"        // a window function's output
	SlotWindowKey    SlotFamily = "__winkey_"     // a materialized PARTITION BY / ORDER BY / argument
	SlotSortKey      SlotFamily = "__sortkey_"    // a materialized ORDER BY term
	SlotGroupKey     SlotFamily = "__gb_expr_"    // a computed GROUP BY key
	SlotAggInput     SlotFamily = "__agg_expr_"   // an aggregate's derived argument
	SlotNestedAgg    SlotFamily = "__agg_"        // the nested-aggregate rewrite
	SlotScalar       SlotFamily = "__scalar_"     // a scalar subquery's answer
	SlotHaving       SlotFamily = "__having_"     // a materialized HAVING term
	SlotTwoLevel     SlotFamily = "__tl_"         // the two-level distinct rewrite
	SlotSetOpCount   SlotFamily = "__setop_"      // set-operation arm counters
	SlotAvgSum       SlotFamily = "__avg_sum"     // AVG's decomposed SUM leg
	SlotAvgCount     SlotFamily = "__avg_count"   // AVG's decomposed COUNT leg
	SlotVarState     SlotFamily = "__var_state"   // STDDEV/VARIANCE partial state
	SlotCovarState   SlotFamily = "__covar_state" // CORR/COVAR partial state
	SlotGrouping     SlotFamily = "__grouping_"   // a GROUPING(...) bitmask
	// The SUFFIX-minted families: their names carry a discriminator rather than
	// a bare index, so they are rendered with fmt.Sprintf against the constant
	// rather than through SlotName. They are reserved on the same grounds.
	SlotPreComputedAgg SlotFamily = "__precomp_agg_" // pre-computed aggregate substitution
	SlotSubsumeFlag    SlotFamily = "__subsume_f"    // subsumed-filter marker
	SlotRowLocator     SlotFamily = "__row_loc"      // row-locator sentinel
	SlotRowCountOnly   SlotFamily = "__rowcount_only__"
	SlotDefaultPart    SlotFamily = "__default__"
)

The slot families the planner mints. Every one is reserved.

type StarNode

type StarNode struct {
	Table string
}

StarNode represents * or table.* in SELECT.

func (*StarNode) String

func (s *StarNode) String() string

type SubqueryNode

type SubqueryNode struct {
	SQL string
}

SubqueryNode wraps a subquery as raw SQL.

func (*SubqueryNode) String

func (s *SubqueryNode) String() string

type TableColumns

type TableColumns func(table string) []string

TableColumns reports the column names of a table named in a subquery's FROM clause, or nil when the table is unknown (a CTE, a table function, a planner with no catalog). It is what lets correlation analysis apply the SQL scoping rule: an unqualified name inside a subquery is resolved against the subquery's own FROM first, and only a name that does NOT resolve there is a reference to the outer query.

type TableRef

type TableRef struct {
	Name           string
	Qualifier      string // schema or catalog.schema written before the name
	Alias          string
	IsFunction     bool              // true for table functions like read_json(...)
	FuncArgs       []string          // positional arguments
	FuncNamedArgs  map[string]string // named arguments (key=value)
	WithOrdinality bool              // UNNEST(...) WITH ORDINALITY
	ColumnAliases  []string          // AS alias(col1, col2, ...)
	SampleMethod   string            // TABLESAMPLE method: BERNOULLI, SYSTEM
	SamplePercent  string            // percentage for TABLESAMPLE
	// contains filtered or unexported fields
}

TableRef is a reference to a table or table-producing function.

func (*TableRef) SubSelect added in v0.18.35

func (t *TableRef) SubSelect() (*SelectInfo, error)

SubSelect returns the parsed SELECT body of a DERIVED TABLE reference, memoized on the reference. It returns (nil, nil) when the reference is not a derived table, and the parse error when the body does not parse — callers wrap that in their own message, which is why the error is memoized too.

type TokenType

type TokenType int

TokenType identifies the kind of lexical token.

const (
	// Special
	TokenError TokenType = iota // lexing error (val contains message)
	TokenEOF                    // end of input

	// Literals
	TokenIdent  // identifier — unquoted, or double-quoted (token.quoted is set)
	TokenString // single-quoted string literal (val has quotes stripped, ” unescaped)
	TokenNumber // integer or decimal

	// Punctuation
	TokenLParen    // (
	TokenRParen    // )
	TokenComma     // ,
	TokenSemicolon // ;
	TokenStar      // *
	TokenDot       // .
	TokenLBracket  // [
	TokenRBracket  // ]
	TokenLBrace    // {
	TokenRBrace    // }

	// Operators
	TokenPlus            // +
	TokenMinus           // -
	TokenSlash           // /
	TokenPercent         // %
	TokenConcat          // ||
	TokenDoubleColon     // ::
	TokenJSONArrow       // ->
	TokenJSONDoubleArrow // ->>
	TokenEq              // =
	TokenNotEq           // != or <>
	TokenLT              // <
	TokenLTEq            // <=
	TokenGT              // >
	TokenGTEq            // >=

	// Keywords (case-insensitive, val is always uppercase)
	TokenKWCreate
	TokenKWOr
	TokenKWReplace
	TokenKWFunction
	TokenKWAs
	TokenKWDrop
	TokenKWIf
	TokenKWExists
	TokenKWShow
	TokenKWFunctions
	TokenKWColumns
	TokenKWFrom
	TokenKWExplain
	TokenKWVerbose
	TokenKWAnalyze
	TokenKWDescribe
	TokenKWDesc
	TokenKWWith
	TokenKWLock
	TokenKWTable
	TokenKWTables
	TokenKWNot
	TokenKWNull
	TokenKWPartition
	TokenKWBy

	// SQL query keywords
	TokenKWSelect
	TokenKWWhere
	TokenKWGroup
	TokenKWHaving
	TokenKWOrder
	TokenKWLimit
	TokenKWOffset
	TokenKWAsc
	TokenKWDistinct
	TokenKWAll
	TokenKWUnion
	TokenKWIntersect
	TokenKWExcept
	TokenKWAnd
	TokenKWIn
	TokenKWBetween
	TokenKWLike
	TokenKWILike
	TokenKWIs
	TokenKWTrue
	TokenKWFalse
	TokenKWCase
	TokenKWWhen
	TokenKWThen
	TokenKWElse
	TokenKWEnd
	TokenKWCast
	TokenKWJoin
	TokenKWOn
	TokenKWInner
	TokenKWLeft
	TokenKWRight
	TokenKWOuter
	TokenKWFull
	TokenKWCross
	TokenKWNatural
	TokenKWOver
	TokenKWNulls
	TokenKWFirst
	TokenKWLast
	TokenKWRows
	TokenKWRange
	TokenKWUnbounded
	TokenKWPreceding
	TokenKWFollowing
	TokenKWCurrent
	TokenKWRow
	TokenKWCube
	TokenKWRollup
	TokenKWGrouping
	TokenKWSets

	// Clause keywords
	TokenKWFetch
	TokenKWView
	TokenKWAlter
	TokenKWAdd
	TokenKWColumn
	TokenKWRename
	TokenKWTo

	// DML keywords
	TokenKWUpdate
	TokenKWSet
	TokenKWDelete
	TokenKWInsert
	TokenKWInto
	TokenKWValues
	TokenKWMerge
	TokenKWUsing
	TokenKWMatched

	// Alert DDL keywords
	TokenKWAlert
	TokenKWEvery
	TokenKWWebhook
	TokenKWHeaders
	TokenKWEnable
	TokenKWDisable
	TokenKWSeconds
	TokenKWMinutes
	TokenKWHours

	// Snapshot keywords
	TokenKWSnapshot

	// Raw capture
	TokenRawBody // everything after AS until terminator
)

type TupleNode

type TupleNode struct {
	Elements []Node
}

TupleNode represents a tuple expression: (a, b, c).

func (*TupleNode) String

func (t *TupleNode) String() string

type UnaryOp

type UnaryOp struct {
	Op    string // -, +
	Inner Node
}

UnaryOp is a unary operator expression (-, +).

func (*UnaryOp) String

func (u *UnaryOp) String() string

type UnionInfo

type UnionInfo struct {
	Left  *SelectInfo
	Right *SelectInfo
	All   bool  // true for UNION ALL / INTERSECT ALL / EXCEPT ALL (no dedup)
	Op    SetOp // the set operation type (defaults to UNION for backwards compat)
}

UnionInfo describes a set operation (UNION, INTERSECT, EXCEPT) with left and right sides.

type UpdateInfo

type UpdateInfo struct {
	DMLTarget
	SetClauses []SetClause // SET column = value pairs
}

UpdateInfo holds details for an UPDATE statement.

type WhenClause

type WhenClause struct {
	Cond   Node
	Result Node
}

WhenClause is a single WHEN ... THEN ... clause.

type WindowFrame

type WindowFrame struct {
	Mode  FrameMode
	Start FrameBound
	End   *FrameBound // nil means "to CURRENT ROW"
}

WindowFrame describes a window frame specification.

type WindowFuncNode

type WindowFuncNode struct {
	Func        *FuncCallNode
	PartitionBy []Node
	OrderBy     []WindowOrderBy
	Frame       *WindowFrame
}

WindowFuncNode represents a window function call: FUNC(...) OVER (...)

func FindAllWindowFuncs added in v0.18.4

func FindAllWindowFuncs(node Node) []*WindowFuncNode

FindAllWindowFuncs walks an expression tree and returns every window function call found, in left-to-right order. It is the window analogue of FindAllAggregates and lets the logical builder detect a window call nested inside a larger expression — SUM(x) OVER (...) + 1, COALESCE(LAG(x) OVER (...), 0), a CASE branch — not just the bare top-level form. It does not recurse into a window node's own argument/OVER subtrees: a window over a window is not a shape this handles, and stopping keeps the returned nodes disjoint so each maps to one output column.

func (*WindowFuncNode) String

func (n *WindowFuncNode) String() string

type WindowOrderBy

type WindowOrderBy struct {
	Expr       Node
	Desc       bool
	NullsFirst *bool
}

WindowOrderBy describes ordering in a window function's OVER clause.

type WindowOrderItem

type WindowOrderItem struct {
	Column     string
	Desc       bool
	NullsFirst *bool
}

WindowOrderItem describes a column + direction in a window ORDER BY.

type WindowSpec

type WindowSpec struct {
	FuncName    string
	Args        string // raw arg string (e.g., "amount", "*", "")
	PartitionBy []string
	OrderBy     []WindowOrderItem
	Alias       string       // output column name
	Frame       *WindowFrame // optional frame specification
}

WindowSpec describes a window function specification.

Jump to

Keyboard shortcuts

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