001package gudusoft.gsqlparser.resolver2.format; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.TAttributeNode; 005import gudusoft.gsqlparser.nodes.TObjectName; 006import gudusoft.gsqlparser.nodes.TTable; 007import gudusoft.gsqlparser.resolver2.ScopeBuildResult; 008import gudusoft.gsqlparser.resolver2.TSQLResolver2; 009import gudusoft.gsqlparser.resolver2.model.ColumnSource; 010import gudusoft.gsqlparser.resolver2.namespace.INamespace; 011import gudusoft.gsqlparser.resolver2.namespace.TableNamespace; 012import gudusoft.gsqlparser.sqlenv.CanonKey; 013import gudusoft.gsqlparser.sqlenv.CollatorProvider; 014import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType; 015import gudusoft.gsqlparser.sqlenv.IdentifierInputForm; 016import gudusoft.gsqlparser.sqlenv.IdentifierProfile; 017import gudusoft.gsqlparser.sqlenv.IdentifierService; 018import gudusoft.gsqlparser.sqlenv.TSQLTable; 019 020import java.util.ArrayList; 021import java.util.Collection; 022import java.util.Collections; 023import java.util.IdentityHashMap; 024import java.util.LinkedHashMap; 025import java.util.LinkedHashSet; 026import java.util.List; 027import java.util.Map; 028import java.util.Set; 029 030/** 031 * Decides which {@code t.*} entries of a {@code Tables:}/{@code Fields:} listing say nothing 032 * the listing does not already say — from the identity of what was emitted, never from the 033 * rendered text. 034 * 035 * <p>The rule (owner's, 2026-09-15): a star entry is listed only while it carries information. 036 * Once every column of the table it stands for has reached the output under the same table 037 * prefix, the star adds nothing and is dropped. Anything that cannot be PROVEN is kept — a 038 * redundant star is a cosmetic extra, while a star dropped in error silently deletes the only 039 * record that the table has further columns. 040 * 041 * <p>Why identity and not text. The namespace keys are the names as the catalog STORES them 042 * and a rendered field is the spelling as the query WROTE it, minus the delimiters the display 043 * layer strips. Oracle shows the two forms disagreeing in both directions: {@code SELECT c123} 044 * renders {@code c123} against a stored {@code C123}, and 045 * {@code CREATE TABLE t(foo int, "foo" int)} stores {@code FOO} and {@code foo}, both of which 046 * render as {@code foo}. No single normalization of the rendered text recovers both, measured. 047 * So every emitted entry is recorded with what produced it and joined to the namespace by one of 048 * three stable identities: 049 * 050 * <ul> 051 * <li>the reference's own {@link ColumnSource}, when it IS the namespace's object (explicit 052 * references to a FROM table carry exactly that object);</li> 053 * <li>the {@link ColumnSource} resolver2 expanded a star into, recorded by 054 * {@link TSQLResolver2#getStarExpansionSource(TAttributeNode)} at expansion time;</li> 055 * <li>for a reference the resolver attributes to the star's table — the same {@link TTable} 056 * occurrence, its {@code getFinalTable()}, or another occurrence of the same catalog 057 * {@link TSQLTable} such as the {@code CREATE TABLE} that declared it — the namespace's 058 * own lookup of the reference's lexical spelling, {@code SQL_LEXICAL} against 059 * {@code CATALOG_STORED} through {@link IdentifierService}.</li> 060 * </ul> 061 * 062 * <p>Two entries that render to the same text are one line in the output. A column that only 063 * reached the output as such a shared line has not reached it in any way a reader can see, so it 064 * does not count, and the star survives to say that the table has more. 065 * 066 * <p>One star text can stand for several table occurrences ({@code SELECT * FROM t; SELECT t.* 067 * FROM t}); it is dropped only when every one of them is proven. And the star texts are kept 068 * apart from the column entries by the callers, so a column whose bare display name happens to 069 * be {@code *} can never be reached by the pruning at all. 070 */ 071public final class StarRedundancyLedger { 072 073 private static final class StarEntry { 074 final String text; 075 final String prefix; 076 final TTable table; 077 078 StarEntry(String text, String prefix, TTable table) { 079 this.text = text; 080 this.prefix = prefix; 081 this.table = table; 082 } 083 } 084 085 private static final class Emitted { 086 final String prefix; 087 final String rendered; 088 final TObjectName column; 089 final TTable table; 090 final TAttributeNode attribute; 091 final ColumnSource source; 092 093 Emitted(String prefix, String rendered, TObjectName column, TTable table, TAttributeNode attribute, 094 ColumnSource source) { 095 this.prefix = prefix; 096 this.rendered = rendered; 097 this.column = column; 098 this.table = table; 099 this.attribute = attribute; 100 this.source = source; 101 } 102 } 103 104 /** Marks a rendered line that an entry NOT attributable to the star's table also produced. */ 105 private static final Object FOREIGN = new Object(); 106 107 private final ScopeBuildResult buildResult; 108 private final TSQLResolver2 resolver; 109 private final IdentifierService identifiers; 110 private final List<StarEntry> stars = new ArrayList<StarEntry>(); 111 /** 112 * Every recorded entry, in one list: a collision is a property of the WHOLE rendered line, 113 * and two lines can be equal while their recorded prefixes differ (a table {@code x} with a 114 * member {@code "y.z"} and a table {@code "x.y"} with a member {@code z} both print 115 * {@code x.y.z} once the display layer has dropped the delimiters). 116 */ 117 private final List<Emitted> emitted = new ArrayList<Emitted>(); 118 119 /** 120 * @param resolver the resolver whose scope build the output was produced from; {@code null} 121 * when there is none (the legacy resolver), in which case nothing can be 122 * proven and every star is kept 123 * @param vendor the vendor whose identifier rules apply; {@code null} disables the lexical 124 * join, which again only keeps stars 125 */ 126 public StarRedundancyLedger(TSQLResolver2 resolver, EDbVendor vendor) { 127 this.resolver = resolver; 128 this.buildResult = resolver == null ? null : resolver.getScopeBuildResult(); 129 this.identifiers = identifierServiceFor(resolver, vendor); 130 } 131 132 /** 133 * The identifier rules the lexical join runs under. The environment's own service when there 134 * is one — it carries the configured SQL Server collation, and the namespace keys were made 135 * by it — otherwise a service built the way {@code TSQLEnv} builds its own, collator included, 136 * so a SQL Server comparison never falls back to plain Unicode folding. 137 */ 138 private static IdentifierService identifierServiceFor(TSQLResolver2 resolver, EDbVendor vendor) { 139 if (resolver != null && resolver.getSqlEnv() != null 140 && resolver.getSqlEnv().getIdentifierService() != null) { 141 return resolver.getSqlEnv().getIdentifierService(); 142 } 143 if (vendor == null) return null; 144 CollatorProvider collator = (vendor == EDbVendor.dbvmssql || vendor == EDbVendor.dbvazuresql) 145 ? new CollatorProvider() : null; 146 return new IdentifierService( 147 IdentifierProfile.forVendor(vendor, IdentifierProfile.VendorFlags.defaults()), collator); 148 } 149 150 /** A star entry that would be written as {@code text}, standing for {@code table}, under {@code prefix}. */ 151 public void recordStar(String text, String prefix, TTable table) { 152 if (text == null || prefix == null) return; 153 stars.add(new StarEntry(text, prefix, table)); 154 } 155 156 /** A column entry written as {@code rendered} for the reference {@code column}, shown under {@code prefix} as a column of {@code renderedTable}. */ 157 public void recordColumn(String prefix, String rendered, TObjectName column, TTable renderedTable) { 158 if (prefix == null || rendered == null || column == null) return; 159 emitted.add(new Emitted(prefix, rendered, column, renderedTable, null, null)); 160 } 161 162 /** A column entry written as {@code rendered} from a star's expanded attribute. */ 163 public void recordExpandedAttribute(String prefix, String rendered, TAttributeNode attribute) { 164 if (prefix == null || rendered == null || attribute == null) return; 165 emitted.add(new Emitted(prefix, rendered, null, null, attribute, null)); 166 } 167 168 /** A column entry written as {@code rendered} directly for a namespace member. */ 169 public void recordNamespaceColumn(String prefix, String rendered, ColumnSource source) { 170 if (prefix == null || rendered == null || source == null) return; 171 emitted.add(new Emitted(prefix, rendered, null, null, null, source)); 172 } 173 174 /** 175 * A line the writer put into its output WITHOUT a producer this ledger can name — an orphan's 176 * {@code missed.} line, a DDL constraint column, a PIVOT output column. It can prove nothing, 177 * but it still folds with recorded lines in the writer's de-duplication, and a member whose 178 * only line it swallowed has not reached the reader. So it is bucketed like every other line. 179 */ 180 public void recordForeignLine(String rendered) { 181 if (rendered == null) return; 182 emitted.add(new Emitted("", rendered, null, null, null, null)); 183 } 184 185 public boolean hasStars() { 186 return !stars.isEmpty(); 187 } 188 189 public void clear() { 190 stars.clear(); 191 emitted.clear(); 192 } 193 194 /** 195 * The star texts that every recorded occurrence proves redundant. 196 * 197 * @param finalLines every column line the writer has already put into its output, whether or 198 * not it was recorded here. A line that reached the output without passing 199 * through this ledger (an orphan {@code missed.} line, a DDL constraint 200 * column) still folds with the recorded lines in the writer's 201 * de-duplication, so it takes part in the collision accounting as a line 202 * that stands for nothing this ledger can name. 203 */ 204 public Set<String> redundantStarEntries(Collection<String> finalLines) { 205 Set<String> unrecorded = new java.util.TreeSet<String>(String.CASE_INSENSITIVE_ORDER); 206 if (finalLines != null) { 207 unrecorded.addAll(finalLines); 208 for (Emitted entry : emitted) unrecorded.remove(entry.rendered); 209 } 210 Map<String, Boolean> verdict = new LinkedHashMap<String, Boolean>(); 211 for (StarEntry star : stars) { 212 boolean proven = isRedundant(star, unrecorded); 213 Boolean earlier = verdict.get(star.text); 214 verdict.put(star.text, earlier == null ? proven : (earlier && proven)); 215 } 216 Set<String> redundant = new LinkedHashSet<String>(); 217 for (Map.Entry<String, Boolean> entry : verdict.entrySet()) { 218 if (entry.getValue()) redundant.add(entry.getKey()); 219 } 220 return redundant; 221 } 222 223 private boolean isRedundant(StarEntry star, Set<String> unrecordedLines) { 224 if (buildResult == null || star.table == null) return false; 225 INamespace namespace = buildResult.getNamespaceForTable(star.table); 226 if (namespace == null) return false; 227 Map<String, ColumnSource> declared = namespace.getAllColumnSources(); 228 if (declared == null || declared.isEmpty()) return false; 229 230 // The column list has to be KNOWN and COMPLETE. A table without metadata still gets a 231 // namespace — the star placeholder itself, plus whatever the query names, all 232 // inferred_from_usage — and that shape says nothing about what the table really has. 233 // Nor does "every member is definite": a table function with a partial alias list 234 // (`FROM my_func() AS f("*")`) holds one definite member while the namespace itself 235 // stays non-authoritative because the function's remaining outputs are unknown. Only 236 // the namespace can say whether it enumerates the whole schema, and it says so with 237 // MetadataState.FOUND. 238 if (namespace.getMetadataState() != gudusoft.gsqlparser.resolver2.namespace.MetadataState.FOUND) { 239 return false; 240 } 241 Set<ColumnSource> wanted = identitySet(); 242 for (ColumnSource source : declared.values()) { 243 if (source == null || source.isInferred()) return false; 244 wanted.add(source); 245 } 246 247 // EVERY line the writer will print goes into a bucket, not only the ones under this 248 // star's prefix: a line only counts for a member if NOTHING else prints the same line. 249 // Two catalog tables can render to one prefix (Oracle `t` and `"t"`), prefixes can differ 250 // only by case, and equal lines can come from different prefixes altogether once the 251 // display layer has dropped delimiters (`x."y.z"` and `"x.y".z` both print `x.y.z`). An 252 // entry that cannot be attributed to a member of this table — including every entry 253 // under another prefix, and every line that never passed through the ledger — marks its 254 // line as shared, so no member can be counted as visible through it. Buckets use the 255 // writers' equality (both de-duplicate lines case-insensitively). 256 // non-identifier-compare: the writers' rendered-line de-duplication, mirrored 257 Map<String, Set<Object>> byRendered = 258 new java.util.TreeMap<String, Set<Object>>(String.CASE_INSENSITIVE_ORDER); 259 for (Emitted entry : emitted) { 260 boolean underThisPrefix = String.CASE_INSENSITIVE_ORDER.compare(entry.prefix, star.prefix) == 0; 261 ColumnSource source = underThisPrefix ? attribute(entry, star.table, declared, wanted) : null; 262 Set<Object> sharing = byRendered.get(entry.rendered); 263 if (sharing == null) { 264 sharing = Collections.newSetFromMap(new IdentityHashMap<Object, Boolean>()); 265 byRendered.put(entry.rendered, sharing); 266 } 267 sharing.add(source == null ? FOREIGN : source); 268 } 269 for (String line : unrecordedLines) { 270 Set<Object> sharing = byRendered.get(line); 271 if (sharing == null) { 272 sharing = Collections.newSetFromMap(new IdentityHashMap<Object, Boolean>()); 273 byRendered.put(line, sharing); 274 } 275 sharing.add(FOREIGN); 276 } 277 // Every star text is a printed line too (a retained `"x.Y".*` prints `x.Y.*`, the same 278 // line as table x's member named `Y.*`), so it shares like any other line. 279 for (StarEntry other : stars) { 280 Set<Object> sharing = byRendered.get(other.text); 281 if (sharing == null) { 282 sharing = Collections.newSetFromMap(new IdentityHashMap<Object, Boolean>()); 283 byRendered.put(other.text, sharing); 284 } 285 sharing.add(FOREIGN); 286 } 287 288 Set<ColumnSource> present = identitySet(); 289 for (Set<Object> sharing : byRendered.values()) { 290 if (sharing.size() != 1) continue; 291 Object only = sharing.iterator().next(); 292 if (only instanceof ColumnSource) present.add((ColumnSource) only); 293 } 294 return present.size() == wanted.size(); 295 } 296 297 /** 298 * The namespace member an emitted entry stands for, or {@code null} when that cannot be 299 * established. Only members of {@code wanted} are ever returned. 300 */ 301 private ColumnSource attribute(Emitted entry, TTable starTable, Map<String, ColumnSource> declared, 302 Set<ColumnSource> wanted) { 303 // A line with leading or trailing whitespace (a member named `"z "`) is not printed as 304 // recorded — the writer trims its text — so it cannot vouch for the member. 305 // non-identifier-compare: rendered text vs what the writer prints 306 if (!entry.rendered.equals(entry.rendered.trim())) return null; 307 // A name containing a line break is printed across two physical lines by both writers, 308 // so no single printed line is the member's. 309 if (entry.rendered.indexOf('\n') >= 0 || entry.rendered.indexOf('\r') >= 0) return null; 310 if (entry.source != null) { 311 return wanted.contains(entry.source) ? entry.source : null; 312 } 313 if (entry.attribute != null) { 314 ColumnSource source = resolver == null ? null : resolver.getStarExpansionSource(entry.attribute); 315 return source != null && wanted.contains(source) ? source : null; 316 } 317 if (entry.column == null) return null; 318 319 ColumnSource own = entry.column.getColumnSource(); 320 if (own != null && wanted.contains(own)) return own; 321 322 // The resolver attributed this reference to the star's table but handed it a 323 // ColumnSource of its own (a reference traced through a subquery star carries a copy, 324 // measured), or none at all (a CREATE TABLE column definition). The reference's lexical 325 // spelling then selects the member through the namespace's own lookup rule. 326 boolean sameTable = entry.table == starTable 327 || (own != null && own.getFinalTable() == starTable) 328 || sameCatalogTable(entry.table, starTable); 329 if (!sameTable) return null; 330 return lookupLexical(declared, entry.column.getColumnNameOnly()); 331 } 332 333 private boolean sameCatalogTable(TTable a, TTable b) { 334 TSQLTable catalogA = catalogTableOf(a); 335 return catalogA != null && catalogA == catalogTableOf(b); 336 } 337 338 private TSQLTable catalogTableOf(TTable table) { 339 if (table == null) return null; 340 if (table.getResolvedTable() != null) return table.getResolvedTable(); 341 INamespace namespace = buildResult.getNamespaceForTable(table); 342 if (namespace instanceof TableNamespace) { 343 return ((TableNamespace) namespace).getResolvedTable(); 344 } 345 return null; 346 } 347 348 private ColumnSource lookupLexical(Map<String, ColumnSource> declared, String lexical) { 349 if (identifiers == null || lexical == null || lexical.isEmpty()) return null; 350 CanonKey wanted = identifiers.canonKeyOf(lexical, ESQLDataObjectType.dotColumn, 351 IdentifierInputForm.SQL_LEXICAL); 352 for (Map.Entry<String, ColumnSource> entry : declared.entrySet()) { 353 String stored = entry.getKey(); 354 if (stored == null || stored.isEmpty() || entry.getValue() == null) continue; 355 CanonKey key = identifiers.canonKeyOf(stored, ESQLDataObjectType.dotColumn, 356 IdentifierInputForm.CATALOG_STORED); 357 if (wanted.equals(key)) return entry.getValue(); 358 } 359 return null; 360 } 361 362 private static Set<ColumnSource> identitySet() { 363 return Collections.newSetFromMap(new IdentityHashMap<ColumnSource, Boolean>()); 364 } 365}