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}