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