001package gudusoft.gsqlparser.sqlenv.compat;
002
003import gudusoft.gsqlparser.catalog.runtime.CatalogEntry;
004import gudusoft.gsqlparser.catalog.runtime.CatalogIdentifierPolicy;
005import gudusoft.gsqlparser.catalog.runtime.CatalogObjectKind;
006import gudusoft.gsqlparser.catalog.runtime.CatalogQualifiedName;
007import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType;
008import gudusoft.gsqlparser.sqlenv.TSQLEnv;
009import gudusoft.gsqlparser.sqlenv.TSQLFunction;
010import gudusoft.gsqlparser.sqlenv.TSQLOraclePackage;
011import gudusoft.gsqlparser.sqlenv.TSQLProcedure;
012import gudusoft.gsqlparser.sqlenv.TSQLSchemaObject;
013import gudusoft.gsqlparser.sqlenv.TSQLSynonyms;
014import gudusoft.gsqlparser.sqlenv.TSQLTable;
015
016import java.util.Collections;
017import java.util.List;
018
019/**
020 * Materializes a runtime {@link CatalogEntry} into the legacy {@link TSQLSchemaObject}
021 * shape consumed by {@code TSQLResolver2}.
022 *
023 * <p>Plan §6 / §7.4. Mapping is driven by {@link CatalogObjectKind}; the mapper
024 * delegates to the existing public {@link TSQLEnv} mutation API (per spike T0.3
025 * inventory) so the eager bridge uses no reflection and no private-field access.</p>
026 *
027 * <p>Sequences are not first-class on {@link TSQLEnv} (no {@code dotSequence} type
028 * in {@link ESQLDataObjectType}), so {@link #toTSQLSequence} returns {@code null}.
029 * The runtime layer still represents sequences in {@link gudusoft.gsqlparser.catalog.runtime.CatalogSnapshot};
030 * callers that need them should consult the runtime directly.</p>
031 */
032public final class CatalogEntryToSQLEnvMapper {
033
034    public CatalogEntryToSQLEnvMapper() {
035    }
036
037    /**
038     * Dispatch helper: route a {@link CatalogEntry} to the right typed mapper based on its
039     * {@link CatalogObjectKind}. Kinds that do not have a {@link TSQLEnv} representation
040     * (CATALOG, SCHEMA, COLUMN, CONSTRAINT, INDEX, TYPE, MATERIALIZED_VIEW collapsed to
041     * VIEW, SEQUENCE) return {@code null}; the loader and bridge treat that as "skip".
042     */
043    public TSQLSchemaObject toSQLSchemaObject(CatalogEntry entry, TSQLEnv targetEnv) {
044        return toSQLSchemaObject(entry, targetEnv, Collections.<CatalogEntry>emptyList());
045    }
046
047    /**
048     * Same as {@link #toSQLSchemaObject(CatalogEntry, TSQLEnv)} but lets the caller
049     * supply a list of column entries that will be attached to a TABLE / VIEW /
050     * MATERIALIZED_VIEW result via {@link TSQLTable#addColumn(String)}. The legacy
051     * resolver reads {@code TSQLTable.getColumnList()} directly during the column
052     * push-down phase, so a lazy materialization that drops columns silently
053     * regresses column resolution. The lazy bridge fills this list from
054     * {@code CatalogRuntime.findChildren(parentId, COLUMN)}.
055     */
056    public TSQLSchemaObject toSQLSchemaObject(CatalogEntry entry, TSQLEnv targetEnv,
057                                              List<CatalogEntry> columnChildren) {
058        requireArgs(entry, targetEnv);
059        switch (entry.kind()) {
060            case TABLE:
061                return toTSQLTable(entry, targetEnv, columnChildren);
062            case VIEW:
063            case MATERIALIZED_VIEW:
064                return toTSQLView(entry, targetEnv, columnChildren);
065            case FUNCTION:
066                return toTSQLFunction(entry, targetEnv);
067            case PROCEDURE:
068                return toTSQLProcedure(entry, targetEnv);
069            case PACKAGE:
070                return toTSQLOraclePackage(entry, targetEnv);
071            case ROUTINE:
072                // Generic routine — TSQLEnv has no first-class type. Route to procedure
073                // since the resolver looks up routines through the procedure path.
074                return toTSQLProcedure(entry, targetEnv);
075            case SYNONYM:
076                return toTSQLSynonym(entry, targetEnv);
077            case SEQUENCE:
078                return toTSQLSequence(entry, targetEnv);
079            case TRIGGER:
080                return toTSQLTrigger(entry, targetEnv);
081            default:
082                // CATALOG / SCHEMA / COLUMN / CONSTRAINT / INDEX / TYPE — no direct
083                // TSQLEnv schema-object representation. Loader handles them via the
084                // parent table / schema mutation paths.
085                return null;
086        }
087    }
088
089    public TSQLTable toTSQLTable(CatalogEntry entry, TSQLEnv targetEnv) {
090        return toTSQLTable(entry, targetEnv, Collections.<CatalogEntry>emptyList());
091    }
092
093    /**
094     * Materialize a TABLE entry and attach the supplied column entries. The lazy bridge
095     * uses this overload so the produced {@link TSQLTable} carries a non-empty
096     * {@code getColumnList()}; the resolver's column push-down path reads that list and
097     * silently fails on empty results otherwise.
098     */
099    public TSQLTable toTSQLTable(CatalogEntry entry, TSQLEnv targetEnv,
100                                 List<CatalogEntry> columnChildren) {
101        requireArgs(entry, targetEnv);
102        requireKind(entry, CatalogObjectKind.TABLE);
103        TSQLTable t = targetEnv.addTable(rawQualified(entry.name()), false);
104        attachColumns(t, columnChildren);
105        return t;
106    }
107
108    /** Views are represented in the legacy model as {@link TSQLTable} with view metadata. */
109    public TSQLTable toTSQLView(CatalogEntry entry, TSQLEnv targetEnv) {
110        return toTSQLView(entry, targetEnv, Collections.<CatalogEntry>emptyList());
111    }
112
113    /**
114     * Same as {@link #toTSQLView(CatalogEntry, TSQLEnv)} but with explicit column entries
115     * for the lazy bridge path.
116     */
117    public TSQLTable toTSQLView(CatalogEntry entry, TSQLEnv targetEnv,
118                                List<CatalogEntry> columnChildren) {
119        requireArgs(entry, targetEnv);
120        if (entry.kind() != CatalogObjectKind.VIEW
121            && entry.kind() != CatalogObjectKind.MATERIALIZED_VIEW) {
122            throw new IllegalArgumentException(
123                "toTSQLView requires VIEW or MATERIALIZED_VIEW; got " + entry.kind());
124        }
125        TSQLTable v = targetEnv.addView(rawQualified(entry.name()), false);
126        if (v != null) {
127            Object def = entry.properties().get("definition");
128            if (def instanceof String) {
129                v.setDefinition((String) def);
130            }
131            attachColumns(v, columnChildren);
132        }
133        return v;
134    }
135
136    private static void attachColumns(TSQLTable table, List<CatalogEntry> columnChildren) {
137        if (table == null || columnChildren == null || columnChildren.isEmpty()) {
138            return;
139        }
140        for (CatalogEntry col : columnChildren) {
141            if (col == null || col.name() == null) continue;
142            // Use the local (last) segment of the qualified name as the column name —
143            // matches what the eager loader does via TSQLTable.addColumn(name).
144            String colName = col.name().localName();
145            if (colName == null || colName.isEmpty()) continue;
146            table.addColumn(colName);
147        }
148    }
149
150    public TSQLFunction toTSQLFunction(CatalogEntry entry, TSQLEnv targetEnv) {
151        requireArgs(entry, targetEnv);
152        requireKind(entry, CatalogObjectKind.FUNCTION);
153        return targetEnv.addFunction(rawQualified(entry.name()), false);
154    }
155
156    public TSQLProcedure toTSQLProcedure(CatalogEntry entry, TSQLEnv targetEnv) {
157        requireArgs(entry, targetEnv);
158        if (entry.kind() != CatalogObjectKind.PROCEDURE
159            && entry.kind() != CatalogObjectKind.ROUTINE) {
160            throw new IllegalArgumentException(
161                "toTSQLProcedure requires PROCEDURE or ROUTINE; got " + entry.kind());
162        }
163        return targetEnv.addProcedure(rawQualified(entry.name()), false);
164    }
165
166    public TSQLOraclePackage toTSQLOraclePackage(CatalogEntry entry, TSQLEnv targetEnv) {
167        requireArgs(entry, targetEnv);
168        requireKind(entry, CatalogObjectKind.PACKAGE);
169        // addOraclePackage returns TSQLProcedure in the legacy API but the underlying
170        // schema object is a TSQLOraclePackage. Route through doAddSchemaObject for the
171        // correct concrete type.
172        TSQLSchemaObject obj = targetEnv.doAddSchemaObject(
173            rawQualified(entry.name()), ESQLDataObjectType.dotOraclePackage);
174        return obj instanceof TSQLOraclePackage ? (TSQLOraclePackage) obj : null;
175    }
176
177    public TSQLSchemaObject toTSQLSynonym(CatalogEntry entry, TSQLEnv targetEnv) {
178        requireArgs(entry, targetEnv);
179        requireKind(entry, CatalogObjectKind.SYNONYM);
180        TSQLSchemaObject obj =
181            targetEnv.doAddSchemaObject(rawQualified(entry.name()), ESQLDataObjectType.dotSynonyms);
182        // Carry the base target (stored as the "target" property by the runtime
183        // provider / reverse adapter) so the synonym can be dereferenced to its base.
184        if (obj instanceof TSQLSynonyms) {
185            Object target = entry.properties() != null ? entry.properties().get("target") : null;
186            if (target instanceof String && !((String) target).isEmpty()) {
187                applySynonymTarget((TSQLSynonyms) obj, (String) target, targetEnv);
188            }
189        }
190        return obj;
191    }
192
193    private static void applySynonymTarget(TSQLSynonyms syn, String target, TSQLEnv targetEnv) {
194        List<String> segments;
195        try {
196            CatalogQualifiedName parsed = CatalogIdentifierPolicy.parse(
197                target, CatalogObjectKind.SYNONYM, null, targetEnv.getDBVendor());
198            segments = parsed.raw();
199        } catch (RuntimeException ex) {
200            return; // malformed target: leave the synonym name-only
201        }
202        int n = segments.size();
203        String sourceName = n >= 1 ? segments.get(n - 1) : null;
204        String sourceSchema = n >= 2 ? segments.get(n - 2) : null;
205        String sourceDatabase = n >= 3 ? segments.get(n - 3) : null;
206        syn.setBaseTarget(sourceDatabase, sourceSchema, sourceName);
207    }
208
209    /**
210     * Sequences are not modeled in {@link TSQLEnv} (no {@code dotSequence} member of
211     * {@link ESQLDataObjectType}). Returns {@code null} so callers can skip; the
212     * runtime layer still represents sequences in {@link gudusoft.gsqlparser.catalog.runtime.CatalogSnapshot}.
213     */
214    public TSQLSchemaObject toTSQLSequence(CatalogEntry entry, TSQLEnv targetEnv) {
215        requireArgs(entry, targetEnv);
216        requireKind(entry, CatalogObjectKind.SEQUENCE);
217        return null;
218    }
219
220    public TSQLSchemaObject toTSQLTrigger(CatalogEntry entry, TSQLEnv targetEnv) {
221        requireArgs(entry, targetEnv);
222        requireKind(entry, CatalogObjectKind.TRIGGER);
223        return targetEnv.addTrigger(rawQualified(entry.name()), false);
224    }
225
226    /**
227     * Build a dotted "[catalog.]schema.object" string from the raw segments of a
228     * {@link CatalogQualifiedName}. The legacy {@link TSQLEnv} mutation API takes the raw
229     * (un-normalized) form because it normalizes internally via {@code SQLUtil}; passing
230     * normalized segments would double-fold and confuse vendor-specific casing.
231     */
232    static String rawQualified(CatalogQualifiedName name) {
233        List<String> segs = name.raw();
234        if (segs.size() == 1) {
235            return segs.get(0);
236        }
237        StringBuilder sb = new StringBuilder();
238        for (int i = 0; i < segs.size(); i++) {
239            if (i > 0) sb.append('.');
240            sb.append(segs.get(i));
241        }
242        return sb.toString();
243    }
244
245    private static void requireArgs(CatalogEntry entry, TSQLEnv env) {
246        if (entry == null) {
247            throw new IllegalArgumentException("CatalogEntryToSQLEnvMapper: entry is required");
248        }
249        if (env == null) {
250            throw new IllegalArgumentException("CatalogEntryToSQLEnvMapper: targetEnv is required");
251        }
252        // Defensive checks for arbitrary CatalogEntry implementations: the factory
253        // (CatalogEntries.builder()) enforces these invariants, but third-party impls
254        // may not. Fail fast with a clear message instead of NPE deep inside the mapper.
255        if (entry.kind() == null) {
256            throw new IllegalArgumentException("CatalogEntryToSQLEnvMapper: entry.kind() is null");
257        }
258        if (entry.name() == null) {
259            throw new IllegalArgumentException("CatalogEntryToSQLEnvMapper: entry.name() is null");
260        }
261    }
262
263    private static void requireKind(CatalogEntry entry, CatalogObjectKind expected) {
264        if (entry.kind() != expected) {
265            throw new IllegalArgumentException(
266                "CatalogEntryToSQLEnvMapper expected " + expected + "; got " + entry.kind());
267        }
268    }
269}