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}