001package gudusoft.gsqlparser.ir.semantic.binding;
002
003import gudusoft.gsqlparser.ir.semantic.RelationKind;
004import gudusoft.gsqlparser.nodes.TObjectName;
005import gudusoft.gsqlparser.nodes.TResultColumn;
006import gudusoft.gsqlparser.nodes.TResultColumnList;
007import gudusoft.gsqlparser.nodes.TTable;
008import gudusoft.gsqlparser.resolver2.ResolutionStatus;
009import gudusoft.gsqlparser.resolver2.model.ColumnSource;
010import gudusoft.gsqlparser.resolver2.model.ResolutionResult;
011import gudusoft.gsqlparser.sqlenv.TSQLColumn;
012import gudusoft.gsqlparser.sqlenv.TSQLEnv;
013import gudusoft.gsqlparser.sqlenv.TSQLTable;
014import gudusoft.gsqlparser.stmt.TSelectSqlStatement;
015
016import java.util.ArrayList;
017import java.util.Collections;
018import java.util.HashMap;
019import java.util.HashSet;
020import java.util.List;
021import java.util.Locale;
022import java.util.Map;
023import java.util.Set;
024
025/**
026 * {@link NameBindingProvider} backed by the data already attached to AST
027 * nodes by {@code TSQLResolver2} during {@code TGSqlParser.parse()}.
028 *
029 * <p>Slice 1/2 only handled {@link RelationKind#TABLE}. Slice 3 adds
030 * {@link RelationKind#CTE} via {@link #withCteContext(Set)}: when the
031 * provider is given a non-empty CTE-name set, a FROM-clause reference
032 * whose name (case-insensitive) is in the set binds as
033 * {@code RelationKind.CTE} instead of {@code RelationKind.TABLE}.
034 *
035 * <p><b>API status: advanced/preview.</b> The no-argument constructor is the
036 * metadata-less low-level builder path. When catalog metadata is present, the
037 * parser and provider must receive the same {@link TSQLEnv}. Prefer
038 * {@link gudusoft.gsqlparser.ir.semantic.SqlSemanticAnalyzer} for supported
039 * one-step analysis.
040 */
041public final class Resolver2NameBindingProvider implements NameBindingProvider {
042
043    /**
044     * Slice 58 — optional catalog source for {@link
045     * #getRelationColumnNames(TTable)}. May be {@code null} (no catalog
046     * available); in that case {@code getRelationColumnNames} returns
047     * {@code null} and the builder emits a structured "requires catalog"
048     * diagnostic.
049     */
050    private final TSQLEnv sqlEnv;
051
052    private final Set<String> cteNamesInScope;
053
054    /**
055     * Slice 60 — REPLACE-semantics map of in-scope CTE / FROM-subquery
056     * names → published column names for star expansion. Always
057     * non-null (empty map when no scope is set). Keys are lower-case
058     * (defensive copy in the canonical constructor); the value lists
059     * are wrapped with {@link Collections#unmodifiableList} so callers
060     * cannot mutate the snapshot the provider observed.
061     */
062    private final Map<String, List<String>> inScopeRelationColumns;
063
064    /**
065     * Slice 65 — REPLACE-semantics using-key scope for the current
066     * SELECT body. Always non-null (defaults to {@link UsingScope#EMPTY}).
067     * Each {@code buildSelectStatementImpl} invocation resets this at
068     * entry so an enclosing SELECT's USING cannot leak into recursive
069     * nested builds.
070     */
071    private final UsingScope usingScope;
072
073    /**
074     * Slice 93 — when true, {@link #bindColumn} promotes a column
075     * reference whose Phase-2 ({@code TSQLResolver2}) {@code resolution}
076     * is null but whose Phase-1 ({@code linkColumnToTable})
077     * {@code sourceTable} is set from {@code NOT_FOUND} to
078     * {@code EXACT_MATCH}. Used for Hive multi-insert sub-SELECTs whose
079     * secondary branches are not traversed by Resolver2 during
080     * {@code TGSqlParser.parse()}. The promotion additionally requires
081     * the column's SQL-written qualifier (if any) to be consistent with
082     * the source table's name / alias (see {@link #bindColumn}).
083     */
084    private final boolean sourceTableFallback;
085
086    /**
087     * Slice 117 — set of inner local relation aliases (lowercased) used
088     * by the tolerant-outer-binding fallback in {@link #bindColumn}. When
089     * non-empty, any qualified ref whose Phase-2 binding is not
090     * {@link ResolutionStatus#EXACT_MATCH} AND whose qualifier is NOT in
091     * this set is promoted to a synthetic EXACT_MATCH binding with
092     * {@code (qualifier, columnName)}. Qualifiers IN this set fall
093     * through to strict binding (a real typo on a local alias still
094     * rejects). Empty set disables the fallback.
095     */
096    private final Set<String> tolerantInnerLocalAliases;
097
098    /**
099     * Structural anchor for the {@code COLUMN_BINDING_NON_EXACT} degrade: true
100     * when the current SELECT body's FROM clause resolved a fully-built join
101     * graph (two or more endpoints from explicit ON / CROSS / comma
102     * predicates). Set per-SELECT by the IR builder after relations are bound
103     * and reset at every nested build entry so it cannot leak across scopes.
104     * See {@link #withJoinStructureAnchor(boolean)}.
105     */
106    private final boolean joinStructureAnchor;
107
108    public Resolver2NameBindingProvider() {
109        this(null, Collections.<String>emptySet(),
110                Collections.<String, List<String>>emptyMap(),
111                UsingScope.EMPTY, false, Collections.<String>emptySet(), false);
112    }
113
114    /**
115     * Slice 58 — construct a provider with catalog access. The {@code
116     * sqlEnv} is used only by {@link #getRelationColumnNames(TTable)} for
117     * star expansion; the resolver binding paths remain unchanged.
118     */
119    public Resolver2NameBindingProvider(TSQLEnv sqlEnv) {
120        this(sqlEnv, Collections.<String>emptySet(),
121                Collections.<String, List<String>>emptyMap(),
122                UsingScope.EMPTY, false, Collections.<String>emptySet(), false);
123    }
124
125    /**
126     * Slice 60 / 65 — canonical private constructor. All public and
127     * narrower entry points delegate here so every Resolver2-backed
128     * provider instance has all four fields populated explicitly.
129     * Adding another facet later is a single signature change here +
130     * mirror updates at the narrowers ({@link #withCteContext},
131     * {@link #withInScopeRelationColumns}, {@link #withUsingScope}).
132     */
133    private Resolver2NameBindingProvider(TSQLEnv sqlEnv,
134                                         Set<String> cteNamesInScope,
135                                         Map<String, List<String>> inScopeRelationColumns,
136                                         UsingScope usingScope,
137                                         boolean sourceTableFallback,
138                                         Set<String> tolerantInnerLocalAliases,
139                                         boolean joinStructureAnchor) {
140        this.sqlEnv = sqlEnv;
141        this.sourceTableFallback = sourceTableFallback;
142        this.joinStructureAnchor = joinStructureAnchor;
143        // Slice 117: defensive copy + lowercased. Empty set disables
144        // the tolerant-outer-binding fallback.
145        Set<String> normalizedTolerant = new HashSet<>();
146        if (tolerantInnerLocalAliases != null) {
147            for (String a : tolerantInnerLocalAliases) {
148                if (a != null && !a.isEmpty()) {
149                    normalizedTolerant.add(a.toLowerCase(Locale.ROOT));
150                }
151            }
152        }
153        this.tolerantInnerLocalAliases =
154                Collections.unmodifiableSet(normalizedTolerant);
155        // Defensive copy + lowercased for case-insensitive lookup.
156        // NOTE: this is a slice-3 simplification; quoted/case-sensitive
157        // identifiers (e.g. Oracle's `"a"` vs `a`, PostgreSQL's lowercased
158        // unquoted names) are not yet handled. A later slice should fold
159        // identifiers through the parser/resolver's vendor-aware identifier
160        // model rather than `String.toLowerCase()`.
161        Set<String> normalized = new HashSet<>();
162        if (cteNamesInScope != null) {
163            for (String n : cteNamesInScope) {
164                if (n != null && !n.isEmpty()) {
165                    normalized.add(n.toLowerCase(Locale.ROOT));
166                }
167            }
168        }
169        this.cteNamesInScope = Collections.unmodifiableSet(normalized);
170        // Slice 60: deep-copy the map. Lower-case keys; wrap value
171        // lists in unmodifiableList. Empty entries are skipped — an
172        // empty list never means "in scope but has no columns" (it
173        // would surface as `NO_INSCOPE_RELATION_COLUMNS` at the
174        // expander anyway, but explicit drop here keeps the provider
175        // invariant clean).
176        Map<String, List<String>> normalizedColumns = new HashMap<>();
177        if (inScopeRelationColumns != null) {
178            for (Map.Entry<String, List<String>> e : inScopeRelationColumns.entrySet()) {
179                String k = e.getKey();
180                List<String> v = e.getValue();
181                if (k == null || k.isEmpty() || v == null || v.isEmpty()) continue;
182                normalizedColumns.put(
183                        k.toLowerCase(Locale.ROOT),
184                        Collections.unmodifiableList(new ArrayList<>(v)));
185            }
186        }
187        this.inScopeRelationColumns = Collections.unmodifiableMap(normalizedColumns);
188        this.usingScope = usingScope == null ? UsingScope.EMPTY : usingScope;
189    }
190
191    @Override
192    public NameBindingProvider withCteContext(Set<String> cteNamesInScope) {
193        // Slice 58 preserves sqlEnv. Slice 60 also preserves
194        // inScopeRelationColumns: the CTE-context narrowing is
195        // orthogonal to star-expansion scope. Without this, the
196        // CTE-body build path's bodyProvider would lose the outer
197        // map and CTE-body star expansion of an EARLIER CTE would
198        // hit NO_INSCOPE_RELATION_COLUMNS. Slice 65 also preserves
199        // usingScope so a withCteContext call inside an already-narrowed
200        // build doesn't lose the current SELECT's USING scope.
201        // Slice 93: also preserves sourceTableFallback.
202        // Slice 117: also preserves tolerantInnerLocalAliases.
203        return new Resolver2NameBindingProvider(this.sqlEnv, cteNamesInScope,
204                this.inScopeRelationColumns, this.usingScope,
205                this.sourceTableFallback, this.tolerantInnerLocalAliases,
206                this.joinStructureAnchor);
207    }
208
209    @Override
210    public NameBindingProvider withInScopeRelationColumns(
211            Map<String, List<String>> nameToColumns) {
212        // Slice 60 replace-semantics: the supplied map fully describes
213        // the new scope. Preserves sqlEnv (catalog access for
214        // base-table star expansion stays available even when scoped
215        // to a CTE body) AND cteNamesInScope (so a later
216        // bindRelation() call still classifies CTE-bound relations
217        // correctly). Slice 65: also preserves usingScope.
218        // Slice 93: also preserves sourceTableFallback.
219        // Slice 117: also preserves tolerantInnerLocalAliases.
220        return new Resolver2NameBindingProvider(this.sqlEnv,
221                this.cteNamesInScope, nameToColumns, this.usingScope,
222                this.sourceTableFallback, this.tolerantInnerLocalAliases,
223                this.joinStructureAnchor);
224    }
225
226    @Override
227    public NameBindingProvider withUsingScope(UsingScope scope) {
228        // Slice 65 replace-semantics: the supplied scope fully describes
229        // the merged-key context for the current SELECT body. Preserves
230        // sqlEnv, cteNamesInScope, and inScopeRelationColumns so the
231        // other facets are unaffected.
232        // Slice 93: also preserves sourceTableFallback.
233        // Slice 117: also preserves tolerantInnerLocalAliases.
234        return new Resolver2NameBindingProvider(this.sqlEnv,
235                this.cteNamesInScope, this.inScopeRelationColumns,
236                scope == null ? UsingScope.EMPTY : scope,
237                this.sourceTableFallback, this.tolerantInnerLocalAliases,
238                this.joinStructureAnchor);
239    }
240
241    @Override
242    public NameBindingProvider withSourceTableFallback(boolean enabled) {
243        // Slice 93 — preserves all other facets. Returns same instance
244        // when state already matches the requested mode (cheap no-op).
245        // Slice 117: also preserves tolerantInnerLocalAliases.
246        if (this.sourceTableFallback == enabled) {
247            return this;
248        }
249        return new Resolver2NameBindingProvider(this.sqlEnv,
250                this.cteNamesInScope, this.inScopeRelationColumns,
251                this.usingScope, enabled, this.tolerantInnerLocalAliases,
252                this.joinStructureAnchor);
253    }
254
255    @Override
256    public NameBindingProvider withTolerantOuterBinding(
257            Set<String> innerLocalAliasesLower) {
258        // Slice 117 — REPLACE semantics: the supplied set fully
259        // describes the inner-local guard for tolerant-outer-binding.
260        // Preserves all other facets (sqlEnv / cteNamesInScope /
261        // inScopeRelationColumns / usingScope / sourceTableFallback).
262        // Passing null / empty disables the fallback in the canonical
263        // constructor (which normalises a null/empty set to an empty
264        // unmodifiable set).
265        return new Resolver2NameBindingProvider(this.sqlEnv,
266                this.cteNamesInScope, this.inScopeRelationColumns,
267                this.usingScope, this.sourceTableFallback,
268                innerLocalAliasesLower, this.joinStructureAnchor);
269    }
270
271    @Override
272    public UsingScope getUsingScope() {
273        return usingScope;
274    }
275
276    @Override
277    public NameBindingProvider withJoinStructureAnchor(boolean anchored) {
278        // REPLACE semantics: the supplied flag fully describes whether THIS
279        // SELECT body has a fully-built join graph. Preserves all other facets
280        // (sqlEnv / cteNamesInScope / inScopeRelationColumns / usingScope /
281        // sourceTableFallback / tolerantInnerLocalAliases). Returns the same
282        // instance when state already matches (cheap no-op).
283        if (this.joinStructureAnchor == anchored) {
284            return this;
285        }
286        return new Resolver2NameBindingProvider(this.sqlEnv,
287                this.cteNamesInScope, this.inScopeRelationColumns,
288                this.usingScope, this.sourceTableFallback,
289                this.tolerantInnerLocalAliases, anchored);
290    }
291
292    @Override
293    public boolean hasJoinStructureAnchor() {
294        return joinStructureAnchor;
295    }
296
297    @Override
298    public Map<String, List<String>> getInScopeRelationColumns() {
299        return inScopeRelationColumns;
300    }
301
302    @Override
303    public List<String> getRelationColumnNames(TTable table) {
304        if (sqlEnv == null || table == null) {
305            return null;
306        }
307        // Only base-table relations are eligible for catalog lookup;
308        // CTE / FROM-subquery / function tables resolve via separate
309        // binding paths and are out of scope for slice 58 (S60).
310        if (table.getTableType() != gudusoft.gsqlparser.ETableSource.objectname) {
311            return null;
312        }
313        TObjectName tableName = table.getTableName();
314        if (tableName == null) {
315            return null;
316        }
317        // TSQLEnv.searchTable(TObjectName) handles bare names via the
318        // "..<name>" fallback (TSQLEnv.java:1167-1171) for PG/Oracle/
319        // Snowflake. Other dialects (e.g. MSSQL with ".dbo." expansion)
320        // require the caller to register the table under the matching
321        // qualified form.
322        TSQLTable tbl = sqlEnv.searchTable(tableName);
323        if (tbl == null) {
324            return null;
325        }
326        List<TSQLColumn> cols = tbl.getColumnList();
327        if (cols == null || cols.isEmpty()) {
328            return null;
329        }
330        // Slice 58 dedup. TSQLTable.getColumnList iterates columnMap.keySet,
331        // and addColumn stores each column under both the legacy
332        // normalization key AND the IdentifierService key when the two
333        // differ (TSQLTable.java:127-150). For unquoted identifiers on
334        // most dialects those keys disagree, so the same TSQLColumn
335        // surfaces twice (the SAME instance under two keys). Identity-
336        // based dedup keeps distinct case-sensitive/quoted catalog
337        // columns intact (codex round-1 diff review SHOULD) — case-fold
338        // dedup would have collapsed e.g. catalog columns "Id" and "id"
339        // declared as separate quoted identifiers.
340        List<String> names = new ArrayList<>(cols.size());
341        java.util.IdentityHashMap<TSQLColumn, Boolean> seen = new java.util.IdentityHashMap<>();
342        for (TSQLColumn c : cols) {
343            if (c == null) {
344                continue;
345            }
346            if (seen.put(c, Boolean.TRUE) != null) {
347                continue;
348            }
349            String n = c.getNameKeepCase();
350            if (n == null || n.isEmpty()) {
351                n = c.getName();
352            }
353            if (n == null || n.isEmpty()) {
354                continue;
355            }
356            names.add(n);
357        }
358        if (names.isEmpty()) {
359            return null;
360        }
361        return Collections.unmodifiableList(names);
362    }
363
364    @Override
365    public RelationBinding bindRelation(TTable table) {
366        if (table == null) {
367            return null;
368        }
369        // Slice 5 added FROM-clause subqueries: a TTable of type
370        // ETableSource.subquery binds as RelationKind.SUBQUERY using its
371        // alias as the qualifiedName (no globally-visible name exists).
372        // Slice 74 extended this to admit anonymous (unaliased) FROM
373        // subqueries by synthesizing a position-keyed alias via
374        // FromSubqueryNaming.synthAliasFor.
375        // NOTE: aliases are matched case-insensitively elsewhere in the
376        // builder (see e.g. cte alias lookup) — quoted/case-sensitive
377        // aliases share the same slice-3 limitation.
378        if (table.getTableType() == gudusoft.gsqlparser.ETableSource.subquery) {
379            String alias = table.getAliasName();
380            if (alias == null || alias.isEmpty()) {
381                alias = FromSubqueryNaming.synthAliasFor(table);
382            }
383            if (alias == null || alias.isEmpty()) {
384                return null;
385            }
386            return new RelationBinding(RelationKind.SUBQUERY, alias);
387        }
388        // Table-valued function source (SQL Server CROSS/OUTER APPLY's
389        // right operand, or a plain FROM dbo.fn(...) t). Bind as an opaque
390        // FUNCTION relation whose qualifiedName is the function name (the
391        // stable lineage terminal, matching resolver2's resolution of the
392        // function's output columns) — the source alias is carried by
393        // RelationSource.alias via effectiveAliasOf. The column set is
394        // opaque, so referenced columns (t.q) resolve to this relation but
395        // are never expanded/validated.
396        //
397        // Boundary (uniform across APPLY and non-APPLY positions): the
398        // function ARGUMENTS are not modelled as lineage/correlation here.
399        // For a correlated APPLY arg (CROSS APPLY dbo.fn(o.id) t) the
400        // o.id->orders correlation is therefore not surfaced as an edge;
401        // for a non-APPLY CROSS JOIN the right side is genuinely
402        // independent. A correlated argument in a non-APPLY join is invalid
403        // T-SQL (correlation requires APPLY), so that shape is GIGO rather
404        // than a supported case. Argument lineage is a documented follow-up.
405        if (table.getTableType() == gudusoft.gsqlparser.ETableSource.function) {
406            String fnName = (table.getTableName() != null)
407                    ? table.getTableName().toString() : null;
408            if (fnName == null || fnName.isEmpty()) {
409                fnName = table.getName();
410            }
411            if (fnName == null || fnName.isEmpty()) {
412                fnName = table.getAliasName();
413            }
414            if (fnName == null || fnName.isEmpty()) {
415                return null;
416            }
417            return new RelationBinding(RelationKind.FUNCTION, fnName);
418        }
419        // Otherwise only base tables (ETableSource.objectname) are bound.
420        // Other source kinds (rowList, etc.) return null so the builder
421        // fails fast.
422        if (table.getTableType() != gudusoft.gsqlparser.ETableSource.objectname) {
423            return null;
424        }
425        String name = table.getName();
426        if (name == null || name.isEmpty()) {
427            return null;
428        }
429        if (cteNamesInScope.contains(name.toLowerCase(Locale.ROOT))) {
430            return new RelationBinding(RelationKind.CTE, name);
431        }
432        return new RelationBinding(RelationKind.TABLE, name);
433    }
434
435    @Override
436    public ColumnBinding bindColumn(TObjectName columnRef) {
437        if (columnRef == null) {
438            return null;
439        }
440        String columnName = columnRef.getColumnNameOnly();
441        if (columnName == null || columnName.isEmpty() || "*".equals(columnName)) {
442            return null;
443        }
444        // Effective in-statement alias: prefer the prefix actually written in
445        // the SQL (e.g. `e` in `e.id`); fall back to the resolved source-table
446        // name when the column was written unqualified.
447        // Slice 74: when the source table is an unaliased FROM-subquery,
448        // route through FromSubqueryNaming so the ColumnRef.relationAlias
449        // matches the synth name used by buildRelation / processDirectSubqueryTable
450        // (otherwise we'd emit `relationAlias = "subquery"`, which no
451        // relation map knows about, and projection lookups would fail
452        // with "references unknown relation 'subquery'").
453        String relationAlias = columnRef.getTableString();
454        if (relationAlias == null || relationAlias.isEmpty()) {
455            if (columnRef.getSourceTable() != null) {
456                gudusoft.gsqlparser.nodes.TTable st = columnRef.getSourceTable();
457                String sourceAlias = st.getAliasName();
458                if (sourceAlias != null && !sourceAlias.isEmpty()) {
459                    relationAlias = sourceAlias;
460                } else if (st.getTableType() == gudusoft.gsqlparser.ETableSource.subquery) {
461                    relationAlias = FromSubqueryNaming.synthAliasFor(st);
462                } else {
463                    relationAlias = st.getName();
464                }
465            }
466        }
467        if (relationAlias == null || relationAlias.isEmpty()) {
468            return null;
469        }
470
471        ResolutionResult resolution = columnRef.getResolution();
472        ResolutionStatus status = resolution == null ? ResolutionStatus.NOT_FOUND : resolution.getStatus();
473        String finalTable = null;
474        if (resolution != null && resolution.getStatus() == ResolutionStatus.EXACT_MATCH) {
475            ColumnSource source = resolution.getColumnSource();
476            if (source != null && source.getFinalTable() != null) {
477                finalTable = source.getFinalTable().getName();
478            }
479        }
480        // Slice 93 — Phase-1 source-table fallback for Hive multi-insert
481        // sub-SELECTs. Resolver2 does not traverse secondary multi-insert
482        // branches, so their columns have resolution == null (Phase 2 did
483        // not run). When Phase 1's linkColumnToTable has set sourceTable,
484        // trust that and promote the status to EXACT_MATCH so
485        // collectColumnRefs admits the binding.
486        //
487        // CRITICAL DISCRIMINATORS:
488        //  1. Only fire when resolution == null (Phase 2 did not run).
489        //     Do NOT fire on an explicit NOT_FOUND/AMBIGUOUS status from
490        //     Resolver2 — that would overrule Resolver2's deliberate
491        //     rejections (round-2 codex Q1 BLOCKING).
492        //  2. If the column has a SQL-written qualifier (e.g. `s.id`),
493        //     the qualifier MUST match Phase 1's chosen source table by
494        //     name or alias (case-insensitive). Otherwise Phase 1 may have
495        //     heuristically picked a source the user did not name, and
496        //     promoting would silently mis-bind (round-3 codex P0 BLOCKING).
497        //  3. The source table must have a non-empty resolvable name; an
498        //     anonymous source table is not a trustworthy fallback target.
499        if (sourceTableFallback && resolution == null
500                && columnRef.getSourceTable() != null
501                && qualifierMatchesSource(columnRef)) {
502            status = ResolutionStatus.EXACT_MATCH;
503            // finalTable stays null — Phase 1 only knows the source table,
504            // not the catalog's final binding. Downstream consumers should
505            // tolerate finalTable == null on fallback-promoted bindings.
506        }
507        // Slice 117 — tolerant-outer-binding fallback for the UPDATE
508        // SET-RHS scalar-subquery extractor. When the binding is still
509        // non-EXACT_MATCH at this point (Resolver2 marked the ref as
510        // NOT_FOUND because the qualifier resolves to neither an inner
511        // local relation nor a Phase-1 sourceTable), AND the ref carries
512        // a non-empty SQL-written qualifier, AND that qualifier is NOT
513        // in the inner local FROM aliases, promote to a synthetic
514        // EXACT_MATCH binding with (qualifier, columnName). The slice-11
515        // promoter then sees the resulting ColumnRef and synthesises an
516        // OUTER_REFERENCE relation against the enclosing scope.
517        //
518        // Qualifiers IN the inner local FROM aliases fall through to
519        // strict binding so real typos (e.g. `o.bad_col` where `o` is the
520        // inner FROM alias) still reject as COLUMN_BINDING_NON_EXACT.
521        // Unqualified refs also fall through (their binding is genuinely
522        // ambiguous between inner and outer; the caller throws the same
523        // diagnostic).
524        if (status != ResolutionStatus.EXACT_MATCH
525                && !tolerantInnerLocalAliases.isEmpty()) {
526            String qual = columnRef.getTableString();
527            if (qual != null && !qual.isEmpty()
528                    && !tolerantInnerLocalAliases.contains(
529                            qual.toLowerCase(Locale.ROOT))) {
530                // Use the SQL-written qualifier as the relationAlias so
531                // promoteCorrelatedRefsToOuterReference looks up the
532                // enclosing scope by the user-written name (matches the
533                // slice-14 alias-preserving convention).
534                return new ColumnBinding(qual, columnName, /*finalTable=*/ null,
535                        ResolutionStatus.EXACT_MATCH);
536            }
537        }
538        return new ColumnBinding(relationAlias, columnName, finalTable, status);
539    }
540
541    /**
542     * Slice 93 — safety predicate for the {@link #sourceTableFallback}
543     * path. Returns true when it's safe to trust Phase 1's
544     * {@code sourceTable} on a column reference whose Phase 2 resolution
545     * is null.
546     *
547     * <p>Safe when:
548     * <ul>
549     *   <li>The source table has a non-empty name (anonymous tables are
550     *       not trustworthy fallback targets), AND</li>
551     *   <li>Either the column reference is unqualified (single-source
552     *       FROMs in Hive multi-insert make Phase 1's choice unambiguous),
553     *       or the qualifier matches the source's name or alias
554     *       (case-insensitive) — a mismatched qualifier means Phase 1
555     *       picked a different source than the user named.</li>
556     * </ul>
557     */
558    private static boolean qualifierMatchesSource(TObjectName columnRef) {
559        gudusoft.gsqlparser.nodes.TTable st = columnRef.getSourceTable();
560        if (st == null) {
561            return false;
562        }
563        String srcName = st.getName();
564        String srcAlias = st.getAliasName();
565        boolean hasIdentifiableSource = (srcName != null && !srcName.isEmpty())
566                || (srcAlias != null && !srcAlias.isEmpty());
567        if (!hasIdentifiableSource) {
568            return false;
569        }
570        String qual = columnRef.getTableString();
571        if (qual == null || qual.isEmpty()) {
572            // Unqualified — Phase 1's choice stands. In single-source FROM
573            // contexts (the only Hive multi-insert shape currently
574            // admitted) this is unambiguous.
575            return true;
576        }
577        // Qualified — qualifier must match source name or alias.
578        return qual.equalsIgnoreCase(srcName) || qual.equalsIgnoreCase(srcAlias);
579    }
580
581    /**
582     * Slice 19: detect alias-bound PARTITION BY / OVER ORDER BY refs.
583     *
584     * <p>The check fires only when ALL of:
585     * <ol>
586     *   <li>{@code columnRef} is unqualified ({@code getTableToken() == null});
587     *       a qualified ref like {@code e.doubled} explicitly names a FROM
588     *       relation, not a SELECT alias.</li>
589     *   <li>The resolver's binding lacks definite FROM-scope evidence
590     *       (i.e. {@code !hasDefiniteEvidence()}); the discriminator only
591     *       fires for the heuristic {@code inferred_from_usage} fallback
592     *       in {@code TableNamespace.resolveColumn}.</li>
593     *   <li>Some result column in {@code enclosingSelect}'s result-column
594     *       list exposes the same name (case-insensitive) AND its
595     *       expression is a calculated expression (anything but a simple
596     *       column reference / star).</li>
597     * </ol>
598     *
599     * <p>If multiple result columns share the exposed name and at least
600     * one is calculated, the method returns {@code true} — order-
601     * independent rejection keeps the slice invariant deterministic.
602     *
603     * <p>Classification reuses {@code ColumnSource.isCalculatedColumn()}
604     * by constructing a transient {@code ColumnSource} pinned to the
605     * candidate {@code TResultColumn}; the helper inspects only the
606     * definition-node expression and is independent of namespace state.
607     */
608    @Override
609    public boolean isCalculatedProjectionAliasFallback(TObjectName columnRef,
610                                                       TSelectSqlStatement enclosingSelect) {
611        if (columnRef == null || enclosingSelect == null) {
612            return false;
613        }
614        // Unqualified-only.
615        if (columnRef.getTableToken() != null) {
616            return false;
617        }
618        String columnName = columnRef.getColumnNameOnly();
619        if (columnName == null || columnName.isEmpty()) {
620            return false;
621        }
622        // Definite-evidence guard: skip when the resolver has positive
623        // FROM-scope evidence (DDL / SQLEnv / explicit metadata).
624        ResolutionResult resolution = columnRef.getResolution();
625        if (resolution == null || resolution.getStatus() != ResolutionStatus.EXACT_MATCH) {
626            return false;
627        }
628        ColumnSource source = resolution.getColumnSource();
629        if (source == null) {
630            return false;
631        }
632        if (source.hasDefiniteEvidence()) {
633            return false;
634        }
635        // AST walk: any matching exposed name on a calculated expression?
636        TResultColumnList rcl = enclosingSelect.getResultColumnList();
637        if (rcl == null) {
638            return false;
639        }
640        for (int i = 0; i < rcl.size(); i++) {
641            TResultColumn rc = rcl.getResultColumn(i);
642            if (rc == null) {
643                continue;
644            }
645            String exposed;
646            if (rc.getColumnAlias() != null && !rc.getColumnAlias().isEmpty()) {
647                exposed = rc.getColumnAlias();
648            } else {
649                exposed = rc.getColumnNameOnly();
650            }
651            if (exposed == null || exposed.isEmpty()) {
652                continue;
653            }
654            if (!exposed.equalsIgnoreCase(columnName)) {
655                continue;
656            }
657            // Reuse ColumnSource's calculated-column classification by
658            // pinning the definition node to this result column. The
659            // transient source has no namespace; isCalculatedColumn()
660            // looks only at the definition expression.
661            ColumnSource transientSource = new ColumnSource(
662                    null, exposed, rc, 0.0, "slice19_alias_classifier");
663            if (transientSource.isCalculatedColumn()) {
664                return true;
665            }
666        }
667        return false;
668    }
669}