001package gudusoft.gsqlparser.sqlenv.compat;
002
003import gudusoft.gsqlparser.EDbVendor;
004import gudusoft.gsqlparser.catalog.input.CatalogLoadOptions;
005import gudusoft.gsqlparser.catalog.runtime.CatalogRuntime;
006import gudusoft.gsqlparser.sqlenv.TSQLCatalog;
007import gudusoft.gsqlparser.sqlenv.TSQLEnv;
008import gudusoft.gsqlparser.sqlenv.catalog.ICatalogProvider;
009
010/**
011 * Lazy bridge: a {@link TSQLEnv} subclass whose only job is to install
012 * {@link CatalogBackedCatalogProvider} via the legacy {@link ICatalogProvider} hook.
013 *
014 * <p>Plan §5.3 / §8.3. Per spike {@code bridge-access-trace.md} no method override is
015 * required: {@link TSQLEnv#doSearchSchemaObject} already dispatches to
016 * {@link ICatalogProvider#findObject} for fully-qualified lookups, and the legacy
017 * mutation paths ({@code addFunction}, {@code setDefaultCatalogName}, {@code addObject})
018 * land naturally on the wrapped delegate.</p>
019 *
020 * <p>The constructor calls {@code super(vendor)} (which builds the default
021 * {@code CatalogStoreProvider}), captures that as the delegate, and then installs the
022 * {@link CatalogBackedCatalogProvider} wrapper via the protected
023 * {@code setCatalogProviderForCompatibility} hook added in P1D.1. Subsequent
024 * {@link #getCatalogProvider()} calls return the bridged provider; lookups missing
025 * from the legacy overlay will round-trip through the runtime resolver, materialize
026 * via {@link CatalogEntryToSQLEnvMapper}, and be cached back into the delegate so the
027 * next lookup is O(1).</p>
028 *
029 * <p>{@link #initSQLEnv()} is a no-op: the runtime is the metadata source, the bridge
030 * is just a thin compatibility veneer. Construction also applies defaults from
031 * {@link CatalogLoadOptions} (default catalog / schema / server) so the bridge plays the
032 * same active-context role as the corresponding eager-loaded {@link TSQLEnv}.</p>
033 */
034public final class CatalogRuntimeToSQLEnvBridge extends TSQLEnv {
035
036    private final CatalogRuntime runtime;
037    private final CatalogLoadOptions options;
038
039    /**
040     * Construct a bridge for the given runtime. Vendor is read from the runtime; the
041     * options carry the default catalog/schema/server applied to {@link TSQLEnv}.
042     *
043     * @param runtime the runtime that owns the snapshot, overlay, and lazy provider.
044     * @param options optional load options; {@code null} is allowed — defaults are
045     *                read from the runtime alone in that case.
046     */
047    public CatalogRuntimeToSQLEnvBridge(CatalogRuntime runtime, CatalogLoadOptions options) {
048        super(requireRuntime(runtime).vendor());
049        this.runtime = runtime;
050        this.options = options;
051        applyDefaults(options);
052        ICatalogProvider delegate = getCatalogProvider();
053        CatalogBackedCatalogProvider bridged = new CatalogBackedCatalogProvider(
054            runtime, delegate, new CatalogEntryToSQLEnvMapper(), this,
055            options != null ? options.identifierConfig() : null,
056            options != null ? options.diagnosticSink() : null);
057        setCatalogProviderForCompatibility(bridged);
058    }
059
060    /** Construct a bridge for the given runtime and explicit vendor. Vendor must match. */
061    public CatalogRuntimeToSQLEnvBridge(EDbVendor vendor, CatalogRuntime runtime,
062                                        CatalogLoadOptions options) {
063        super(vendor);
064        if (runtime == null) {
065            throw new IllegalArgumentException(
066                "CatalogRuntimeToSQLEnvBridge: runtime must not be null");
067        }
068        if (vendor == null) {
069            throw new IllegalArgumentException(
070                "CatalogRuntimeToSQLEnvBridge: vendor must not be null");
071        }
072        if (runtime.vendor() != vendor) {
073            throw new IllegalArgumentException(
074                "CatalogRuntimeToSQLEnvBridge: runtime.vendor=" + runtime.vendor()
075                    + " does not match vendor=" + vendor);
076        }
077        this.runtime = runtime;
078        this.options = options;
079        applyDefaults(options);
080        ICatalogProvider delegate = getCatalogProvider();
081        CatalogBackedCatalogProvider bridged = new CatalogBackedCatalogProvider(
082            runtime, delegate, new CatalogEntryToSQLEnvMapper(), this,
083            options != null ? options.identifierConfig() : null,
084            options != null ? options.diagnosticSink() : null);
085        setCatalogProviderForCompatibility(bridged);
086    }
087
088    /**
089     * The runtime backing this bridge. Useful for tests that want to inspect the
090     * snapshot/overlay directly without reaching through {@link #getCatalogProvider}.
091     */
092    public CatalogRuntime runtime() {
093        return runtime;
094    }
095
096    /**
097     * Load options the bridge was constructed with, or {@code null} if none. Retained so
098     * downstream callers can introspect the loading mode / fetch caps without re-reading
099     * the runtime.
100     */
101    public CatalogLoadOptions options() {
102        return options;
103    }
104
105    @Override
106    public void initSQLEnv() {
107        // The runtime IS the metadata source; nothing to materialize lazily here.
108        // Legacy callers that build a TSQLEnv via parser.sqlenv = bridge expect
109        // initSQLEnv() to be a no-op when metadata is supplied externally.
110    }
111
112    private static CatalogRuntime requireRuntime(CatalogRuntime runtime) {
113        if (runtime == null) {
114            throw new IllegalArgumentException(
115                "CatalogRuntimeToSQLEnvBridge: runtime must not be null");
116        }
117        return runtime;
118    }
119
120    private void applyDefaults(CatalogLoadOptions opts) {
121        if (opts == null) return;
122        if (opts.defaultCatalog() != null && !opts.defaultCatalog().isEmpty()) {
123            setDefaultCatalogName(opts.defaultCatalog());
124        }
125        if (opts.defaultSchema() != null && !opts.defaultSchema().isEmpty()) {
126            setDefaultSchemaName(opts.defaultSchema());
127        }
128        if (opts.defaultServer() != null && !opts.defaultServer().isEmpty()) {
129            setDefaultServerName(opts.defaultServer());
130        }
131        // Pre-register an empty default catalog/schema in the legacy catalogList so the
132        // legacy {@code TSQLEnv.doSearchSchemaObject} '..table' fallback can find them
133        // and route through {@code catalogProvider.findObject} (= the bridge's lookup
134        // path). Without this, unqualified names like {@code SELECT * FROM employees}
135        // never reach the bridge: the legacy path iterates {@code catalogList} which is
136        // empty until something is materialized through the bridge, and the iteration
137        // body — which is what calls findObject — never runs. Registering empties is
138        // safe: the catalog/schema are real {@code TSQLCatalog}/{@code TSQLSchema}
139        // instances that hold zero materialized objects until the bridge populates them
140        // on demand.
141        if (opts.defaultCatalog() != null && !opts.defaultCatalog().isEmpty()) {
142            TSQLCatalog c = getSQLCatalog(opts.defaultCatalog(), true);
143            if (c != null && opts.defaultSchema() != null && !opts.defaultSchema().isEmpty()) {
144                c.getSchema(opts.defaultSchema(), true);
145            }
146        }
147    }
148}