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