001package gudusoft.gsqlparser.resolver2.binding;
002
003import gudusoft.gsqlparser.EDbVendor;
004import gudusoft.gsqlparser.EResolverType;
005import gudusoft.gsqlparser.TGSqlParser;
006import gudusoft.gsqlparser.resolver2.TSQLResolverConfig;
007import gudusoft.gsqlparser.sqlenv.TSQLEnv;
008
009/**
010 * Public, static façade for the binding-diagnostic API.
011 *
012 * <p>Plan §5.3 / §7.3 S17: third-party consumers (SQL Guard, lineage,
013 * lint, IDE) that just want a {@link BindingResult} for a single SQL
014 * snippet can call {@link #resolve(String, EDbVendor, TSQLEnv,
015 * TSQLResolverConfig)} instead of wiring a {@link TGSqlParser} themselves.</p>
016 *
017 * <p>The façade is a thin convenience wrapper around the standard
018 * parse-and-resolve pipeline:</p>
019 *
020 * <ol>
021 *   <li>Construct a {@link TGSqlParser} for the requested vendor.</li>
022 *   <li>Apply the supplied {@link TSQLEnv} and {@link TSQLResolverConfig}
023 *       (or default empty config when {@code null}).</li>
024 *   <li>Force resolver2 ({@link EResolverType#RESOLVER2}) so the binding
025 *       post-pass has its required substrate.</li>
026 *   <li>Call {@link TGSqlParser#parse()} <em>exactly once</em>. Resolver2
027 *       is auto-invoked from inside {@code parse()} per the project
028 *       contract (CLAUDE.md §"TSQLResolver2 is Automatically Invoked"). The
029 *       façade <em>never</em> calls {@code resolver.resolve()} again —
030 *       doing so would double-mutate the AST (plan §7.5 #2).</li>
031 *   <li>Return the {@link BindingResult} produced by the post-pass; on
032 *       syntax error, returns the empty result.</li>
033 * </ol>
034 *
035 * <p>If the supplied config does not enable any binding flag, the result
036 * is non-null and empty — same as direct parser usage. To get diagnostics,
037 * supply a config with at least
038 * {@link TSQLResolverConfig#setEmitBindingDiagnostics(boolean)} on.</p>
039 *
040 * <p>Thread-safety: stateless. Each call constructs its own parser.</p>
041 *
042 * @since 4.1
043 */
044public final class SqlBinding {
045
046    private SqlBinding() {
047        // Static façade — not instantiable.
048    }
049
050    /**
051     * Parse and resolve {@code sql} against the supplied vendor / catalog,
052     * returning the binding result.
053     *
054     * @param sql    the SQL text; must not be {@code null}
055     * @param vendor the SQL dialect; must not be {@code null}
056     * @param env    catalog metadata supplied to the resolver. May be
057     *               {@code null}; in that case the parser is left without
058     *               authoritative metadata and most diagnostics will report
059     *               {@code METADATA_UNAVAILABLE} rather than
060     *               {@code UNKNOWN_COLUMN} (plan §5.5).
061     * @param config resolver configuration. May be {@code null}; in that
062     *               case a default config is used (no binding diagnostics
063     *               will be emitted because all flags default off).
064     * @return the {@link BindingResult}; never {@code null}, even when
065     *         parsing fails (returns {@link BindingResult#empty()}).
066     */
067    public static BindingResult resolve(String sql,
068                                        EDbVendor vendor,
069                                        TSQLEnv env,
070                                        TSQLResolverConfig config) {
071        if (sql == null) {
072            throw new IllegalArgumentException("sql must not be null");
073        }
074        if (vendor == null) {
075            throw new IllegalArgumentException("vendor must not be null");
076        }
077
078        TGSqlParser parser = new TGSqlParser(vendor);
079        // Force resolver2 — the binding post-pass is wired into resolver2
080        // and only runs when EResolverType.RESOLVER2 is selected. Honoring
081        // a caller's overridden TBaseType state would silently swallow
082        // diagnostics if the caller had flipped the global flag.
083        parser.setResolverType(EResolverType.RESOLVER2);
084        if (env != null) {
085            parser.setSqlEnv(env);
086        }
087        if (config != null) {
088            parser.setResolver2Config(config);
089        }
090        parser.sqltext = sql;
091        // parse() auto-invokes resolver2.resolve() and the binding post-pass
092        // exactly once (TSQLResolver2.resolve, plan §5.6). The façade never
093        // calls resolver.resolve() again — that would double-mutate the AST
094        // per plan §7.5 #2 and CLAUDE.md "TSQLResolver2 is Automatically
095        // Invoked During parse()".
096        int rc = parser.parse();
097        if (rc != 0) {
098            // Syntax error — accessors are contracted to return empty
099            // (plan §5.6.9). Mirror that here.
100            return BindingResult.empty();
101        }
102        return parser.getBindingResult();
103    }
104}