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}