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}