001package gudusoft.gsqlparser.resolver2.model; 002 003import gudusoft.gsqlparser.nodes.TParseTreeNode; 004import gudusoft.gsqlparser.resolver2.inference.EvidenceType; 005 006/** 007 * Unified evidence model for column resolution. 008 * 009 * <p>This class consolidates the scattered evidence/confidence information 010 * from various resolver components into a single, consistent structure. 011 * It provides: 012 * <ul> 013 * <li>Structured evidence type (enum, not string magic values)</li> 014 * <li>Confidence weight with clear semantics</li> 015 * <li>Traceability through source location and node</li> 016 * <li>Human-readable messages for debugging</li> 017 * </ul> 018 * 019 * <h3>Confidence Score Semantics</h3> 020 * The confidence score represents "how verifiable is this resolution": 021 * <ul> 022 * <li><b>1.0 (Definite)</b>: Hard evidence - DDL/metadata/explicit definition</li> 023 * <li><b>~0.9 (High-confidence)</b>: Qualified reference without metadata verification</li> 024 * <li><b>~0.7 (Inferred)</b>: Star column inference, CTE reverse fill</li> 025 * <li><b>~0.5 (Weak)</b>: Context-based guess, should not auto-resolve</li> 026 * </ul> 027 * 028 * @see EvidenceType 029 * @see ColumnSource 030 */ 031public class ResolutionEvidence { 032 033 /** Type of evidence */ 034 private final EvidenceType type; 035 036 /** Confidence weight [0.0, 1.0] */ 037 private final double weight; 038 039 /** Human-readable description of how evidence was obtained */ 040 private final String message; 041 042 /** Location in source where this evidence originates */ 043 private final SourceLocation location; 044 045 /** The AST node that provides this evidence (optional, for debugging) */ 046 private final TParseTreeNode sourceNode; 047 048 /** 049 * Create a new ResolutionEvidence with all fields. 050 * 051 * @param type The evidence type 052 * @param weight Confidence weight [0.0, 1.0] 053 * @param message Human-readable description 054 * @param location Source location 055 * @param sourceNode The AST node (optional) 056 */ 057 public ResolutionEvidence(EvidenceType type, double weight, String message, 058 SourceLocation location, TParseTreeNode sourceNode) { 059 this.type = type != null ? type : EvidenceType.INFERRED_FROM_USAGE; 060 this.weight = Math.max(0.0, Math.min(1.0, weight)); 061 this.message = message; 062 this.location = location; 063 this.sourceNode = sourceNode; 064 } 065 066 /** 067 * Create a ResolutionEvidence without source node. 068 */ 069 public ResolutionEvidence(EvidenceType type, double weight, String message, SourceLocation location) { 070 this(type, weight, message, location, null); 071 } 072 073 /** 074 * Create a ResolutionEvidence from an AST node. 075 */ 076 public ResolutionEvidence(EvidenceType type, double weight, String message, TParseTreeNode node) { 077 this(type, weight, message, node != null ? new SourceLocation(node) : null, node); 078 } 079 080 /** 081 * Create a simple ResolutionEvidence with just type and weight. 082 */ 083 public ResolutionEvidence(EvidenceType type, double weight) { 084 this(type, weight, null, (SourceLocation) null, null); 085 } 086 087 // ========== Factory Methods for Common Evidence Types ========== 088 089 /** 090 * Create evidence for metadata-backed resolution (DDL, SQLEnv). 091 * Confidence: 1.0 (definite) 092 */ 093 public static ResolutionEvidence fromMetadata(String tableName, String columnName, TParseTreeNode node) { 094 return new ResolutionEvidence( 095 EvidenceType.METADATA, 096 1.0, 097 "Column '" + columnName + "' found in metadata for table '" + tableName + "'", 098 node 099 ); 100 } 101 102 /** 103 * Create evidence for DDL-defined column (CREATE TABLE in same script). 104 * Confidence: 1.0 (definite) 105 */ 106 public static ResolutionEvidence fromDDL(String tableName, String columnName, TParseTreeNode node) { 107 return new ResolutionEvidence( 108 EvidenceType.METADATA, 109 1.0, 110 "Column '" + columnName + "' defined in DDL for table '" + tableName + "'", 111 node 112 ); 113 } 114 115 /** 116 * Create evidence for qualified reference (table.column). 117 * Confidence: 0.95 if we have metadata, 0.9 otherwise 118 */ 119 public static ResolutionEvidence fromQualifiedReference(String tableName, String columnName, 120 boolean hasMetadata, TParseTreeNode node) { 121 double confidence = hasMetadata ? 0.95 : 0.9; 122 return new ResolutionEvidence( 123 EvidenceType.QUALIFIED_REFERENCE, 124 confidence, 125 "Qualified reference: " + tableName + "." + columnName, 126 node 127 ); 128 } 129 130 /** 131 * Create evidence for unqualified reference. 132 * Confidence depends on number of tables in scope. 133 */ 134 public static ResolutionEvidence fromUnqualifiedReference(String columnName, int tableCount, 135 TParseTreeNode node) { 136 double confidence = tableCount == 1 ? 0.8 : 0.5; 137 return new ResolutionEvidence( 138 EvidenceType.UNQUALIFIED_REFERENCE, 139 confidence, 140 "Unqualified reference: " + columnName + " (" + tableCount + " tables in scope)", 141 node 142 ); 143 } 144 145 /** 146 * Create evidence for CTE explicit column list. 147 * Confidence: 1.0 (explicit definition) 148 */ 149 public static ResolutionEvidence fromCTEExplicitColumn(String cteName, String columnName, 150 TParseTreeNode node) { 151 return new ResolutionEvidence( 152 EvidenceType.CTE_COLUMN_LIST, 153 1.0, 154 "Explicit CTE column: " + cteName + "(" + columnName + ")", 155 node 156 ); 157 } 158 159 /** 160 * Create evidence for CTE implicit column (from subquery). 161 * Confidence: 1.0 (defined in subquery) 162 */ 163 public static ResolutionEvidence fromCTEImplicitColumn(String cteName, String columnName, 164 TParseTreeNode node) { 165 return new ResolutionEvidence( 166 EvidenceType.SUBQUERY_RESULT, 167 1.0, 168 "CTE implicit column from subquery: " + cteName + "." + columnName, 169 node 170 ); 171 } 172 173 /** 174 * Create evidence for star column inference (outer reference pushdown). 175 * Confidence: 0.7-0.9 depending on context 176 */ 177 public static ResolutionEvidence fromStarInference(String columnName, String namespaceName, 178 boolean isQualified, TParseTreeNode node) { 179 double confidence = isQualified ? 0.9 : 0.7; 180 return new ResolutionEvidence( 181 EvidenceType.INFERRED_FROM_USAGE, 182 confidence, 183 "Inferred from outer reference to star namespace: " + namespaceName + "." + columnName, 184 node 185 ); 186 } 187 188 /** 189 * Create evidence for JOIN condition. 190 * Confidence: 0.9 (explicit relationship) 191 */ 192 public static ResolutionEvidence fromJoinCondition(String tableName, String columnName, 193 TParseTreeNode node) { 194 return new ResolutionEvidence( 195 EvidenceType.JOIN_CONDITION, 196 0.9, 197 "JOIN condition: " + tableName + "." + columnName, 198 node 199 ); 200 } 201 202 /** 203 * Create evidence for INSERT column list. 204 * Confidence: 1.0 (explicit definition) 205 */ 206 public static ResolutionEvidence fromInsertColumnList(String tableName, String columnName, 207 TParseTreeNode node) { 208 return new ResolutionEvidence( 209 EvidenceType.INSERT_COLUMN_LIST, 210 1.0, 211 "INSERT column: " + tableName + "." + columnName, 212 node 213 ); 214 } 215 216 /** 217 * Create evidence for UPDATE SET clause. 218 * Confidence: 0.95 (explicit assignment) 219 */ 220 public static ResolutionEvidence fromUpdateSet(String tableName, String columnName, 221 TParseTreeNode node) { 222 return new ResolutionEvidence( 223 EvidenceType.UPDATE_SET, 224 0.95, 225 "UPDATE SET: " + tableName + "." + columnName, 226 node 227 ); 228 } 229 230 /** 231 * Create evidence for SELECT list. 232 * Confidence: 0.85 233 */ 234 public static ResolutionEvidence fromSelectList(String tableName, String columnName, 235 TParseTreeNode node) { 236 return new ResolutionEvidence( 237 EvidenceType.SELECT_LIST, 238 0.85, 239 "SELECT list: " + tableName + "." + columnName, 240 node 241 ); 242 } 243 244 /** 245 * Create evidence for struct field access (BigQuery/Snowflake). 246 * Confidence: inherits from base column resolution 247 */ 248 public static ResolutionEvidence fromStructFieldAccess(String baseColumn, String fieldPath, 249 double baseConfidence, TParseTreeNode node) { 250 return new ResolutionEvidence( 251 EvidenceType.INFERRED_FROM_USAGE, 252 baseConfidence, 253 "Struct field access: " + baseColumn + "." + fieldPath, 254 node 255 ); 256 } 257 258 /** 259 * Create evidence for guess strategy application. 260 * Confidence: 0.7 (guessed from candidates) 261 */ 262 public static ResolutionEvidence fromGuessStrategy(String columnName, String chosenTable, 263 String strategy, TParseTreeNode node) { 264 return new ResolutionEvidence( 265 EvidenceType.INFERRED_FROM_USAGE, 266 0.7, 267 "Guessed by " + strategy + " strategy: " + chosenTable + "." + columnName, 268 node 269 ); 270 } 271 272 /** 273 * Create evidence from legacy string evidence. 274 * This is for backward compatibility during migration. 275 * 276 * @deprecated Use specific factory methods instead 277 */ 278 @Deprecated 279 public static ResolutionEvidence fromLegacyEvidence(String legacyEvidence, double confidence, 280 TParseTreeNode node) { 281 EvidenceType type = inferTypeFromLegacyEvidence(legacyEvidence); 282 return new ResolutionEvidence(type, confidence, legacyEvidence, node); 283 } 284 285 /** 286 * Infer EvidenceType from legacy string evidence. 287 */ 288 private static EvidenceType inferTypeFromLegacyEvidence(String evidence) { 289 if (evidence == null) { 290 return EvidenceType.INFERRED_FROM_USAGE; 291 } 292 293 String lower = evidence.toLowerCase(); 294 295 if (lower.contains("metadata") || lower.contains("ddl") || lower.contains("sqlenv")) { 296 return EvidenceType.METADATA; 297 } 298 if (lower.contains("qualified") || lower.contains("table.column")) { 299 return EvidenceType.QUALIFIED_REFERENCE; 300 } 301 if (lower.contains("cte_explicit")) { 302 return EvidenceType.CTE_COLUMN_LIST; 303 } 304 if (lower.contains("cte_implicit") || lower.contains("subquery")) { 305 return EvidenceType.SUBQUERY_RESULT; 306 } 307 if (lower.contains("join")) { 308 return EvidenceType.JOIN_CONDITION; 309 } 310 if (lower.contains("insert")) { 311 return EvidenceType.INSERT_COLUMN_LIST; 312 } 313 if (lower.contains("update") || lower.contains("set")) { 314 return EvidenceType.UPDATE_SET; 315 } 316 if (lower.contains("select")) { 317 return EvidenceType.SELECT_LIST; 318 } 319 if (lower.contains("outer_reference") || lower.contains("star") || lower.contains("inferred")) { 320 return EvidenceType.INFERRED_FROM_USAGE; 321 } 322 323 return EvidenceType.UNQUALIFIED_REFERENCE; 324 } 325 326 // ========== Getters ========== 327 328 public EvidenceType getType() { 329 return type; 330 } 331 332 public double getWeight() { 333 return weight; 334 } 335 336 public String getMessage() { 337 return message; 338 } 339 340 public SourceLocation getLocation() { 341 return location; 342 } 343 344 public TParseTreeNode getSourceNode() { 345 return sourceNode; 346 } 347 348 // ========== Utility Methods ========== 349 350 /** 351 * Check if this evidence represents a definite resolution. 352 * Definite means confidence >= 1.0 (has hard evidence like DDL/metadata). 353 */ 354 public boolean isDefinite() { 355 return weight >= 1.0; 356 } 357 358 /** 359 * Check if this evidence represents a high-confidence resolution. 360 * High-confidence means confidence >= 0.9. 361 */ 362 public boolean isHighConfidence() { 363 return weight >= 0.9; 364 } 365 366 /** 367 * Check if this evidence represents an inferred resolution. 368 * Inferred means confidence < 1.0 or type is INFERRED_FROM_USAGE. 369 */ 370 public boolean isInferred() { 371 return weight < 1.0 || type == EvidenceType.INFERRED_FROM_USAGE; 372 } 373 374 /** 375 * Create a copy with updated weight. 376 */ 377 public ResolutionEvidence withWeight(double newWeight) { 378 return new ResolutionEvidence(type, newWeight, message, location, sourceNode); 379 } 380 381 /** 382 * Create a copy with appended message. 383 */ 384 public ResolutionEvidence withAppendedMessage(String additionalMessage) { 385 String newMessage = message != null 386 ? message + "; " + additionalMessage 387 : additionalMessage; 388 return new ResolutionEvidence(type, weight, newMessage, location, sourceNode); 389 } 390 391 /** 392 * Convert to legacy string evidence format. 393 * For backward compatibility with existing code. 394 */ 395 public String toLegacyEvidence() { 396 // Map type to legacy string 397 switch (type) { 398 case METADATA: 399 if (message != null && message.toLowerCase().contains("ddl")) { 400 return "ddl_metadata"; 401 } 402 return "metadata"; 403 case QUALIFIED_REFERENCE: 404 return "qualified_reference"; 405 case UNQUALIFIED_REFERENCE: 406 return "unqualified_reference"; 407 case CTE_COLUMN_LIST: 408 return "cte_explicit_column"; 409 case SUBQUERY_RESULT: 410 return "cte_implicit_column"; 411 case JOIN_CONDITION: 412 return "join_condition"; 413 case INSERT_COLUMN_LIST: 414 return "insert_column_list"; 415 case UPDATE_SET: 416 return "update_set"; 417 case SELECT_LIST: 418 return "select_list"; 419 case INFERRED_FROM_USAGE: 420 if (message != null) { 421 if (message.contains("outer_reference") || message.contains("outer reference")) { 422 return "outer_reference"; 423 } 424 if (message.contains("star")) { 425 return "star_push_down"; 426 } 427 if (message.contains("guess") || message.contains("strategy")) { 428 if (message.contains("nearest")) return "guess_strategy_nearest"; 429 if (message.contains("farthest")) return "guess_strategy_farthest"; 430 } 431 if (message.contains("struct_field") || message.contains("Struct field")) { 432 return "struct_field_access"; 433 } 434 } 435 return "inferred_from_usage"; 436 default: 437 return message != null ? message : "unknown"; 438 } 439 } 440 441 @Override 442 public String toString() { 443 StringBuilder sb = new StringBuilder(); 444 sb.append("ResolutionEvidence{type=").append(type); 445 sb.append(", weight=").append(String.format("%.2f", weight)); 446 if (message != null) { 447 sb.append(", msg='").append(message).append("'"); 448 } 449 if (location != null) { 450 sb.append(", loc=").append(location); 451 } 452 sb.append("}"); 453 return sb.toString(); 454 } 455 456 @Override 457 public boolean equals(Object obj) { 458 if (this == obj) return true; 459 if (!(obj instanceof ResolutionEvidence)) return false; 460 461 ResolutionEvidence other = (ResolutionEvidence) obj; 462 return type == other.type && 463 Double.compare(weight, other.weight) == 0 && 464 (message == null ? other.message == null : message.equals(other.message)); 465 } 466 467 @Override 468 public int hashCode() { 469 int result = type.hashCode(); 470 long temp = Double.doubleToLongBits(weight); 471 result = 31 * result + (int) (temp ^ (temp >>> 32)); 472 result = 31 * result + (message != null ? message.hashCode() : 0); 473 return result; 474 } 475}