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