001package gudusoft.gsqlparser.sqlenv.compat;
002
003import gudusoft.gsqlparser.EDbVendor;
004import gudusoft.gsqlparser.catalog.diagnostic.CatalogDiagnostic;
005import gudusoft.gsqlparser.catalog.diagnostic.CatalogDiagnosticCode;
006import gudusoft.gsqlparser.catalog.diagnostic.CatalogDiagnosticSeverity;
007import gudusoft.gsqlparser.catalog.diagnostic.CatalogDiagnosticSink;
008import gudusoft.gsqlparser.catalog.diagnostic.CatalogException;
009import gudusoft.gsqlparser.catalog.input.CatalogLoadOptions;
010import gudusoft.gsqlparser.catalog.input.CatalogLoadResult;
011import gudusoft.gsqlparser.catalog.input.CatalogModelValidator;
012import gudusoft.gsqlparser.catalog.input.CatalogValidationResult;
013import gudusoft.gsqlparser.catalog.input.model.CatalogModel;
014import gudusoft.gsqlparser.catalog.input.model.ColumnModel;
015import gudusoft.gsqlparser.catalog.input.model.RoutineModel;
016import gudusoft.gsqlparser.catalog.input.model.SchemaModel;
017import gudusoft.gsqlparser.catalog.input.model.SequenceModel;
018import gudusoft.gsqlparser.catalog.input.model.SynonymModel;
019import gudusoft.gsqlparser.catalog.input.model.TableModel;
020import gudusoft.gsqlparser.catalog.input.model.UnifiedCatalogModel;
021import gudusoft.gsqlparser.catalog.input.model.ViewModel;
022import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType;
023import gudusoft.gsqlparser.sqlenv.TDDLSQLEnv;
024import gudusoft.gsqlparser.sqlenv.TSQLCatalog;
025import gudusoft.gsqlparser.sqlenv.TSQLEnv;
026import gudusoft.gsqlparser.sqlenv.TSQLSchema;
027import gudusoft.gsqlparser.sqlenv.TSQLTable;
028
029import java.util.ArrayList;
030import java.util.List;
031
032/**
033 * Eager bridge facade: walks a {@link UnifiedCatalogModel} and writes every catalog/schema/
034 * table/view/routine into a {@link TSQLEnv} via the existing public mutation API.
035 *
036 * <p>Plan §8.2 / §12 (T1C.2). Per spike T0.3 inventory the loader uses only public TSQLEnv
037 * mutation methods and the public catalog-tree API ({@link TSQLEnv#getSQLCatalog},
038 * {@link TSQLCatalog#getSchema}, {@link TSQLSchema#createTable}, etc.) — no reflection,
039 * no private-field access. The catalog-tree path mirrors {@code SqlflowSQLEnv} so
040 * schema-less dialects (MySQL, where {@link TSQLEnv#supportSchema(EDbVendor)} is
041 * {@code false}) route through {@link TSQLEnv#DEFAULT_SCHEMA_NAME} the same way the
042 * legacy loader does.</p>
043 *
044 * <p>Sequences are not modeled in {@link TSQLEnv} (no {@code dotSequence} member of
045 * {@link ESQLDataObjectType}); they are represented in the runtime layer instead. The
046 * loader records an INFO diagnostic for each skipped sequence so callers can correlate
047 * the gap.</p>
048 */
049public final class SQLEnvCatalogLoader {
050
051    public SQLEnvCatalogLoader() {
052    }
053
054    /**
055     * Walk {@code model} and apply it to {@code env}. Validation runs first; default mode
056     * fails on ERROR-severity diagnostics and strict mode also fails on WARN, matching
057     * {@code DefaultCatalogLoader} (plan §15).
058     *
059     * <p>On a validation failure this method throws {@link CatalogException} carrying the
060     * counts of ERROR / WARN diagnostics — {@link CatalogLoadResult} is only returned on
061     * success. This matches {@code CatalogLoaders.DefaultCatalogLoader.load(...)}; the
062     * sink (when set on {@code options}) still receives every diagnostic before the
063     * exception fires so callers can correlate. On non-validation success the returned
064     * result carries the populated env plus any informational diagnostics emitted by the
065     * loader (e.g. unrepresented sequences).</p>
066     */
067    public CatalogLoadResult loadIntoSQLEnv(TSQLEnv env, UnifiedCatalogModel model,
068                                            CatalogLoadOptions options) {
069        if (env == null) {
070            throw new IllegalArgumentException("SQLEnvCatalogLoader.loadIntoSQLEnv: env is required");
071        }
072        if (model == null) {
073            throw new IllegalArgumentException("SQLEnvCatalogLoader.loadIntoSQLEnv: model is required");
074        }
075        if (options == null) {
076            throw new IllegalArgumentException(
077                "SQLEnvCatalogLoader.loadIntoSQLEnv: options is required");
078        }
079        if (env.getDBVendor() != options.vendor()) {
080            throw new IllegalArgumentException(
081                "SQLEnvCatalogLoader.loadIntoSQLEnv: env.vendor=" + env.getDBVendor()
082                    + " does not match options.vendor=" + options.vendor());
083        }
084        if (model.vendor() != options.vendor()) {
085            throw new IllegalArgumentException(
086                "SQLEnvCatalogLoader.loadIntoSQLEnv: model.vendor=" + model.vendor()
087                    + " does not match options.vendor=" + options.vendor());
088        }
089
090        List<CatalogDiagnostic> diagnostics = validate(model, options);
091        applyDefaults(env, model, options);
092        boolean dialectHasSchema = TSQLEnv.supportSchema(env.getDBVendor());
093        for (CatalogModel c : model.catalogs()) {
094            applyCatalog(env, c, dialectHasSchema, diagnostics, options.diagnosticSink());
095        }
096        return CatalogLoadResult.ok(env, diagnostics);
097    }
098
099    /**
100     * Convenience: spin up a fresh {@link TSQLEnv} (concrete {@link TDDLSQLEnv} subclass
101     * with empty defaults) and apply {@code model} to it. Throws {@link CatalogException}
102     * on failure to match {@code DefaultCatalogLoader.load(...)} semantics.
103     */
104    public TSQLEnv loadToSQLEnv(UnifiedCatalogModel model, CatalogLoadOptions options) {
105        if (model == null) {
106            throw new IllegalArgumentException("SQLEnvCatalogLoader.loadToSQLEnv: model is required");
107        }
108        if (options == null) {
109            throw new IllegalArgumentException(
110                "SQLEnvCatalogLoader.loadToSQLEnv: options is required");
111        }
112        if (model.vendor() != options.vendor()) {
113            throw new IllegalArgumentException(
114                "SQLEnvCatalogLoader.loadToSQLEnv: model.vendor=" + model.vendor()
115                    + " does not match options.vendor=" + options.vendor());
116        }
117        TSQLEnv env = newSQLEnv(options.vendor());
118        CatalogLoadResult result = loadIntoSQLEnv(env, model, options);
119        if (!result.ok()) {
120            throw new CatalogException(
121                "SQLEnvCatalogLoader.loadToSQLEnv: model failed validation ("
122                    + countOf(result.diagnostics(), CatalogDiagnosticSeverity.ERROR) + " ERROR, "
123                    + countOf(result.diagnostics(), CatalogDiagnosticSeverity.WARN) + " WARN"
124                    + (options.strict() ? ", strict mode" : "") + ")");
125        }
126        return env;
127    }
128
129    // ---- internals -------------------------------------------------------
130
131    /**
132     * Construct a concrete {@link TSQLEnv} since the base class is abstract. Phase 1 picks
133     * {@link TDDLSQLEnv} because it is the project's existing concrete subclass that has a
134     * trivial constructor and is already used by tests. The bridge work in P1D introduces a
135     * dedicated subclass; until then, this is the simplest path that does not extend
136     * {@code TSQLEnv} from the new package.
137     */
138    private static TSQLEnv newSQLEnv(EDbVendor vendor) {
139        // Pass null for sql so TDDLSQLEnv stays in its inert / un-initialized state —
140        // the loader never asks the env to parse DDL, it only mutates the catalog tree.
141        return new TDDLSQLEnv(null, null, null, vendor, null);
142    }
143
144    private List<CatalogDiagnostic> validate(UnifiedCatalogModel model,
145                                             CatalogLoadOptions options) {
146        CatalogValidationResult validation = new CatalogModelValidator().validate(model, options);
147        List<CatalogDiagnostic> diagnostics = new ArrayList<CatalogDiagnostic>(validation.diagnostics());
148        if (options.diagnosticSink() != null) {
149            for (CatalogDiagnostic d : diagnostics) {
150                options.diagnosticSink().accept(d);
151            }
152        }
153        int errors = countOf(diagnostics, CatalogDiagnosticSeverity.ERROR);
154        int warns = countOf(diagnostics, CatalogDiagnosticSeverity.WARN);
155        // Plan §15 — default mode: ERROR diagnostics fail the load; strict mode escalates
156        // WARN diagnostics to load failure too. Mirrors DefaultCatalogLoader.validate.
157        if (errors > 0 || (options.strict() && warns > 0)) {
158            throw new CatalogException(
159                "SQLEnvCatalogLoader: model failed validation ("
160                    + errors + " ERROR, " + warns + " WARN"
161                    + (options.strict() ? ", strict mode" : "") + ")");
162        }
163        return diagnostics;
164    }
165
166    private static void applyDefaults(TSQLEnv env, UnifiedCatalogModel model,
167                                      CatalogLoadOptions options) {
168        // Options take precedence over the model's defaults — they're the per-call knobs.
169        String catalog = nonEmpty(options.defaultCatalog(), model.defaults().defaultCatalog());
170        String schema = nonEmpty(options.defaultSchema(), model.defaults().defaultSchema());
171        String server = nonEmpty(options.defaultServer(), model.defaults().defaultServer());
172        if (catalog != null) env.setDefaultCatalogName(catalog);
173        if (schema != null) env.setDefaultSchemaName(schema);
174        if (server != null) env.setDefaultServerName(server);
175    }
176
177    private static String nonEmpty(String first, String fallback) {
178        if (first != null && !first.isEmpty()) return first;
179        if (fallback != null && !fallback.isEmpty()) return fallback;
180        return null;
181    }
182
183    private static void applyCatalog(TSQLEnv env, CatalogModel c, boolean dialectHasSchema,
184                                     List<CatalogDiagnostic> diagnostics,
185                                     CatalogDiagnosticSink sink) {
186        TSQLCatalog catalog = env.getSQLCatalog(c.name(), true);
187        for (SchemaModel s : c.schemas()) {
188            applySchema(catalog, s, dialectHasSchema, diagnostics, sink);
189        }
190    }
191
192    private static void applySchema(TSQLCatalog catalog, SchemaModel s, boolean dialectHasSchema,
193                                    List<CatalogDiagnostic> diagnostics,
194                                    CatalogDiagnosticSink sink) {
195        // Schema-less dialect: route every object through the dialect's DEFAULT schema bucket
196        // exactly the way SqlflowSQLEnv does. The model's schema name (if non-empty) is
197        // discarded in that case — it has no meaningful place to land in a single-tier env.
198        String schemaName = (!dialectHasSchema || s.name() == null || s.name().isEmpty())
199            ? TSQLEnv.DEFAULT_SCHEMA_NAME : s.name();
200        TSQLSchema schema = catalog.getSchema(schemaName, true);
201
202        for (TableModel t : s.tables()) {
203            TSQLTable tbl = schema.createTable(t.name());
204            if (tbl != null) {
205                for (ColumnModel col : t.columns()) {
206                    tbl.addColumn(col.name());
207                }
208            }
209        }
210        for (ViewModel v : s.views()) {
211            TSQLTable view = schema.createTable(v.name());
212            if (view != null) {
213                view.setView(true);
214                if (v.definition() != null) view.setDefinition(v.definition());
215                for (ColumnModel col : v.columns()) {
216                    view.addColumn(col.name());
217                }
218            }
219        }
220        for (RoutineModel r : s.routines()) {
221            applyRoutine(schema, r);
222        }
223        for (SynonymModel syn : s.synonyms()) {
224            schema.createSynonyms(syn.name());
225        }
226        for (SequenceModel sq : s.sequences()) {
227            // Sequences have no TSQLEnv representation. Record a WARN per plan §15
228            // ("partial catalog" row): an unrepresented object surfaces as WARN so
229            // strict-mode callers can escalate. The runtime snapshot still carries the
230            // sequence — only the TSQLEnv view is missing it.
231            CatalogDiagnostic d = CatalogDiagnostic.builder()
232                .severity(CatalogDiagnosticSeverity.WARN)
233                .code(CatalogDiagnosticCode.CATALOG_LOAD_UNSUPPORTED_KIND)
234                .message("Sequence '" + sq.name()
235                    + "' has no TSQLEnv representation; runtime-only")
236                .build();
237            diagnostics.add(d);
238            if (sink != null) sink.accept(d);
239        }
240    }
241
242    private static void applyRoutine(TSQLSchema schema, RoutineModel r) {
243        switch (r.kind()) {
244            case FUNCTION:
245                schema.createFunction(r.name());
246                break;
247            case PROCEDURE:
248            case ROUTINE:
249                schema.createProcedure(r.name());
250                break;
251            case PACKAGE:
252                schema.createOraclePackage(r.name());
253                break;
254            default:
255                throw new IllegalStateException(
256                    "RoutineModel.kind must be FUNCTION/PROCEDURE/PACKAGE/ROUTINE; got " + r.kind());
257        }
258    }
259
260    private static int countOf(List<CatalogDiagnostic> diagnostics,
261                               CatalogDiagnosticSeverity severity) {
262        int n = 0;
263        for (CatalogDiagnostic d : diagnostics) {
264            if (d.severity() == severity) n++;
265        }
266        return n;
267    }
268}