Documentation
¶
Overview ¶
Package semantic is the Go port of Java's `com.apple.foundationdb.relational.recordlayer.query.SemanticAnalyzer` plus related Identifier / Expression / reference-resolution helpers.
The Java class is a 1280-line monolith doing identifier normalization, table / CTE / function lookup, index validation, ORDER BY validation, type inference, star expansion, nested-field resolution, and correlated-identifier resolution. The Go port breaks that up across focused files:
- identifier.go — Identifier type + case-folding normalization.
- (future) — table / column / CTE / index lookup, type inference, star expansion.
Currently ships only the identifier machinery; the resolution + type passes are follow-up work.
Index ¶
- func NormalizeString(s string, caseSensitive bool) string
- type AmbiguousColumnError
- type Analyzer
- func (a *Analyzer) BuildScopeFromFromClause(parent *Scope, fromCtx antlrgen.IFromClauseContext) (*Scope, error)
- func (a *Analyzer) CaseSensitive() bool
- func (a *Analyzer) Catalog() Catalog
- func (a *Analyzer) ExpandQualifiedStar(scope *Scope, qualifier Identifier) ([]ExpandedColumn, error)
- func (a *Analyzer) ExpandScopeStar(scope *Scope) []ExpandedColumn
- func (a *Analyzer) ExpandStar(table Table) []Column
- func (a *Analyzer) ResolveColumn(table Table, id Identifier) (Column, error)
- func (a *Analyzer) ResolveColumnRefNested(scope *Scope, qualifier, id Identifier) (Column, ScopeSource, []NestedAccessor, error)
- func (a *Analyzer) ResolveColumnRefPath(scope *Scope, segs []Identifier) (Column, ScopeSource, []NestedAccessor, error)
- func (a *Analyzer) ResolveTable(name QualifiedName) (Table, error)
- func (a *Analyzer) ResolveTableRef(ctx antlrgen.IFullIdContext) (Table, error)
- type Catalog
- type Column
- type ColumnNotFoundError
- type CorrelatedShadowError
- type DuplicateAliasError
- type ExpandedColumn
- type FunctionArityError
- type FunctionCatalog
- type FunctionKind
- type FunctionNotFoundError
- type FunctionSpec
- type Identifier
- type InMemoryCatalog
- type NestedAccessor
- type NestedResolutionError
- type QualifiedName
- func (q QualifiedName) EqualsIgnoreQuoting(other QualifiedName) bool
- func (q QualifiedName) IsQualified() bool
- func (q QualifiedName) IsZero() bool
- func (q QualifiedName) LeafIdentifier() Identifier
- func (q QualifiedName) Name() string
- func (q QualifiedName) PrefixedWith(prefix QualifiedName) bool
- func (q QualifiedName) Qualifier() []string
- func (q QualifiedName) Segments() []string
- func (q QualifiedName) String() string
- type Scope
- func (s *Scope) AddSource(src ScopeSource) error
- func (s *Scope) AllSourcesRecursive() []ScopeSource
- func (s *Scope) Parent() *Scope
- func (s *Scope) ResolveColumn(id Identifier) (Column, ScopeSource, error)
- func (s *Scope) ResolvePathNested(segs []Identifier) (Column, ScopeSource, []NestedAccessor, error)
- func (s *Scope) ResolveQualifiedColumn(qualifier, col Identifier) (Column, ScopeSource, error)
- func (s *Scope) ResolveQualifiedColumnNested(qualifier, col Identifier) (Column, ScopeSource, []NestedAccessor, error)
- func (s *Scope) ResolveSourceQualifiedPath(segs []Identifier) (Column, ScopeSource, []NestedAccessor, error)
- func (s *Scope) Sources() []ScopeSource
- type ScopeSource
- type SourceNotFoundError
- type StaticTable
- type Table
- type TableNotFoundError
- type UnresolvableSourceError
- type UnsupportedFromShapeError
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func NormalizeString ¶
NormalizeString is the lower-level helper underlying New. Exposed for call sites that have a raw string and don't need the full Identifier wrapper (e.g. dedup keys in the parser). Mirrors Java's `SemanticAnalyzer.normalizeString`.
- Empty string → empty string.
- Quoted (single or double) → strip quotes verbatim.
- Unquoted + caseSensitive → return unchanged.
- Unquoted + !caseSensitive → upper-case.
Types ¶
type AmbiguousColumnError ¶
type AmbiguousColumnError struct {
Id Identifier
// Qualifier is the reference's qualifier for the QUALIFIED form
// (`a.id` over duplicate aliases both carrying id); zero for a bare
// reference. Callers render Java's exact message from the reference
// as written: `Ambiguous reference A.ID` / `Ambiguous reference ID`.
Qualifier Identifier
// Path is the reference's FULL segment list when it was resolved as a
// dotted path (`a.n.sk` → [A N SK]). Qualifier/Id keep the first and last
// segments so two-segment callers are unaffected, but they cannot render a
// deeper reference AS WRITTEN — and the rendering is the message operand
// Java prints, so a three-segment ambiguity would otherwise report a
// reference the user never typed. Empty for callers that pass no path.
Path []Identifier
// Matches is always equal to len(Sources); exists as a
// convenience accessor for callers who don't need the full
// alias list. Future API tightening may remove it — prefer
// len(Sources) for new code.
Matches int
// Sources is the list of ScopeSource aliases that matched,
// allowing the user-facing message to suggest
// `alias.column` for each candidate.
Sources []Identifier
}
AmbiguousColumnError is returned when a column reference matches multiple sources at the same scope level — bare (two tables expose the name) or qualified (two same-aliased sources both carry the column, Java's per-attribute 42702). Carries the conflicting identifier and the conflicting source aliases so the user knows which tables to qualify against.
func (*AmbiguousColumnError) Error ¶
func (e *AmbiguousColumnError) Error() string
func (*AmbiguousColumnError) Reference ¶
func (e *AmbiguousColumnError) Reference() string
Reference renders the ambiguous reference AS WRITTEN (normalized) — Java's message operand: `A.ID` for a qualified reference, `ID` for a bare one. The callers' user-facing mapping is `Ambiguous reference %s` byte-equal to Java's SemanticAnalyzer text (verified for duplicate AND distinct aliases, bare AND qualified).
type Analyzer ¶
type Analyzer struct {
// contains filtered or unexported fields
}
Analyzer ties Catalog lookups + identifier normalization into the resolution helpers that rule authors / logical-plan builders invoke. Mirrors the instance surface of Java's `SemanticAnalyzer` (the static methods on that class — case folding etc. — live as free functions in this package).
Seed scope: resolve table references, resolve bare/qualified column references. Star expansion, nested-field lookup, correlated-identifier resolution all land in follow-up shifts.
Not safe for concurrent mutation of the underlying Catalog; the Analyzer itself is stateless once constructed.
func NewAnalyzer ¶
NewAnalyzer wires up an Analyzer against the given catalog. A nil catalog is rejected — callers who don't have a real schema yet should use NewInMemoryCatalog() with no tables.
func (*Analyzer) BuildScopeFromFromClause ¶
func (a *Analyzer) BuildScopeFromFromClause(parent *Scope, fromCtx antlrgen.IFromClauseContext) (*Scope, error)
BuildScopeFromFromClause walks a parsed FROM clause and produces a Scope populated with one ScopeSource per TableSource. The analyzer resolves each table via its catalog; missing tables return the first TableNotFoundError encountered.
Supports the simple shape (comma-separated AtomTableItem entries + optional alias); subquery-in-FROM and JOIN clauses are deferred until the join/derived-table resolution passes land. Unsupported shapes return an UnsupportedFromShapeError so callers can fall back to the existing logical-builder path cleanly.
Pass parent=nil for a top-level query; pass the enclosing scope for correlated subqueries.
func (*Analyzer) CaseSensitive ¶
CaseSensitive reports the analyzer's case-sensitivity setting.
func (*Analyzer) Catalog ¶
Catalog returns the underlying Catalog. Exposed so higher-level passes (e.g. LogicalPlan builder) can thread the same catalog through without re-wiring.
func (*Analyzer) ExpandQualifiedStar ¶
func (a *Analyzer) ExpandQualifiedStar(scope *Scope, qualifier Identifier) ([]ExpandedColumn, error)
ExpandQualifiedStar implements `SELECT alias.*` against a Scope: looks up the named source, then its columns. Walks the parent chain for correlated-star references. Returns SourceNotFoundError (with the Available alias list populated from every visible scope for "did you mean?" rendering) when no source matches.
func (*Analyzer) ExpandScopeStar ¶
func (a *Analyzer) ExpandScopeStar(scope *Scope) []ExpandedColumn
ExpandScopeStar implements unqualified `SELECT *` against a Scope: concatenates each source's columns in FROM-order. Ambiguity is NOT flagged here — Java's SQL lets two sources expose same-named columns through `SELECT *` (the output just gets two columns); only bare *references* error. Downstream callers tag each ExpandedColumn with its source so later projection rewrites can qualify.
func (*Analyzer) ExpandStar ¶
ExpandStar implements the `SELECT *` rewrite — returns the full column list of the given table in declared order. Each Column is returned unchanged (same Id, Type, Nullable) so downstream plan builders can wrap each into a ColumnReference / ProjectionItem.
Mirrors the single-qualifier case of Java's `SemanticAnalyzer.expandStar`. The multi-table / alias-qualified cases (`SELECT t.* FROM t JOIN u`) come with the FROM-scope port.
func (*Analyzer) ResolveColumn ¶
func (a *Analyzer) ResolveColumn(table Table, id Identifier) (Column, error)
ResolveColumn looks up a column by identifier against a resolved table. Mirrors the simple case of Java's resolveIdentifier — qualifier resolution (`t.col` → column on aliased table) comes later with the FROM-clause scope machinery.
One table, so there is nothing to adjudicate across sources and the lookup is the relaxed one: exact spelling first, then case-insensitive. That is what the scope would answer for the same reference against the same single source, and an entry point that answered differently would be a second resolution rule.
func (*Analyzer) ResolveColumnRefNested ¶
func (a *Analyzer) ResolveColumnRefNested(scope *Scope, qualifier, id Identifier) (Column, ScopeSource, []NestedAccessor, error)
ResolveColumnRefNested is the one-shot column-reference resolver: given a qualifier (may be zero) and a column identifier, dispatch to bare or qualified lookup against the provided scope. This is the analyzer's top-level hook for every identifier reference the expression resolver sees.
- qualifier.IsZero() → Scope.ResolveColumn (bare). - qualifier non-zero → Scope.ResolveQualifiedColumnNested.
Returns the same typed errors as the underlying scope methods, plus the accessor chain a reference that descends INTO a struct column resolves to — Java's lookupNestedField result (SemanticAnalyzer.java:578-601). The chain is empty for every reference that addresses a source column directly.
The chain is part of the RESULT, not an optional extra: a caller that mints a value from (Column, ScopeSource) alone and ignores the chain has resolved `struct.member` to the whole struct — a wrong-column read that raises no error. There is deliberately no chain-discarding sibling of this method for a caller to reach for; Java has no such split either, because lookupNestedField fuses the descent onto the value before any caller sees it (SemanticAnalyzer.java:599-600). expr.fuseNestedAccessorsIfAny is the Go side of that fuse.
BE PRECISE ABOUT WHAT THAT BUYS. Removing the discarding sibling RAISED THE COST of the mistake; it did not make it unrepresentable. Each mint still calls the fuse itself, so a NEW mint that resolves through this method and forgets the call is the same silent wrong-column read as before — it just has to be written deliberately rather than by picking the shorter-named function. The change that would remove the shape is a single mint, or the fuse moved inside whatever the mints share; neither exists yet.
The BARE arm never descends, and that is Java's rule, not an omission: lookupNestedField returns empty immediately when the requested identifier has one segment (SemanticAnalyzer.java:557-559), because a descent needs a prefix to consume before there is anything left to walk into.
func (*Analyzer) ResolveColumnRefPath ¶
func (a *Analyzer) ResolveColumnRefPath(scope *Scope, segs []Identifier) (Column, ScopeSource, []NestedAccessor, error)
ResolveColumnRefPath is ResolveColumnRefNested for a reference of ARBITRARY segment depth — `a.n.sk` and deeper. Java has no arity cap on this path (`fullId : uid (DOT uid)*` in the grammar, an unbounded remainingPath loop in SemanticAnalyzer.lookupNestedField), so neither does this.
A single segment takes the BARE arm, which never descends: Java's lookupNestedField returns empty immediately for a one-segment identifier (SemanticAnalyzer.java:557-559), because a descent needs a prefix to consume before there is anything left to walk into.
func (*Analyzer) ResolveTable ¶
func (a *Analyzer) ResolveTable(name QualifiedName) (Table, error)
ResolveTable looks up a table by qualified name. Returns a typed error when the name is missing so callers can wrap it into the API-level error shape without string-matching.
func (*Analyzer) ResolveTableRef ¶
func (a *Analyzer) ResolveTableRef(ctx antlrgen.IFullIdContext) (Table, error)
ResolveTableRef is the parse-tree convenience wrapper over ResolveTable. Reads the IFullIdContext (ANTLR's table reference node), builds a QualifiedName with the analyzer's case-sensitivity, then looks it up in the catalog.
Returns TableNotFoundError with the QualifiedName the caller requested; callers preserve user-facing names through `err.Name.String()`.
type Catalog ¶
type Catalog interface {
// LookupTable returns a Table handle for the given qualified
// name, or (nil, false) if no such table exists. Name
// normalization is the caller's job — QualifiedName already
// carries the case-folded form.
LookupTable(name QualifiedName) (Table, bool)
// TableExists reports whether a Table with the given name is
// registered. Equivalent to LookupTable's second return value;
// surfaced separately because it's the common fast-path check
// in existence-gating rules.
TableExists(name QualifiedName) bool
// AllTableNames returns every registered table's qualified
// name. Order is unspecified; callers that need deterministic
// ordering should sort. Used for INFORMATION_SCHEMA-style
// reflection and for error messages that enumerate candidate
// tables.
AllTableNames() []QualifiedName
}
Catalog is the semantic analyzer's view of the schema. The analyzer looks up tables, columns, and indexes through this interface; concrete implementations bridge to `RecordMetaData` (for the embedded engine) or to test fixtures.
Mirrors the subset of Java's `SchemaTemplate` that the Go SemanticAnalyzer port needs. Keeping it narrow so the seed doesn't drag in the full RecordLayer metadata surface — callers who need more adapter methods can extend the Table interface later.
All lookups take QualifiedName so the analyzer can handle schema-qualified references uniformly; concrete impls decide how to resolve un-qualified names (walk the search path, default schema, etc.).
type Column ¶
type Column struct {
// Id is the column name.
Id Identifier
// Type is the column's SQL-ish data type as a string (e.g.
// "INT", "STRING", "BYTES"). Will be replaced with a richer
// `DataType` once the Type hierarchy is ported.
Type string
// Nullable reports whether the column allows NULL values.
// Matters for NOT-NULL-gated simplifications (x = x → TRUE).
Nullable bool
// IsArray reports whether the column is an ARRAY (a repeated proto
// field). The placeholder Type string carries only the scalar/element
// kind, so this is the array signal callers need to type the resolved
// column Value as an ArrayType — e.g. CARDINALITY()'s isArray() check.
// When true, Type is the ELEMENT type string.
IsArray bool
// Ephemeral marks a column that is resolvable BY NAME but invisible to
// star expansion — Java's Expression.asEphemeral() attribute, applied to
// the __ROW_VERSION pseudo-field appended by generateTableAccess
// (LogicalOperator.java:296-301); star expansion consumes
// nonEphemeralVisible() (SemanticAnalyzer.java:346-348). An ephemeral
// column still occupies its trailing slot in the flowed row layout, so
// ordinal binding (sourceRowType) keeps it.
Ephemeral bool
// StructFields is the DECLARED field list of a STRUCT column (Type
// "RECORD"), in declared order, and empty for every other column. It is
// the thing Java's lookupNestedField scans when it turns the path
// segments left over after the matched attribute prefix into
// FieldValue.Accessor(name, ordinal) entries (SemanticAnalyzer.java:
// 578-597, `((DataType.StructType) type).getFields()`).
//
// The ORDINAL a nested accessor carries is the field's POSITION IN THIS
// SLICE, which is why declared order is part of the contract and not an
// incidental property of how the slice was built: Java resolves the same
// accessor against Type.Record's field list and stores that list position
// (FieldValue.java:288-295 via getFieldNameToOrdinalMap), so the two
// agree only while both lists are the descriptor's own field order.
//
// Recursive by construction — a nested struct field carries its own
// StructFields — because a path may descend more than one level.
StructFields []Column
// StructTypeName is the exact declared identity of a STRUCT column's
// record type. Type remains the SQL kind "RECORD" so existing semantic
// kind checks stay stable; this companion carries the descriptor full name
// needed when the resolver mints an executable values.RecordType. Without
// it, every distinct STRUCT became a record literally named "RECORD", so a
// resolved nested FieldValue and the scan edge for the same proto field had
// different exact types.
//
// Empty for non-STRUCT columns and for synthetic catalog fixtures that do
// not declare a nominal record identity.
StructTypeName string
}
Column is the analyzer's view of a table column. Type is a placeholder string until the DataType / Type hierarchy port lands (Phase 4.0 continuation).
func LookupColumnRelaxed ¶
func LookupColumnRelaxed(tbl Table, id Identifier) (Column, bool)
LookupColumnRelaxed asks ONE table whether it declares a column the given reference could name: the exact spelling first, then a case-insensitive match — the same two steps Scope's relaxed pass applies.
It exists for the SINGLE-SOURCE questions that are not resolutions: "does this leg still carry the column the outer reference named?", "does the CTE body project this name?". Those have nothing to adjudicate across sources, which is the whole reason Table.LookupColumn itself stays exact — a table that relaxed on its own would let one source's loose match compete with another source's exact one. Do NOT use this to resolve a reference; use the Scope methods, which run the two passes level by level and count candidates.
func NonEphemeral ¶
NonEphemeral filters ephemeral columns out of a column list — the star visibility rule (Java's Expressions.nonEphemeralVisible, consumed by SemanticAnalyzer.expandStar at SemanticAnalyzer.java:346-348). Resolution by NAME keeps seeing ephemeral columns (LookupColumn is unfiltered).
func (Column) LookupStructField ¶
func (c Column) LookupStructField(id Identifier) (Column, int, bool)
LookupStructField scans a STRUCT column's declared fields for one named by id and returns it with its ORDINAL (position in StructFields).
This is Java's lookupNestedField inner loop (SemanticAnalyzer.java:584-593): a linear scan, FIRST name match wins, and — the load-bearing part — a MISS is not an error. Java returns Optional.empty() both for "this is not a struct" (:581-583) and for "no field of that name" (:594-596); the failure surfaces later and generically as UNDEFINED_COLUMN once every attribute of every operator has declined. Reporting a bespoke "no such field" here would turn a candidate that merely lost into a hard error, and a reference that Java resolves against a LATER source would die on an EARLIER one.
type ColumnNotFoundError ¶
type ColumnNotFoundError struct {
TableName QualifiedName
Id Identifier
}
ColumnNotFoundError is returned when ResolveColumn can't find a column on the given table.
func (*ColumnNotFoundError) Error ¶
func (e *ColumnNotFoundError) Error() string
type CorrelatedShadowError ¶
type CorrelatedShadowError struct {
}
CorrelatedShadowError is returned when a qualified reference resolves to a PARENT-scope source (Java's zero-match fallthrough) whose correlation name is SHADOWED by a local FROM source that lacks the column — an emitted-uncorrelatable case. Emitting QOV(correlation) would bind the local (inner) leg's quantifier, so resolution declines LOUDLY (never wrong rows); this matches Java (unique quantifier ids) and will flip once cross-scope binding ids are supported. Carries the reference as written for the surfaced message.
func (*CorrelatedShadowError) Error ¶
func (e *CorrelatedShadowError) Error() string
type DuplicateAliasError ¶
type DuplicateAliasError struct {
Alias Identifier
}
func (*DuplicateAliasError) Error ¶
func (e *DuplicateAliasError) Error() string
type ExpandedColumn ¶
type ExpandedColumn struct {
Column Column
Source ScopeSource
}
ExpandedColumn pairs a Column with the ScopeSource it came from. The scope-aware star expander / qualified-star expander returns these so downstream plan builders know which FROM source to attribute each projected column to.
type FunctionArityError ¶
FunctionArityError signals too few / too many arguments.
func (*FunctionArityError) Error ¶
func (e *FunctionArityError) Error() string
type FunctionCatalog ¶
type FunctionCatalog struct {
// contains filtered or unexported fields
}
FunctionCatalog holds the set of functions the analyzer recognizes. Seed ships the core SQL aggregates; scalar function catalogues come as the embedded engine's scalar-function library gets ported.
func NewFunctionCatalog ¶
func NewFunctionCatalog() *FunctionCatalog
NewFunctionCatalog builds an empty catalog. Use RegisterDefaults or Register to populate.
func (*FunctionCatalog) Contains ¶
func (c *FunctionCatalog) Contains(name Identifier) bool
Contains reports whether a function with the given identifier is registered. Equivalent to Lookup's second return.
func (*FunctionCatalog) Lookup ¶
func (c *FunctionCatalog) Lookup(name Identifier) (FunctionSpec, bool)
Lookup returns the FunctionSpec for name (case-insensitive), or (_, false) when not registered.
func (*FunctionCatalog) Register ¶
func (c *FunctionCatalog) Register(spec FunctionSpec) error
Register adds a FunctionSpec. Returns an error on duplicate name; caller can ignore when registering a known stable set or bubble up when the registry is built from user extensions.
func (*FunctionCatalog) RegisterDefaults ¶
func (c *FunctionCatalog) RegisterDefaults()
RegisterDefaults populates the catalog with the standard SQL aggregate functions. Scalar functions are not seeded — they come from the dedicated scalar-function catalogue once that's ported.
type FunctionKind ¶
type FunctionKind int
FunctionKind enumerates the classes of function the analyzer supports.
Values are assigned explicitly (not via `iota`) so inserting a new kind between existing ones doesn't renumber anything — future serialized-plan formats can assume these values are stable.
const ( // FunctionScalar: per-row function (UPPER, LOWER, ABS, etc.). FunctionScalar FunctionKind = 1 // FunctionAggregate: spans multiple rows (COUNT, SUM, MIN, MAX, AVG). FunctionAggregate FunctionKind = 2 )
func (FunctionKind) String ¶
func (k FunctionKind) String() string
String returns the kind as a debug-friendly string.
type FunctionNotFoundError ¶
type FunctionNotFoundError struct {
Name Identifier
}
FunctionNotFoundError signals a lookup miss — the function name isn't registered in the catalogue.
func (*FunctionNotFoundError) Error ¶
func (e *FunctionNotFoundError) Error() string
type FunctionSpec ¶
type FunctionSpec struct {
// Name is the canonical, case-folded function name (e.g. "COUNT").
Name string
// Kind classifies the function — scalar or aggregate (window
// functions come later).
Kind FunctionKind
// MinArgs / MaxArgs bound accepted arity. A MaxArgs of -1 means
// no upper bound (variadic).
MinArgs int
MaxArgs int
// AllowsStar reports whether the function accepts `*` as its
// argument (currently only COUNT does).
AllowsStar bool
// AllowsDistinct reports whether the function accepts a leading
// DISTINCT modifier — e.g. `COUNT(DISTINCT col)`. All aggregates
// in the SQL standard accept DISTINCT; the flag exists here so
// future scalar extensions can opt out.
AllowsDistinct bool
}
FunctionSpec describes a SQL-visible function the analyzer can resolve. The seed differentiates scalar vs aggregate functions since rule-matching / plan-building treats them differently.
func (FunctionSpec) ValidateArity ¶
func (spec FunctionSpec) ValidateArity(argCount int) error
ValidateArity reports whether argCount is acceptable for spec. Zero = no arguments. A MaxArgs of -1 is treated as "no upper bound". Returns a typed error on mismatch so callers can build user-facing messages without string-matching.
Callers handling `*` (star-argument) functions should check `AllowsStar` BEFORE calling ValidateArity and skip the arity check for the star case. The star is syntactically 0 arguments, but COUNT(*) is legal despite COUNT's MinArgs=1 — that's not a contradiction the arity check should reason about.
type Identifier ¶
type Identifier struct {
// contains filtered or unexported fields
}
Identifier is a SQL identifier — a table, column, alias, or function name. Carries the normalized form (case-folded per SQL rules) so Identifiers can live in maps and compare by `==` directly. The original source text is the caller's responsibility (store it alongside via the parse tree's token stream when you need user-spelling-preserving error messages).
Mirrors Java's `com.apple.foundationdb.relational.recordlayer. query.Identifier`, trimmed to the essential equality surface.
func FromNormalized ¶
func FromNormalized(name string) Identifier
FromNormalized wraps a string that was ALREADY normalized by the parse capture (functions.NormalizeIdentifier: unquoted segments folded UPPER, quoted segments verbatim with the quotes removed). Unlike NewUnquoted it performs NO re-normalization — re-folding a captured quoted-verbatim alias (`AS "q$1"` → captured `q$1`) would corrupt it to `Q$1` and split resolution across positions (the FROM registration and the quote-aware reference channel disagreeing on the same alias). The quoting FLAG is not recoverable from the captured string, which is fine everywhere this is used: resolution equality ignores the flag (EqualsIgnoreQuoting) and only the normalized text matters.
func FromUidContext ¶
func FromUidContext(ctx antlrgen.IUidContext, caseSensitive bool) Identifier
FromUidContext converts a single IUidContext to an Identifier. Used for unqualified references (column aliases, CTE names, etc.) where the full-id shape is overkill.
func New ¶
func New(raw string, caseSensitive bool) Identifier
New constructs an Identifier by normalizing raw per SQL rules. When caseSensitive is true, unquoted identifiers retain their source casing; otherwise they're upper-cased. Quoted identifiers always retain case regardless of caseSensitive.
func NewUnquoted ¶
func NewUnquoted(raw string) Identifier
NewUnquoted is the common path — a case-insensitive bare identifier. Equivalent to New(raw, false).
func (Identifier) EqualsIgnoreQuoting ¶
func (i Identifier) EqualsIgnoreQuoting(other Identifier) bool
EqualsIgnoreQuoting compares by Name only, ignoring the quoting flag. For most lookups a quoted-vs-unquoted distinction doesn't matter — e.g. an `ORDER BY` clause referring to `"age"` still targets the same column as a SELECT projecting `age`. Use `==` on Identifier when the quoting distinction matters (resolving against a reserved-word shadow).
func (Identifier) IsZero ¶
func (i Identifier) IsZero() bool
IsZero reports whether i is the zero-value Identifier (empty). Useful for nil-check replacement since Identifier is a value type.
func (Identifier) Name ¶
func (i Identifier) Name() string
Name returns the normalized identifier text. Two Identifiers with the same Name AND same WasQuoted are equal via `==`.
func (Identifier) WasQuoted ¶
func (i Identifier) WasQuoted() bool
WasQuoted reports whether the source text was quoted. Callers that need to preserve user intent (e.g. "keyword" vs keyword) check this flag.
type InMemoryCatalog ¶
type InMemoryCatalog struct {
// contains filtered or unexported fields
}
InMemoryCatalog is a test-friendly Catalog built from a fixed list of tables. Keeps the test surface small — production impls bridge to RecordMetaData; tests construct one of these in a line or two.
func NewInMemoryCatalog ¶
func NewInMemoryCatalog(tables ...Table) *InMemoryCatalog
NewInMemoryCatalog builds a Catalog from the given tables. Table names key the map by their canonical String() form so lookups are O(1).
func (*InMemoryCatalog) AllTableNames ¶
func (c *InMemoryCatalog) AllTableNames() []QualifiedName
AllTableNames implements Catalog. Iterates the map — order is unspecified.
func (*InMemoryCatalog) LookupTable ¶
func (c *InMemoryCatalog) LookupTable(name QualifiedName) (Table, bool)
LookupTable implements Catalog.
func (*InMemoryCatalog) TableExists ¶
func (c *InMemoryCatalog) TableExists(name QualifiedName) bool
TableExists implements Catalog.
type NestedAccessor ¶
type NestedAccessor struct {
// Name is the struct field's declared name.
Name string
// Ordinal is its position in the enclosing struct's field list.
Ordinal int
// Col is the field's own column view, so a consumer can type the result
// and a deeper descent can continue from it.
Col Column
}
NestedAccessor is one resolved step of a descent INTO a struct column — Java's FieldValue.Accessor(name, ordinal) as lookupNestedField mints it (SemanticAnalyzer.java:586-588). Ordinal is the field's position in the enclosing struct's declared field list, which is the ordinal Java's resolveFieldPath ends up storing (FieldValue.java:288-295).
type NestedResolutionError ¶
type NestedResolutionError struct {
Qualifier Identifier
Id Identifier
}
NestedResolutionError reports a reference that resolved by DESCENDING into a struct column, asked of a lookup form that cannot express the descent. It is never a user-facing error: every SQL path resolves through ResolveQualifiedColumnNested. It exists so the chain-free form fails LOUDLY instead of answering with the struct root.
func (*NestedResolutionError) Error ¶
func (e *NestedResolutionError) Error() string
type QualifiedName ¶
type QualifiedName struct {
// contains filtered or unexported fields
}
QualifiedName is a dot-separated SQL name like `schema.table.col`. The leaf (last segment) is the "simple name"; the preceding segments are the "qualifier". Both normalized together — callers never see a QualifiedName with one segment upper-cased and another in source case.
Mirrors Java's Identifier-with-qualifier shape. The Go port breaks it off into its own type because:
- Slices aren't comparable, so bundling segments into Identifier would lose the map-as-key ergonomics.
- Most callsites touch only the leaf — an unqualified Identifier stays simple.
Use QualifiedName when you need to preserve the qualifier chain (table aliases, schema-scoped tables). Use Identifier for bare column / alias references.
func FromFullIdContext ¶
func FromFullIdContext(ctx antlrgen.IFullIdContext, caseSensitive bool) QualifiedName
FromFullIdContext converts an ANTLR IFullIdContext parse-tree node to a QualifiedName. Each Uid segment is read via GetText() (which preserves source casing), then normalized per caseSensitive.
The typical call site is table-name resolution:
tbl := semantic.FromFullIdContext(tblCtx.FullId(), false)
if resolved, err := catalog.LookupTable(tbl); err != nil { ... }
Returns the zero QualifiedName when ctx is nil or has no Uid children — callers should test IsZero before trusting the result.
func FromSegments ¶
func FromSegments(segments []string, caseSensitive bool) QualifiedName
FromSegments builds a QualifiedName from already-normalized segments (each passed through NormalizeString or equivalent). Empty input returns the zero value.
func ParseQualifiedName ¶
func ParseQualifiedName(raw string, caseSensitive bool) QualifiedName
ParseQualifiedName splits a raw dotted string into a QualifiedName. Each segment is normalized independently (stripped of its own surrounding quotes, case-folded unless caseSensitive or quoted).
Semantics match Java's token-per-segment handling: `t."X"` parses as qualifier=[T], name="X" (first segment upper-cased because unquoted, second preserved because quoted).
Quote-embedded dots are NOT handled (e.g. `"a.b".c` still splits on every dot). The ANTLR parser already tokenises individual identifiers, so callers feeding us pre-tokenised segments won't hit this edge — see FromSegments for that path.
func (QualifiedName) EqualsIgnoreQuoting ¶
func (q QualifiedName) EqualsIgnoreQuoting(other QualifiedName) bool
EqualsIgnoreQuoting compares segment-by-segment by normalized text only, ignoring per-segment quoting flags. This is the common "these target the same database object" semantics that Java's Identifier.equals uses.
func (QualifiedName) IsQualified ¶
func (q QualifiedName) IsQualified() bool
IsQualified reports whether the name has at least one qualifier segment preceding the leaf.
func (QualifiedName) IsZero ¶
func (q QualifiedName) IsZero() bool
IsZero reports whether q is the zero-value (empty) QualifiedName.
func (QualifiedName) LeafIdentifier ¶
func (q QualifiedName) LeafIdentifier() Identifier
LeafIdentifier returns the leaf segment wrapped as an Identifier (preserving the leaf's wasQuoted flag). Useful when a caller resolved a qualified name and now wants to work with just the column name.
func (QualifiedName) Name ¶
func (q QualifiedName) Name() string
Name returns the leaf (last) segment. For `schema.table.col` this is `col`. Zero QualifiedName returns the empty string.
func (QualifiedName) PrefixedWith ¶
func (q QualifiedName) PrefixedWith(prefix QualifiedName) bool
PrefixedWith reports whether q starts with prefix's segments. `schema.table.col` is prefixed with `schema.table`. Useful for resolving a bare column against a set of alias-qualified candidates.
func (QualifiedName) Qualifier ¶
func (q QualifiedName) Qualifier() []string
Qualifier returns the segments before the leaf. For `schema.table.col` this returns [schema, table]. An unqualified name returns an empty slice (never nil).
func (QualifiedName) Segments ¶
func (q QualifiedName) Segments() []string
Segments returns all segments, leaf-last. Zero QualifiedName returns nil; otherwise a defensive copy.
func (QualifiedName) String ¶
func (q QualifiedName) String() string
String returns the canonical dotted representation. Suitable for map keys (two QualifiedNames with the same String value are equal under EqualsIgnoreQuoting).
type Scope ¶
type Scope struct {
// contains filtered or unexported fields
}
Scope is the set of named resolutions visible at a point during query analysis. A scope knows about the FROM-clause sources (tables + their aliases) at that level and, via parent-chain, inherits correlated sources from enclosing scopes (for nested subqueries).
Mirrors the subset of Java's `LogicalPlanFragment` + scope chain the analyzer uses for identifier resolution.
Construction: start with NewScope(parent) — parent nil means the outermost query. Call AddSource to push each FROM source as the analyzer walks FROM clauses left-to-right.
Not concurrency-safe; the analyzer is single-threaded per query.
func NewScope ¶
NewScope constructs a Scope inheriting from parent. parent may be nil for the outermost query.
func (*Scope) AddSource ¶
func (s *Scope) AddSource(src ScopeSource) error
AddSource appends a FROM-clause source. Duplicate PLAIN aliases at the same level are ACCEPTED — Java registers quantifiers freely (unique ids; the SQL alias is only a display qualifier) and errors per-ATTRIBUTE at reference resolution; the caller distinguishes duplicate legs via CorrelationName (the parser-minted binding id). A duplicate involving a SHADOWING source (a lateral-unnest AS/AT binding, either direction) still errors: Java genuinely forbids a duplicate unnest alias at FROM (RFC-142), and the scope-level signal is what the join-ON builder's drop-risk taxonomy keys on.
func (*Scope) AllSourcesRecursive ¶
func (s *Scope) AllSourcesRecursive() []ScopeSource
AllSourcesRecursive returns sources from this scope and every ancestor, inner-first. Useful for "did you mean?" error suggestions when a qualifier misses — callers can enumerate all visible aliases and suggest the closest.
func (*Scope) ResolveColumn ¶
func (s *Scope) ResolveColumn(id Identifier) (Column, ScopeSource, error)
ResolveColumn looks up a bare column reference (no qualifier) against the scope's sources, following the parent chain if no local match. Ambiguous matches within a single scope level (multiple tables with a column of this name) return an error — the caller should instruct the user to qualify.
Mirrors Java's resolution: inner scopes shadow outer; within a scope, ambiguity is a hard error.
func (*Scope) ResolvePathNested ¶
func (s *Scope) ResolvePathNested(segs []Identifier) (Column, ScopeSource, []NestedAccessor, error)
ResolvePathNested resolves a dotted reference of ARBITRARY depth — the shape Java resolves natively because its Identifier carries `name` plus a `List<String> qualifier` and every rule reasons over the joined `fullyQualifiedName()` list (IdentifierVisitor.java:56-64 builds it segment by segment; SemanticAnalyzer.lookupNestedField consumes a matched PREFIX and walks whatever remains). Go's two-argument (qualifier, column) shape could express only the first two segments, so `a.n.sk` was flattened into a qualifier string "A.N" that names neither a source nor a struct column and the reference died as UNDEFINED_COLUMN.
Two candidate kinds compete per source, counted TOGETHER so a reference both could answer is an ambiguity rather than a silent preference:
- STRUCT-RELATIVE: segs[0] names a column of the source and segs[1:] descend into it (`n.sk`, `n.inner.leaf`). This carries no source qualifier at all, so it is tried against EVERY source in scope.
- ALIAS-QUALIFIED: segs[0] names the source, segs[1] one of its columns, and segs[2:] descend into that column (`a.id`, `a.n.sk`).
The alias-qualified arm is what keeps two sources declaring the same struct apart: `a.n.sk` and `b.n.sk` each match exactly one source because the leading segment is compared against the source ALIAS, not discarded.
The scope-chain walk is unchanged from the two-segment form it subsumes: ambiguity at a level is terminal, a zero-match level falls through to the parent, and exhaustion reports ColumnNotFound when some alias matched somewhere and SourceNotFound when nothing did.
func (*Scope) ResolveQualifiedColumn ¶
func (s *Scope) ResolveQualifiedColumn(qualifier, col Identifier) (Column, ScopeSource, error)
ResolveQualifiedColumn handles `alias.col` with Java's PER-ATTRIBUTE semantics (SemanticAnalyzer.resolveIdentifierMaybe + resolveAcrossFragments):
- ALL alias-matching sources at a level are candidates; >1 carrying the column is ambiguous (42702 at the caller) — ambiguity is TERMINAL, it never falls through to a parent scope;
- ZERO matches at a level fall through to the PARENT — even when the alias exists locally without the column (the correlated shadow shape: `SELECT p.v FROM t1 AS p WHERE EXISTS(SELECT 1 FROM t2 AS p WHERE p.v = 10)` ANSWERS in Java, live-verified);
- at chain exhaustion: ColumnNotFoundError when some alias-matching source existed anywhere on the chain (named after the innermost one), SourceNotFoundError when the qualifier matched nothing.
This is the chain-free form, for callers that need only the resolved (column, source) IDENTITY. A resolution that DESCENDED into a struct column has no identity to report without its chain — the Column it would hand back is the struct ROOT, not the field the reference named — so this form DECLINES it with NestedResolutionError rather than returning the root. Returning the root is a wrong-column answer that no caller can detect, and a caller that wants the descent already has ResolveQualifiedColumnNested.
func (*Scope) ResolveQualifiedColumnNested ¶
func (s *Scope) ResolveQualifiedColumnNested(qualifier, col Identifier) (Column, ScopeSource, []NestedAccessor, error)
ResolveQualifiedColumnNested is ResolveQualifiedColumn plus Java's lookupNestedField rule (SemanticAnalyzer.java:481-488 — the fifth and last matching rule `lookup` applies per output attribute).
Two candidate kinds compete at each scope level:
- a DIRECT match: `qualifier` names a FROM source and `col` is one of its columns. This is the shape every reference had before struct columns existed, and it is unchanged.
- a NESTED match: `qualifier` names a STRUCT COLUMN of some source in scope and `col` is one of that struct's fields, so the reference `home_address.city` descends rather than addressing a source.
They are counted TOGETHER, which is what makes a reference that both kinds could answer an ambiguity rather than a silent preference. Java reaches the same place by a different route: rules 1-4 and rule 5 all append into the one `directMatchesBuilder` list and `resolveIdentifierMaybe` errors when that list holds more than one entry (SemanticAnalyzer.java:433-437, "Ambiguous reference %s", ErrorCode.AMBIGUOUS_COLUMN). A nested candidate evaluated only after direct resolution FAILED would resolve that collision by order of attempt, and order of attempt is not a semantics.
Everything else about the level walk is unchanged, including the property that a zero-match level falls through to the parent.
func (*Scope) ResolveSourceQualifiedPath ¶
func (s *Scope) ResolveSourceQualifiedPath(segs []Identifier) (Column, ScopeSource, []NestedAccessor, error)
ResolveSourceQualifiedPath resolves a path whose leading segment is already proven by the grammar/caller to name a FROM source. Unlike ResolvePathNested, it does not also consider the leading segment as a struct column. That distinction is load-bearing for a self-named lateral source such as `FROM t, t.records AS item, item.item AS leaf`: the ordinary SQL expression `item.item` is intentionally ambiguous when both the source-qualified and struct-relative rules answer it, but the FROM-item classifier has already established that the first ITEM names the preceding source.
Duplicate source aliases retain Java's per-attribute ambiguity rule: all alias-matching sources at one scope level are considered, and more than one complete match is loud. Zero matches fall through to the parent exactly as ResolvePathNested does. Callers must not use this method to impose source precedence on an ordinary expression whose leading segment has not already been classified as a source alias.
func (*Scope) Sources ¶
func (s *Scope) Sources() []ScopeSource
Sources returns the FROM-clause sources at this scope level (defensive copy, does NOT include parent sources).
type ScopeSource ¶
type ScopeSource struct {
// Table is the resolved schema-level table.
Table Table
// Alias is the name used to reference this source in the
// enclosing query (column qualifier). For `FROM t AS x` → Alias
// is `x`; for `FROM t` with no alias → Alias is `t`.
Alias Identifier
// CorrelationName is the identifier the analyzer uses to tie
// this source back to a Quantifier when building
// QuantifiedObjectValue / FieldValue trees that reference it.
// Stored as a string so the semantic package doesn't take a
// dependency on cascades/values/CorrelationIdentifier — callers wrap
// this into a cascades.values.CorrelationIdentifier themselves.
CorrelationName string
// AdditionalQualifiers are query-block-local spellings that may qualify
// this source without changing its runtime correlation identity. The narrow
// live use is Java's table-first alias/schema collision: in
// `FROM PA AS s, s.PB AS B`, where `s` is also the active schema, `PA.ID`
// continues to address the PA source even though its range alias is `s`.
// Ordinary aliased sources leave this empty, so SQL's usual alias-hides-table
// rule and the correlation mint remain unchanged.
AdditionalQualifiers []Identifier
// Shadowing marks a source whose columns SHADOW same-named columns of
// non-shadowing sources at this scope level (instead of colliding into
// an ambiguity error). A lateral array unnest (`FROM t, t.arr AS x`)
// uses this: its AS/AT binding shadows a same-named real column of `t`
// — Java's generateCorrelatedFieldAccess binding wins over the outer
// (RFC-142). When ≥1 shadowing source matches a bare column, the
// shadowing match is taken and the non-shadowing matches are ignored;
// two shadowing matches are still ambiguous.
Shadowing bool
// FlowedColumns is the exact row layout carried by this source's quantified
// object when that layout differs from the columns exposed for SQL name
// resolution. It is nil for ordinary tables and for a scalar lateral-unnest
// element (whose quantified object is the whole element). WITH ORDINALITY is
// the motivating row-valued virtual source: SQL exposes the AS/AT aliases,
// while an AT-only source still physically carries the unexposed element in
// slot 0 and the ordinal in slot 1.
FlowedColumns []Column
// FlowedNullable is the record-level nullability of FlowedColumns. It is
// meaningful only when FlowedColumns is non-empty.
FlowedNullable bool
// HiddenColumns names columns of this source that UNQUALIFIED
// references skip — Java's Expression visibility
// (SemanticAnalyzer.java:468: an unqualified reference ignores a
// non-visible attribute; a qualified reference still binds it). A
// JOIN … USING marks the RIGHT side's copy of each USING column
// hidden (QueryVisitor.resolveJoinUsingClause → asHidden), which is
// what makes a bare reference to the USING column resolve the LEFT
// copy instead of being ambiguous. Keys are UPPER-folded bare names.
HiddenColumns map[string]struct{}
}
ScopeSource is one FROM-clause entry: a resolved Table plus the alias it's visible under. Alias is always non-zero — when the user doesn't write AS, the Table's own name fills in.
type SourceNotFoundError ¶
type SourceNotFoundError struct {
Alias Identifier
Available []Identifier
}
SourceNotFoundError is returned when a qualifier doesn't match any FROM-clause alias in the scope chain. Carries the list of available aliases (inner-first) so callers can render a "did you mean?" suggestion.
func (*SourceNotFoundError) Error ¶
func (e *SourceNotFoundError) Error() string
type StaticTable ¶
type StaticTable struct {
TableName QualifiedName
TableColumns []Column
TableIndexes []string
}
StaticTable is a test-friendly Table impl backing InMemoryCatalog. Production code should implement Table directly (bridging to RecordType) rather than use this value-type.
func (*StaticTable) Columns ¶
func (t *StaticTable) Columns() []Column
Columns implements Table; returns a defensive copy so callers can't mutate the backing slice.
func (*StaticTable) LookupColumn ¶
func (t *StaticTable) LookupColumn(id Identifier) (Column, bool)
LookupColumn implements Table — EXACT-name match on Identifier.Name (EqualsIgnoreQuoting ignores only the quoting FLAG, never case). Unquoted lookups arrive pre-folded by the identifier constructor, so folded registrations match; a case-preserved quoted name matches only its exact spelling.
type Table ¶
type Table interface {
// Name returns the qualified table name.
Name() QualifiedName
// Columns returns the table's column definitions in declared
// order. Empty slice if the table has no columns (a valid state
// for views / CTEs). Never nil.
Columns() []Column
// LookupColumn returns a Column by identifier, matching
// case-insensitively under SQL rules (the Identifier's
// normalized form). Returns (Column{}, false) if no match.
LookupColumn(id Identifier) (Column, bool)
// Indexes returns the index names defined on this table. The
// seed returns just names; richer IndexInfo follows once
// index-pushdown rules need per-index metadata.
Indexes() []string
}
Table is the analyzer's view of a single SQL table. Minimal for the seed — Name + Columns + Indexes. Richer methods (PK shape, fields-of-interest, RecordType bridging) land as the analyzer grows.
type TableNotFoundError ¶
type TableNotFoundError struct {
Name QualifiedName
}
TableNotFoundError is returned when ResolveTable can't find a table. Carries the qualified name the caller requested; follows the error-type pattern from CLAUDE.md (Java exception = Go error struct).
func (*TableNotFoundError) Error ¶
func (e *TableNotFoundError) Error() string
type UnresolvableSourceError ¶
type UnresolvableSourceError struct {
Alias Identifier
}
DuplicateAliasError is returned by AddSource when the same alias is already registered at this scope level. UnresolvableSourceError reports an attempt to add a scope source with no Table — the declared-but-underivable CTE tombstone reaching a resolver.
func (*UnresolvableSourceError) Error ¶
func (e *UnresolvableSourceError) Error() string
type UnsupportedFromShapeError ¶
type UnsupportedFromShapeError struct {
Shape string
}
UnsupportedFromShapeError signals a FROM-clause shape the seed analyzer doesn't handle yet. Carried up so callers can fall back to the existing logical-builder path rather than erroring out at the SQL level.
func (*UnsupportedFromShapeError) Error ¶
func (e *UnsupportedFromShapeError) Error() string