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