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