001package gudusoft.gsqlparser.sqlenv.compat; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.catalog.diagnostic.CatalogDiagnostic; 005import gudusoft.gsqlparser.catalog.diagnostic.CatalogDiagnosticSink; 006import gudusoft.gsqlparser.catalog.input.model.IdentifierConfig; 007import gudusoft.gsqlparser.catalog.runtime.CatalogContext; 008import gudusoft.gsqlparser.catalog.runtime.CatalogEntry; 009import gudusoft.gsqlparser.catalog.runtime.CatalogIdentifierPolicy; 010import gudusoft.gsqlparser.catalog.runtime.CatalogObjectKind; 011import gudusoft.gsqlparser.catalog.runtime.CatalogQualifiedName; 012import gudusoft.gsqlparser.catalog.runtime.CatalogResolutionResult; 013import gudusoft.gsqlparser.catalog.runtime.CatalogResolver; 014import gudusoft.gsqlparser.catalog.runtime.CatalogRuntime; 015import gudusoft.gsqlparser.catalog.runtime.CatalogSearchPath; 016import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType; 017import gudusoft.gsqlparser.sqlenv.TSQLCatalog; 018import gudusoft.gsqlparser.sqlenv.TSQLEnv; 019import gudusoft.gsqlparser.sqlenv.TSQLSchemaObject; 020import gudusoft.gsqlparser.sqlenv.catalog.ICatalogProvider; 021 022import java.util.ArrayList; 023import java.util.List; 024 025/** 026 * Lazy bridge from the legacy {@link ICatalogProvider} hook to a new-world 027 * {@link CatalogRuntime}. Plan §5.3 / §8.3. 028 * 029 * <p>Read path: {@link #findObject} delegates to the wrapped provider first; on miss it asks 030 * the runtime resolver, materializes the hit via {@link CatalogEntryToSQLEnvMapper}, writes 031 * the resulting {@link TSQLSchemaObject} into the delegate's overlay so subsequent lookups 032 * within the same parse never re-cross the bridge, and returns it. On miss-after-runtime 033 * the method returns {@code null} (preserves today's "missing object" semantics).</p> 034 * 035 * <p>Write path: {@link #addObject} writes straight to the delegate. DDL discovered 036 * mid-parse and temp-table registrations therefore go to the existing legacy overlay 037 * (the {@code CatalogStoreProvider} default for a {@link TSQLEnv}); they never touch the 038 * immutable {@code CatalogSnapshot} owned by the runtime — the runtime overlay stays 039 * untouched here on purpose, the bridge's job is to feed the legacy lookup path.</p> 040 * 041 * <p>All other {@link ICatalogProvider} methods delegate verbatim to the wrapped provider. 042 * Construction validates that the runtime, delegate, mapper and target env are non-null. 043 * Lookups silently return {@code null} on null arguments to mirror the legacy contract: 044 * {@code TSQLEnv.doSearchSchemaObject} only invokes the provider when at least three 045 * normalized segments are present, but defensive null handling here matches the rest of 046 * the {@link ICatalogProvider} contract.</p> 047 */ 048public final class CatalogBackedCatalogProvider implements ICatalogProvider { 049 050 private final CatalogRuntime runtime; 051 private final ICatalogProvider delegate; 052 private final CatalogEntryToSQLEnvMapper mapper; 053 private final TSQLEnv targetEnv; 054 private final IdentifierConfig identifierConfig; 055 private final CatalogDiagnosticSink diagnosticSink; 056 057 public CatalogBackedCatalogProvider(CatalogRuntime runtime, ICatalogProvider delegate, 058 CatalogEntryToSQLEnvMapper mapper, TSQLEnv targetEnv) { 059 this(runtime, delegate, mapper, targetEnv, 060 /* identifierConfig */ null, /* diagnosticSink */ null); 061 } 062 063 /** 064 * Convenience constructor without a diagnostic sink. Equivalent to 065 * {@link #CatalogBackedCatalogProvider(CatalogRuntime, ICatalogProvider, 066 * CatalogEntryToSQLEnvMapper, TSQLEnv, IdentifierConfig, CatalogDiagnosticSink)} 067 * with {@code diagnosticSink=null}. 068 */ 069 public CatalogBackedCatalogProvider(CatalogRuntime runtime, ICatalogProvider delegate, 070 CatalogEntryToSQLEnvMapper mapper, TSQLEnv targetEnv, 071 IdentifierConfig identifierConfig) { 072 this(runtime, delegate, mapper, targetEnv, identifierConfig, /* diagnosticSink */ null); 073 } 074 075 /** 076 * Construct a bridge provider with an explicit {@link IdentifierConfig} and an 077 * optional {@link CatalogDiagnosticSink}. When {@code identifierConfig} is 078 * {@code null} the bridge falls back to {@link IdentifierConfig#defaultsFor(EDbVendor)}; 079 * non-null configs are honored so snapshot keys built under (e.g.) MySQL 080 * {@code lower_case_table_names=2} or MSSQL collation overrides match at lookup time. 081 * 082 * <p>Diagnostics emitted by the runtime resolver (fetch-cap WARNs/ERRORs, partial 083 * results, fetch failures) are forwarded to {@code diagnosticSink} when configured. 084 * The {@link ICatalogProvider#findObject} contract still returns {@code null} on 085 * miss (no behavior change for legacy callers), but a configured sink lets a 086 * caller observe what the runtime saw — required for plan §10.6 fetch-cap 087 * diagnostics to be visible at the bridge layer.</p> 088 */ 089 public CatalogBackedCatalogProvider(CatalogRuntime runtime, ICatalogProvider delegate, 090 CatalogEntryToSQLEnvMapper mapper, TSQLEnv targetEnv, 091 IdentifierConfig identifierConfig, 092 CatalogDiagnosticSink diagnosticSink) { 093 if (runtime == null) { 094 throw new IllegalArgumentException( 095 "CatalogBackedCatalogProvider: runtime must not be null"); 096 } 097 if (delegate == null) { 098 throw new IllegalArgumentException( 099 "CatalogBackedCatalogProvider: delegate must not be null"); 100 } 101 if (mapper == null) { 102 throw new IllegalArgumentException( 103 "CatalogBackedCatalogProvider: mapper must not be null"); 104 } 105 if (targetEnv == null) { 106 throw new IllegalArgumentException( 107 "CatalogBackedCatalogProvider: targetEnv must not be null"); 108 } 109 if (runtime.vendor() != targetEnv.getDBVendor()) { 110 throw new IllegalArgumentException( 111 "CatalogBackedCatalogProvider: runtime.vendor=" + runtime.vendor() 112 + " does not match targetEnv.vendor=" + targetEnv.getDBVendor()); 113 } 114 if (identifierConfig != null && identifierConfig.vendor() != runtime.vendor()) { 115 throw new IllegalArgumentException( 116 "CatalogBackedCatalogProvider: identifierConfig.vendor=" + identifierConfig.vendor() 117 + " does not match runtime.vendor=" + runtime.vendor()); 118 } 119 this.runtime = runtime; 120 this.delegate = delegate; 121 this.mapper = mapper; 122 this.targetEnv = targetEnv; 123 this.identifierConfig = identifierConfig != null 124 ? identifierConfig : IdentifierConfig.defaultsFor(runtime.vendor()); 125 this.diagnosticSink = diagnosticSink; 126 } 127 128 @Override 129 public TSQLCatalog getCatalog(String catalogName) { 130 return delegate.getCatalog(catalogName); 131 } 132 133 @Override 134 public TSQLCatalog createCatalog(String catalogName) { 135 return delegate.createCatalog(catalogName); 136 } 137 138 @Override 139 public List<TSQLCatalog> getAllCatalogs() { 140 return delegate.getAllCatalogs(); 141 } 142 143 @Override 144 public String getDefaultCatalogName() { 145 return delegate.getDefaultCatalogName(); 146 } 147 148 @Override 149 public void setDefaultCatalogName(String name) { 150 delegate.setDefaultCatalogName(name); 151 } 152 153 @Override 154 public TSQLSchemaObject findObject(String catalog, String schema, String objectName, 155 ESQLDataObjectType type) { 156 // 1. Delegate first — overlay/cache/last-good answer takes priority over 157 // re-crossing the bridge. This matches the §10.5 lookup precedence: the 158 // legacy delegate's overlay is the bridge's "Phase 1" before the snapshot. 159 TSQLSchemaObject existing = delegate.findObject(catalog, schema, objectName, type); 160 if (existing != null) { 161 return existing; 162 } 163 if (objectName == null || objectName.isEmpty()) { 164 return null; 165 } 166 CatalogObjectKind kind = mapType(type); 167 if (kind == null) { 168 return null; 169 } 170 171 // 2. Build a CatalogQualifiedName from the segments TSQLEnv passes us. The 172 // segments are already normalized by IdentifierService.normalizeSegment 173 // upstream (see TSQLEnv.doSearchSchemaObject), so going through the parse 174 // path again would (a) re-normalize an already-normalized value (mostly 175 // harmless but wasteful), and (b) split on dots inside quoted-embedded-dot 176 // names like Oracle "a.b"."c" that have already been segmented correctly 177 // upstream. Use the dedicated factory that takes already-split segments. 178 // 179 // fromAlreadyNormalizedSegments only rejects null/empty inputs, and 180 // buildSegments guarantees at least one non-null/non-empty entry (the 181 // object name was non-null at the top of the method), so the factory 182 // cannot throw on this call site. If it ever does, that's a real bug 183 // in the upstream segment-builder — let it propagate so it's visible 184 // in diagnostics instead of being indistinguishable from a normal miss. 185 EDbVendor vendor = runtime.vendor(); 186 List<String> segments = buildSegments(catalog, schema, objectName); 187 CatalogQualifiedName name = 188 CatalogIdentifierPolicy.fromAlreadyNormalizedSegments(segments, kind, vendor); 189 190 // 3. Ask the runtime resolver. The resolver walks overlay → lazy cache → 191 // snapshot → on-miss provider fetch (when LAZY/AUTO) and applies the 192 // candidate-expansion ladder per plan §9.3 (defaults, search path, kind 193 // widening). The bridge is invoked only when TSQLEnv has at least three 194 // segments, so candidate expansion is mostly a no-op here — but the kind- 195 // widening step is what lets a TABLE lookup match a VIEW snapshot entry. 196 CatalogContext ctx = CatalogContext.builder() 197 .vendor(vendor) 198 .activeCatalog(catalog) 199 .activeSchema(schema) 200 .searchPath(CatalogSearchPath.empty()) 201 .identifierConfig(identifierConfig) 202 .build(); 203 CatalogResolver resolver = runtime.resolver(); 204 CatalogResolutionResult result = resolver.resolve(ctx, name); 205 // Forward diagnostics from the resolver to the configured sink (if any) so 206 // fetch-cap WARN/ERROR, partial-result INFO, and fetch-failed WARN are 207 // visible at the bridge layer. Does nothing when no sink was configured — 208 // findObject's null-on-miss contract is unaffected either way. 209 forwardDiagnostics(result); 210 if (!result.resolved() || !result.binding().isPresent()) { 211 return null; 212 } 213 214 // 4. Materialize the runtime entry into the legacy schema-object shape via 215 // the mapper. Use CatalogRuntime.findEntry so we see the actual entry from 216 // overlay/lazyCache/snapshot (the binding alone does not carry properties); 217 // pull column children too so the materialized TSQLTable carries the columns 218 // the resolver's column push-down will read. 219 CatalogQualifiedName resolvedName = result.binding().get().resolvedName(); 220 CatalogObjectKind resolvedKind = result.binding().get().kind(); 221 CatalogEntry entry = runtime.findEntry(resolvedName, resolvedKind); 222 if (entry == null) { 223 // Defensive: a resolver hit without a backing entry shouldn't happen with 224 // the in-tree CatalogRuntime, but third-party CatalogResolver impls may 225 // return a binding without populating the snapshot or lazy cache. Build a 226 // synthetic entry from the binding so the mapper still has a CatalogEntry 227 // to dispatch on. Columns will be empty for synthetic entries. 228 entry = syntheticEntry(result); 229 } 230 List<CatalogEntry> columns = (resolvedKind == CatalogObjectKind.TABLE 231 || resolvedKind == CatalogObjectKind.VIEW 232 || resolvedKind == CatalogObjectKind.MATERIALIZED_VIEW) 233 ? runtime.findChildren(entry.id(), CatalogObjectKind.COLUMN) 234 : new ArrayList<CatalogEntry>(); 235 TSQLSchemaObject materialized = mapper.toSQLSchemaObject(entry, targetEnv, columns); 236 // The mapper's addTable/addView/addProcedure paths invoke 237 // TSQLEnv.putSchemaObject which already calls catalogProvider.addObject on the 238 // delegate — no explicit cache write-back is needed here, doing one would 239 // double-insert into CatalogStore.objectsByName. 240 return materialized; 241 } 242 243 @Override 244 public void addObject(TSQLSchemaObject object) { 245 // DDL/temp-table writes must go to the delegate (the wrapped TSQLEnv's 246 // overlay), never to the immutable snapshot. The runtime's CatalogOverlay is 247 // a separate concept used only by callers that target the new SPI directly; 248 // legacy callers writing through TSQLEnv keep using the legacy overlay so 249 // their reads (which still go through the legacy code path) see their 250 // writes immediately. 251 delegate.addObject(object); 252 } 253 254 @Override 255 public boolean removeObject(TSQLSchemaObject object) { 256 return delegate.removeObject(object); 257 } 258 259 @Override 260 public void clear() { 261 delegate.clear(); 262 } 263 264 @Override 265 public int size() { 266 return delegate.size(); 267 } 268 269 /** 270 * Map a legacy {@link ESQLDataObjectType} to the new {@link CatalogObjectKind}. 271 * Returns {@code null} for kinds that have no first-class runtime representation 272 * (column, parameter, dblink, datatype, unknown) — the bridge then preserves 273 * today's "missing object" semantics by returning null from {@link #findObject}. 274 */ 275 static CatalogObjectKind mapType(ESQLDataObjectType type) { 276 if (type == null) { 277 return null; 278 } 279 switch (type) { 280 case dotTable: 281 // The runtime resolver already widens TABLE → VIEW / MATERIALIZED_VIEW 282 // (plan §9.3) so a relation reference always finds a snapshot hit 283 // regardless of whether it was registered as a table or a view. 284 return CatalogObjectKind.TABLE; 285 case dotFunction: 286 return CatalogObjectKind.FUNCTION; 287 case dotProcedure: 288 return CatalogObjectKind.PROCEDURE; 289 case dotOraclePackage: 290 return CatalogObjectKind.PACKAGE; 291 case dotTrigger: 292 return CatalogObjectKind.TRIGGER; 293 case dotSynonyms: 294 return CatalogObjectKind.SYNONYM; 295 case dotRoutine: 296 return CatalogObjectKind.ROUTINE; 297 default: 298 // dotColumn, dotParameter, dotDblink, dotDataType, dotCatalog, 299 // dotSchema, dotUnknown — no direct CatalogEntry materialization 300 // path. The legacy resolver doesn't ask the bridge for these in 301 // practice (column lookup goes through table-then-walk). 302 return null; 303 } 304 } 305 306 private void forwardDiagnostics(CatalogResolutionResult result) { 307 if (diagnosticSink == null || result == null) { 308 return; 309 } 310 List<CatalogDiagnostic> diags = result.diagnostics(); 311 if (diags == null || diags.isEmpty()) { 312 return; 313 } 314 for (CatalogDiagnostic d : diags) { 315 try { 316 diagnosticSink.accept(d); 317 } catch (RuntimeException ignored) { 318 // A misbehaving sink must not break catalog lookup. Diagnostics are 319 // best-effort — if the sink throws, fall through to null-on-miss. 320 } 321 } 322 } 323 324 /** 325 * Build a list of already-normalized segments from the three legacy {@code findObject} 326 * parameters. Drops null/empty entries so partial qualifications still produce a 327 * valid 1- or 2-segment list. The resulting segments are fed verbatim to 328 * {@link CatalogIdentifierPolicy#fromAlreadyNormalizedSegments} — no joining, no 329 * re-splitting on dots, so quoted-embedded-dot names parsed correctly upstream 330 * survive the round-trip. 331 */ 332 static List<String> buildSegments(String catalog, String schema, String object) { 333 List<String> out = new ArrayList<String>(3); 334 if (catalog != null && !catalog.isEmpty()) out.add(catalog); 335 if (schema != null && !schema.isEmpty()) out.add(schema); 336 out.add(object); 337 return out; 338 } 339 340 /** 341 * Build a synthetic {@link CatalogEntry} for a binding when the actual entry is not 342 * recoverable (e.g. third-party resolver impl that returns a binding without a 343 * matching snapshot entry). The synthetic entry carries the binding's id, name, 344 * kind, and properties — enough for the mapper to materialize the legacy object. 345 */ 346 private static CatalogEntry syntheticEntry(CatalogResolutionResult result) { 347 final gudusoft.gsqlparser.catalog.runtime.CatalogBinding b = result.binding().get(); 348 return new CatalogEntry() { 349 @Override 350 public gudusoft.gsqlparser.catalog.runtime.CatalogObjectId id() { 351 return b.objectId(); 352 } 353 354 @Override 355 public CatalogQualifiedName name() { 356 return b.resolvedName(); 357 } 358 359 @Override 360 public CatalogObjectKind kind() { 361 return b.kind(); 362 } 363 364 @Override 365 public java.util.Map<String, Object> properties() { 366 return b.properties(); 367 } 368 }; 369 } 370}