001package gudusoft.gsqlparser.sqlenv.compat;
002
003import gudusoft.gsqlparser.EDbVendor;
004import gudusoft.gsqlparser.catalog.diagnostic.CatalogDiagnostic;
005import gudusoft.gsqlparser.catalog.diagnostic.CatalogDiagnosticCode;
006import gudusoft.gsqlparser.catalog.diagnostic.CatalogDiagnosticSeverity;
007import gudusoft.gsqlparser.catalog.input.CatalogLoadingMode;
008import gudusoft.gsqlparser.catalog.input.model.IdentifierConfig;
009import gudusoft.gsqlparser.catalog.runtime.CatalogEntries;
010import gudusoft.gsqlparser.catalog.runtime.CatalogIdentifierPolicy;
011import gudusoft.gsqlparser.catalog.runtime.CatalogObjectId;
012import gudusoft.gsqlparser.catalog.runtime.CatalogObjectKind;
013import gudusoft.gsqlparser.catalog.runtime.CatalogProviderConfig;
014import gudusoft.gsqlparser.catalog.runtime.CatalogQualifiedName;
015import gudusoft.gsqlparser.catalog.runtime.CatalogRuntime;
016import gudusoft.gsqlparser.catalog.runtime.CatalogSnapshot;
017import gudusoft.gsqlparser.catalog.runtime.InMemoryCatalogProvider;
018import gudusoft.gsqlparser.catalog.runtime.InMemoryCatalogSnapshot;
019import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType;
020import gudusoft.gsqlparser.sqlenv.TSQLCatalog;
021import gudusoft.gsqlparser.sqlenv.TSQLColumn;
022import gudusoft.gsqlparser.sqlenv.TSQLEnv;
023import gudusoft.gsqlparser.sqlenv.TSQLSchema;
024import gudusoft.gsqlparser.sqlenv.TSQLSchemaObject;
025import gudusoft.gsqlparser.sqlenv.TSQLSynonyms;
026import gudusoft.gsqlparser.sqlenv.TSQLTable;
027
028/**
029 * Reverse bridge: read-only {@link CatalogRuntime} view over a hand-built {@link TSQLEnv}.
030 *
031 * <p>Plan §6 / §8.5. Lets new consumers (SQLGuard, Semantic IR, anything that targets
032 * the new SPI) accept a hand-built {@link TSQLEnv} from upstream callers without
033 * re-implementing the catalog traversal each time.</p>
034 *
035 * <p>The resulting runtime is read-only at the snapshot level: subsequent mutations to
036 * the underlying {@link TSQLEnv} are not reflected, since {@link #adapt} takes a
037 * point-in-time snapshot. Callers that need fresh views should call {@link #adapt}
038 * again. The mutable {@link gudusoft.gsqlparser.catalog.runtime.CatalogOverlay} is
039 * still available on the runtime for new consumers (DDL discovered downstream).</p>
040 */
041public final class SQLEnvToCatalogRuntimeAdapter {
042
043    public SQLEnvToCatalogRuntimeAdapter() {
044    }
045
046    /**
047     * Build a {@link CatalogRuntime} that exposes the contents of {@code env} as an
048     * immutable {@link CatalogSnapshot}. The snapshot covers every catalog → schema →
049     * table/view/routine/synonym walk reachable from {@link TSQLEnv#getCatalogList()}
050     * including columns on tables and views.
051     */
052    public CatalogRuntime adapt(TSQLEnv env) {
053        if (env == null) {
054            throw new IllegalArgumentException("SQLEnvToCatalogRuntimeAdapter.adapt: env is required");
055        }
056        EDbVendor vendor = env.getDBVendor();
057        IdentifierConfig cfg = IdentifierConfig.defaultsFor(vendor);
058        CatalogSnapshot snapshot = buildSnapshot(env, cfg, vendor);
059
060        InMemoryCatalogProvider provider = new InMemoryCatalogProvider(snapshot);
061        provider.open(CatalogProviderConfig.empty());
062        return CatalogRuntime.builder()
063            .provider(provider)
064            .vendor(vendor)
065            // EAGER: the snapshot is fully materialized; the resolver answers without
066            // ever calling provider.snapshot(...) again.
067            .loadingMode(CatalogLoadingMode.EAGER)
068            .initialSnapshot(snapshot)
069            .build();
070    }
071
072    /**
073     * Same as {@link #adapt(TSQLEnv)} but with an explicit {@link IdentifierConfig}
074     * override so the snapshot keys are built under a non-default policy (e.g. MSSQL
075     * collation, MySQL {@code lower_case_table_names}). Vendor must match {@code env}.
076     */
077    public CatalogRuntime adapt(TSQLEnv env, IdentifierConfig identifierConfig) {
078        if (env == null) {
079            throw new IllegalArgumentException("SQLEnvToCatalogRuntimeAdapter.adapt: env is required");
080        }
081        if (identifierConfig == null) {
082            throw new IllegalArgumentException(
083                "SQLEnvToCatalogRuntimeAdapter.adapt: identifierConfig is required");
084        }
085        if (identifierConfig.vendor() != env.getDBVendor()) {
086            throw new IllegalArgumentException(
087                "SQLEnvToCatalogRuntimeAdapter.adapt: identifierConfig.vendor="
088                    + identifierConfig.vendor() + " does not match env.vendor=" + env.getDBVendor());
089        }
090        EDbVendor vendor = env.getDBVendor();
091        CatalogSnapshot snapshot = buildSnapshot(env, identifierConfig, vendor);
092
093        InMemoryCatalogProvider provider = new InMemoryCatalogProvider(snapshot);
094        provider.open(CatalogProviderConfig.empty());
095        return CatalogRuntime.builder()
096            .provider(provider)
097            .vendor(vendor)
098            .loadingMode(CatalogLoadingMode.EAGER)
099            .initialSnapshot(snapshot)
100            .build();
101    }
102
103    /**
104     * Build the snapshot directly without wrapping it in a runtime. Useful when callers
105     * already manage their own provider lifecycle and just want the catalog-tree walk.
106     */
107    public CatalogSnapshot snapshot(TSQLEnv env) {
108        if (env == null) {
109            throw new IllegalArgumentException(
110                "SQLEnvToCatalogRuntimeAdapter.snapshot: env is required");
111        }
112        EDbVendor vendor = env.getDBVendor();
113        IdentifierConfig cfg = IdentifierConfig.defaultsFor(vendor);
114        return buildSnapshot(env, cfg, vendor);
115    }
116
117    // ---- internals -------------------------------------------------------
118
119    private static CatalogSnapshot buildSnapshot(TSQLEnv env, IdentifierConfig cfg,
120                                                 EDbVendor vendor) {
121        InMemoryCatalogSnapshot.Builder b = InMemoryCatalogSnapshot.builder().vendor(vendor);
122
123        for (TSQLCatalog catalog : env.getCatalogList()) {
124            String catalogName = catalog.getName();
125            CatalogQualifiedName cq = CatalogIdentifierPolicy.parse(
126                catalogName, CatalogObjectKind.CATALOG, cfg, vendor);
127            CatalogObjectId catalogId = CatalogEntries.derivedIdFor(cq);
128            b.put(CatalogEntries.builder()
129                .id(catalogId).name(cq).kind(CatalogObjectKind.CATALOG).build(), null);
130
131            for (TSQLSchema schema : catalog.getSchemaList()) {
132                String schemaName = schema.getName();
133                String schemaQ = catalogName + "." + schemaName;
134                CatalogQualifiedName sq = CatalogIdentifierPolicy.parse(
135                    schemaQ, CatalogObjectKind.SCHEMA, cfg, vendor);
136                CatalogObjectId schemaId = CatalogEntries.derivedIdFor(sq);
137                b.put(CatalogEntries.builder()
138                    .id(schemaId).name(sq).kind(CatalogObjectKind.SCHEMA).build(), catalogId);
139
140                for (TSQLSchemaObject obj : schema.getSchemaObjectList()) {
141                    addObject(b, obj, schemaQ, schemaId, cfg, vendor);
142                }
143            }
144        }
145        return b.materializedAtMillis(System.currentTimeMillis()).build();
146    }
147
148    private static void addObject(InMemoryCatalogSnapshot.Builder b,
149                                  TSQLSchemaObject obj, String schemaQ,
150                                  CatalogObjectId schemaId, IdentifierConfig cfg,
151                                  EDbVendor vendor) {
152        ESQLDataObjectType dot = obj.getDataObjectType();
153        String objectQ = schemaQ + "." + obj.getName();
154        switch (dot) {
155            case dotTable:
156                if (obj instanceof TSQLTable) {
157                    TSQLTable t = (TSQLTable) obj;
158                    CatalogObjectKind kind = t.isView()
159                        ? CatalogObjectKind.VIEW : CatalogObjectKind.TABLE;
160                    CatalogQualifiedName tq = CatalogIdentifierPolicy.parse(
161                        objectQ, kind, cfg, vendor);
162                    CatalogObjectId tid = CatalogEntries.derivedIdFor(tq);
163                    CatalogEntries.Builder eb = CatalogEntries.builder()
164                        .id(tid).name(tq).kind(kind);
165                    if (t.isView() && t.getDefinition() != null) {
166                        eb.property("definition", t.getDefinition());
167                    }
168                    b.put(eb.build(), schemaId);
169                    for (TSQLColumn col : t.getColumnList()) {
170                        CatalogQualifiedName colQ = CatalogIdentifierPolicy.parse(
171                            objectQ + "." + col.getName(),
172                            CatalogObjectKind.COLUMN, cfg, vendor);
173                        CatalogEntries.Builder cb = CatalogEntries.builder()
174                            .id(CatalogEntries.derivedIdFor(colQ))
175                            .name(colQ).kind(CatalogObjectKind.COLUMN);
176                        if (col.getColumnDataType() != null) {
177                            cb.property("dataType", col.getColumnDataType().toString());
178                        }
179                        b.put(cb.build(), tid);
180                    }
181                }
182                break;
183            case dotFunction:
184                emitTopLevelObject(b, objectQ, CatalogObjectKind.FUNCTION,
185                    schemaId, cfg, vendor);
186                break;
187            case dotProcedure:
188                emitTopLevelObject(b, objectQ, CatalogObjectKind.PROCEDURE,
189                    schemaId, cfg, vendor);
190                break;
191            case dotOraclePackage:
192                emitTopLevelObject(b, objectQ, CatalogObjectKind.PACKAGE,
193                    schemaId, cfg, vendor);
194                break;
195            case dotTrigger:
196                emitTopLevelObject(b, objectQ, CatalogObjectKind.TRIGGER,
197                    schemaId, cfg, vendor);
198                break;
199            case dotSynonyms:
200                if (obj instanceof TSQLSynonyms) {
201                    CatalogQualifiedName synQ = CatalogIdentifierPolicy.parse(
202                        objectQ, CatalogObjectKind.SYNONYM, cfg, vendor);
203                    CatalogEntries.Builder sb = CatalogEntries.builder()
204                        .id(CatalogEntries.derivedIdFor(synQ))
205                        .name(synQ).kind(CatalogObjectKind.SYNONYM);
206                    String target = synonymTarget((TSQLSynonyms) obj);
207                    if (target != null) {
208                        sb.property("target", target);
209                    }
210                    b.put(sb.build(), schemaId);
211                } else {
212                    emitTopLevelObject(b, objectQ, CatalogObjectKind.SYNONYM,
213                        schemaId, cfg, vendor);
214                }
215                break;
216            case dotRoutine:
217            case dotDblink:
218            case dotDataType:
219                // These types exist in TSQLSchema but have no first-class CatalogObjectKind
220                // counterpart yet (Q16 in the design plan defers DOMAIN, EXTENSION,
221                // FOREIGN_TABLE etc. to per-stream additions). Emit an INFO diagnostic so
222                // consumers can see that something was dropped rather than silently
223                // disappearing. WARN would be too aggressive: it's expected that the
224                // first iteration of the catalog SPI doesn't represent every TSQLEnv
225                // sub-type.
226                b.addDiagnostic(CatalogDiagnostic.builder()
227                    .severity(CatalogDiagnosticSeverity.INFO)
228                    .code(CatalogDiagnosticCode.CATALOG_LOAD_UNSUPPORTED_KIND)
229                    .message("TSQLEnv object '" + objectQ + "' of type " + dot
230                        + " has no CatalogObjectKind counterpart; not surfaced in snapshot")
231                    .build());
232                break;
233            default:
234                // dotCatalog / dotSchema / dotColumn / dotParameter / dotUnknown —
235                // not first-class children of a TSQLSchema in this context. Skip
236                // silently; the runtime layer is a snapshot view, not an exhaustive
237                // enumerator of every TSQLObject subtype.
238                break;
239        }
240    }
241
242    /**
243     * Build the dotted base-target reference for a synonym from the parts it carries,
244     * preserving the original granularity (only the known source segments are joined,
245     * so an object-only target stays object-only). Returns {@code null} when the
246     * synonym has no base target.
247     *
248     * <p>This follows the single dotted-string {@code "target"} convention used by
249     * {@code ModelBackedCatalogProvider} and {@code SynonymModel.targetQualifiedName}.
250     * Like that convention, a segment that contains a literal dot must already carry
251     * SQL quotes ({@code "a.b"}) to survive the quote-aware re-parse on read-back; an
252     * unquoted embedded dot is ambiguous and will be re-split into multiple segments.
253     */
254    private static String synonymTarget(TSQLSynonyms syn) {
255        if (!syn.hasBaseTarget()) {
256            return null;
257        }
258        StringBuilder sb = new StringBuilder();
259        if (syn.getSourceDatabase() != null && !syn.getSourceDatabase().isEmpty()) {
260            sb.append(syn.getSourceDatabase()).append('.');
261        }
262        if (syn.getSourceSchema() != null && !syn.getSourceSchema().isEmpty()) {
263            sb.append(syn.getSourceSchema()).append('.');
264        }
265        sb.append(syn.getSourceName());
266        return sb.toString();
267    }
268
269    private static void emitTopLevelObject(InMemoryCatalogSnapshot.Builder b,
270                                           String objectQ, CatalogObjectKind kind,
271                                           CatalogObjectId schemaId, IdentifierConfig cfg,
272                                           EDbVendor vendor) {
273        CatalogQualifiedName q = CatalogIdentifierPolicy.parse(objectQ, kind, cfg, vendor);
274        b.put(CatalogEntries.builder()
275            .id(CatalogEntries.derivedIdFor(q))
276            .name(q).kind(kind).build(),
277            schemaId);
278    }
279}