Add text-to-sql module: schema prompt through a restricted role, JSqlParser guard, read-only role with column grants and timeout, row cap, execution-based evaluation on a real PostgreSQL

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JXVi2GMQ7bR5EmbUFdDj7N
This commit is contained in:
Claude
2026-10-09 07:22:28 +00:00
parent d67ac0630b
commit 85a3359186
26 changed files with 1179 additions and 0 deletions
@@ -0,0 +1,52 @@
package com.ankurm.texttosql;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import com.ankurm.texttosql.support.Transcript;
import org.junit.jupiter.api.Test;
/** What the parser-based guard accepts and rejects, including legitimate queries it wrongly refuses. */
class GuardCasesTest {
static final SqlGuard GUARD = new SqlGuard(Set.copyOf(TextToSql.TABLES), SqlGuard.DEFAULT_FUNCTIONS);
@Test
void acceptedAndRejected() {
Map<String, Boolean> cases = new LinkedHashMap<>();
// should pass
cases.put("SELECT count(*) FROM orders WHERE status = 'paid'", true);
cases.put("SELECT c.country, count(*) FROM customers c JOIN orders o ON o.customer_id = c.id GROUP BY c.country ORDER BY 2 DESC", true);
cases.put("WITH big AS (SELECT order_id FROM order_items GROUP BY order_id HAVING sum(quantity) > 5) SELECT count(*) FROM big", true);
cases.put("SELECT * FROM orders WHERE id IN (SELECT order_id FROM order_items WHERE quantity > 2)", true);
cases.put("SELECT 1 /* ; DROP TABLE orders */", true);
cases.put("SELECT name FROM public.customers", true);
// should be refused
cases.put("DELETE FROM orders", false);
cases.put("SELECT * FROM orders; DROP TABLE orders", false);
cases.put("SELECT * FROM api_keys", false);
cases.put("SELECT usename FROM pg_user", false);
cases.put("SELECT pg_sleep(3)", false);
cases.put("SELECT pg_read_file('/etc/passwd')", false);
cases.put("WITH d AS (DELETE FROM orders RETURNING *) SELECT count(*) FROM d", false);
cases.put("SELECT * FROM orders FOR UPDATE", false);
cases.put("SELECT * INTO newtab FROM orders", false);
cases.put("SELECT count(*) FROM generate_series(1, 2000000000)", false);
cases.put("SELECT set_config('default_transaction_read_only', 'off', false)", false);
cases.put("SELECT * FROM orders o, \"api_keys\" k", false);
// legitimate but refused (cost of an allow-list)
cases.put("SELECT date_part('month', ordered_at) FROM orders", false);
cases.put("SELECT now()", false);
try (Transcript t = new Transcript("03-guard-cases.txt", "What the parser guard accepts and refuses")) {
for (Map.Entry<String, Boolean> e : cases.entrySet()) {
SqlGuard.Verdict v = GUARD.check(e.getKey());
t.line("%-8s %-110s %s", v.ok() ? "ACCEPT" : "REFUSE", e.getKey().length() > 108 ? e.getKey().substring(0, 105) + "..." : e.getKey(),
v.ok() ? "" : "(" + v.reason() + ")");
assertThat(v.ok()).as(e.getKey()).isEqualTo(e.getValue());
}
}
}
}