001package gudusoft.gsqlparser.catalog.input; 002 003import gudusoft.gsqlparser.catalog.diagnostic.CatalogDiagnostic; 004import gudusoft.gsqlparser.catalog.diagnostic.CatalogDiagnosticSeverity; 005import gudusoft.gsqlparser.catalog.diagnostic.CatalogException; 006import gudusoft.gsqlparser.catalog.input.model.UnifiedCatalogModel; 007import gudusoft.gsqlparser.catalog.runtime.CatalogProvider; 008import gudusoft.gsqlparser.catalog.runtime.CatalogProviderConfig; 009import gudusoft.gsqlparser.catalog.runtime.CatalogRuntime; 010import gudusoft.gsqlparser.catalog.runtime.ModelBackedCatalogProvider; 011import gudusoft.gsqlparser.sqlenv.TSQLEnv; 012import gudusoft.gsqlparser.sqlenv.compat.SQLEnvCatalogLoader; 013import gudusoft.gsqlparser.sqlenv.compat.SqlEnvCatalogBridge; 014 015import java.util.ArrayList; 016import java.util.List; 017 018/** 019 * One-line convenience facades over {@link CatalogLoader}. 020 * 021 * <p>Plan §7.1 / §13.4. {@link #loadToSQLEnv(CatalogInputSource, CatalogLoadOptions)} 022 * and {@link #loadIntoSQLEnv(TSQLEnv, UnifiedCatalogModel, CatalogLoadOptions)} delegate 023 * to {@code SQLEnvCatalogLoader} (sqlenv/compat) — that bridge ships in P1C; until 024 * then those entry points throw a clear {@link UnsupportedOperationException} pointing 025 * at the bridge work item. {@link #loadRuntime(CatalogInputSource, CatalogLoadOptions)} 026 * and {@link #fromProvider(CatalogProvider, CatalogProviderConfig)} are fully wired.</p> 027 */ 028public final class CatalogLoaders { 029 030 private CatalogLoaders() { 031 // Static utility — no instances. 032 } 033 034 public static CatalogLoader defaultLoader() { 035 return new DefaultCatalogLoader(); 036 } 037 038 /** 039 * Convenience: read a {@link CatalogInputSource} into a {@link UnifiedCatalogModel} 040 * via {@link CatalogInputReaders#forSource} and materialize a {@link CatalogRuntime} 041 * around a {@link ModelBackedCatalogProvider}. Diagnostics from validation are 042 * forwarded to {@code options.diagnosticSink()} when present. 043 */ 044 public static CatalogRuntime loadRuntime(CatalogInputSource source, CatalogLoadOptions options) { 045 if (source == null) { 046 throw new IllegalArgumentException("CatalogLoaders.loadRuntime: source is required"); 047 } 048 if (options == null) { 049 throw new IllegalArgumentException("CatalogLoaders.loadRuntime: options is required"); 050 } 051 UnifiedCatalogModel model; 052 try { 053 CatalogInputReader reader = CatalogInputReaders.forSource(source, options); 054 model = reader.read(source, options); 055 } catch (CatalogInputException ex) { 056 throw new CatalogException( 057 "CatalogLoaders.loadRuntime failed to read source " + source.name() + ": " 058 + ex.getMessage(), ex); 059 } 060 return new DefaultCatalogLoader().load(model, options); 061 } 062 063 /** 064 * Wrap a caller-managed {@link CatalogProvider} in a {@link CatalogRuntime}. The 065 * provider must already be {@link CatalogProvider#open(CatalogProviderConfig) opened} 066 * by the caller; this facade does not do lifecycle management. 067 * 068 * <p>The runtime is configured with vendor {@link gudusoft.gsqlparser.EDbVendor#dbvgeneric}, 069 * which means resolver candidate-expansion will not apply per-vendor identifier folding 070 * or search-path rules. Most callers want 071 * {@link #fromProvider(CatalogProvider, CatalogProviderConfig, gudusoft.gsqlparser.EDbVendor)} 072 * instead — the vendor-explicit overload — because correct resolution depends on 073 * matching the snapshot's vendor. We deliberately do NOT probe the provider for a 074 * vendor here: live/lazy providers treat an empty {@code requestedNames} 075 * {@link gudusoft.gsqlparser.catalog.runtime.CatalogQuery} as "fetch every object", 076 * which would defeat the lazy contract just to discover a vendor.</p> 077 */ 078 public static CatalogRuntime fromProvider(CatalogProvider provider, CatalogProviderConfig config) { 079 if (provider == null) { 080 throw new IllegalArgumentException("CatalogLoaders.fromProvider: provider is required"); 081 } 082 if (config != null) { 083 provider.open(config); 084 } 085 return CatalogRuntime.builder() 086 .provider(provider) 087 .vendor(gudusoft.gsqlparser.EDbVendor.dbvgeneric) 088 // LAZY by default so the resolver's on-miss fetch consults the provider — 089 // the runtime would otherwise keep the cached (null) snapshot and miss 090 // every name. Callers that want EAGER seeding should construct via 091 // CatalogRuntime.builder() and call runtime.snapshot(...) themselves. 092 .loadingMode(gudusoft.gsqlparser.catalog.input.CatalogLoadingMode.LAZY) 093 .build(); 094 } 095 096 /** 097 * Vendor-explicit overload of {@link #fromProvider(CatalogProvider, CatalogProviderConfig)}. 098 * Preferred form when the caller knows the provider's vendor — bypasses the probe 099 * snapshot and ensures the runtime's resolver candidate-expansion uses the right 100 * vendor for identifier folding and search-path resolution. 101 */ 102 public static CatalogRuntime fromProvider(CatalogProvider provider, 103 CatalogProviderConfig config, 104 gudusoft.gsqlparser.EDbVendor vendor) { 105 if (provider == null) { 106 throw new IllegalArgumentException("CatalogLoaders.fromProvider: provider is required"); 107 } 108 if (vendor == null) { 109 throw new IllegalArgumentException("CatalogLoaders.fromProvider: vendor is required"); 110 } 111 if (config != null) { 112 provider.open(config); 113 } 114 return CatalogRuntime.builder() 115 .provider(provider) 116 .vendor(vendor) 117 .loadingMode(gudusoft.gsqlparser.catalog.input.CatalogLoadingMode.LAZY) 118 .build(); 119 } 120 121 122 /** 123 * Convenience: read {@code source} into a {@link UnifiedCatalogModel} and apply it to 124 * a freshly-constructed {@link TSQLEnv} via {@link SqlEnvCatalogBridge#from}. This is 125 * the eager path the design §5.1 demo uses. 126 */ 127 public static TSQLEnv loadToSQLEnv(CatalogInputSource source, CatalogLoadOptions options) { 128 return SqlEnvCatalogBridge.from(source, options); 129 } 130 131 /** 132 * Apply {@code model} to an existing caller-managed {@link TSQLEnv} via 133 * {@link SQLEnvCatalogLoader#loadIntoSQLEnv}. 134 */ 135 public static CatalogLoadResult loadIntoSQLEnv(TSQLEnv env, UnifiedCatalogModel model, 136 CatalogLoadOptions options) { 137 return new SQLEnvCatalogLoader().loadIntoSQLEnv(env, model, options); 138 } 139 140 /** 141 * Phase 1F optional optimization. Pre-scans {@code sqlText} via 142 * {@code TGSqlParser.getrawsqlstatements()} to populate {@code CatalogQuery.requestedNames} 143 * before opening the provider, so live providers can batch fetches. 144 */ 145 public static TSQLEnv loadToSQLEnvForSql(CatalogInputSource source, CatalogLoadOptions options, 146 String sqlText) { 147 throw new UnsupportedOperationException("Optional Phase 1F entry point — implemented in P1F (T1F.1)"); 148 } 149 150 /** 151 * Default {@link CatalogLoader} implementation. Materializes the model into an 152 * eager {@link CatalogRuntime} backed by {@link ModelBackedCatalogProvider}. The 153 * SQLEnv-returning entry points are deferred to P1C because they require the 154 * compatibility bridge in {@code sqlenv/compat/}. 155 */ 156 static final class DefaultCatalogLoader implements CatalogLoader { 157 158 @Override 159 public CatalogRuntime load(UnifiedCatalogModel model, CatalogLoadOptions options) { 160 if (model == null) { 161 throw new IllegalArgumentException("CatalogLoader.load: model is required"); 162 } 163 if (options == null) { 164 throw new IllegalArgumentException("CatalogLoader.load: options is required"); 165 } 166 // Validate before serving — diagnostics flow to the configured sink and to the 167 // CatalogException raised for ERROR-level findings (always) plus WARN findings 168 // under strict mode. 169 validate(model, options); 170 // Pass the explicit IdentifierConfig from options through to materialization 171 // so the snapshot keys are built under the policy validation just accepted — 172 // otherwise an option-level override (MySQL lower_case_table_names, MSSQL 173 // collation) would be silently dropped and resolution would fail later. 174 gudusoft.gsqlparser.catalog.input.model.IdentifierConfig override = 175 options.hasExplicitIdentifierConfig() ? options.identifierConfig() : null; 176 CatalogProvider provider = new ModelBackedCatalogProvider(model, override); 177 provider.open(CatalogProviderConfig.empty()); 178 CatalogRuntime runtime = CatalogRuntime.builder() 179 .provider(provider) 180 .vendor(model.vendor()) 181 .loadingMode(options.loadingMode()) 182 .ttlMillis(options.ttlMillis()) 183 .maxFetchesPerAnalysis(options.maxFetchesPerAnalysis()) 184 .includeColumns(options.includeColumns()) 185 .includeViews(options.includeViews()) 186 .includeRoutines(options.includeRoutines()) 187 .build(); 188 // For EAGER providers, seed the snapshot now so the resolver can answer 189 // immediately. LAZY/AUTO callers fetch on-miss, so leave the snapshot null. 190 if (options.loadingMode() == gudusoft.gsqlparser.catalog.input.CatalogLoadingMode.EAGER) { 191 runtime.snapshot(gudusoft.gsqlparser.catalog.runtime.CatalogQuery.builder() 192 .vendor(model.vendor()).build()); 193 } 194 return runtime; 195 } 196 197 @Override 198 public CatalogLoadResult loadIntoSQLEnv(TSQLEnv env, UnifiedCatalogModel model, 199 CatalogLoadOptions options) { 200 return new SQLEnvCatalogLoader().loadIntoSQLEnv(env, model, options); 201 } 202 203 @Override 204 public TSQLEnv loadToSQLEnv(UnifiedCatalogModel model, CatalogLoadOptions options) { 205 return new SQLEnvCatalogLoader().loadToSQLEnv(model, options); 206 } 207 208 private List<CatalogDiagnostic> validate(UnifiedCatalogModel model, 209 CatalogLoadOptions options) { 210 CatalogValidationResult validation = 211 new CatalogModelValidator().validate(model, options); 212 List<CatalogDiagnostic> diagnostics = 213 new ArrayList<CatalogDiagnostic>(validation.diagnostics()); 214 if (options.diagnosticSink() != null) { 215 for (CatalogDiagnostic d : diagnostics) { 216 options.diagnosticSink().accept(d); 217 } 218 } 219 int errors = countOf(diagnostics, CatalogDiagnosticSeverity.ERROR); 220 int warns = countOf(diagnostics, CatalogDiagnosticSeverity.WARN); 221 // Plan §15: 222 // - default mode: ERROR diagnostics fail the load (duplicate names, 223 // missing-required fields, dangling references would produce ambiguous 224 // or incomplete metadata otherwise). WARN diagnostics surface but do 225 // NOT fail. 226 // - strict mode: WARN diagnostics also escalate to load failure 227 // (identifier-bypass warnings, etc.). 228 if (errors > 0 || (options.strict() && warns > 0)) { 229 throw new CatalogException( 230 "CatalogLoader: model failed validation (" 231 + errors + " ERROR, " + warns + " WARN" 232 + (options.strict() ? ", strict mode" : "") + ")"); 233 } 234 return diagnostics; 235 } 236 237 private static int countOf(List<CatalogDiagnostic> diagnostics, 238 CatalogDiagnosticSeverity severity) { 239 int n = 0; 240 for (CatalogDiagnostic d : diagnostics) { 241 if (d.severity() == severity) n++; 242 } 243 return n; 244 } 245 } 246}