001package gudusoft.gsqlparser.resolver2; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.TBaseType; 005import gudusoft.gsqlparser.resolver2.binding.BindingDiagnosticCode; 006import gudusoft.gsqlparser.resolver2.binding.BindingDiagnosticSeverity; 007import gudusoft.gsqlparser.resolver2.format.DisplayNameMode; 008import gudusoft.gsqlparser.resolver2.format.DisplayNamePolicy; 009import gudusoft.gsqlparser.resolver2.matcher.DefaultNameMatcher; 010import gudusoft.gsqlparser.resolver2.matcher.INameMatcher; 011import gudusoft.gsqlparser.resolver2.matcher.VendorNameMatcher; 012import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType; 013import gudusoft.gsqlparser.sqlenv.IdentifierProfile; 014import gudusoft.gsqlparser.sqlenv.IdentifierService; 015 016import java.util.EnumMap; 017import java.util.Map; 018 019/** 020 * Configuration for TSQLResolver2. 021 * Controls various aspects of name resolution behavior. 022 */ 023public class TSQLResolverConfig { 024 025 /** Name matcher for case sensitivity and matching rules */ 026 private INameMatcher nameMatcher = new DefaultNameMatcher(); 027 028 /** Database vendor for vendor-specific name matching */ 029 private EDbVendor vendor = null; 030 031 /** Whether to enable legacy compatibility mode (sync results to TTable.linkedColumns) */ 032 private boolean legacyCompatibilityEnabled = true; 033 034 /** 035 * Minimum confidence threshold for syncing to legacy structures. 036 * 037 * Recommended values: 038 * - 1.0 (default): Only sync definite results (safest, for SQL validation) 039 * - 0.7: Include high-confidence inferences (for data lineage analysis) 040 * - 0.5: Include all inferences (may mislead legacy code, not recommended) 041 * 042 * Use cases: 043 * - Data lineage tools may want 0.7 inference results 044 * - SQL formatters only need 1.0 definite results 045 */ 046 private double legacySyncMinConfidence = 1.0; 047 048 /** Maximum iterations for iterative resolution */ 049 private int maxIterations = 10; 050 051 /** Minimum progress rate to continue iteration (0.0 - 1.0) */ 052 private double minProgressRate = 0.01; // 1% 053 054 /** Number of stable passes required to declare convergence */ 055 private int stablePassesForConvergence = 2; 056 057 /** Whether to collect full candidates for ambiguous columns */ 058 private boolean collectFullCandidates = true; 059 060 /** Whether to enable legacy evidence collection (deprecated, use NamespaceEnhancer) */ 061 private boolean evidenceCollectionEnabled = false; 062 063 /** 064 * Whether to show datatype information for columns from CREATE TABLE statements. 065 * When enabled, columns from CREATE TABLE will include datatype info in format: 066 * columnName:datatypeName:length or columnName:datatypeName:precision:scale 067 */ 068 private boolean showDatatype = false; 069 070 /** 071 * Whether to show CTE (Common Table Expression) tables and their columns in output. 072 * When enabled, CTE tables are included in the tables list and CTE columns 073 * are included in the fields list with "(CTE)" suffix. 074 * Default is false for backward compatibility. 075 */ 076 private boolean showCTE = false; 077 078 // ========== DISPLAY NAME Configuration ========== 079 // Controls how identifier names are rendered for output 080 081 /** 082 * Display name mode - controls how identifiers are rendered. 083 * <ul> 084 * <li>DISPLAY: Strip delimiters, preserve original case (default, recommended for debugging)</li> 085 * <li>SQL_RENDER: Preserve delimiters for valid SQL regeneration</li> 086 * <li>CANONICAL: Apply vendor-specific case folding</li> 087 * </ul> 088 */ 089 private DisplayNameMode displayNameMode = DisplayNameMode.DISPLAY; 090 091 /** 092 * Display name policy - controls which occurrence to use when same object 093 * appears multiple times with different spellings. 094 * <ul> 095 * <li>PREFER_DEFINITION_SITE: Use spelling from definition (CTE def, CREATE TABLE, etc.)</li> 096 * <li>PREFER_FIRST_OCCURRENCE: Use first occurrence in SQL text</li> 097 * <li>PREFER_METADATA: Use spelling from database metadata</li> 098 * </ul> 099 */ 100 private DisplayNamePolicy displayNamePolicy = DisplayNamePolicy.PREFER_DEFINITION_SITE; 101 102 /** 103 * Whether to strip delimiters (quotes, backticks, brackets) from identifier display. 104 * Only applies when displayNameMode is DISPLAY. 105 * Default is true. 106 */ 107 private boolean stripDelimitersForDisplay = true; 108 109 // ========== GUESS_COLUMN_STRATEGY Configuration ========== 110 // Strategy for handling ambiguous columns (when a column could belong to multiple tables) 111 112 /** Pick the first candidate table (nearest in FROM clause order) */ 113 public static final int GUESS_COLUMN_STRATEGY_NEAREST = TBaseType.GUESS_COLUMN_STRATEGY_NEAREST; 114 115 /** Pick the last candidate table (farthest in FROM clause order) */ 116 public static final int GUESS_COLUMN_STRATEGY_FARTHEST = TBaseType.GUESS_COLUMN_STRATEGY_FARTHEST; 117 118 /** Do not pick any candidate, leave as unresolved/ambiguous */ 119 public static final int GUESS_COLUMN_STRATEGY_NOT_PICKUP = TBaseType.GUESS_COLUMN_STRATEGY_NOT_PICKUP; 120 121 /** Human-readable names for strategy values */ 122 public static final String[] GUESS_COLUMN_STRATEGY_NAMES = TBaseType.GUESS_COLUMN_STRATEGY_MSG; 123 124 /** 125 * Strategy for handling ambiguous columns. 126 * Default: reads from TBaseType.GUESS_COLUMN_STRATEGY for backward compatibility. 127 * Can be overridden per-config instance. 128 */ 129 private Integer guessColumnStrategy = null; // null means use TBaseType default 130 131 // ========== CONFIDENCE THRESHOLD Configuration ========== 132 // Controls when resolutions are considered "definite" vs "inferred" and when guessing is allowed 133 134 /** 135 * Minimum confidence threshold for a resolution to be considered "definite". 136 * 137 * <p>Resolutions with confidence >= this threshold are treated as having 138 * strong evidence (e.g., DDL metadata, qualified references). Resolutions 139 * below this threshold are considered "inferred" or "uncertain".</p> 140 * 141 * <p>Recommended values:</p> 142 * <ul> 143 * <li>0.9 (default): High bar - only high-confidence resolutions are definite</li> 144 * <li>0.7: Include more inferred resolutions as definite</li> 145 * <li>1.0: Only metadata-backed resolutions are definite</li> 146 * </ul> 147 */ 148 private double minDefiniteConfidence = 0.9; 149 150 /** 151 * Minimum confidence threshold to allow guessing from ambiguous candidates. 152 * 153 * <p>When multiple candidates exist and GUESS_COLUMN_STRATEGY is not NOT_PICKUP, 154 * at least one candidate must have confidence >= this threshold for guessing 155 * to be allowed. If all candidates are below this threshold, the column 156 * remains AMBIGUOUS regardless of strategy.</p> 157 * 158 * <p>Recommended values:</p> 159 * <ul> 160 * <li>0.95 (default): Very high bar - only guess with strong evidence</li> 161 * <li>0.9: Allow guessing with high-confidence candidates</li> 162 * <li>0.7: Allow guessing with inferred candidates (not recommended)</li> 163 * </ul> 164 */ 165 private double minConfidenceToGuess = 0.95; 166 167 /** 168 * Whether to allow guessing when all candidates are inferred (no DDL metadata). 169 * 170 * <p>When false (default), if all candidates have confidence below 171 * {@link #minDefiniteConfidence}, the column remains AMBIGUOUS even if 172 * GUESS_COLUMN_STRATEGY would normally pick one. This prevents guessing 173 * based on uncertain evidence.</p> 174 * 175 * <p>When true, guessing is allowed even when all candidates are inferred, 176 * as long as the GUESS_COLUMN_STRATEGY is not NOT_PICKUP. Use with caution 177 * as this may produce misleading lineage results.</p> 178 */ 179 private boolean allowGuessWhenAllInferred = false; 180 181 // ========== Binding Diagnostic Configuration (plan §5.3, S2) ========== 182 183 /** 184 * When {@code true}, the resolver2 binding-diagnostic post-pass runs once 185 * after iterative convergence and populates {@link 186 * gudusoft.gsqlparser.resolver2.binding.BindingResult}. Default {@code 187 * false} — accessors return the {@link 188 * gudusoft.gsqlparser.resolver2.binding.BindingResult#empty()} singleton 189 * with zero parse-time overhead. 190 */ 191 private boolean emitBindingDiagnostics = false; 192 193 /** 194 * When {@code true} (and {@link #emitBindingDiagnostics} is on), strict 195 * catalog mode emits {@code UNKNOWN_TABLE} / {@code UNKNOWN_ALIAS} / 196 * {@code CATALOG_METADATA_UNAVAILABLE}. Default {@code false} so that 197 * tables without authoritative metadata are silent (plan §5.5). 198 */ 199 private boolean bindingStrictCatalogValidation = false; 200 201 /** 202 * When {@code true}, successful {@link 203 * gudusoft.gsqlparser.resolver2.binding.BindingReference} entries are 204 * included in the result. Default {@code false} — IDE/lint consumers opt 205 * in; non-IDE consumers avoid the payload bloat. 206 */ 207 private boolean bindingIncludeSuccessfulReferences = false; 208 209 /** 210 * When {@code true}, the resolver records a 211 * {@link gudusoft.gsqlparser.resolver2.binding.BindingTrace} for every 212 * reference it binds (the dynamic-SQL publication proof's P3 input). 213 * Default {@code false}: no registry is created and the capture hooks are 214 * a single null check — zero overhead and zero behavior change on the 215 * normal path. Observation-only when enabled. 216 */ 217 private boolean captureBindingTrace = false; 218 219 /** 220 * Lazy map of per-code severity overrides. Allocated only when {@link 221 * #setBindingSeverityFor(BindingDiagnosticCode, BindingDiagnosticSeverity)} 222 * is first called so that the default-off path stays allocation-free. 223 */ 224 private EnumMap<BindingDiagnosticCode, BindingDiagnosticSeverity> bindingSeverityOverrides; 225 226 public TSQLResolverConfig() { 227 // Default configuration 228 } 229 230 public INameMatcher getNameMatcher() { 231 return nameMatcher; 232 } 233 234 public void setNameMatcher(INameMatcher nameMatcher) { 235 if (nameMatcher == null) { 236 throw new IllegalArgumentException("Name matcher cannot be null"); 237 } 238 this.nameMatcher = nameMatcher; 239 } 240 241 public boolean isLegacyCompatibilityEnabled() { 242 return legacyCompatibilityEnabled; 243 } 244 245 public void setLegacyCompatibilityEnabled(boolean enabled) { 246 this.legacyCompatibilityEnabled = enabled; 247 } 248 249 public double getLegacySyncMinConfidence() { 250 return legacySyncMinConfidence; 251 } 252 253 public void setLegacySyncMinConfidence(double threshold) { 254 if (threshold < 0.0 || threshold > 1.0) { 255 throw new IllegalArgumentException("Confidence threshold must be in [0.0, 1.0]"); 256 } 257 this.legacySyncMinConfidence = threshold; 258 } 259 260 public int getMaxIterations() { 261 return maxIterations; 262 } 263 264 public void setMaxIterations(int maxIterations) { 265 if (maxIterations < 1) { 266 throw new IllegalArgumentException("Max iterations must be at least 1"); 267 } 268 this.maxIterations = maxIterations; 269 } 270 271 public double getMinProgressRate() { 272 return minProgressRate; 273 } 274 275 public void setMinProgressRate(double minProgressRate) { 276 if (minProgressRate < 0.0 || minProgressRate > 1.0) { 277 throw new IllegalArgumentException("Progress rate must be in [0.0, 1.0]"); 278 } 279 this.minProgressRate = minProgressRate; 280 } 281 282 public int getStablePassesForConvergence() { 283 return stablePassesForConvergence; 284 } 285 286 public void setStablePassesForConvergence(int stablePasses) { 287 if (stablePasses < 1) { 288 throw new IllegalArgumentException("Stable passes must be at least 1"); 289 } 290 this.stablePassesForConvergence = stablePasses; 291 } 292 293 public boolean isCollectFullCandidates() { 294 return collectFullCandidates; 295 } 296 297 public void setCollectFullCandidates(boolean collectFullCandidates) { 298 this.collectFullCandidates = collectFullCandidates; 299 } 300 301 /** 302 * @deprecated Use NamespaceEnhancer instead 303 */ 304 public boolean isEvidenceCollectionEnabled() { 305 return evidenceCollectionEnabled; 306 } 307 308 /** 309 * @deprecated Use NamespaceEnhancer instead 310 */ 311 public void setEvidenceCollectionEnabled(boolean enabled) { 312 this.evidenceCollectionEnabled = enabled; 313 } 314 315 /** 316 * Check if datatype information should be shown for columns from CREATE TABLE statements. 317 * 318 * @return true if datatype information should be included in column names 319 */ 320 public boolean isShowDatatype() { 321 return showDatatype; 322 } 323 324 /** 325 * Set whether to show datatype information for columns from CREATE TABLE statements. 326 * When enabled, columns from CREATE TABLE will include datatype info in format: 327 * columnName:datatypeName:length or columnName:datatypeName:precision:scale 328 * 329 * @param showDatatype true to include datatype information 330 */ 331 public void setShowDatatype(boolean showDatatype) { 332 this.showDatatype = showDatatype; 333 } 334 335 /** 336 * Check if CTE (Common Table Expression) tables and columns should be shown in output. 337 * 338 * @return true if CTE tables and columns should be included 339 */ 340 public boolean isShowCTE() { 341 return showCTE; 342 } 343 344 /** 345 * Set whether to show CTE (Common Table Expression) tables and columns in output. 346 * When enabled, CTE tables are included in the tables list and CTE columns 347 * are included in the fields list with "(CTE)" suffix. 348 * 349 * @param showCTE true to include CTE tables and columns 350 */ 351 public void setShowCTE(boolean showCTE) { 352 this.showCTE = showCTE; 353 } 354 355 // ========== Display Name Getters and Setters ========== 356 357 /** 358 * Get the display name mode. 359 * 360 * @return the current display name mode 361 */ 362 public DisplayNameMode getDisplayNameMode() { 363 return displayNameMode; 364 } 365 366 /** 367 * Set the display name mode. 368 * 369 * @param mode the display name mode 370 */ 371 public void setDisplayNameMode(DisplayNameMode mode) { 372 this.displayNameMode = mode != null ? mode : DisplayNameMode.DISPLAY; 373 } 374 375 /** 376 * Get the display name policy. 377 * 378 * @return the current display name policy 379 */ 380 public DisplayNamePolicy getDisplayNamePolicy() { 381 return displayNamePolicy; 382 } 383 384 /** 385 * Set the display name policy. 386 * 387 * @param policy the display name policy 388 */ 389 public void setDisplayNamePolicy(DisplayNamePolicy policy) { 390 this.displayNamePolicy = policy != null ? policy : DisplayNamePolicy.PREFER_DEFINITION_SITE; 391 } 392 393 /** 394 * Check if delimiters should be stripped for display. 395 * 396 * @return true if delimiters should be stripped 397 */ 398 public boolean isStripDelimitersForDisplay() { 399 return stripDelimitersForDisplay; 400 } 401 402 /** 403 * Set whether to strip delimiters for display. 404 * 405 * @param stripDelimiters true to strip delimiters 406 */ 407 public void setStripDelimitersForDisplay(boolean stripDelimiters) { 408 this.stripDelimitersForDisplay = stripDelimiters; 409 } 410 411 /** 412 * Get the strategy for handling ambiguous columns. 413 * Returns the configured value, or TBaseType.GUESS_COLUMN_STRATEGY if not set. 414 * 415 * @return One of GUESS_COLUMN_STRATEGY_NEAREST, GUESS_COLUMN_STRATEGY_FARTHEST, 416 * or GUESS_COLUMN_STRATEGY_NOT_PICKUP 417 */ 418 public int getGuessColumnStrategy() { 419 return guessColumnStrategy != null ? guessColumnStrategy : TBaseType.GUESS_COLUMN_STRATEGY; 420 } 421 422 /** 423 * Set the strategy for handling ambiguous columns. 424 * 425 * @param strategy One of GUESS_COLUMN_STRATEGY_NEAREST, GUESS_COLUMN_STRATEGY_FARTHEST, 426 * or GUESS_COLUMN_STRATEGY_NOT_PICKUP 427 */ 428 public void setGuessColumnStrategy(int strategy) { 429 if (strategy < GUESS_COLUMN_STRATEGY_NEAREST || strategy > GUESS_COLUMN_STRATEGY_NOT_PICKUP) { 430 throw new IllegalArgumentException("Invalid strategy: " + strategy + 431 ". Must be GUESS_COLUMN_STRATEGY_NEAREST (0), GUESS_COLUMN_STRATEGY_FARTHEST (1), " + 432 "or GUESS_COLUMN_STRATEGY_NOT_PICKUP (2)"); 433 } 434 this.guessColumnStrategy = strategy; 435 } 436 437 /** 438 * Check if a custom guess column strategy has been set on this config. 439 * If false, the strategy from TBaseType.GUESS_COLUMN_STRATEGY will be used. 440 * 441 * @return true if a custom strategy is set 442 */ 443 public boolean hasCustomGuessColumnStrategy() { 444 return guessColumnStrategy != null; 445 } 446 447 /** 448 * Clear any custom guess column strategy, reverting to TBaseType.GUESS_COLUMN_STRATEGY. 449 */ 450 public void clearGuessColumnStrategy() { 451 this.guessColumnStrategy = null; 452 } 453 454 /** 455 * Get the human-readable name for the current strategy. 456 * 457 * @return Strategy name (e.g., "GUESS_COLUMN_STRATEGY_NEAREST") 458 */ 459 public String getGuessColumnStrategyName() { 460 int strategy = getGuessColumnStrategy(); 461 if (strategy >= 0 && strategy < GUESS_COLUMN_STRATEGY_NAMES.length) { 462 return GUESS_COLUMN_STRATEGY_NAMES[strategy]; 463 } 464 return "UNKNOWN(" + strategy + ")"; 465 } 466 467 // ========== Confidence Threshold Getters and Setters ========== 468 469 /** 470 * Get the minimum confidence threshold for definite resolutions. 471 * 472 * @return Threshold value [0.0, 1.0] 473 */ 474 public double getMinDefiniteConfidence() { 475 return minDefiniteConfidence; 476 } 477 478 /** 479 * Set the minimum confidence threshold for definite resolutions. 480 * 481 * @param threshold Threshold value [0.0, 1.0] 482 */ 483 public void setMinDefiniteConfidence(double threshold) { 484 if (threshold < 0.0 || threshold > 1.0) { 485 throw new IllegalArgumentException("Confidence threshold must be in [0.0, 1.0]"); 486 } 487 this.minDefiniteConfidence = threshold; 488 } 489 490 /** 491 * Get the minimum confidence threshold to allow guessing. 492 * 493 * @return Threshold value [0.0, 1.0] 494 */ 495 public double getMinConfidenceToGuess() { 496 return minConfidenceToGuess; 497 } 498 499 /** 500 * Set the minimum confidence threshold to allow guessing. 501 * 502 * @param threshold Threshold value [0.0, 1.0] 503 */ 504 public void setMinConfidenceToGuess(double threshold) { 505 if (threshold < 0.0 || threshold > 1.0) { 506 throw new IllegalArgumentException("Confidence threshold must be in [0.0, 1.0]"); 507 } 508 this.minConfidenceToGuess = threshold; 509 } 510 511 /** 512 * Check if guessing is allowed when all candidates are inferred. 513 * 514 * @return true if guessing is allowed with inferred candidates 515 */ 516 public boolean isAllowGuessWhenAllInferred() { 517 return allowGuessWhenAllInferred; 518 } 519 520 /** 521 * Set whether to allow guessing when all candidates are inferred. 522 * 523 * @param allow true to allow guessing with inferred candidates 524 */ 525 public void setAllowGuessWhenAllInferred(boolean allow) { 526 this.allowGuessWhenAllInferred = allow; 527 } 528 529 /** 530 * Check if a confidence value represents a definite resolution. 531 * 532 * @param confidence The confidence value to check 533 * @return true if the confidence is >= minDefiniteConfidence 534 */ 535 public boolean isDefiniteConfidence(double confidence) { 536 return confidence >= minDefiniteConfidence; 537 } 538 539 /** 540 * Check if a confidence value is sufficient to allow guessing. 541 * 542 * @param confidence The confidence value to check 543 * @return true if the confidence is >= minConfidenceToGuess 544 */ 545 public boolean canGuessWithConfidence(double confidence) { 546 return confidence >= minConfidenceToGuess; 547 } 548 549 // ========== Binding Diagnostic Getters and Setters (plan §5.3, S2) ========== 550 551 /** 552 * @return whether the binding-diagnostic post-pass is enabled 553 */ 554 public boolean isEmitBindingDiagnostics() { 555 return emitBindingDiagnostics; 556 } 557 558 /** 559 * Enable or disable the binding-diagnostic post-pass. 560 * 561 * <p>Default {@code false}. When off, the post-pass is not invoked and 562 * accessors return {@link 563 * gudusoft.gsqlparser.resolver2.binding.BindingResult#empty()}.</p> 564 * 565 * @param on whether to emit binding diagnostics 566 * @return this config (fluent) 567 */ 568 public TSQLResolverConfig setEmitBindingDiagnostics(boolean on) { 569 this.emitBindingDiagnostics = on; 570 return this; 571 } 572 573 /** 574 * @return whether strict catalog validation is enabled 575 */ 576 public boolean isBindingStrictCatalogValidation() { 577 return bindingStrictCatalogValidation; 578 } 579 580 /** 581 * Enable or disable strict catalog validation. Default {@code false}. 582 * 583 * @param on whether to enable strict mode 584 * @return this config (fluent) 585 */ 586 public TSQLResolverConfig setBindingStrictCatalogValidation(boolean on) { 587 this.bindingStrictCatalogValidation = on; 588 return this; 589 } 590 591 /** 592 * @return whether successful binding references are included in the 593 * result payload 594 */ 595 public boolean isBindingIncludeSuccessfulReferences() { 596 return bindingIncludeSuccessfulReferences; 597 } 598 599 /** 600 * Enable or disable successful-reference emission. Default {@code false} 601 * (payload bloat for non-IDE consumers). 602 * 603 * @param on whether to include successful references 604 * @return this config (fluent) 605 */ 606 public TSQLResolverConfig setBindingIncludeSuccessfulReferences(boolean on) { 607 this.bindingIncludeSuccessfulReferences = on; 608 return this; 609 } 610 611 /** @see #captureBindingTrace */ 612 public boolean isCaptureBindingTrace() { 613 return captureBindingTrace; 614 } 615 616 /** @see #captureBindingTrace */ 617 public TSQLResolverConfig setCaptureBindingTrace(boolean on) { 618 this.captureBindingTrace = on; 619 return this; 620 } 621 622 /** 623 * Override the severity emitted for a given binding diagnostic code. 624 * 625 * <p>The override map is allocated lazily so callers that never customize 626 * severities pay no allocation cost.</p> 627 * 628 * @param code the diagnostic code (must not be null) 629 * @param severity the severity to emit for this code (must not be null) 630 * @return this config (fluent) 631 * @throws IllegalArgumentException if either argument is null 632 */ 633 public TSQLResolverConfig setBindingSeverityFor(BindingDiagnosticCode code, 634 BindingDiagnosticSeverity severity) { 635 if (code == null) { 636 throw new IllegalArgumentException("BindingDiagnosticCode is required"); 637 } 638 if (severity == null) { 639 throw new IllegalArgumentException("BindingDiagnosticSeverity is required"); 640 } 641 if (bindingSeverityOverrides == null) { 642 bindingSeverityOverrides = new EnumMap<BindingDiagnosticCode, BindingDiagnosticSeverity>( 643 BindingDiagnosticCode.class); 644 } 645 bindingSeverityOverrides.put(code, severity); 646 return this; 647 } 648 649 /** 650 * Resolve the configured severity for a diagnostic code. Returns the 651 * override when one is set, otherwise the code's frozen default 652 * severity (plan §5.4). 653 * 654 * @param code the diagnostic code (must not be null) 655 * @return the configured or default severity for this code 656 */ 657 public BindingDiagnosticSeverity getBindingSeverityFor(BindingDiagnosticCode code) { 658 if (code == null) { 659 throw new IllegalArgumentException("BindingDiagnosticCode is required"); 660 } 661 if (bindingSeverityOverrides != null) { 662 BindingDiagnosticSeverity override = bindingSeverityOverrides.get(code); 663 if (override != null) { 664 return override; 665 } 666 } 667 return code.defaultSeverity(); 668 } 669 670 /** 671 * @return whether any per-code severity override has been configured. 672 * Used to keep the default-off path allocation-free in tests. 673 */ 674 public boolean hasBindingSeverityOverrides() { 675 return bindingSeverityOverrides != null && !bindingSeverityOverrides.isEmpty(); 676 } 677 678 /** 679 * @return an immutable view of the configured severity overrides, or an 680 * empty map when none are configured. Never null. 681 */ 682 public Map<BindingDiagnosticCode, BindingDiagnosticSeverity> getBindingSeverityOverrides() { 683 if (bindingSeverityOverrides == null || bindingSeverityOverrides.isEmpty()) { 684 return java.util.Collections.<BindingDiagnosticCode, BindingDiagnosticSeverity>emptyMap(); 685 } 686 return java.util.Collections.unmodifiableMap(bindingSeverityOverrides); 687 } 688 689 /** 690 * Create a default configuration 691 */ 692 public static TSQLResolverConfig createDefault() { 693 return new TSQLResolverConfig(); 694 } 695 696 /** 697 * Create configuration for case-sensitive matching 698 */ 699 public static TSQLResolverConfig createCaseSensitive() { 700 TSQLResolverConfig config = new TSQLResolverConfig(); 701 config.setNameMatcher(new DefaultNameMatcher(true)); 702 return config; 703 } 704 705 /** 706 * Create configuration for standalone mode (no legacy sync) 707 */ 708 public static TSQLResolverConfig createStandalone() { 709 TSQLResolverConfig config = new TSQLResolverConfig(); 710 config.setLegacyCompatibilityEnabled(false); 711 return config; 712 } 713 714 /** 715 * Create configuration with showDatatype enabled. 716 * This configuration includes datatype information for columns from CREATE TABLE statements. 717 */ 718 public static TSQLResolverConfig createWithDatatype() { 719 TSQLResolverConfig config = new TSQLResolverConfig(); 720 config.setShowDatatype(true); 721 return config; 722 } 723 724 /** 725 * Create configuration with showCTE enabled. 726 * This configuration includes CTE tables and columns in the output. 727 */ 728 public static TSQLResolverConfig createWithCTE() { 729 TSQLResolverConfig config = new TSQLResolverConfig(); 730 config.setShowCTE(true); 731 return config; 732 } 733 734 /** 735 * Create configuration for a specific database vendor. 736 * 737 * <p>This factory method creates a configuration with vendor-specific 738 * name matching rules. The VendorNameMatcher uses IdentifierService 739 * to properly handle case sensitivity and quote handling for each vendor.</p> 740 * 741 * <p>Example vendor behaviors:</p> 742 * <ul> 743 * <li>Oracle: Unquoted identifiers fold to UPPER, quoted are case-sensitive</li> 744 * <li>PostgreSQL: Unquoted identifiers fold to LOWER, quoted are case-sensitive</li> 745 * <li>MySQL: Depends on lower_case_table_names setting</li> 746 * <li>BigQuery: Table names are case-sensitive, column names are case-insensitive</li> 747 * </ul> 748 * 749 * @param vendor the database vendor 750 * @return configuration with vendor-specific name matcher 751 */ 752 public static TSQLResolverConfig createForVendor(EDbVendor vendor) { 753 TSQLResolverConfig config = new TSQLResolverConfig(); 754 config.vendor = vendor; 755 config.nameMatcher = new VendorNameMatcher(vendor); 756 return config; 757 } 758 759 /** 760 * Create configuration whose name matching runs under ONE analysis's identifier 761 * configuration instead of the vendor default. 762 * 763 * @param profile the effective identifier configuration for this resolution 764 * @return configuration whose matcher observes {@code profile} 765 */ 766 public static TSQLResolverConfig createForProfile(IdentifierProfile profile) { 767 if (profile == null) { 768 throw new IllegalArgumentException( 769 "TSQLResolverConfig.createForProfile: profile is required"); 770 } 771 TSQLResolverConfig config = new TSQLResolverConfig(); 772 config.setIdentifierProfile(profile); 773 return config; 774 } 775 776 /** 777 * Create configuration for a specific database vendor with datatype display enabled. 778 * 779 * @param vendor the database vendor 780 * @return configuration with vendor-specific name matcher and datatype display 781 */ 782 public static TSQLResolverConfig createForVendorWithDatatype(EDbVendor vendor) { 783 TSQLResolverConfig config = createForVendor(vendor); 784 config.setShowDatatype(true); 785 return config; 786 } 787 788 /** 789 * Get the database vendor, if set. 790 * 791 * @return the database vendor, or null if not set 792 */ 793 public EDbVendor getVendor() { 794 return vendor; 795 } 796 797 /** 798 * Set the database vendor and update name matcher accordingly. 799 * 800 * @param vendor the database vendor 801 */ 802 public void setVendor(EDbVendor vendor) { 803 this.vendor = vendor; 804 if (vendor != null) { 805 if (identifierProfile != null && identifierProfile.getVendor() != vendor) { 806 throw new IllegalArgumentException("TSQLResolverConfig.setVendor: vendor " + vendor 807 + " conflicts with the identifier profile already set for " 808 + identifierProfile.getVendor()); 809 } 810 // Keep an explicitly configured profile: setVendor is called unconditionally by 811 // TGSqlParser on every parse, and rebuilding a vendor-default matcher here would 812 // silently discard the caller's configuration right before resolution starts. 813 this.nameMatcher = identifierProfile != null 814 ? new VendorNameMatcher(identifierService(), ESQLDataObjectType.dotColumn) 815 : new VendorNameMatcher(vendor); 816 } 817 } 818 819 /** Explicit identifier configuration for this resolution; null means the vendor default. */ 820 private IdentifierProfile identifierProfile = null; 821 private IdentifierService identifierServiceCache = null; 822 823 /** 824 * Set the identifier configuration this resolution compares and keys names under. 825 * 826 * <p>Replaces the name matcher with one running on that configuration, so resolver2's 827 * scopes and namespaces observe the same per-category rules the catalog was indexed with. 828 * Passing null restores the vendor default. 829 * 830 * @param profile the effective identifier configuration, or null for the vendor default 831 */ 832 public void setIdentifierProfile(IdentifierProfile profile) { 833 this.identifierProfile = profile; 834 this.identifierServiceCache = null; 835 if (profile == null) { 836 if (this.vendor != null) { 837 this.nameMatcher = new VendorNameMatcher(this.vendor); 838 } 839 return; 840 } 841 if (this.vendor != null && this.vendor != profile.getVendor()) { 842 throw new IllegalArgumentException("TSQLResolverConfig.setIdentifierProfile: profile is" 843 + " for vendor " + profile.getVendor() + " but this config is " + this.vendor); 844 } 845 this.vendor = profile.getVendor(); 846 this.nameMatcher = new VendorNameMatcher(identifierService(), ESQLDataObjectType.dotColumn); 847 } 848 849 /** 850 * The identifier configuration of this resolution, or null when it is the vendor default. 851 * 852 * @return the effective identifier profile, or null 853 */ 854 public IdentifierProfile getIdentifierProfile() { 855 return identifierProfile; 856 } 857 858 /** 859 * The single comparison engine this resolution uses — configured when 860 * {@link #setIdentifierProfile} was called, else the vendor's default service. 861 * 862 * @return the identifier service; never null once a vendor or profile is set 863 */ 864 public IdentifierService identifierService() { 865 IdentifierService cached = identifierServiceCache; 866 if (cached == null) { 867 cached = identifierProfile != null 868 ? IdentifierService.forProfile(identifierProfile) 869 : IdentifierService.defaultServiceFor( 870 vendor != null ? vendor : EDbVendor.dbvgeneric); 871 identifierServiceCache = cached; 872 } 873 return cached; 874 } 875 876 /** 877 * Check if vendor-specific name matching is enabled. 878 * 879 * @return true if a vendor is configured 880 */ 881 public boolean hasVendor() { 882 return vendor != null; 883 } 884 885 @Override 886 public String toString() { 887 return String.format( 888 "TSQLResolverConfig{vendor=%s, nameMatcher=%s, legacyCompat=%s, minConfidence=%.2f, maxIter=%d, guessStrategy=%s, " + 889 "minDefiniteConf=%.2f, minGuessConf=%.2f, allowGuessInferred=%s, showDatatype=%s, showCTE=%s, displayMode=%s, displayPolicy=%s}", 890 vendor, 891 nameMatcher, 892 legacyCompatibilityEnabled, 893 legacySyncMinConfidence, 894 maxIterations, 895 getGuessColumnStrategyName(), 896 minDefiniteConfidence, 897 minConfidenceToGuess, 898 allowGuessWhenAllInferred, 899 showDatatype, 900 showCTE, 901 displayNameMode, 902 displayNamePolicy 903 ); 904 } 905}