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