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}