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}