001package gudusoft.gsqlparser.ir.semantic.binding;
002
003import gudusoft.gsqlparser.ETableSource;
004import gudusoft.gsqlparser.TSourceToken;
005import gudusoft.gsqlparser.nodes.TTable;
006
007/**
008 * Slice 74 — synthetic naming for anonymous FROM-clause subqueries.
009 *
010 * <p>Lives in the binding package so both {@link Resolver2NameBindingProvider}
011 * and {@code SemanticIRBuilder} (in the {@code builder} package) can reach it
012 * without a circular dependency.
013 *
014 * <p>The slice-74 decision is to <em>admit</em> FROM subqueries that lack a
015 * written alias, mirroring the parity baseline established by {@code dlineage}
016 * (which already accepts these unconditionally). The synthetic alias is keyed
017 * on the parser's start-token position so it is:
018 * <ul>
019 *   <li>deterministic — same SQL produces the same synthetic name across
020 *       builds;</li>
021 *   <li>unique per source location — two distinct anonymous subqueries
022 *       cannot share their opening-paren position, so the names cannot
023 *       collide;</li>
024 *   <li>not user-writable in normal SQL — the leading and trailing double
025 *       underscores combined with the {@code l<n>_c<n>} suffix make
026 *       accidental collisions with hand-written identifiers effectively
027 *       impossible.</li>
028 * </ul>
029 *
030 * <p>If two anonymous subqueries somehow shared a start token (parser bug),
031 * the existing {@link gudusoft.gsqlparser.ir.semantic.DiagnosticCode#DUPLICATE_FROM_SUBQUERY_ALIAS}
032 * rejecter catches the collision the same way it catches a literal
033 * user-written duplicate alias.
034 */
035public final class FromSubqueryNaming {
036
037    /** Prefix marker for synthetic alias strings; useful for diagnostics. */
038    public static final String SYNTH_PREFIX = "__subquery_";
039
040    private FromSubqueryNaming() {
041        // utility — no instances
042    }
043
044    /**
045     * Compute the synthetic alias for an unaliased FROM-clause subquery
046     * {@code TTable}. The result is keyed on the parser's start-token
047     * line/column. Callers should only invoke this for {@link TTable}
048     * nodes whose {@link TTable#getTableType()} is
049     * {@link ETableSource#subquery} and whose {@link TTable#getAliasName()}
050     * is {@code null} or empty — but the method is defensive about both.
051     *
052     * <p>Returns {@code "__subquery_anonymous__"} as a defensive fallback
053     * when the start token is missing (should not happen for a parsed AST).
054     */
055    public static String synthAliasFor(TTable t) {
056        if (t == null) return null;
057        TSourceToken st = t.getStartToken();
058        if (st != null && st.lineNo > 0) {
059            return SYNTH_PREFIX + "l" + st.lineNo + "_c" + st.columnNo + "__";
060        }
061        return SYNTH_PREFIX + "anonymous__";
062    }
063
064    /**
065     * Effective alias for any FROM-clause {@link TTable}: the SQL-written
066     * alias if non-empty, else the synthetic alias for unaliased
067     * FROM-subquery TTables, else {@code null}. Callers handling base
068     * tables fall back to {@link TTable#getName()} when this returns
069     * {@code null}.
070     */
071    public static String effectiveAliasOrNull(TTable t) {
072        if (t == null) return null;
073        String written = t.getAliasName();
074        if (written != null && !written.isEmpty()) {
075            return written;
076        }
077        if (t.getTableType() == ETableSource.subquery) {
078            return synthAliasFor(t);
079        }
080        return null;
081    }
082}