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}