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.input.CatalogLoadingMode; 008import gudusoft.gsqlparser.catalog.input.model.IdentifierConfig; 009import gudusoft.gsqlparser.catalog.runtime.CatalogEntries; 010import gudusoft.gsqlparser.catalog.runtime.CatalogIdentifierPolicy; 011import gudusoft.gsqlparser.catalog.runtime.CatalogObjectId; 012import gudusoft.gsqlparser.catalog.runtime.CatalogObjectKind; 013import gudusoft.gsqlparser.catalog.runtime.CatalogProviderConfig; 014import gudusoft.gsqlparser.catalog.runtime.CatalogQualifiedName; 015import gudusoft.gsqlparser.catalog.runtime.CatalogRuntime; 016import gudusoft.gsqlparser.catalog.runtime.CatalogSnapshot; 017import gudusoft.gsqlparser.catalog.runtime.InMemoryCatalogProvider; 018import gudusoft.gsqlparser.catalog.runtime.InMemoryCatalogSnapshot; 019import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType; 020import gudusoft.gsqlparser.sqlenv.TSQLCatalog; 021import gudusoft.gsqlparser.sqlenv.TSQLColumn; 022import gudusoft.gsqlparser.sqlenv.TSQLEnv; 023import gudusoft.gsqlparser.sqlenv.TSQLSchema; 024import gudusoft.gsqlparser.sqlenv.TSQLSchemaObject; 025import gudusoft.gsqlparser.sqlenv.TSQLSynonyms; 026import gudusoft.gsqlparser.sqlenv.TSQLTable; 027 028/** 029 * Reverse bridge: read-only {@link CatalogRuntime} view over a hand-built {@link TSQLEnv}. 030 * 031 * <p>Plan §6 / §8.5. Lets new consumers (SQLGuard, Semantic IR, anything that targets 032 * the new SPI) accept a hand-built {@link TSQLEnv} from upstream callers without 033 * re-implementing the catalog traversal each time.</p> 034 * 035 * <p>The resulting runtime is read-only at the snapshot level: subsequent mutations to 036 * the underlying {@link TSQLEnv} are not reflected, since {@link #adapt} takes a 037 * point-in-time snapshot. Callers that need fresh views should call {@link #adapt} 038 * again. The mutable {@link gudusoft.gsqlparser.catalog.runtime.CatalogOverlay} is 039 * still available on the runtime for new consumers (DDL discovered downstream).</p> 040 */ 041public final class SQLEnvToCatalogRuntimeAdapter { 042 043 public SQLEnvToCatalogRuntimeAdapter() { 044 } 045 046 /** 047 * Build a {@link CatalogRuntime} that exposes the contents of {@code env} as an 048 * immutable {@link CatalogSnapshot}. The snapshot covers every catalog → schema → 049 * table/view/routine/synonym walk reachable from {@link TSQLEnv#getCatalogList()} 050 * including columns on tables and views. 051 */ 052 public CatalogRuntime adapt(TSQLEnv env) { 053 if (env == null) { 054 throw new IllegalArgumentException("SQLEnvToCatalogRuntimeAdapter.adapt: env is required"); 055 } 056 EDbVendor vendor = env.getDBVendor(); 057 IdentifierConfig cfg = IdentifierConfig.defaultsFor(vendor); 058 CatalogSnapshot snapshot = buildSnapshot(env, cfg, vendor); 059 060 InMemoryCatalogProvider provider = new InMemoryCatalogProvider(snapshot); 061 provider.open(CatalogProviderConfig.empty()); 062 return CatalogRuntime.builder() 063 .provider(provider) 064 .vendor(vendor) 065 // EAGER: the snapshot is fully materialized; the resolver answers without 066 // ever calling provider.snapshot(...) again. 067 .loadingMode(CatalogLoadingMode.EAGER) 068 .initialSnapshot(snapshot) 069 .build(); 070 } 071 072 /** 073 * Same as {@link #adapt(TSQLEnv)} but with an explicit {@link IdentifierConfig} 074 * override so the snapshot keys are built under a non-default policy (e.g. MSSQL 075 * collation, MySQL {@code lower_case_table_names}). Vendor must match {@code env}. 076 */ 077 public CatalogRuntime adapt(TSQLEnv env, IdentifierConfig identifierConfig) { 078 if (env == null) { 079 throw new IllegalArgumentException("SQLEnvToCatalogRuntimeAdapter.adapt: env is required"); 080 } 081 if (identifierConfig == null) { 082 throw new IllegalArgumentException( 083 "SQLEnvToCatalogRuntimeAdapter.adapt: identifierConfig is required"); 084 } 085 if (identifierConfig.vendor() != env.getDBVendor()) { 086 throw new IllegalArgumentException( 087 "SQLEnvToCatalogRuntimeAdapter.adapt: identifierConfig.vendor=" 088 + identifierConfig.vendor() + " does not match env.vendor=" + env.getDBVendor()); 089 } 090 EDbVendor vendor = env.getDBVendor(); 091 CatalogSnapshot snapshot = buildSnapshot(env, identifierConfig, vendor); 092 093 InMemoryCatalogProvider provider = new InMemoryCatalogProvider(snapshot); 094 provider.open(CatalogProviderConfig.empty()); 095 return CatalogRuntime.builder() 096 .provider(provider) 097 .vendor(vendor) 098 .loadingMode(CatalogLoadingMode.EAGER) 099 .initialSnapshot(snapshot) 100 .build(); 101 } 102 103 /** 104 * Build the snapshot directly without wrapping it in a runtime. Useful when callers 105 * already manage their own provider lifecycle and just want the catalog-tree walk. 106 */ 107 public CatalogSnapshot snapshot(TSQLEnv env) { 108 if (env == null) { 109 throw new IllegalArgumentException( 110 "SQLEnvToCatalogRuntimeAdapter.snapshot: env is required"); 111 } 112 EDbVendor vendor = env.getDBVendor(); 113 IdentifierConfig cfg = IdentifierConfig.defaultsFor(vendor); 114 return buildSnapshot(env, cfg, vendor); 115 } 116 117 // ---- internals ------------------------------------------------------- 118 119 private static CatalogSnapshot buildSnapshot(TSQLEnv env, IdentifierConfig cfg, 120 EDbVendor vendor) { 121 InMemoryCatalogSnapshot.Builder b = InMemoryCatalogSnapshot.builder().vendor(vendor); 122 123 for (TSQLCatalog catalog : env.getCatalogList()) { 124 String catalogName = catalog.getName(); 125 CatalogQualifiedName cq = CatalogIdentifierPolicy.parse( 126 catalogName, CatalogObjectKind.CATALOG, cfg, vendor); 127 CatalogObjectId catalogId = CatalogEntries.derivedIdFor(cq); 128 b.put(CatalogEntries.builder() 129 .id(catalogId).name(cq).kind(CatalogObjectKind.CATALOG).build(), null); 130 131 for (TSQLSchema schema : catalog.getSchemaList()) { 132 String schemaName = schema.getName(); 133 String schemaQ = catalogName + "." + schemaName; 134 CatalogQualifiedName sq = CatalogIdentifierPolicy.parse( 135 schemaQ, CatalogObjectKind.SCHEMA, cfg, vendor); 136 CatalogObjectId schemaId = CatalogEntries.derivedIdFor(sq); 137 b.put(CatalogEntries.builder() 138 .id(schemaId).name(sq).kind(CatalogObjectKind.SCHEMA).build(), catalogId); 139 140 for (TSQLSchemaObject obj : schema.getSchemaObjectList()) { 141 addObject(b, obj, schemaQ, schemaId, cfg, vendor); 142 } 143 } 144 } 145 return b.materializedAtMillis(System.currentTimeMillis()).build(); 146 } 147 148 private static void addObject(InMemoryCatalogSnapshot.Builder b, 149 TSQLSchemaObject obj, String schemaQ, 150 CatalogObjectId schemaId, IdentifierConfig cfg, 151 EDbVendor vendor) { 152 ESQLDataObjectType dot = obj.getDataObjectType(); 153 String objectQ = schemaQ + "." + obj.getName(); 154 switch (dot) { 155 case dotTable: 156 if (obj instanceof TSQLTable) { 157 TSQLTable t = (TSQLTable) obj; 158 CatalogObjectKind kind = t.isView() 159 ? CatalogObjectKind.VIEW : CatalogObjectKind.TABLE; 160 CatalogQualifiedName tq = CatalogIdentifierPolicy.parse( 161 objectQ, kind, cfg, vendor); 162 CatalogObjectId tid = CatalogEntries.derivedIdFor(tq); 163 CatalogEntries.Builder eb = CatalogEntries.builder() 164 .id(tid).name(tq).kind(kind); 165 if (t.isView() && t.getDefinition() != null) { 166 eb.property("definition", t.getDefinition()); 167 } 168 b.put(eb.build(), schemaId); 169 for (TSQLColumn col : t.getColumnList()) { 170 CatalogQualifiedName colQ = CatalogIdentifierPolicy.parse( 171 objectQ + "." + col.getName(), 172 CatalogObjectKind.COLUMN, cfg, vendor); 173 CatalogEntries.Builder cb = CatalogEntries.builder() 174 .id(CatalogEntries.derivedIdFor(colQ)) 175 .name(colQ).kind(CatalogObjectKind.COLUMN); 176 if (col.getColumnDataType() != null) { 177 cb.property("dataType", col.getColumnDataType().toString()); 178 } 179 b.put(cb.build(), tid); 180 } 181 } 182 break; 183 case dotFunction: 184 emitTopLevelObject(b, objectQ, CatalogObjectKind.FUNCTION, 185 schemaId, cfg, vendor); 186 break; 187 case dotProcedure: 188 emitTopLevelObject(b, objectQ, CatalogObjectKind.PROCEDURE, 189 schemaId, cfg, vendor); 190 break; 191 case dotOraclePackage: 192 emitTopLevelObject(b, objectQ, CatalogObjectKind.PACKAGE, 193 schemaId, cfg, vendor); 194 break; 195 case dotTrigger: 196 emitTopLevelObject(b, objectQ, CatalogObjectKind.TRIGGER, 197 schemaId, cfg, vendor); 198 break; 199 case dotSynonyms: 200 if (obj instanceof TSQLSynonyms) { 201 CatalogQualifiedName synQ = CatalogIdentifierPolicy.parse( 202 objectQ, CatalogObjectKind.SYNONYM, cfg, vendor); 203 CatalogEntries.Builder sb = CatalogEntries.builder() 204 .id(CatalogEntries.derivedIdFor(synQ)) 205 .name(synQ).kind(CatalogObjectKind.SYNONYM); 206 String target = synonymTarget((TSQLSynonyms) obj); 207 if (target != null) { 208 sb.property("target", target); 209 } 210 b.put(sb.build(), schemaId); 211 } else { 212 emitTopLevelObject(b, objectQ, CatalogObjectKind.SYNONYM, 213 schemaId, cfg, vendor); 214 } 215 break; 216 case dotRoutine: 217 case dotDblink: 218 case dotDataType: 219 // These types exist in TSQLSchema but have no first-class CatalogObjectKind 220 // counterpart yet (Q16 in the design plan defers DOMAIN, EXTENSION, 221 // FOREIGN_TABLE etc. to per-stream additions). Emit an INFO diagnostic so 222 // consumers can see that something was dropped rather than silently 223 // disappearing. WARN would be too aggressive: it's expected that the 224 // first iteration of the catalog SPI doesn't represent every TSQLEnv 225 // sub-type. 226 b.addDiagnostic(CatalogDiagnostic.builder() 227 .severity(CatalogDiagnosticSeverity.INFO) 228 .code(CatalogDiagnosticCode.CATALOG_LOAD_UNSUPPORTED_KIND) 229 .message("TSQLEnv object '" + objectQ + "' of type " + dot 230 + " has no CatalogObjectKind counterpart; not surfaced in snapshot") 231 .build()); 232 break; 233 default: 234 // dotCatalog / dotSchema / dotColumn / dotParameter / dotUnknown — 235 // not first-class children of a TSQLSchema in this context. Skip 236 // silently; the runtime layer is a snapshot view, not an exhaustive 237 // enumerator of every TSQLObject subtype. 238 break; 239 } 240 } 241 242 /** 243 * Build the dotted base-target reference for a synonym from the parts it carries, 244 * preserving the original granularity (only the known source segments are joined, 245 * so an object-only target stays object-only). Returns {@code null} when the 246 * synonym has no base target. 247 * 248 * <p>This follows the single dotted-string {@code "target"} convention used by 249 * {@code ModelBackedCatalogProvider} and {@code SynonymModel.targetQualifiedName}. 250 * Like that convention, a segment that contains a literal dot must already carry 251 * SQL quotes ({@code "a.b"}) to survive the quote-aware re-parse on read-back; an 252 * unquoted embedded dot is ambiguous and will be re-split into multiple segments. 253 */ 254 private static String synonymTarget(TSQLSynonyms syn) { 255 if (!syn.hasBaseTarget()) { 256 return null; 257 } 258 StringBuilder sb = new StringBuilder(); 259 if (syn.getSourceDatabase() != null && !syn.getSourceDatabase().isEmpty()) { 260 sb.append(syn.getSourceDatabase()).append('.'); 261 } 262 if (syn.getSourceSchema() != null && !syn.getSourceSchema().isEmpty()) { 263 sb.append(syn.getSourceSchema()).append('.'); 264 } 265 sb.append(syn.getSourceName()); 266 return sb.toString(); 267 } 268 269 private static void emitTopLevelObject(InMemoryCatalogSnapshot.Builder b, 270 String objectQ, CatalogObjectKind kind, 271 CatalogObjectId schemaId, IdentifierConfig cfg, 272 EDbVendor vendor) { 273 CatalogQualifiedName q = CatalogIdentifierPolicy.parse(objectQ, kind, cfg, vendor); 274 b.put(CatalogEntries.builder() 275 .id(CatalogEntries.derivedIdFor(q)) 276 .name(q).kind(kind).build(), 277 schemaId); 278 } 279}