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    /** The runtime's catalog IS the metadata, although nothing is registered until it is asked for. */
089    @Override
090    public boolean hasCatalogMetadata() {
091        return true;
092    }
093
094    /**
095     * The runtime backing this bridge. Useful for tests that want to inspect the
096     * snapshot/overlay directly without reaching through {@link #getCatalogProvider}.
097     */
098    public CatalogRuntime runtime() {
099        return runtime;
100    }
101
102    /**
103     * Load options the bridge was constructed with, or {@code null} if none. Retained so
104     * downstream callers can introspect the loading mode / fetch caps without re-reading
105     * the runtime.
106     */
107    public CatalogLoadOptions options() {
108        return options;
109    }
110
111    @Override
112    public void initSQLEnv() {
113        // The runtime IS the metadata source; nothing to materialize lazily here.
114        // Legacy callers that build a TSQLEnv via parser.sqlenv = bridge expect
115        // initSQLEnv() to be a no-op when metadata is supplied externally.
116    }
117
118    private static CatalogRuntime requireRuntime(CatalogRuntime runtime) {
119        if (runtime == null) {
120            throw new IllegalArgumentException(
121                "CatalogRuntimeToSQLEnvBridge: runtime must not be null");
122        }
123        return runtime;
124    }
125
126    private void applyDefaults(CatalogLoadOptions opts) {
127        if (opts == null) return;
128        if (opts.defaultCatalog() != null && !opts.defaultCatalog().isEmpty()) {
129            setDefaultCatalogName(opts.defaultCatalog());
130        }
131        if (opts.defaultSchema() != null && !opts.defaultSchema().isEmpty()) {
132            setDefaultSchemaName(opts.defaultSchema());
133        }
134        if (opts.defaultServer() != null && !opts.defaultServer().isEmpty()) {
135            setDefaultServerName(opts.defaultServer());
136        }
137        // Pre-register an empty default catalog/schema in the legacy catalogList so the
138        // legacy {@code TSQLEnv.doSearchSchemaObject} '..table' fallback can find them
139        // and route through {@code catalogProvider.findObject} (= the bridge's lookup
140        // path). Without this, unqualified names like {@code SELECT * FROM employees}
141        // never reach the bridge: the legacy path iterates {@code catalogList} which is
142        // empty until something is materialized through the bridge, and the iteration
143        // body — which is what calls findObject — never runs. Registering empties is
144        // safe: the catalog/schema are real {@code TSQLCatalog}/{@code TSQLSchema}
145        // instances that hold zero materialized objects until the bridge populates them
146        // on demand.
147        if (opts.defaultCatalog() != null && !opts.defaultCatalog().isEmpty()) {
148            TSQLCatalog c = getSQLCatalog(opts.defaultCatalog(), true);
149            if (c != null && opts.defaultSchema() != null && !opts.defaultSchema().isEmpty()) {
150                c.getSchema(opts.defaultSchema(), true);
151            }
152        }
153    }
154}