001package gudusoft.gsqlparser.resolver2.model; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.EExpressionType; 005import gudusoft.gsqlparser.nodes.TExpression; 006import gudusoft.gsqlparser.nodes.TObjectName; 007import gudusoft.gsqlparser.nodes.TParseTreeNode; 008import gudusoft.gsqlparser.nodes.TResultColumn; 009import gudusoft.gsqlparser.nodes.TTable; 010import gudusoft.gsqlparser.resolver2.inference.EvidenceType; 011import gudusoft.gsqlparser.resolver2.matcher.INameMatcher; 012import gudusoft.gsqlparser.resolver2.matcher.VendorNameMatcher; 013import gudusoft.gsqlparser.resolver2.namespace.AbstractNamespace; 014import gudusoft.gsqlparser.resolver2.namespace.INamespace; 015import gudusoft.gsqlparser.resolver2.namespace.SubqueryNamespace; 016import gudusoft.gsqlparser.resolver2.namespace.CTENamespace; 017import gudusoft.gsqlparser.resolver2.namespace.UnionNamespace; 018import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType; 019import gudusoft.gsqlparser.sqlenv.IdentifierService; 020import gudusoft.gsqlparser.stmt.TSelectSqlStatement; 021 022import java.util.Collections; 023import java.util.HashSet; 024import java.util.IdentityHashMap; 025import java.util.List; 026import java.util.Set; 027 028/** 029 * Represents the source of a column reference. 030 * Tracks where a column comes from, including intermediate transformations 031 * through subqueries and CTEs. 032 * 033 * Design principles: 034 * 1. Immutable - once created, cannot be modified 035 * 2. Recursive - can trace back through subquery/CTE layers 036 * 3. Confidence-scored - supports evidence-based inference 037 */ 038public class ColumnSource { 039 /** The namespace where this column is exposed (e.g., subquery, table) */ 040 private final INamespace sourceNamespace; 041 042 /** The name by which this column is exposed in the namespace */ 043 private final String exposedName; 044 045 /** The AST node where this column is defined (TResultColumn, TTableColumn, etc.) */ 046 private final TParseTreeNode definitionNode; 047 048 /** Location information for the definition */ 049 private final SourceLocation definitionLocation; 050 051 /** 052 * Confidence score [0.0, 1.0]: 053 * - 1.0: Definite (from metadata or explicit definition) 054 * - 0.7-0.9: High confidence inference (strong evidence) 055 * - 0.5-0.7: Medium confidence inference (some evidence) 056 * - 0.0-0.5: Low confidence guess 057 */ 058 private final double confidence; 059 060 /** 061 * Evidence that supports this resolution. 062 * Used for debugging and explaining inference decisions. 063 * 064 * @deprecated Use {@link #evidenceDetail} instead. This field is kept for backward 065 * compatibility and will be derived from evidenceDetail if not explicitly set. 066 */ 067 private final String evidence; 068 069 /** 070 * Structured evidence detail for this resolution. 071 * Provides type-safe evidence with confidence weight and source traceability. 072 * This is the preferred way to access resolution evidence. 073 * 074 * @see ResolutionEvidence 075 */ 076 private final ResolutionEvidence evidenceDetail; 077 078 /** 079 * Override table for traced columns. 080 * When set, getFinalTable() returns this instead of namespace's table. 081 */ 082 private final TTable overrideTable; 083 084 /** 085 * Candidate tables for ambiguous columns. 086 * When a column could come from multiple tables (e.g., SELECT * FROM t1, t2), 087 * this list contains all possible source tables so end users can access them. 088 */ 089 private final List<TTable> candidateTables; 090 091 /** 092 * Field path for deep/record field access (e.g., struct.field.subfield). 093 * 094 * <p>When a column reference includes field access beyond the base column, 095 * this captures the field path. For example, in {@code customer.address.city}, 096 * if base column is {@code customer}, fieldPath contains {@code ["address", "city"]}.</p> 097 * 098 * <p>This field is null or empty for regular column references without field access.</p> 099 * 100 * @see FieldPath 101 */ 102 private final FieldPath fieldPath; 103 104 public ColumnSource(INamespace sourceNamespace, 105 String exposedName, 106 TParseTreeNode definitionNode, 107 double confidence, 108 String evidence) { 109 this(sourceNamespace, exposedName, definitionNode, confidence, evidence, null, null); 110 } 111 112 public ColumnSource(INamespace sourceNamespace, 113 String exposedName, 114 TParseTreeNode definitionNode, 115 double confidence, 116 String evidence, 117 TTable overrideTable) { 118 this(sourceNamespace, exposedName, definitionNode, confidence, evidence, overrideTable, null); 119 } 120 121 public ColumnSource(INamespace sourceNamespace, 122 String exposedName, 123 TParseTreeNode definitionNode, 124 double confidence, 125 String evidence, 126 TTable overrideTable, 127 List<TTable> candidateTables) { 128 this(sourceNamespace, exposedName, definitionNode, confidence, evidence, overrideTable, candidateTables, null, null); 129 } 130 131 /** 132 * Full constructor with all fields including ResolutionEvidence. 133 */ 134 public ColumnSource(INamespace sourceNamespace, 135 String exposedName, 136 TParseTreeNode definitionNode, 137 double confidence, 138 String evidence, 139 TTable overrideTable, 140 List<TTable> candidateTables, 141 ResolutionEvidence evidenceDetail) { 142 this(sourceNamespace, exposedName, definitionNode, confidence, evidence, overrideTable, candidateTables, evidenceDetail, null); 143 } 144 145 /** 146 * Full constructor with all fields including ResolutionEvidence and FieldPath. 147 * 148 * @param sourceNamespace The namespace where this column is exposed 149 * @param exposedName The name by which this column is exposed 150 * @param definitionNode The AST node where this column is defined 151 * @param confidence Confidence score [0.0, 1.0] 152 * @param evidence Evidence string for this resolution 153 * @param overrideTable Override table for traced columns 154 * @param candidateTables Candidate tables for ambiguous columns 155 * @param evidenceDetail Structured evidence detail 156 * @param fieldPath Field path for deep/record field access 157 */ 158 public ColumnSource(INamespace sourceNamespace, 159 String exposedName, 160 TParseTreeNode definitionNode, 161 double confidence, 162 String evidence, 163 TTable overrideTable, 164 List<TTable> candidateTables, 165 ResolutionEvidence evidenceDetail, 166 FieldPath fieldPath) { 167 this.sourceNamespace = sourceNamespace; 168 this.exposedName = exposedName; 169 this.definitionNode = definitionNode; 170 this.definitionLocation = definitionNode != null 171 ? new SourceLocation(definitionNode) 172 : null; 173 this.confidence = Math.max(0.0, Math.min(1.0, confidence)); 174 this.evidence = evidence; 175 this.overrideTable = overrideTable; 176 this.candidateTables = candidateTables != null ? Collections.unmodifiableList(candidateTables) : null; 177 this.fieldPath = fieldPath; 178 // If evidenceDetail not provided, create from legacy evidence 179 if (evidenceDetail != null) { 180 this.evidenceDetail = evidenceDetail; 181 } else if (evidence != null) { 182 this.evidenceDetail = ResolutionEvidence.fromLegacyEvidence(evidence, confidence, definitionNode); 183 } else { 184 this.evidenceDetail = null; 185 } 186 } 187 188 /** 189 * Constructor with ResolutionEvidence (preferred for new code). 190 */ 191 public ColumnSource(INamespace sourceNamespace, 192 String exposedName, 193 TParseTreeNode definitionNode, 194 ResolutionEvidence evidenceDetail) { 195 this(sourceNamespace, exposedName, definitionNode, 196 evidenceDetail != null ? evidenceDetail.getWeight() : 1.0, 197 evidenceDetail != null ? evidenceDetail.toLegacyEvidence() : "metadata", 198 null, null, evidenceDetail); 199 } 200 201 /** 202 * Constructor with ResolutionEvidence and override table. 203 */ 204 public ColumnSource(INamespace sourceNamespace, 205 String exposedName, 206 TParseTreeNode definitionNode, 207 ResolutionEvidence evidenceDetail, 208 TTable overrideTable) { 209 this(sourceNamespace, exposedName, definitionNode, 210 evidenceDetail != null ? evidenceDetail.getWeight() : 1.0, 211 evidenceDetail != null ? evidenceDetail.toLegacyEvidence() : "metadata", 212 overrideTable, null, evidenceDetail); 213 } 214 215 /** 216 * Constructor for definite matches (confidence = 1.0) 217 */ 218 public ColumnSource(INamespace sourceNamespace, 219 String exposedName, 220 TParseTreeNode definitionNode) { 221 this(sourceNamespace, exposedName, definitionNode, 1.0, "metadata"); 222 } 223 224 public INamespace getSourceNamespace() { 225 return sourceNamespace; 226 } 227 228 public String getExposedName() { 229 return exposedName; 230 } 231 232 public TParseTreeNode getDefinitionNode() { 233 return definitionNode; 234 } 235 236 public SourceLocation getDefinitionLocation() { 237 return definitionLocation; 238 } 239 240 public double getConfidence() { 241 return confidence; 242 } 243 244 public String getEvidence() { 245 return evidence; 246 } 247 248 /** 249 * Get the structured evidence detail for this resolution. 250 * 251 * <p>This is the preferred way to access resolution evidence as it provides: 252 * <ul> 253 * <li>Type-safe evidence type (enum)</li> 254 * <li>Confidence weight with clear semantics</li> 255 * <li>Source location for traceability</li> 256 * <li>Human-readable messages</li> 257 * </ul> 258 * 259 * @return The structured evidence detail, or null if not available 260 */ 261 public ResolutionEvidence getEvidenceDetail() { 262 return evidenceDetail; 263 } 264 265 /** 266 * Get the evidence type from the structured evidence detail. 267 * Convenience method for common use cases. 268 * 269 * @return The evidence type, or null if no evidence detail 270 */ 271 public EvidenceType getEvidenceType() { 272 return evidenceDetail != null ? evidenceDetail.getType() : null; 273 } 274 275 /** 276 * Check if this resolution has definite evidence (not inferred). 277 * Definite evidence comes from DDL, metadata, or explicit definitions. 278 * 279 * @return true if evidence is definite 280 */ 281 public boolean hasDefiniteEvidence() { 282 if (evidenceDetail != null) { 283 return evidenceDetail.isDefinite(); 284 } 285 // Fallback: check legacy evidence and confidence 286 if (confidence >= 1.0) { 287 return true; 288 } 289 if (evidence != null) { 290 String lower = evidence.toLowerCase(); 291 return lower.contains("metadata") || lower.contains("ddl") || 292 lower.contains("explicit") || lower.contains("insert_column"); 293 } 294 return false; 295 } 296 297 /** 298 * Get the <b>final</b> physical table this column originates from after tracing 299 * through all subqueries and CTEs. 300 * 301 * <h3>Semantic Difference: getFinalTable() vs TObjectName.getSourceTable()</h3> 302 * <ul> 303 * <li><b>getFinalTable()</b> (this method): The final physical table after 304 * recursively tracing through all subqueries and CTEs. Use this for data lineage.</li> 305 * <li><b>TObjectName.getSourceTable()</b>: The immediate source in the current scope. 306 * For a column from a subquery, this points to the subquery's TTable itself.</li> 307 * </ul> 308 * 309 * <h3>Example</h3> 310 * <pre>{@code 311 * SELECT title FROM (SELECT * FROM books) sub 312 * 313 * For the 'title' column in outer SELECT: 314 * - TObjectName.getSourceTable() → TTable for subquery 'sub' (immediate source) 315 * - ColumnSource.getFinalTable() → TTable for 'books' (final physical table) 316 * }</pre> 317 * 318 * <p>For calculated columns in subqueries (expressions like {@code START_DT - x AS alias}), 319 * this returns null because such calculated columns don't originate from a physical 320 * table - they are derived values computed in the subquery.</p> 321 * 322 * <p>For aliased columns in subqueries (e.g., {@code SELECT t.id AS col1 FROM my_table t}), 323 * this traces through the alias to find the physical table, because the data still 324 * originates from the physical table even though the column has been renamed.</p> 325 * 326 * <p>Note: For CTEs, calculated columns ARE the CTE's own columns, so they trace 327 * to the CTE itself (handled by CTENamespace.getFinalTable()).</p> 328 * 329 * @return The physical table, or null if unable to determine or if calculated in subquery 330 * @see gudusoft.gsqlparser.nodes.TObjectName#getSourceTable() 331 */ 332 public TTable getFinalTable() { 333 if (sourceNamespace == null && overrideTable == null) { 334 return null; 335 } 336 337 // For SubqueryNamespace: calculated columns should NOT trace to base table 338 // They are derived values that don't exist in the underlying physical table 339 // Example: SELECT *, expr AS alias FROM table - alias is calculated, not from table 340 // 341 // IMPORTANT: Check BEFORE overrideTable to prevent alias/calculated columns 342 // from being traced to base tables even when overrideTable is explicitly set 343 if (sourceNamespace instanceof SubqueryNamespace && isCalculatedColumn()) { 344 return null; 345 } 346 347 // For CTENamespace: calculated columns ARE the CTE's own columns 348 // They should trace to the CTE itself (referencing table), NOT to underlying base tables 349 // Example: WITH cte AS (SELECT SUM(x) AS total FROM t) SELECT total FROM cte 350 // The 'total' column traces to 'cte', not to 't' 351 if (sourceNamespace instanceof CTENamespace && isCalculatedColumn()) { 352 return ((CTENamespace) sourceNamespace).getReferencingTable(); 353 } 354 355 // For SubqueryNamespace: column aliases - trace through to find the source table. 356 // The alias changes the column name but the data still originates from a physical table. 357 // Example: SELECT t.id AS col1 FROM my_table t - col1's data comes from my_table 358 if (sourceNamespace instanceof SubqueryNamespace && isColumnAlias()) { 359 return traceColumnAliasThroughSubquery((SubqueryNamespace) sourceNamespace); 360 } 361 362 // For CTENamespace: column aliases ARE the CTE's own columns 363 // They should trace to the CTE itself, NOT to underlying base tables 364 // Example: WITH cte AS (SELECT x AS y FROM t) SELECT y FROM cte 365 // The 'y' column traces to 'cte', not to 't' 366 if (sourceNamespace instanceof CTENamespace && isColumnAlias()) { 367 return ((CTENamespace) sourceNamespace).getReferencingTable(); 368 } 369 370 // For CTENamespace: explicit column names ARE the CTE's own columns 371 // They should trace to the CTE itself, NOT to underlying base tables 372 // Example: WITH cte(c1, c2) AS (SELECT id, name FROM t) SELECT c1 FROM cte 373 // The 'c1' column traces to 'cte', not to 't' (because 'c1' doesn't exist in 't') 374 if (sourceNamespace instanceof CTENamespace && isCTEExplicitColumn()) { 375 return ((CTENamespace) sourceNamespace).getReferencingTable(); 376 } 377 378 // For SubqueryNamespace: passthrough columns that reference calculated columns should NOT trace 379 // IMPORTANT: This check must come BEFORE isPassthroughToAlias() because 380 // isPassthroughToAlias() returns true for BOTH alias and calculated passthroughs. 381 // Example: SELECT kko_lfz_9 FROM (SELECT CASE...END AS kko_lfz_9 FROM t) subq 382 // The outer kko_lfz_9 references a calculated column in the subquery 383 if (sourceNamespace instanceof SubqueryNamespace && isPassthroughToCalculatedInSubquery()) { 384 return null; 385 } 386 387 // For SubqueryNamespace: passthrough columns that reference aliases - trace through 388 // to find the source table. The data still originates from a physical table. 389 // Example: SELECT stat_typ FROM (SELECT stat_typ = stellplatz_typ FROM t) AS b 390 // The stat_typ in outer query references b.stat_typ → traces to t 391 if (sourceNamespace instanceof SubqueryNamespace && isPassthroughToAlias()) { 392 return traceColumnAliasThroughSubquery((SubqueryNamespace) sourceNamespace); 393 } 394 395 // For CTENamespace: passthrough columns that reference calculated subquery columns should NOT trace 396 // Example: WITH DataCTE AS (SELECT subq.calc_col FROM (SELECT CASE...END AS calc_col FROM t) subq) 397 // The CTE column calc_col references a calculated column in the subquery 398 if (sourceNamespace instanceof CTENamespace && isPassthroughToCalculatedInCTE()) { 399 return null; 400 } 401 402 // For CTE explicit column + star pattern: c1/c2/c3 trace to the star, NOT through the star 403 // Example: WITH cte(c1, c2, c3) AS (SELECT * FROM Employees) 404 // Without metadata, c1 traces to Employees.* (the star), not Employees.c1 (which doesn't exist) 405 // The evidence "cte_explicit_column_via_star".equals(evidence) indicates this pattern 406 if ("cte_explicit_column_via_star".equals(evidence)) { 407 return null; 408 } 409 410 // For inferred columns traced through SELECT * across a CTE/subquery chain: 411 // if the exposed name is defined deeper in the chain as a renamed alias of a 412 // simple column (e.g. {@code SELECT t.col AS bug2}), the alias name does NOT 413 // exist as a column in the underlying physical table. Falling through to the 414 // generic tracing would link the alias name to the base table - producing 415 // bogus entries in TTable.getLinkedColumns(). 416 if (isInferredAliasThroughChain()) { 417 if (sourceNamespace instanceof CTENamespace) { 418 return ((CTENamespace) sourceNamespace).getReferencingTable(); 419 } 420 return null; 421 } 422 423 // If an override table is set (e.g., for traced columns), use it 424 if (overrideTable != null) { 425 return overrideTable; 426 } 427 428 if (sourceNamespace == null) { 429 return null; 430 } 431 432 // For UnionNamespace: UNION columns don't belong to any specific physical table. 433 // They're a combination of multiple branches. UnionNamespace.getFinalTable() returns 434 // the first branch's table which is incorrect for tracking column origins. 435 if (sourceNamespace instanceof UnionNamespace) { 436 return null; 437 } 438 439 // For SubqueryNamespace without override table: if the subquery has multiple tables 440 // AND no qualified star to identify the source, we can't determine which table 441 // the column comes from. Returning the first table would be incorrect. 442 // Example: FROM CDS_H_PARTNER PAR, (SELECT kategorie ... FROM CDS_H_KUNDEN_OBJEKT) subq 443 // But if there's a qualified star like "ta.*", that identifies the source table. 444 // 445 // IMPORTANT: For Teradata, implicit lateral derived tables (auto-added tables when 446 // a column references an undeclared table in WHERE clause) should be excluded from 447 // the multiple-table count. These are syntactic sugar and shouldn't affect column 448 // resolution to the actual source table. 449 if (sourceNamespace instanceof SubqueryNamespace && overrideTable == null) { 450 SubqueryNamespace subNs = (SubqueryNamespace) sourceNamespace; 451 gudusoft.gsqlparser.stmt.TSelectSqlStatement subquery = subNs.getSubquery(); 452 if (subquery != null && subquery.tables != null) { 453 // Count only real tables (excluding implicit lateral derived tables) 454 int realTableCount = countRealTables(subquery.tables); 455 if (realTableCount > 1) { 456 // Check if there's a qualified star that can identify the source 457 if (!hasQualifiedStar(subquery)) { 458 return null; 459 } 460 } 461 } 462 } 463 464 // For CTENamespace with multiple tables (e.g., JOIN): trace the specific column 465 // to its correct source table using the definitionNode 466 // Example: WITH cte AS (SELECT m.album_id, b.band_name FROM albums m JOIN bands b ...) 467 // When tracing 'band_name', we need to find it comes from 'b' (bands), not 'm' (albums) 468 if (sourceNamespace instanceof CTENamespace) { 469 CTENamespace cteNs = (CTENamespace) sourceNamespace; 470 TTable tracedTable = null; 471 472 if (definitionNode instanceof TResultColumn) { 473 // Direct case: definitionNode is a TResultColumn from the CTE's SELECT list 474 tracedTable = traceColumnThroughCTE(cteNs, (TResultColumn) definitionNode); 475 } else { 476 // Indirect case: The column might be traced through a star column 477 // Try to find the column by name in the CTE chain 478 tracedTable = traceColumnByNameThroughCTE(cteNs, exposedName); 479 } 480 481 if (tracedTable != null) { 482 return tracedTable; 483 } 484 485 // For CTEs with multiple tables, if tracing failed (unqualified column), 486 // return null instead of the first table to avoid incorrect lineage. 487 // Example: WITH cte AS (SELECT musicians.id, musician_name, music_bands.band_name 488 // FROM musicians JOIN ... JOIN music_bands) 489 // The unqualified 'musician_name' cannot be traced to any specific table 490 // without metadata, so we should NOT guess and pick the first table. 491 TSelectSqlStatement cteSelect = cteNs.getSelectStatement(); 492 if (cteSelect != null && cteSelect.tables != null) { 493 int tableCount = countRealTables(cteSelect.tables); 494 if (tableCount > 1) { 495 // Cannot determine which table - don't guess 496 return null; 497 } 498 } 499 } 500 501 return sourceNamespace.getFinalTable(); 502 } 503 504 /** 505 * Get the original column name in the physical table when this column is an alias. 506 * 507 * <p>When a column is aliased in a subquery (e.g., {@code SELECT t.id AS col1}), 508 * the exposed name is {@code col1} but the original column in the physical table 509 * is {@code id}. This method returns {@code id} so callers can pair the correct 510 * column name with the physical table returned by {@link #getFinalTable()}.</p> 511 * 512 * <p>For multi-level aliases (e.g., {@code SELECT ADCS_MSISDN FROM (SELECT MSISDN AS ADCS_MSISDN FROM t) sub}), 513 * this recursively traces through all levels to find the original column name ({@code MSISDN}).</p> 514 * 515 * @return The original column name if this is an alias, or null if not an alias 516 * or unable to determine 517 */ 518 public String getFinalColumnName() { 519 return getFinalColumnNameInternal(0); 520 } 521 522 /** 523 * Internal recursive implementation of getFinalColumnName with depth limit. 524 */ 525 private String getFinalColumnNameInternal(int depth) { 526 if (depth > 10) return null; // safety limit for deeply nested aliases 527 528 if (!isColumnAlias() && !isPassthroughToAlias()) { 529 return null; 530 } 531 532 if (definitionNode == null || !(definitionNode instanceof TResultColumn)) { 533 return null; 534 } 535 536 TResultColumn rc = (TResultColumn) definitionNode; 537 TExpression expr = rc.getExpr(); 538 if (expr == null) { 539 return null; 540 } 541 542 TObjectName colRef = null; 543 if (expr.getExpressionType() == EExpressionType.simple_object_name_t) { 544 colRef = expr.getObjectOperand(); 545 } else if (expr.getExpressionType() == EExpressionType.sqlserver_proprietary_column_alias_t) { 546 TExpression rightExpr = expr.getRightOperand(); 547 if (rightExpr != null && rightExpr.getExpressionType() == EExpressionType.simple_object_name_t) { 548 colRef = rightExpr.getObjectOperand(); 549 } 550 } 551 552 if (colRef != null) { 553 // Check if this inner column reference itself has a ColumnSource that's also an alias. 554 // If so, recursively trace to get the deepest original column name. 555 ColumnSource innerSource = colRef.getColumnSource(); 556 if (innerSource != null && (innerSource.isColumnAlias() || innerSource.isPassthroughToAlias())) { 557 String deeperName = innerSource.getFinalColumnNameInternal(depth + 1); 558 if (deeperName != null) { 559 return deeperName; 560 } 561 } 562 return colRef.getColumnNameOnly(); 563 } 564 return null; 565 } 566 567 /** 568 * Trace a column by name through a CTE to find its correct source table. 569 * This handles the case when the column is traced through star columns 570 * and we don't have the direct TResultColumn definition. 571 * 572 * @param cteNs The CTENamespace to trace through 573 * @param columnName The name of the column to find 574 * @return The correct source table, or null if unable to determine 575 */ 576 // ===== S2: vendor-aware identifier matching helpers ===== 577 // Replace raw equalsIgnoreCase compares so quoted identifiers and 578 // vendor-specific case rules (BigQuery: tables sensitive, columns 579 // insensitive; Oracle/Postgres quoted: sensitive; etc.) are honored. 580 // The 2-arg INameMatcher.matches(...) defaults to dotColumn semantics 581 // inside VendorNameMatcher, so we route through VendorNameMatcher 582 // explicitly with the right ESQLDataObjectType when possible. 583 584 /** 585 * Slice S2: alias-equality check (subquery alias / table alias). 586 * Routes through dotTable on a VendorNameMatcher so BigQuery's 587 * table-sensitive rule is honored, falling back to the namespace's 588 * matcher for vendor-agnostic test scopes. 589 */ 590 private boolean aliasMatches(String stored, String input) { 591 return matchesAs(stored, input, ESQLDataObjectType.dotTable); 592 } 593 594 /** 595 * Slice S2: bare table-name equality check. 596 * Same routing as {@link #aliasMatches} (dotTable). Kept as a 597 * separate name so call sites read self-documentingly. 598 */ 599 private boolean tableMatches(String stored, String input) { 600 return matchesAs(stored, input, ESQLDataObjectType.dotTable); 601 } 602 603 /** 604 * Slice S2: column-name equality check. 605 * Routes through dotColumn (the VendorNameMatcher default), but 606 * called explicitly so future audits cannot mistake call sites 607 * for table compares. 608 */ 609 private boolean columnMatches(String stored, String input) { 610 return matchesAs(stored, input, ESQLDataObjectType.dotColumn); 611 } 612 613 /** 614 * Look up the namespace's name matcher and route the compare through 615 * it with the supplied {@link ESQLDataObjectType}. Falls back to 616 * {@link String#equalsIgnoreCase} when the namespace is missing or is 617 * not an {@link AbstractNamespace} (synthetic / unit-test scopes), to 618 * preserve current vendor-agnostic behaviour for those callers. 619 */ 620 private boolean matchesAs(String a, String b, ESQLDataObjectType objectType) { 621 if (a == null || b == null) { 622 return a == b; 623 } 624 INameMatcher matcher = sourceNamespace instanceof AbstractNamespace 625 ? ((AbstractNamespace) sourceNamespace).getNameMatcher() 626 : null; 627 if (matcher instanceof VendorNameMatcher) { 628 return ((VendorNameMatcher) matcher).matches(a, b, objectType); 629 } 630 if (matcher != null) { 631 return matcher.matches(a, b); 632 } 633 return a.equalsIgnoreCase(b); 634 } 635 636 private TTable traceColumnByNameThroughCTE(CTENamespace cteNs, String columnName) { 637 if (columnName == null || columnName.isEmpty()) { 638 return null; 639 } 640 641 // Get the CTE's SELECT statement 642 TSelectSqlStatement cteSelect = cteNs.getSelectStatement(); 643 if (cteSelect == null) { 644 return null; 645 } 646 647 // First, check if this CTE has explicit columns matching the name 648 TTable result = findColumnInSelectList(cteSelect, columnName); 649 if (result != null) { 650 return result; 651 } 652 653 // If not found directly, check if this CTE uses SELECT * from another CTE 654 if (cteSelect.tables != null) { 655 for (int i = 0; i < cteSelect.tables.size(); i++) { 656 TTable table = cteSelect.tables.getTable(i); 657 if (table == null) continue; 658 659 // If it references another CTE, trace through it 660 if (table.isCTEName() && table.getCTE() != null) { 661 gudusoft.gsqlparser.nodes.TCTE underlyingCte = table.getCTE(); 662 TSelectSqlStatement underlyingSelect = underlyingCte.getSubquery(); 663 if (underlyingSelect != null) { 664 result = findColumnInSelectList(underlyingSelect, columnName); 665 if (result != null) { 666 return result; 667 } 668 } 669 } 670 } 671 } 672 673 return null; 674 } 675 676 /** 677 * Trace a column through a CTE to find its correct source table. 678 * This handles CTEs with JOINs where columns come from different tables, 679 * including CTEs with star columns that reference other CTEs. 680 * 681 * @param cteNs The CTENamespace 682 * @param resultColumn The TResultColumn from the CTE's SELECT list 683 * @return The correct source table, or null if unable to determine 684 */ 685 private TTable traceColumnThroughCTE(CTENamespace cteNs, TResultColumn resultColumn) { 686 if (resultColumn == null || resultColumn.getExpr() == null) { 687 return null; 688 } 689 690 TExpression expr = resultColumn.getExpr(); 691 692 // Check if the expression is a star column (e.g., SELECT * FROM other_cte) 693 // In this case, we need to trace through to the underlying CTE 694 if (expr.getExpressionType() == EExpressionType.simple_object_name_t) { 695 TObjectName colRef = expr.getObjectOperand(); 696 if (colRef != null && "*".equals(colRef.getColumnNameOnly())) { 697 // This is a star column - trace through to find the actual column 698 return traceColumnThroughStarInCTE(cteNs, exposedName); 699 } 700 } 701 702 // Check if the expression is a simple column reference 703 if (expr.getExpressionType() != EExpressionType.simple_object_name_t) { 704 return null; 705 } 706 707 TObjectName colRef = expr.getObjectOperand(); 708 if (colRef == null) { 709 return null; 710 } 711 712 // Check if the column has a table qualifier (e.g., "b.band_name") 713 String tableQualifier = colRef.getTableString(); 714 if (tableQualifier == null || tableQualifier.isEmpty()) { 715 // No qualifier - can't determine which table 716 return null; 717 } 718 719 // Get the CTE's subquery to find the table with matching alias 720 TSelectSqlStatement cteSubquery = cteNs.getSelectStatement(); 721 if (cteSubquery == null || cteSubquery.tables == null) { 722 return null; 723 } 724 725 // Search for the table with matching alias or name (S2: vendor-aware compares) 726 for (int i = 0; i < cteSubquery.tables.size(); i++) { 727 TTable table = cteSubquery.tables.getTable(i); 728 if (table == null) continue; 729 730 // Check alias match 731 String alias = table.getAliasName(); 732 if (alias != null && aliasMatches(alias, tableQualifier)) { 733 // Found the table - now trace to its final physical table if needed 734 return traceToPhysicalTable(table); 735 } 736 737 // Check table name match (for unaliased tables) 738 String tableName = table.getTableName() != null ? table.getTableName().toString() : null; 739 if (tableName != null && tableMatches(tableName, tableQualifier)) { 740 return traceToPhysicalTable(table); 741 } 742 } 743 744 return null; 745 } 746 747 /** 748 * Trace a specific column through a CTE that uses SELECT *. 749 * This finds the underlying CTE that defines the column and traces it to the correct table. 750 * 751 * @param cteNs The CTE namespace with SELECT * 752 * @param columnName The name of the column to trace 753 * @return The correct source table, or null if unable to determine 754 */ 755 private TTable traceColumnThroughStarInCTE(CTENamespace cteNs, String columnName) { 756 if (columnName == null || columnName.isEmpty()) { 757 return null; 758 } 759 760 TSelectSqlStatement cteSubquery = cteNs.getSelectStatement(); 761 if (cteSubquery == null || cteSubquery.tables == null) { 762 return null; 763 } 764 765 // Find the underlying CTE or table that the star column references 766 for (int i = 0; i < cteSubquery.tables.size(); i++) { 767 TTable table = cteSubquery.tables.getTable(i); 768 if (table == null) continue; 769 770 // If it's a CTE reference, look for the column in that CTE 771 if (table.isCTEName() && table.getCTE() != null) { 772 gudusoft.gsqlparser.nodes.TCTE underlyingCte = table.getCTE(); 773 TSelectSqlStatement underlyingSubquery = underlyingCte.getSubquery(); 774 if (underlyingSubquery != null) { 775 // Look for the column in the underlying CTE's SELECT list 776 TTable tracedTable = findColumnInSelectList(underlyingSubquery, columnName); 777 if (tracedTable != null) { 778 return tracedTable; 779 } 780 } 781 } 782 } 783 784 return null; 785 } 786 787 /** 788 * Find a column by name in a SELECT list and trace it to its source table. 789 * 790 * @param selectStmt The SELECT statement to search 791 * @param columnName The column name to find 792 * @return The source table for the column, or null if not found 793 */ 794 private TTable findColumnInSelectList(TSelectSqlStatement selectStmt, String columnName) { 795 if (selectStmt == null || selectStmt.getResultColumnList() == null) { 796 return null; 797 } 798 799 gudusoft.gsqlparser.nodes.TResultColumnList resultList = selectStmt.getResultColumnList(); 800 for (int i = 0; i < resultList.size(); i++) { 801 TResultColumn rc = resultList.getResultColumn(i); 802 if (rc == null) continue; 803 804 // Get the exposed name (alias or column name) 805 String exposedColName = rc.getAliasClause() != null 806 ? rc.getAliasClause().toString() 807 : (rc.getExpr() != null && rc.getExpr().getObjectOperand() != null 808 ? rc.getExpr().getObjectOperand().getColumnNameOnly() 809 : null); 810 811 if (exposedColName != null && columnMatches(exposedColName, columnName)) { 812 // Found the column - trace it to its source table (S2: vendor-aware compares) 813 TExpression expr = rc.getExpr(); 814 if (expr != null && expr.getExpressionType() == EExpressionType.simple_object_name_t) { 815 TObjectName colRef = expr.getObjectOperand(); 816 if (colRef != null) { 817 String tableQualifier = colRef.getTableString(); 818 if (tableQualifier != null && !tableQualifier.isEmpty()) { 819 // Find the table with this qualifier in the FROM clause 820 if (selectStmt.tables != null) { 821 for (int j = 0; j < selectStmt.tables.size(); j++) { 822 TTable table = selectStmt.tables.getTable(j); 823 if (table == null) continue; 824 825 String alias = table.getAliasName(); 826 if (alias != null && aliasMatches(alias, tableQualifier)) { 827 return traceToPhysicalTable(table); 828 } 829 830 String tableName = table.getTableName() != null 831 ? table.getTableName().toString() : null; 832 if (tableName != null && tableMatches(tableName, tableQualifier)) { 833 return traceToPhysicalTable(table); 834 } 835 } 836 } 837 } 838 } 839 } 840 } 841 } 842 843 return null; 844 } 845 846 /** 847 * Trace a table to its underlying physical table. 848 * Handles CTEs, subqueries, and JOINs. 849 */ 850 private TTable traceToPhysicalTable(TTable table) { 851 return traceToPhysicalTable(table, 852 Collections.newSetFromMap(new IdentityHashMap<TTable, Boolean>())); 853 } 854 855 private TTable traceToPhysicalTable(TTable table, Set<TTable> visited) { 856 if (table == null) { 857 return null; 858 } 859 860 if (!visited.add(table)) { 861 return null; 862 } 863 864 // If it's already a physical table, return it 865 if (table.getTableType() == gudusoft.gsqlparser.ETableSource.objectname && !table.isCTEName()) { 866 return table; 867 } 868 869 // If it's a CTE reference, trace through the CTE 870 if (table.isCTEName() && table.getCTE() != null) { 871 // Use a simple approach - get the first physical table from the CTE 872 // This could be enhanced to trace specific columns through nested CTEs 873 gudusoft.gsqlparser.nodes.TCTE nestedCte = table.getCTE(); 874 if (nestedCte.getSubquery() != null && nestedCte.getSubquery().tables != null) { 875 for (int i = 0; i < nestedCte.getSubquery().tables.size(); i++) { 876 TTable nestedTable = nestedCte.getSubquery().tables.getTable(i); 877 TTable physical = traceToPhysicalTable(nestedTable, visited); 878 if (physical != null) { 879 return physical; 880 } 881 } 882 } 883 } 884 885 // If it's a subquery, trace through it 886 if (table.getSubquery() != null) { 887 SubqueryNamespace nestedNs = new SubqueryNamespace( 888 table.getSubquery(), 889 table.getAliasName(), 890 null // nameMatcher not needed for simple tracing 891 ); 892 return nestedNs.getFinalTable(); 893 } 894 895 return null; 896 } 897 898 /** 899 * Trace a column alias through a subquery to find its source table. 900 * When a column is aliased (e.g., SELECT t.id AS col1 FROM my_table t), 901 * the alias changes the column name but the data still comes from the 902 * underlying table. This method traces through to find that table. 903 * 904 * <p>Used by {@link #getTracedFinalTable()} to provide alias-aware lineage 905 * tracing. Handles both qualified (t.id AS col1) and unqualified (id AS col1) 906 * column references, as well as SQL Server proprietary alias syntax.</p> 907 * 908 * @param subNs The SubqueryNamespace containing the aliased column 909 * @return The physical table the column traces to, or null if undetermined 910 */ 911 private TTable traceColumnAliasThroughSubquery(SubqueryNamespace subNs) { 912 if (definitionNode == null || !(definitionNode instanceof TResultColumn)) { 913 return null; 914 } 915 916 TResultColumn rc = (TResultColumn) definitionNode; 917 TExpression expr = rc.getExpr(); 918 if (expr == null) { 919 return null; 920 } 921 922 TObjectName colRef = null; 923 924 if (expr.getExpressionType() == EExpressionType.simple_object_name_t) { 925 // Standard: SELECT col AS alias 926 colRef = expr.getObjectOperand(); 927 } else if (expr.getExpressionType() == EExpressionType.sqlserver_proprietary_column_alias_t) { 928 // SQL Server: SELECT alias = col 929 TExpression rightExpr = expr.getRightOperand(); 930 if (rightExpr != null && rightExpr.getExpressionType() == EExpressionType.simple_object_name_t) { 931 colRef = rightExpr.getObjectOperand(); 932 } 933 } 934 935 if (colRef == null) { 936 return null; 937 } 938 939 // Use the resolved sourceTable from the inner column reference 940 TTable sourceTable = colRef.getSourceTable(); 941 if (sourceTable != null) { 942 return traceToPhysicalTable(sourceTable); 943 } 944 945 // Fallback: if no sourceTable resolved, try to find by table qualifier (S2: vendor-aware) 946 String tableQualifier = colRef.getTableString(); 947 if (tableQualifier != null && !tableQualifier.isEmpty()) { 948 TSelectSqlStatement subquery = subNs.getSubquery(); 949 if (subquery != null && subquery.tables != null) { 950 for (int i = 0; i < subquery.tables.size(); i++) { 951 TTable table = subquery.tables.getTable(i); 952 if (table == null) continue; 953 954 String alias = table.getAliasName(); 955 if (alias != null && aliasMatches(alias, tableQualifier)) { 956 return traceToPhysicalTable(table); 957 } 958 959 String tableName = table.getTableName() != null ? table.getTableName().toString() : null; 960 if (tableName != null && tableMatches(tableName, tableQualifier)) { 961 return traceToPhysicalTable(table); 962 } 963 } 964 } 965 } 966 967 return null; 968 } 969 970 /** 971 * Get all physical tables that this column might originate from. 972 * 973 * <p>For columns from UNION queries, this returns tables from ALL branches, 974 * not just the first one. This is essential for proper lineage tracking 975 * where a column like {@code actor_id} in a UNION query should be linked 976 * to {@code actor.actor_id}, {@code actor2.actor_id}, {@code actor3.actor_id}.</p> 977 * 978 * <p>For regular single-table sources, this returns a single-element list 979 * with the same table as {@link #getFinalTable()}.</p> 980 * 981 * @return List of all physical tables, or empty list if unable to determine 982 */ 983 public java.util.List<TTable> getAllFinalTables() { 984 // If this ColumnSource has explicit candidateTables set (e.g., from UNION inference), 985 // use those instead of delegating to namespace. This is critical for UNION queries 986 // where only branches with SELECT * should contribute candidate tables for inferred columns. 987 // An EMPTY list means "no matching tables" - return it as-is without delegating. 988 // A NULL means "not determined" - delegate to namespace. 989 if (candidateTables != null) { 990 return candidateTables; 991 } 992 993 if (sourceNamespace == null) { 994 if (overrideTable != null) { 995 return java.util.Collections.singletonList(overrideTable); 996 } 997 return java.util.Collections.emptyList(); 998 } 999 1000 // For calculated columns and aliases in SubqueryNamespace, don't trace 1001 if (sourceNamespace instanceof SubqueryNamespace) { 1002 if (isCalculatedColumn() || isColumnAlias()) { 1003 return java.util.Collections.emptyList(); 1004 } 1005 } 1006 1007 // For CTENamespace calculated/alias/explicit columns, trace to CTE itself 1008 if (sourceNamespace instanceof CTENamespace) { 1009 if (isCalculatedColumn() || isColumnAlias() || isCTEExplicitColumn()) { 1010 TTable cteTable = ((CTENamespace) sourceNamespace).getReferencingTable(); 1011 if (cteTable != null) { 1012 return java.util.Collections.singletonList(cteTable); 1013 } 1014 return java.util.Collections.emptyList(); 1015 } 1016 } 1017 1018 // Delegate to namespace - handles UNION queries via UnionNamespace.getAllFinalTables() 1019 return sourceNamespace.getAllFinalTables(); 1020 } 1021 1022 /** 1023 * Check if this column is a passthrough reference to an underlying alias. 1024 * 1025 * <p>A passthrough column is a simple column reference in a subquery that 1026 * references another column from its FROM clause. If that underlying column 1027 * is an alias, then this passthrough should not trace to the base table.</p> 1028 * 1029 * <p>Example: In {@code SELECT stat_typ FROM (SELECT stat_typ = col FROM t) AS b}, 1030 * the outer {@code stat_typ} is a passthrough to {@code b.stat_typ}, which is an alias.</p> 1031 * 1032 * @return true if this is a passthrough to an alias 1033 */ 1034 private boolean isPassthroughToAlias() { 1035 if (definitionNode == null || !(definitionNode instanceof TResultColumn)) { 1036 return false; 1037 } 1038 1039 TResultColumn rc = (TResultColumn) definitionNode; 1040 TExpression expr = rc.getExpr(); 1041 if (expr == null) { 1042 return false; 1043 } 1044 1045 // Only check simple column references (passthroughs) 1046 if (expr.getExpressionType() != EExpressionType.simple_object_name_t) { 1047 return false; 1048 } 1049 1050 // If this column itself has an alias that differs, it's already handled by isColumnAlias() 1051 if (rc.getAliasClause() != null && rc.getAliasClause().getAliasName() != null) { 1052 return false; 1053 } 1054 1055 // Get the column name being referenced 1056 gudusoft.gsqlparser.nodes.TObjectName objName = expr.getObjectOperand(); 1057 if (objName == null) { 1058 return false; 1059 } 1060 String columnName = objName.getColumnNameOnly(); 1061 if (columnName == null || columnName.isEmpty()) { 1062 return false; 1063 } 1064 1065 // Resolve this column in the subquery's FROM scope to find the underlying ColumnSource 1066 if (sourceNamespace instanceof SubqueryNamespace) { 1067 SubqueryNamespace subNs = (SubqueryNamespace) sourceNamespace; 1068 ColumnSource underlyingSource = subNs.resolveColumnInFromScope(columnName); 1069 if (underlyingSource != null) { 1070 // Check if the underlying column is an alias or calculated 1071 if (underlyingSource.isColumnAlias() || underlyingSource.isCalculatedColumn()) { 1072 return true; 1073 } 1074 // Recursively check if it's a passthrough to alias 1075 if (underlyingSource.isPassthroughToAlias()) { 1076 return true; 1077 } 1078 } 1079 } 1080 1081 return false; 1082 } 1083 1084 /** 1085 * Check if this subquery column is a passthrough reference to a calculated column. 1086 * 1087 * <p>A subquery column is a passthrough to calculated if:</p> 1088 * <ol> 1089 * <li>The column definition is a simple column reference (e.g., kko_lfz_9)</li> 1090 * <li>The referenced column in the FROM scope is calculated (CASE, function, etc.)</li> 1091 * </ol> 1092 * 1093 * <p>Example:</p> 1094 * <pre> 1095 * SELECT kko_lfz_9 AS KKO_LFZ_9 1096 * FROM (SELECT CASE WHEN... END AS kko_lfz_9 FROM t) subq 1097 * </pre> 1098 * <p>Here, kko_lfz_9 in the outer query is a passthrough to a calculated column in subq.</p> 1099 * 1100 * <p>This differs from {@link #isPassthroughToAlias()} which skips columns with aliases. 1101 * Here we check even aliased passthroughs to see if they reference calculated columns.</p> 1102 * 1103 * @return true if this is a passthrough to a calculated column in a subquery 1104 */ 1105 private boolean isPassthroughToCalculatedInSubquery() { 1106 if (definitionNode == null || !(definitionNode instanceof TResultColumn)) { 1107 return false; 1108 } 1109 1110 TResultColumn rc = (TResultColumn) definitionNode; 1111 TExpression expr = rc.getExpr(); 1112 if (expr == null) { 1113 return false; 1114 } 1115 1116 // Only check simple column references (passthroughs) 1117 if (expr.getExpressionType() != EExpressionType.simple_object_name_t) { 1118 return false; 1119 } 1120 1121 // Get the column name being referenced 1122 gudusoft.gsqlparser.nodes.TObjectName objName = expr.getObjectOperand(); 1123 if (objName == null) { 1124 return false; 1125 } 1126 String columnName = objName.getColumnNameOnly(); 1127 if (columnName == null || columnName.isEmpty()) { 1128 return false; 1129 } 1130 1131 // Resolve this column in the subquery's FROM scope to find the underlying ColumnSource 1132 if (sourceNamespace instanceof SubqueryNamespace) { 1133 SubqueryNamespace subNs = (SubqueryNamespace) sourceNamespace; 1134 ColumnSource underlyingSource = subNs.resolveColumnInFromScope(columnName); 1135 if (underlyingSource != null) { 1136 // Check if the underlying column is calculated 1137 if (underlyingSource.isCalculatedColumn()) { 1138 return true; 1139 } 1140 // Recursively check if it's a passthrough to calculated 1141 if (underlyingSource.isPassthroughToCalculatedInSubquery()) { 1142 return true; 1143 } 1144 } 1145 } 1146 1147 return false; 1148 } 1149 1150 /** 1151 * Check if this CTE column is a passthrough reference to a calculated column in a subquery or nested CTE. 1152 * 1153 * <p>A CTE column is a passthrough to calculated if:</p> 1154 * <ol> 1155 * <li>The column definition is a simple qualified column reference (e.g., subq.calc_col or cte.calc_col)</li> 1156 * <li>The qualifier refers to a subquery or CTE in the CTE's body</li> 1157 * <li>The referenced column in that subquery/CTE is calculated (CASE, function, etc.)</li> 1158 * </ol> 1159 * 1160 * <p>Example with subquery:</p> 1161 * <pre> 1162 * WITH DataCTE AS ( 1163 * SELECT ErrorCountsCTE.ErrorSeverityCategory -- passthrough 1164 * FROM (SELECT CASE...END AS ErrorSeverityCategory FROM t) ErrorCountsCTE 1165 * ) 1166 * </pre> 1167 * 1168 * <p>Example with nested CTE:</p> 1169 * <pre> 1170 * WITH attendance_summary AS ( 1171 * SELECT date_trunc('month', attendance_date) as month FROM attendance 1172 * ) 1173 * WITH outer_cte AS ( 1174 * SELECT a.month FROM attendance_summary a -- passthrough to calculated in nested CTE 1175 * ) 1176 * </pre> 1177 * 1178 * @return true if this is a passthrough to a calculated column in a CTE 1179 */ 1180 private boolean isPassthroughToCalculatedInCTE() { 1181 if (definitionNode == null || !(definitionNode instanceof TResultColumn)) { 1182 return false; 1183 } 1184 1185 TResultColumn rc = (TResultColumn) definitionNode; 1186 TExpression expr = rc.getExpr(); 1187 if (expr == null) { 1188 return false; 1189 } 1190 1191 // Only check simple qualified column references (passthroughs like subq.column) 1192 if (expr.getExpressionType() != EExpressionType.simple_object_name_t) { 1193 return false; 1194 } 1195 1196 // Get the column reference 1197 gudusoft.gsqlparser.nodes.TObjectName objName = expr.getObjectOperand(); 1198 if (objName == null) { 1199 return false; 1200 } 1201 1202 // Must have a table qualifier (e.g., "ErrorCountsCTE" in "ErrorCountsCTE.ErrorSeverityCategory") 1203 String tableQualifier = objName.getTableString(); 1204 if (tableQualifier == null || tableQualifier.isEmpty()) { 1205 return false; 1206 } 1207 1208 String columnName = objName.getColumnNameOnly(); 1209 if (columnName == null || columnName.isEmpty()) { 1210 return false; 1211 } 1212 1213 // Get the CTE's subquery to find the referenced subquery alias 1214 if (!(sourceNamespace instanceof CTENamespace)) { 1215 return false; 1216 } 1217 1218 CTENamespace cteNs = (CTENamespace) sourceNamespace; 1219 gudusoft.gsqlparser.nodes.TCTE cte = cteNs.getCTE(); 1220 if (cte == null || cte.getSubquery() == null) { 1221 return false; 1222 } 1223 1224 // Find the subquery/table with this alias in the CTE's body 1225 gudusoft.gsqlparser.stmt.TSelectSqlStatement cteBody = cte.getSubquery(); 1226 TTable referencedTable = findTableByAlias(cteBody, tableQualifier); 1227 if (referencedTable == null) { 1228 return false; 1229 } 1230 1231 // Case 1: Referenced table is a subquery 1232 if (referencedTable.getSubquery() != null) { 1233 gudusoft.gsqlparser.stmt.TSelectSqlStatement subquery = referencedTable.getSubquery(); 1234 return isCalculatedColumnInSelect(subquery, columnName); 1235 } 1236 1237 // Case 2: Referenced table is a CTE reference 1238 if (referencedTable.isCTEName() && referencedTable.getCTE() != null) { 1239 gudusoft.gsqlparser.nodes.TCTE referencedCTE = referencedTable.getCTE(); 1240 if (referencedCTE.getSubquery() != null) { 1241 return isCalculatedColumnInSelect(referencedCTE.getSubquery(), columnName); 1242 } 1243 } 1244 1245 return false; 1246 } 1247 1248 /** 1249 * Find a table in a SELECT statement by its alias. 1250 */ 1251 private TTable findTableByAlias(gudusoft.gsqlparser.stmt.TSelectSqlStatement select, String alias) { 1252 if (select == null || select.tables == null || alias == null) { 1253 return null; 1254 } 1255 1256 for (int i = 0; i < select.tables.size(); i++) { 1257 TTable table = select.tables.getTable(i); 1258 if (table != null) { 1259 String tableAlias = table.getAliasName(); 1260 // S2: vendor-aware alias / table-name compare (dotTable) 1261 if (tableAlias != null && aliasMatches(tableAlias, alias)) { 1262 return table; 1263 } 1264 // Also check table name for non-aliased references 1265 if (tableAlias == null && table.getTableName() != null) { 1266 String tableName = table.getTableName().toString(); 1267 if (tableName != null && tableMatches(tableName, alias)) { 1268 return table; 1269 } 1270 } 1271 } 1272 } 1273 return null; 1274 } 1275 1276 /** 1277 * Check if a column in a SELECT statement is calculated (not a simple column reference). 1278 */ 1279 private boolean isCalculatedColumnInSelect(gudusoft.gsqlparser.stmt.TSelectSqlStatement select, String columnName) { 1280 if (select == null || select.getResultColumnList() == null || columnName == null) { 1281 return false; 1282 } 1283 1284 for (int i = 0; i < select.getResultColumnList().size(); i++) { 1285 TResultColumn rc = select.getResultColumnList().getResultColumn(i); 1286 if (rc == null) continue; 1287 1288 // Get the column name for this result column 1289 String rcName = null; 1290 if (rc.getAliasClause() != null && rc.getAliasClause().getAliasName() != null) { 1291 rcName = rc.getAliasClause().getAliasName().toString(); 1292 } else if (rc.getExpr() != null && 1293 rc.getExpr().getExpressionType() == EExpressionType.simple_object_name_t && 1294 rc.getExpr().getObjectOperand() != null) { 1295 rcName = rc.getExpr().getObjectOperand().getColumnNameOnly(); 1296 } 1297 1298 if (rcName != null && columnMatches(rcName, columnName)) { 1299 // S2: vendor-aware column-name compare. Found the column - check if it's calculated. 1300 TExpression expr = rc.getExpr(); 1301 if (expr != null && expr.getExpressionType() != EExpressionType.simple_object_name_t) { 1302 // Non-simple expression = calculated 1303 return true; 1304 } 1305 } 1306 } 1307 return false; 1308 } 1309 1310 /** 1311 * Check if this column source represents a calculated expression. 1312 * 1313 * <p>A column is calculated if its definition is a TResultColumn with 1314 * a non-simple expression (not a direct column reference or star).</p> 1315 * 1316 * <p>For inferred columns (via star expansion), we trace back to the 1317 * source CTE/subquery to check if the original column is calculated.</p> 1318 * 1319 * @return true if this is a calculated column 1320 */ 1321 public boolean isCalculatedColumn() { 1322 if (definitionNode == null) { 1323 // For inferred columns through star expansion, check if the underlying 1324 // column in the source CTE/subquery is calculated 1325 return isInferredFromCalculatedColumn(); 1326 } 1327 1328 if (!(definitionNode instanceof TResultColumn)) { 1329 return false; 1330 } 1331 1332 TResultColumn rc = (TResultColumn) definitionNode; 1333 TExpression expr = rc.getExpr(); 1334 if (expr == null) { 1335 return false; 1336 } 1337 1338 EExpressionType exprType = expr.getExpressionType(); 1339 1340 // Simple column reference - NOT calculated (passthrough) 1341 if (exprType == EExpressionType.simple_object_name_t) { 1342 return false; 1343 } 1344 1345 // Star column - NOT calculated (passthrough) 1346 String colText = rc.toString(); 1347 if (colText != null && colText.endsWith("*")) { 1348 return false; 1349 } 1350 1351 // SQL Server proprietary column alias (col = expr) 1352 if (exprType == EExpressionType.sqlserver_proprietary_column_alias_t) { 1353 if (expr.getRightOperand() != null && 1354 expr.getRightOperand().getExpressionType() == EExpressionType.simple_object_name_t) { 1355 return false; 1356 } 1357 } 1358 1359 // Any other expression type is calculated 1360 return true; 1361 } 1362 1363 /** 1364 * Check if this is an inferred column (via star expansion) that originates from 1365 * a calculated column in the source CTE/subquery. 1366 * 1367 * <p>When a column is resolved through star expansion (e.g., SELECT * FROM CTE), 1368 * the definitionNode is null. We need to trace back to the source namespace 1369 * to check if the original column is calculated.</p> 1370 * 1371 * @return true if this inferred column traces back to a calculated column 1372 */ 1373 private boolean isInferredFromCalculatedColumn() { 1374 // Only check for inferred columns (evidence contains "auto_inferred") 1375 if (evidence == null || !evidence.contains("auto_inferred")) { 1376 return false; 1377 } 1378 1379 // Need the source namespace and column name to trace 1380 if (sourceNamespace == null || exposedName == null) { 1381 return false; 1382 } 1383 1384 // For CTE namespace, check if the column is calculated in the CTE's SELECT list 1385 if (sourceNamespace instanceof CTENamespace) { 1386 CTENamespace cteNs = (CTENamespace) sourceNamespace; 1387 gudusoft.gsqlparser.nodes.TCTE cte = cteNs.getCTE(); 1388 if (cte != null && cte.getSubquery() != null) { 1389 // First check the CTE's direct SELECT list 1390 if (isCalculatedColumnInSelect(cte.getSubquery(), exposedName)) { 1391 return true; 1392 } 1393 1394 // If the CTE has a star column, trace through to referenced CTEs 1395 if (cteNs.hasStarColumn()) { 1396 return isCalculatedInCTEChain(cte.getSubquery(), exposedName); 1397 } 1398 } 1399 } 1400 1401 // For Subquery namespace, check if the column is calculated in the subquery's SELECT list 1402 if (sourceNamespace instanceof SubqueryNamespace) { 1403 SubqueryNamespace subNs = (SubqueryNamespace) sourceNamespace; 1404 gudusoft.gsqlparser.stmt.TSelectSqlStatement subquery = subNs.getSubquery(); 1405 if (subquery != null) { 1406 // First check the subquery's direct SELECT list 1407 if (isCalculatedColumnInSelect(subquery, exposedName)) { 1408 return true; 1409 } 1410 1411 // If the subquery has a star column, trace through to source tables 1412 if (subNs.hasStarColumn()) { 1413 return isCalculatedInSubqueryChain(subquery, exposedName); 1414 } 1415 } 1416 } 1417 1418 return false; 1419 } 1420 1421 /** 1422 * Check if a column is calculated by tracing through CTE references or 1423 * inline subqueries in the FROM clause. This handles cases like 1424 * Stage4 -> Stage3 -> Stage2 where the column is calculated at some 1425 * intermediate level, regardless of whether each level is a named CTE or 1426 * an inline derived table. 1427 * 1428 * <p>Example covered by the subquery branch:</p> 1429 * <pre> 1430 * WITH tbl AS ( 1431 * SELECT t.* FROM (SELECT CASE...END AS bug1 FROM base) t 1432 * ) 1433 * SELECT bug1 FROM tbl 1434 * </pre> 1435 * <p>The CTE body's FROM clause is a subquery (not another CTE), but 1436 * bug1 is still calculated in the inner subquery and must not be traced 1437 * to {@code base}.</p> 1438 */ 1439 private boolean isCalculatedInCTEChain(gudusoft.gsqlparser.stmt.TSelectSqlStatement select, String columnName) { 1440 return isCalculatedInCTEChain(select, columnName, 1441 new HashSet<gudusoft.gsqlparser.stmt.TSelectSqlStatement>()); 1442 } 1443 1444 private boolean isCalculatedInCTEChain( 1445 gudusoft.gsqlparser.stmt.TSelectSqlStatement select, 1446 String columnName, 1447 java.util.Set<gudusoft.gsqlparser.stmt.TSelectSqlStatement> visited) { 1448 if (select == null || select.tables == null) { 1449 return false; 1450 } 1451 // Guard against recursive CTEs (anchor body references itself via the 1452 // CTE name) and self-cycling subquery DAGs. 1453 if (!visited.add(select)) { 1454 return false; 1455 } 1456 1457 // Walk every table in the FROM clause - both CTE references and inline 1458 // subqueries can shadow underlying physical columns with calculated 1459 // expressions. 1460 for (int i = 0; i < select.tables.size(); i++) { 1461 TTable table = select.tables.getTable(i); 1462 if (table == null) continue; 1463 1464 if (table.isCTEName() && table.getCTE() != null) { 1465 gudusoft.gsqlparser.nodes.TCTE referencedCTE = table.getCTE(); 1466 if (referencedCTE.getSubquery() != null) { 1467 if (isCalculatedColumnInSelect(referencedCTE.getSubquery(), columnName)) { 1468 return true; 1469 } 1470 if (isCalculatedInCTEChain(referencedCTE.getSubquery(), columnName, visited)) { 1471 return true; 1472 } 1473 } 1474 } else if (table.getSubquery() != null) { 1475 gudusoft.gsqlparser.stmt.TSelectSqlStatement subquery = table.getSubquery(); 1476 if (isCalculatedColumnInSelect(subquery, columnName)) { 1477 return true; 1478 } 1479 if (isCalculatedInCTEChain(subquery, columnName, visited)) { 1480 return true; 1481 } 1482 } 1483 } 1484 return false; 1485 } 1486 1487 /** 1488 * Check if a column is calculated by tracing through subquery references. 1489 */ 1490 private boolean isCalculatedInSubqueryChain(gudusoft.gsqlparser.stmt.TSelectSqlStatement select, String columnName) { 1491 return isCalculatedInSubqueryChain(select, columnName, 1492 new HashSet<gudusoft.gsqlparser.stmt.TSelectSqlStatement>()); 1493 } 1494 1495 private boolean isCalculatedInSubqueryChain( 1496 gudusoft.gsqlparser.stmt.TSelectSqlStatement select, 1497 String columnName, 1498 java.util.Set<gudusoft.gsqlparser.stmt.TSelectSqlStatement> visited) { 1499 if (select == null || select.tables == null) { 1500 return false; 1501 } 1502 if (!visited.add(select)) { 1503 return false; 1504 } 1505 1506 // Look for subquery tables in the FROM clause 1507 for (int i = 0; i < select.tables.size(); i++) { 1508 TTable table = select.tables.getTable(i); 1509 if (table != null && table.getSubquery() != null) { 1510 gudusoft.gsqlparser.stmt.TSelectSqlStatement subquery = table.getSubquery(); 1511 if (isCalculatedColumnInSelect(subquery, columnName)) { 1512 return true; 1513 } 1514 if (isCalculatedInSubqueryChain(subquery, columnName, visited)) { 1515 return true; 1516 } 1517 } 1518 // Also check CTE references within subqueries 1519 if (table != null && table.isCTEName() && table.getCTE() != null) { 1520 gudusoft.gsqlparser.nodes.TCTE referencedCTE = table.getCTE(); 1521 if (referencedCTE.getSubquery() != null) { 1522 if (isCalculatedColumnInSelect(referencedCTE.getSubquery(), columnName)) { 1523 return true; 1524 } 1525 if (isCalculatedInCTEChain(referencedCTE.getSubquery(), columnName, visited)) { 1526 return true; 1527 } 1528 } 1529 } 1530 } 1531 return false; 1532 } 1533 1534 /** 1535 * Check if this inferred column traces through SELECT * across a CTE or 1536 * subquery chain to a renamed alias of a simple column. 1537 * 1538 * <p>Used by {@link #getFinalTable()} to avoid linking renamed alias names 1539 * to the underlying physical table. Unlike {@link #isColumnAlias()}, this 1540 * works for inferred columns whose {@code definitionNode} is null because 1541 * they were exposed through {@code SELECT *}.</p> 1542 * 1543 * <p>Example:</p> 1544 * <pre> 1545 * WITH tbl AS ( 1546 * SELECT t.* FROM (SELECT max_hxdate AS bug2 FROM base) t 1547 * ) 1548 * SELECT bug2 FROM tbl 1549 * </pre> 1550 * <p>{@code bug2} is exposed by the inner subquery as an alias of 1551 * {@code max_hxdate}. The base table has no column named {@code bug2}, 1552 * so tracing further must stop here.</p> 1553 */ 1554 private boolean isInferredAliasThroughChain() { 1555 if (evidence == null || !evidence.contains("auto_inferred")) { 1556 return false; 1557 } 1558 if (sourceNamespace == null || exposedName == null) { 1559 return false; 1560 } 1561 1562 gudusoft.gsqlparser.stmt.TSelectSqlStatement select = null; 1563 if (sourceNamespace instanceof CTENamespace) { 1564 CTENamespace cteNs = (CTENamespace) sourceNamespace; 1565 if (cteNs.getCTE() != null) { 1566 select = cteNs.getCTE().getSubquery(); 1567 } 1568 } else if (sourceNamespace instanceof SubqueryNamespace) { 1569 select = ((SubqueryNamespace) sourceNamespace).getSubquery(); 1570 } 1571 if (select == null) { 1572 return false; 1573 } 1574 1575 // Direct match in current SELECT list. 1576 if (isRenamedAliasInSelect(select, exposedName)) { 1577 return true; 1578 } 1579 // Walk through underlying CTE/subquery tables exposed via star. 1580 return isRenamedAliasInChain(select, exposedName, new HashSet<gudusoft.gsqlparser.stmt.TSelectSqlStatement>()); 1581 } 1582 1583 /** 1584 * Check if a SELECT list contains a result column where a simple column 1585 * reference is renamed via an alias whose name matches {@code columnName} 1586 * but differs from the underlying column's own name. 1587 */ 1588 private boolean isRenamedAliasInSelect( 1589 gudusoft.gsqlparser.stmt.TSelectSqlStatement select, String columnName) { 1590 if (select == null || select.getResultColumnList() == null || columnName == null) { 1591 return false; 1592 } 1593 for (int i = 0; i < select.getResultColumnList().size(); i++) { 1594 TResultColumn rc = select.getResultColumnList().getResultColumn(i); 1595 if (rc == null || rc.getAliasClause() == null 1596 || rc.getAliasClause().getAliasName() == null) { 1597 continue; 1598 } 1599 String aliasName = rc.getAliasClause().getAliasName().toString(); 1600 if (aliasName == null || !columnMatches(aliasName, columnName)) { 1601 continue; 1602 } 1603 TExpression expr = rc.getExpr(); 1604 if (expr == null 1605 || expr.getExpressionType() != EExpressionType.simple_object_name_t) { 1606 // Non-simple expressions are handled as "calculated" elsewhere. 1607 continue; 1608 } 1609 gudusoft.gsqlparser.nodes.TObjectName objName = expr.getObjectOperand(); 1610 if (objName == null) continue; 1611 String origName = objName.getColumnNameOnly(); 1612 if (origName != null && !columnMatches(origName, aliasName)) { 1613 return true; 1614 } 1615 } 1616 return false; 1617 } 1618 1619 /** 1620 * Recursive walk through the CTE references and subquery tables in 1621 * {@code select}'s FROM clause that can expose {@code columnName} via 1622 * star expansion, looking for {@link #isRenamedAliasInSelect} matches. 1623 * 1624 * <p>The walk is restricted to star-source tables to avoid false 1625 * positives in queries like {@code SELECT a.* FROM a JOIN (SELECT x AS id 1626 * FROM b_base) b}: when resolving an inferred {@code id} from {@code a.*} 1627 * we must not match the alias inside {@code b}, which never contributed 1628 * to {@code a.*}'s expansion.</p> 1629 */ 1630 private boolean isRenamedAliasInChain( 1631 gudusoft.gsqlparser.stmt.TSelectSqlStatement select, 1632 String columnName, 1633 java.util.Set<gudusoft.gsqlparser.stmt.TSelectSqlStatement> visited) { 1634 if (select == null || select.tables == null || columnName == null) { 1635 return false; 1636 } 1637 if (!visited.add(select)) { 1638 return false; 1639 } 1640 1641 java.util.List<TTable> sources = findStarExpansionSources(select); 1642 if (sources == null) { 1643 // Unqualified star is present - it can expose a column from any 1644 // FROM-clause table, so walk them all. 1645 sources = new java.util.ArrayList<>(); 1646 for (int i = 0; i < select.tables.size(); i++) { 1647 TTable t = select.tables.getTable(i); 1648 if (t != null) sources.add(t); 1649 } 1650 } 1651 1652 for (TTable table : sources) { 1653 gudusoft.gsqlparser.stmt.TSelectSqlStatement nested = null; 1654 if (table.isCTEName() && table.getCTE() != null) { 1655 nested = table.getCTE().getSubquery(); 1656 } else if (table.getSubquery() != null) { 1657 nested = table.getSubquery(); 1658 } 1659 if (nested == null) continue; 1660 1661 if (isRenamedAliasInSelect(nested, columnName)) { 1662 return true; 1663 } 1664 if (isRenamedAliasInChain(nested, columnName, visited)) { 1665 return true; 1666 } 1667 } 1668 return false; 1669 } 1670 1671 /** 1672 * Collect the FROM-clause tables that can expose any inferred column 1673 * through a star expansion in {@code select}'s SELECT list. 1674 * 1675 * <p>Return value semantics:</p> 1676 * <ul> 1677 * <li>{@code null} - an unqualified {@code *} is present, so every 1678 * FROM-clause table is a potential source.</li> 1679 * <li>Non-null, possibly empty - only the resolved qualified-star 1680 * sources (e.g. the {@code t} in {@code t.*}).</li> 1681 * </ul> 1682 */ 1683 private java.util.List<TTable> findStarExpansionSources( 1684 gudusoft.gsqlparser.stmt.TSelectSqlStatement select) { 1685 if (select == null || select.tables == null) { 1686 return java.util.Collections.emptyList(); 1687 } 1688 gudusoft.gsqlparser.nodes.TResultColumnList rcs = select.getResultColumnList(); 1689 if (rcs == null) { 1690 return java.util.Collections.emptyList(); 1691 } 1692 java.util.LinkedHashSet<TTable> sources = new java.util.LinkedHashSet<>(); 1693 for (int i = 0; i < rcs.size(); i++) { 1694 TResultColumn rc = rcs.getResultColumn(i); 1695 if (rc == null) continue; 1696 String text = rc.toString(); 1697 if (text == null) continue; 1698 text = text.trim(); 1699 if (!text.endsWith("*")) continue; 1700 if (text.equals("*")) { 1701 // Unqualified star - any FROM-clause table is a potential source. 1702 return null; 1703 } 1704 int dot = text.lastIndexOf('.'); 1705 if (dot <= 0) continue; 1706 String prefix = text.substring(0, dot).trim(); 1707 TTable matched = findStarPrefixTable(select, prefix); 1708 if (matched != null) { 1709 sources.add(matched); 1710 } 1711 } 1712 return new java.util.ArrayList<>(sources); 1713 } 1714 1715 /** 1716 * Find a FROM-clause table whose alias (or table name, when unaliased) 1717 * matches the prefix of a qualified star like {@code prefix.*}. 1718 */ 1719 private TTable findStarPrefixTable( 1720 gudusoft.gsqlparser.stmt.TSelectSqlStatement select, String prefix) { 1721 if (select == null || select.tables == null || prefix == null || prefix.isEmpty()) { 1722 return null; 1723 } 1724 for (int i = 0; i < select.tables.size(); i++) { 1725 TTable table = select.tables.getTable(i); 1726 if (table == null) continue; 1727 String alias = table.getAliasName(); 1728 if (alias != null && aliasMatches(alias, prefix)) { 1729 return table; 1730 } 1731 if (alias == null && table.getTableName() != null) { 1732 String name = table.getTableName().toString(); 1733 if (name != null && tableMatches(name, prefix)) { 1734 return table; 1735 } 1736 } 1737 } 1738 return null; 1739 } 1740 1741 /** 1742 * Check if this column source represents a column alias (renamed column). 1743 * 1744 * <p>A column is an alias if it's a simple column reference in a subquery 1745 * that has been given a different name via AS or NAMED. For example:</p> 1746 * <ul> 1747 * <li>{@code SELECT col AS alias FROM table} - alias is different from col</li> 1748 * <li>{@code SELECT col (NAMED alias) FROM table} - Teradata NAMED syntax</li> 1749 * <li>{@code SELECT alias = col FROM table} - SQL Server proprietary syntax</li> 1750 * </ul> 1751 * 1752 * <p>Column aliases are traced through in {@link #getFinalTable()} to find the 1753 * physical table the data originates from, since the alias only renames the column 1754 * but the data still comes from the physical table.</p> 1755 * 1756 * @return true if this is a column alias with a different name than the original 1757 */ 1758 public boolean isColumnAlias() { 1759 if (definitionNode == null) { 1760 return false; 1761 } 1762 1763 if (!(definitionNode instanceof TResultColumn)) { 1764 return false; 1765 } 1766 1767 TResultColumn rc = (TResultColumn) definitionNode; 1768 TExpression expr = rc.getExpr(); 1769 if (expr == null) { 1770 return false; 1771 } 1772 1773 EExpressionType exprType = expr.getExpressionType(); 1774 1775 // Handle SQL Server proprietary alias syntax: alias = column 1776 // Example: stat_typ = stellplatz_typ 1777 if (exprType == EExpressionType.sqlserver_proprietary_column_alias_t) { 1778 TExpression rightExpr = expr.getRightOperand(); 1779 TExpression leftExpr = expr.getLeftOperand(); 1780 // Only if right side is a simple column reference 1781 if (rightExpr != null && leftExpr != null && 1782 rightExpr.getExpressionType() == EExpressionType.simple_object_name_t) { 1783 gudusoft.gsqlparser.nodes.TObjectName rightObjName = rightExpr.getObjectOperand(); 1784 gudusoft.gsqlparser.nodes.TObjectName leftObjName = leftExpr.getObjectOperand(); 1785 if (rightObjName != null && leftObjName != null) { 1786 String origName = rightObjName.getColumnNameOnly(); 1787 String aliasName = leftObjName.getColumnNameOnly(); 1788 // If alias name differs from original column name, it's an alias 1789 if (origName != null && aliasName != null && 1790 !columnMatches(origName, aliasName)) { 1791 return true; 1792 } 1793 } 1794 } 1795 return false; 1796 } 1797 1798 // Standard alias syntax: column AS alias 1799 // Only applies to simple column references 1800 if (exprType != EExpressionType.simple_object_name_t) { 1801 return false; 1802 } 1803 1804 // Check if there's an alias that differs from the column name 1805 if (rc.getAliasClause() != null && rc.getAliasClause().getAliasName() != null) { 1806 String aliasName = rc.getAliasClause().getAliasName().toString(); 1807 if (aliasName != null && !aliasName.isEmpty()) { 1808 gudusoft.gsqlparser.nodes.TObjectName objName = expr.getObjectOperand(); 1809 if (objName != null) { 1810 String origName = objName.getColumnNameOnly(); 1811 // If alias name differs from original name, it's an alias 1812 if (origName != null && !columnMatches(origName, aliasName)) { 1813 return true; 1814 } 1815 } 1816 } 1817 } 1818 1819 return false; 1820 } 1821 1822 /** 1823 * Check if this column is a CTE explicit column with a different name than the underlying column. 1824 * 1825 * <p>A CTE explicit column is one defined in the CTE's column list that maps to a 1826 * different column name in the CTE's SELECT list. For example:</p> 1827 * <pre> 1828 * WITH cte(c1, c2) AS (SELECT id, name FROM users) 1829 * SELECT c1 FROM cte -- c1 maps to 'id', names differ 1830 * </pre> 1831 * 1832 * <p>CTE explicit columns should NOT trace to base tables because the explicit 1833 * column name (c1) doesn't exist as an actual column in the base table (users).</p> 1834 * 1835 * @return true if this is a CTE explicit column with a different name 1836 */ 1837 public boolean isCTEExplicitColumn() { 1838 // Must be from a CTENamespace 1839 if (!(sourceNamespace instanceof CTENamespace)) { 1840 return false; 1841 } 1842 1843 // Check evidence for explicit column marker 1844 if (!"cte_explicit_column".equals(evidence)) { 1845 return false; 1846 } 1847 1848 // Get the underlying column name from the definition node 1849 if (definitionNode == null || !(definitionNode instanceof TResultColumn)) { 1850 return false; 1851 } 1852 1853 TResultColumn rc = (TResultColumn) definitionNode; 1854 TExpression expr = rc.getExpr(); 1855 if (expr == null) { 1856 return false; 1857 } 1858 1859 // Get the column name from the SELECT list item 1860 String underlyingName = null; 1861 1862 // Check for alias first 1863 if (rc.getAliasClause() != null && rc.getAliasClause().getAliasName() != null) { 1864 underlyingName = rc.getAliasClause().getAliasName().toString(); 1865 } 1866 // Then check for simple column reference 1867 else if (expr.getExpressionType() == EExpressionType.simple_object_name_t && 1868 expr.getObjectOperand() != null) { 1869 underlyingName = expr.getObjectOperand().getColumnNameOnly(); 1870 } 1871 1872 // If we can't determine the underlying name, assume it's different 1873 // (calculated expressions, etc. are definitely different from explicit column names) 1874 if (underlyingName == null) { 1875 return true; 1876 } 1877 1878 // If the exposed name differs from the underlying column name, it's an explicit column rename 1879 return !columnMatches(exposedName, underlyingName); 1880 } 1881 1882 /** 1883 * Get the override table, if set. 1884 */ 1885 public TTable getOverrideTable() { 1886 return overrideTable; 1887 } 1888 1889 /** 1890 * Get the candidate tables for ambiguous columns. 1891 * 1892 * <p>When a column could come from multiple tables (e.g., SELECT * FROM t1, t2), 1893 * this returns all possible source tables. End users can iterate through this 1894 * list to understand all potential sources for the column.</p> 1895 * 1896 * @return List of candidate tables, or empty list if not ambiguous 1897 */ 1898 public List<TTable> getCandidateTables() { 1899 return candidateTables != null ? candidateTables : Collections.emptyList(); 1900 } 1901 1902 /** 1903 * Check if this column has multiple candidate tables (is ambiguous). 1904 * 1905 * @return true if there are multiple candidate tables 1906 */ 1907 public boolean isAmbiguous() { 1908 return candidateTables != null && candidateTables.size() > 1; 1909 } 1910 1911 /** 1912 * Get the field path for deep/record field access. 1913 * 1914 * <p>When a column reference includes field access beyond the base column, 1915 * this returns the field path. For example, in {@code customer.address.city}, 1916 * if base column is {@code customer}, this returns a FieldPath with 1917 * segments {@code ["address", "city"]}.</p> 1918 * 1919 * @return The field path, or null if no field access 1920 */ 1921 public FieldPath getFieldPath() { 1922 return fieldPath; 1923 } 1924 1925 /** 1926 * Check if this column source has a field path (deep/record field access). 1927 * 1928 * @return true if a non-empty field path exists 1929 */ 1930 public boolean hasFieldPath() { 1931 return fieldPath != null && !fieldPath.isEmpty(); 1932 } 1933 1934 /** 1935 * Check if this is a struct field access (has evidence "struct_field_access"). 1936 * 1937 * <p>This is a convenience method for checking if this column source represents 1938 * a struct/record field dereference operation.</p> 1939 * 1940 * @return true if this is a struct field access 1941 */ 1942 public boolean isStructFieldAccess() { 1943 return "struct_field_access".equals(evidence); 1944 } 1945 1946 /** 1947 * Checks if this is a definite resolution (confidence = 1.0) 1948 */ 1949 public boolean isDefinite() { 1950 return confidence >= 1.0; 1951 } 1952 1953 /** 1954 * Checks if this is an inferred resolution (confidence < 1.0) 1955 */ 1956 public boolean isInferred() { 1957 return confidence < 1.0; 1958 } 1959 1960 @Override 1961 public String toString() { 1962 StringBuilder sb = new StringBuilder(); 1963 sb.append(exposedName); 1964 if (sourceNamespace != null) { 1965 sb.append(" from ").append(sourceNamespace.getDisplayName()); 1966 } 1967 if (confidence < 1.0) { 1968 sb.append(String.format(" (confidence: %.2f)", confidence)); 1969 } 1970 return sb.toString(); 1971 } 1972 1973 /** 1974 * Creates a copy with updated confidence and evidence. 1975 * Used when merging or updating inference results. 1976 * 1977 * @deprecated Use {@link #withEvidence(ResolutionEvidence)} instead 1978 */ 1979 public ColumnSource withConfidence(double newConfidence, String newEvidence) { 1980 return new ColumnSource( 1981 this.sourceNamespace, 1982 this.exposedName, 1983 this.definitionNode, 1984 newConfidence, 1985 newEvidence, 1986 this.overrideTable, 1987 this.candidateTables != null ? new java.util.ArrayList<>(this.candidateTables) : null, 1988 null, // will create from legacy evidence 1989 this.fieldPath 1990 ); 1991 } 1992 1993 /** 1994 * Creates a copy with updated ResolutionEvidence. 1995 * This is the preferred method for updating evidence in new code. 1996 * 1997 * @param newEvidence The new evidence detail 1998 * @return A new ColumnSource with updated evidence 1999 */ 2000 public ColumnSource withEvidence(ResolutionEvidence newEvidence) { 2001 return new ColumnSource( 2002 this.sourceNamespace, 2003 this.exposedName, 2004 this.definitionNode, 2005 newEvidence != null ? newEvidence.getWeight() : this.confidence, 2006 newEvidence != null ? newEvidence.toLegacyEvidence() : this.evidence, 2007 this.overrideTable, 2008 this.candidateTables != null ? new java.util.ArrayList<>(this.candidateTables) : null, 2009 newEvidence, 2010 this.fieldPath 2011 ); 2012 } 2013 2014 /** 2015 * Creates a copy with candidate tables. 2016 * Used when a column could come from multiple tables. 2017 */ 2018 public ColumnSource withCandidateTables(List<TTable> candidates) { 2019 return new ColumnSource( 2020 this.sourceNamespace, 2021 this.exposedName, 2022 this.definitionNode, 2023 this.confidence, 2024 this.evidence, 2025 this.overrideTable, 2026 candidates != null ? new java.util.ArrayList<>(candidates) : null, 2027 this.evidenceDetail, 2028 this.fieldPath 2029 ); 2030 } 2031 2032 /** 2033 * Creates a copy with a field path for deep/record field access. 2034 * 2035 * <p>This method is used when resolving struct/record field access patterns 2036 * like {@code customer.address.city}. The base column is preserved as the 2037 * exposedName, and the field path captures the remaining segments.</p> 2038 * 2039 * @param newFieldPath The field path segments (beyond the base column) 2040 * @return A new ColumnSource with the field path set 2041 */ 2042 public ColumnSource withFieldPath(FieldPath newFieldPath) { 2043 return new ColumnSource( 2044 this.sourceNamespace, 2045 this.exposedName, 2046 this.definitionNode, 2047 this.confidence, 2048 this.evidence, 2049 this.overrideTable, 2050 this.candidateTables != null ? new java.util.ArrayList<>(this.candidateTables) : null, 2051 this.evidenceDetail, 2052 newFieldPath 2053 ); 2054 } 2055 2056 /** 2057 * Creates a copy with a field path from a list of segments. 2058 * 2059 * <p>Convenience method for creating a ColumnSource with a field path 2060 * from a list of string segments.</p> 2061 * 2062 * @param segments The field path segments 2063 * @return A new ColumnSource with the field path set 2064 */ 2065 public ColumnSource withFieldPath(List<String> segments) { 2066 return withFieldPath(FieldPath.of(segments)); 2067 } 2068 2069 /** 2070 * Creates a copy with field path and updated evidence. 2071 * 2072 * <p>This method is used when resolving struct field access, combining 2073 * both the field path and the struct_field_access evidence marker.</p> 2074 * 2075 * @param newFieldPath The field path segments 2076 * @param newEvidence The evidence string (e.g., "struct_field_access") 2077 * @return A new ColumnSource with field path and evidence updated 2078 */ 2079 public ColumnSource withFieldPath(FieldPath newFieldPath, String newEvidence) { 2080 return new ColumnSource( 2081 this.sourceNamespace, 2082 this.exposedName, 2083 this.definitionNode, 2084 this.confidence, 2085 newEvidence, 2086 this.overrideTable, 2087 this.candidateTables != null ? new java.util.ArrayList<>(this.candidateTables) : null, 2088 null, // will create from legacy evidence 2089 newFieldPath 2090 ); 2091 } 2092 2093 /** 2094 * Count the number of "real" tables in a table list, excluding implicit lateral derived tables. 2095 * 2096 * <p>Teradata supports implicit lateral derived tables, which are auto-added when a column 2097 * references an undeclared table in the WHERE clause. These should not be counted when 2098 * determining if a subquery has multiple tables for column resolution purposes.</p> 2099 * 2100 * @param tables The table list to count 2101 * @return The number of real (non-implicit) tables 2102 */ 2103 private static int countRealTables(gudusoft.gsqlparser.nodes.TTableList tables) { 2104 if (tables == null) { 2105 return 0; 2106 } 2107 int count = 0; 2108 for (int i = 0; i < tables.size(); i++) { 2109 TTable table = tables.getTable(i); 2110 if (table != null && table.getEffectType() != gudusoft.gsqlparser.ETableEffectType.tetImplicitLateralDerivedTable) { 2111 count++; 2112 } 2113 } 2114 return count; 2115 } 2116 2117 /** 2118 * Check if a SELECT statement has a qualified star column (e.g., ta.*, tb.*). 2119 * Qualified stars identify which table columns come from in multi-table subqueries. 2120 */ 2121 private static boolean hasQualifiedStar(gudusoft.gsqlparser.stmt.TSelectSqlStatement select) { 2122 if (select == null || select.getResultColumnList() == null) { 2123 return false; 2124 } 2125 gudusoft.gsqlparser.nodes.TResultColumnList resultCols = select.getResultColumnList(); 2126 for (int i = 0; i < resultCols.size(); i++) { 2127 TResultColumn rc = resultCols.getResultColumn(i); 2128 if (rc != null) { 2129 String colStr = rc.toString().trim(); 2130 // Qualified star has format "alias.*" or "table.*" 2131 if (colStr.endsWith("*") && colStr.contains(".")) { 2132 return true; 2133 } 2134 } 2135 } 2136 return false; 2137 } 2138 2139 /** 2140 * Check if a column exists in a table's DDL definition. 2141 * 2142 * <p>This method checks the table's column definitions (from CREATE TABLE statements 2143 * parsed in the same script) to verify if the column name is defined.</p> 2144 * 2145 * @param table The table to check 2146 * @param columnName The column name to look for 2147 * @return true if the column exists in the table's DDL, false if not found or no DDL available 2148 */ 2149 public static boolean isColumnInTableDdl(TTable table, String columnName) { 2150 if (table == null || columnName == null || columnName.isEmpty()) { 2151 return false; 2152 } 2153 2154 // Check if the table has column definitions (from CREATE TABLE DDL) 2155 gudusoft.gsqlparser.nodes.TColumnDefinitionList columnDefs = table.getColumnDefinitions(); 2156 if (columnDefs != null && columnDefs.size() > 0) { 2157 // S2: route compare through IdentifierService so quoted-sensitive 2158 // dialects (Oracle quoted, BigQuery tables) and BigQuery's 2159 // case-insensitive columns are handled per-vendor. 2160 EDbVendor vendor = table.dbvendor != null ? table.dbvendor : EDbVendor.dbvgeneric; 2161 for (int i = 0; i < columnDefs.size(); i++) { 2162 gudusoft.gsqlparser.nodes.TColumnDefinition colDef = columnDefs.getColumn(i); 2163 if (colDef != null && colDef.getColumnName() != null) { 2164 String defColName = colDef.getColumnName().toString(); 2165 if (defColName != null 2166 && IdentifierService.areEqualStatic(vendor, ESQLDataObjectType.dotColumn, defColName, columnName)) { 2167 return true; 2168 } 2169 } 2170 } 2171 // DDL exists but column not found 2172 return false; 2173 } 2174 2175 // No DDL available - return false (cannot verify) 2176 return false; 2177 } 2178 2179 /** 2180 * Check if a table has DDL metadata available (from CREATE TABLE in same script). 2181 * 2182 * @param table The table to check 2183 * @return true if DDL metadata is available for this table 2184 */ 2185 public static boolean hasTableDdl(TTable table) { 2186 if (table == null) { 2187 return false; 2188 } 2189 gudusoft.gsqlparser.nodes.TColumnDefinitionList columnDefs = table.getColumnDefinitions(); 2190 return columnDefs != null && columnDefs.size() > 0; 2191 } 2192 2193 /** 2194 * Check DDL verification status for a candidate table. 2195 * 2196 * <p>Returns a tri-state result:</p> 2197 * <ul> 2198 * <li>1 = Column exists in table's DDL</li> 2199 * <li>0 = Column NOT found in table's DDL (DDL available but column missing)</li> 2200 * <li>-1 = Cannot verify (no DDL available for this table)</li> 2201 * </ul> 2202 * 2203 * @param table The candidate table to check 2204 * @param columnName The column name to verify 2205 * @return DDL verification status: 1 (exists), 0 (not found), -1 (no DDL) 2206 */ 2207 public static int getDdlVerificationStatus(TTable table, String columnName) { 2208 if (table == null || columnName == null) { 2209 return -1; 2210 } 2211 2212 gudusoft.gsqlparser.nodes.TColumnDefinitionList columnDefs = table.getColumnDefinitions(); 2213 if (columnDefs == null || columnDefs.size() == 0) { 2214 return -1; // No DDL available 2215 } 2216 2217 // DDL available - check if column exists (S2: vendor-aware compare) 2218 EDbVendor vendor = table.dbvendor != null ? table.dbvendor : EDbVendor.dbvgeneric; 2219 for (int i = 0; i < columnDefs.size(); i++) { 2220 gudusoft.gsqlparser.nodes.TColumnDefinition colDef = columnDefs.getColumn(i); 2221 if (colDef != null && colDef.getColumnName() != null) { 2222 String defColName = colDef.getColumnName().toString(); 2223 if (defColName != null 2224 && IdentifierService.areEqualStatic(vendor, ESQLDataObjectType.dotColumn, defColName, columnName)) { 2225 return 1; // Column exists in DDL 2226 } 2227 } 2228 } 2229 2230 return 0; // DDL exists but column not found 2231 } 2232 2233 /** 2234 * Get DDL verification status for all candidate tables. 2235 * 2236 * <p>Returns a map from each candidate table to its DDL verification status:</p> 2237 * <ul> 2238 * <li>1 = Column exists in table's DDL</li> 2239 * <li>0 = Column NOT found in table's DDL</li> 2240 * <li>-1 = Cannot verify (no DDL available)</li> 2241 * </ul> 2242 * 2243 * @return Map of candidate tables to their DDL verification status, or empty map if no candidates 2244 */ 2245 public java.util.Map<TTable, Integer> getCandidateTableDdlStatus() { 2246 java.util.Map<TTable, Integer> result = new java.util.LinkedHashMap<>(); 2247 if (candidateTables == null || candidateTables.isEmpty() || exposedName == null) { 2248 return result; 2249 } 2250 2251 for (TTable candidate : candidateTables) { 2252 int status = getDdlVerificationStatus(candidate, exposedName); 2253 result.put(candidate, status); 2254 } 2255 return result; 2256 } 2257}