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