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}