001package gudusoft.gsqlparser.catalog.runtime; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.catalog.diagnostic.CatalogException; 005import gudusoft.gsqlparser.catalog.input.model.CatalogModel; 006import gudusoft.gsqlparser.catalog.input.model.ColumnModel; 007import gudusoft.gsqlparser.catalog.input.model.ConstraintModel; 008import gudusoft.gsqlparser.catalog.input.model.IdentifierConfig; 009import gudusoft.gsqlparser.catalog.input.model.IndexModel; 010import gudusoft.gsqlparser.catalog.input.model.RoutineModel; 011import gudusoft.gsqlparser.catalog.input.model.SchemaModel; 012import gudusoft.gsqlparser.catalog.input.model.SequenceModel; 013import gudusoft.gsqlparser.catalog.input.model.SynonymModel; 014import gudusoft.gsqlparser.catalog.input.model.TableModel; 015import gudusoft.gsqlparser.catalog.input.model.UnifiedCatalogModel; 016import gudusoft.gsqlparser.catalog.input.model.ViewModel; 017import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType; 018 019/** 020 * {@link CatalogProvider} that wraps a single {@link UnifiedCatalogModel}, exposing it as a 021 * fully-materialized snapshot. Used by JSON / YAML / DDL / dbt / dump readers — every 022 * static-file source funnels here. 023 * 024 * <p>Plan §5.1 / §6. The model is translated into an {@link InMemoryCatalogSnapshot} once 025 * during {@link #open(CatalogProviderConfig)}; subsequent {@link #snapshot(CatalogQuery)} 026 * calls return the same instance regardless of the query's narrowing options (model-backed 027 * sources are always eager).</p> 028 * 029 * <p>Identifier handling routes through {@code CatalogIdentifierPolicy.parse} using the 030 * model's {@link IdentifierConfig}; per-segment qualified-name forms therefore honor every 031 * vendor rule the validator enforced at load time.</p> 032 */ 033public final class ModelBackedCatalogProvider implements CatalogProvider { 034 035 private static final CatalogProviderId ID = CatalogProviderId.of("model-backed"); 036 037 private final UnifiedCatalogModel model; 038 /** When non-null, overrides {@code model.identifierConfig()} during materialization. */ 039 private final IdentifierConfig identifierConfigOverride; 040 private CatalogSnapshot snapshot; 041 private boolean open; 042 043 public ModelBackedCatalogProvider(UnifiedCatalogModel model) { 044 this(model, null); 045 } 046 047 /** 048 * Construct a provider with an explicit {@link IdentifierConfig} override that 049 * supersedes {@code model.identifierConfig()} when the snapshot keys are built. 050 * Callers (notably {@code CatalogLoaders}) use this when {@link CatalogLoadOptions} 051 * carries an override the validator already accepted, so materialization keys 052 * agree with the policy validation ran under. 053 */ 054 public ModelBackedCatalogProvider(UnifiedCatalogModel model, IdentifierConfig identifierConfigOverride) { 055 if (model == null) { 056 throw new IllegalArgumentException("ModelBackedCatalogProvider.model is required"); 057 } 058 if (identifierConfigOverride != null 059 && identifierConfigOverride.vendor() != model.vendor()) { 060 throw new IllegalArgumentException( 061 "ModelBackedCatalogProvider.identifierConfigOverride.vendor=" 062 + identifierConfigOverride.vendor() + " does not match model.vendor=" 063 + model.vendor()); 064 } 065 this.model = model; 066 this.identifierConfigOverride = identifierConfigOverride; 067 } 068 069 @Override 070 public CatalogProviderId id() { 071 return ID; 072 } 073 074 @Override 075 public void open(CatalogProviderConfig config) throws CatalogException { 076 this.snapshot = materialize(model, identifierConfigOverride); 077 this.open = true; 078 } 079 080 @Override 081 public CatalogSnapshot snapshot(CatalogQuery query) throws CatalogException { 082 if (!open) { 083 // Eager-by-design — just materialize on first ask if open() was skipped. 084 this.snapshot = materialize(model, identifierConfigOverride); 085 this.open = true; 086 } 087 return snapshot; 088 } 089 090 @Override 091 public CatalogSnapshot refresh(CatalogQuery query) throws CatalogException { 092 this.snapshot = materialize(model, identifierConfigOverride); 093 this.open = true; 094 return snapshot; 095 } 096 097 @Override 098 public void close() throws CatalogException { 099 this.open = false; 100 } 101 102 /** 103 * Translate a {@link UnifiedCatalogModel} into a fully-materialized 104 * {@link InMemoryCatalogSnapshot}. Public so the bridge layer can call this directly 105 * when it needs a snapshot but does not want to wire a provider lifecycle. 106 */ 107 public static InMemoryCatalogSnapshot materialize(UnifiedCatalogModel model) { 108 return materialize(model, null); 109 } 110 111 /** 112 * Same as {@link #materialize(UnifiedCatalogModel)} but uses {@code identifierConfig} 113 * to build snapshot keys when non-null. Pass {@code null} to fall back to 114 * {@code model.identifierConfig()}. Used by {@code CatalogLoaders} so an explicit 115 * {@code CatalogLoadOptions.identifierConfig} flows through to materialization 116 * (otherwise the snapshot keys would diverge from the policy that validation accepted). 117 */ 118 public static InMemoryCatalogSnapshot materialize(UnifiedCatalogModel model, 119 IdentifierConfig identifierConfig) { 120 EDbVendor vendor = model.vendor(); 121 IdentifierConfig cfg = identifierConfig != null ? identifierConfig : model.identifierConfig(); 122 InMemoryCatalogSnapshot.Builder b = InMemoryCatalogSnapshot.builder().vendor(vendor); 123 124 for (CatalogModel c : model.catalogs()) { 125 // Names are parsed below as SQL text, so put each one into SQL spelling first, 126 // per the model's nameForm. Without this a catalog-stored Oracle "Orders" was keyed 127 // as ORDERS: the lazy path resolved orders and missed "Orders", the opposite of 128 // the eager path and of the database. 129 final String cN = model.lexical(ESQLDataObjectType.dotCatalog, c.name()); 130 CatalogQualifiedName catalogName = CatalogIdentifierPolicy.parse( 131 cN, CatalogObjectKind.CATALOG, cfg, vendor); 132 CatalogObjectId catalogId = CatalogEntries.derivedIdFor(catalogName); 133 b.put(CatalogEntries.builder() 134 .id(catalogId).name(catalogName).kind(CatalogObjectKind.CATALOG).build(), 135 null); 136 137 for (SchemaModel s : c.schemas()) { 138 final String sN = model.lexical(ESQLDataObjectType.dotSchema, s.name()); 139 CatalogQualifiedName schemaName = CatalogIdentifierPolicy.parse( 140 qualified(cN, sN), CatalogObjectKind.SCHEMA, cfg, vendor); 141 CatalogObjectId schemaId = CatalogEntries.derivedIdFor(schemaName); 142 b.put(CatalogEntries.builder() 143 .id(schemaId).name(schemaName).kind(CatalogObjectKind.SCHEMA).build(), 144 catalogId); 145 146 for (TableModel t : s.tables()) { 147 String tableQ = qualified(cN, sN, model.lexical(ESQLDataObjectType.dotTable, t.name())); 148 CatalogQualifiedName tableName = CatalogIdentifierPolicy.parse( 149 tableQ, CatalogObjectKind.TABLE, cfg, vendor); 150 CatalogObjectId tableId = CatalogEntries.derivedIdFor(tableName); 151 CatalogEntries.Builder tb = CatalogEntries.builder() 152 .id(tableId).name(tableName).kind(CatalogObjectKind.TABLE); 153 // Carry vendor-extension table properties (Hive/Iceberg/OpenMetadata 154 // metadata) through to the snapshot so consumers can read them. 155 for (java.util.Map.Entry<String, Object> tp : t.properties().entrySet()) { 156 tb.property(tp.getKey(), tp.getValue()); 157 } 158 b.put(tb.build(), schemaId); 159 for (ColumnModel col : t.columns()) { 160 CatalogQualifiedName colName = CatalogIdentifierPolicy.parse( 161 qualifiedColumn(tableQ, model.lexical(ESQLDataObjectType.dotColumn, col.name())), CatalogObjectKind.COLUMN, cfg, vendor); 162 b.put(CatalogEntries.builder() 163 .id(CatalogEntries.derivedIdFor(colName)) 164 .name(colName).kind(CatalogObjectKind.COLUMN) 165 .property("dataType", col.dataType()) 166 .property("nullable", col.nullable()) 167 .build(), 168 tableId); 169 } 170 for (ConstraintModel cs : t.constraints()) { 171 // Constraints are anonymous in some dialects (CHECK, NOT NULL). 172 // Synthesize a name when the model omits it so the snapshot can 173 // still record it as a child of the table. 174 String csName = cs.name() != null && !cs.name().isEmpty() 175 ? cs.name() : "__constraint_" + Integer.toHexString(System.identityHashCode(cs)); 176 CatalogQualifiedName csQName = CatalogIdentifierPolicy.parse( 177 qualifiedColumn(tableQ, model.lexical(ESQLDataObjectType.dotConstraint, csName)), CatalogObjectKind.CONSTRAINT, cfg, vendor); 178 b.put(CatalogEntries.builder() 179 .id(CatalogEntries.derivedIdFor(csQName)) 180 .name(csQName).kind(CatalogObjectKind.CONSTRAINT) 181 .property("type", cs.type()) 182 .property("columns", cs.columns()) 183 .build(), 184 tableId); 185 } 186 for (IndexModel ix : t.indexes()) { 187 CatalogQualifiedName ixName = CatalogIdentifierPolicy.parse( 188 qualifiedColumn(tableQ, model.lexical(ESQLDataObjectType.dotIndex, ix.name())), CatalogObjectKind.INDEX, cfg, vendor); 189 b.put(CatalogEntries.builder() 190 .id(CatalogEntries.derivedIdFor(ixName)) 191 .name(ixName).kind(CatalogObjectKind.INDEX) 192 .property("unique", ix.unique()) 193 .property("columns", ix.columns()) 194 .build(), 195 tableId); 196 } 197 } 198 for (ViewModel v : s.views()) { 199 String viewQ = qualified(cN, sN, model.lexical(ESQLDataObjectType.dotTable, v.name())); 200 CatalogObjectKind kind = v.materialized() 201 ? CatalogObjectKind.MATERIALIZED_VIEW : CatalogObjectKind.VIEW; 202 CatalogQualifiedName viewName = CatalogIdentifierPolicy.parse( 203 viewQ, kind, cfg, vendor); 204 CatalogObjectId viewId = CatalogEntries.derivedIdFor(viewName); 205 CatalogEntries.Builder eb = CatalogEntries.builder() 206 .id(viewId).name(viewName).kind(kind); 207 if (v.definition() != null) eb.property("definition", v.definition()); 208 b.put(eb.build(), schemaId); 209 for (ColumnModel col : v.columns()) { 210 CatalogQualifiedName colName = CatalogIdentifierPolicy.parse( 211 qualifiedColumn(viewQ, model.lexical(ESQLDataObjectType.dotColumn, col.name())), CatalogObjectKind.COLUMN, cfg, vendor); 212 b.put(CatalogEntries.builder() 213 .id(CatalogEntries.derivedIdFor(colName)) 214 .name(colName).kind(CatalogObjectKind.COLUMN) 215 .property("dataType", col.dataType()) 216 .property("nullable", col.nullable()) 217 .build(), 218 viewId); 219 } 220 } 221 for (RoutineModel r : s.routines()) { 222 CatalogQualifiedName rname = CatalogIdentifierPolicy.parse( 223 qualified(cN, sN, model.lexical(routineRole(r.kind()), r.name())), r.kind(), cfg, vendor); 224 CatalogEntries.Builder eb = CatalogEntries.builder() 225 .id(CatalogEntries.derivedIdFor(rname)).name(rname).kind(r.kind()); 226 if (r.returns() != null) eb.property("returns", r.returns()); 227 b.put(eb.build(), schemaId); 228 } 229 for (SynonymModel sy : s.synonyms()) { 230 CatalogQualifiedName syname = CatalogIdentifierPolicy.parse( 231 qualified(cN, sN, model.lexical(ESQLDataObjectType.dotSynonyms, sy.name())), 232 CatalogObjectKind.SYNONYM, cfg, vendor); 233 b.put(CatalogEntries.builder() 234 .id(CatalogEntries.derivedIdFor(syname)) 235 .name(syname).kind(CatalogObjectKind.SYNONYM) 236 .property("target", sy.targetQualifiedName()) 237 .build(), 238 schemaId); 239 } 240 for (SequenceModel sq : s.sequences()) { 241 CatalogQualifiedName sqname = CatalogIdentifierPolicy.parse( 242 qualified(cN, sN, model.lexical(ESQLDataObjectType.dotSequence, sq.name())), 243 CatalogObjectKind.SEQUENCE, cfg, vendor); 244 CatalogEntries.Builder eb = CatalogEntries.builder() 245 .id(CatalogEntries.derivedIdFor(sqname)) 246 .name(sqname).kind(CatalogObjectKind.SEQUENCE); 247 if (sq.startsWith() != null) eb.property("startsWith", sq.startsWith()); 248 if (sq.incrementBy() != null) eb.property("incrementBy", sq.incrementBy()); 249 b.put(eb.build(), schemaId); 250 } 251 } 252 } 253 return b.materializedAtMillis(System.currentTimeMillis()).build(); 254 } 255 256 private static ESQLDataObjectType routineRole(CatalogObjectKind kind) { 257 switch (kind) { 258 case FUNCTION: return ESQLDataObjectType.dotFunction; 259 case PROCEDURE: return ESQLDataObjectType.dotProcedure; 260 case PACKAGE: return ESQLDataObjectType.dotOraclePackage; 261 default: return ESQLDataObjectType.dotRoutine; 262 } 263 } 264 265 private static String qualified(String catalog, String schema) { 266 StringBuilder sb = new StringBuilder(); 267 if (catalog != null && !catalog.isEmpty()) sb.append(catalog); 268 if (schema != null && !schema.isEmpty()) { 269 if (sb.length() > 0) sb.append('.'); 270 sb.append(schema); 271 } 272 return sb.length() == 0 ? "" : sb.toString(); 273 } 274 275 private static String qualified(String catalog, String schema, String object) { 276 StringBuilder sb = new StringBuilder(); 277 if (catalog != null && !catalog.isEmpty()) sb.append(catalog).append('.'); 278 if (schema != null && !schema.isEmpty()) sb.append(schema).append('.'); 279 sb.append(object); 280 return sb.toString(); 281 } 282 283 private static String qualifiedColumn(String tableQualified, String column) { 284 return tableQualified + "." + column; 285 } 286}