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}