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}