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