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}