QueryGate is a Rust CLI that acts as a read-only SQL gateway for AI agents. It parses PostgreSQL SELECT queries with a real SQL AST (via sqlparser), validates them against a user-level allowlist policy, and executes approved queries through read-only database sessions.
| Task | Command |
|---|---|
| Run tests | cargo test |
| Run a single test | cargo test <test_name> |
| Run integration tests (needs PostgreSQL) | export QUERYGATE_TEST_DATABASE_URL="postgres://..." && cargo test -- --ignored |
| Build release binary | cargo build --release |
| Run the CLI locally | cargo run -- <subcommand> |
A query flows through three layers:
- Parse (
src/sql/parse.rs): Usessqlparserwith the PostgreSQL dialect. Only a singleSELECTstatement is accepted; everything else (INSERT, UPDATE, multiple statements) is rejected at parse time. - Validate (
src/sql/validate.rs): Walks the AST and checks every table, column, and function against the loaded policy. Columns are resolved via aQueryScopethat tracks table aliases and derived sources (CTEs, subqueries). Violations are collected and returned as aValidationResult. - Execute (
src/db.rs): Opens a read-only transaction (BEGIN READ ONLY), setsstatement_timeout, wraps the query in an outerLIMITif none exists, executes viatokio-postgres, and returns JSON rows.
src/cli.rs— clap argument definitions. Subcommands:init,databases,schema,validate,run.src/config.rs— Loads~/.queryGate/config.yaml(or via--config/QUERYGATE_CONFIG). DefinesRawConfig(deserialized from YAML) andLoadedConfig(validated, processed).src/policy.rs— Core access-control types:Policy,TablePolicy,ColumnPolicy,QualifiedTable. Identifiers are normalized (lowercased, quotes stripped). Missing access defaults to denied.src/sql/resolve.rs— Scope resolution.QueryScopemaps table aliases to either physical tables or derived sources. Used by the validator to resolve unqualified column names and detect ambiguity.src/init.rs—querygate initimplementation. Connects to PostgreSQL, introspectsinformation_schema.columns, and generates a config YAML. With--suggest-safe, marks columns asallowedif their names/data types don't match a hardcoded sensitive-name/type heuristic.src/output.rs— All CLI output is JSON. Success responses wrap data in{ ok: true, ... }; errors go to stderr as{ ok: false, error: { code, message, hint } }.
| Code | Meaning |
|---|---|
| 0 | Success |
| 2 | Config error |
| 3 | SQL parse error |
| 4 | Policy validation error |
| 5 | Database execution error |
- Unit tests are in
tests/validation.rs(policy/AST validation) andtests/cli.rs(binary-level CLI tests usingCARGO_BIN_EXE_querygate). - Integration tests in
tests/db_integration.rsrequire a live PostgreSQL instance and theQUERYGATE_TEST_DATABASE_URLenv var. They are marked#[ignore]and only run when explicitly invoked withcargo test -- --ignored. tempfileis used in tests to create throwaway config files.
- Deny-by-default: Any table, column, or function not explicitly listed in the policy is denied.
- Identifier normalization: All SQL identifiers are lowercased and double-quotes are stripped before policy lookup. This means the policy file should use lowercase names.
SELECT *is only allowed when a table hasdefault_column_access: allowed.- The validator rejects locking clauses (
FOR UPDATE),SELECT INTO,VALUES, and non-SELECT set expressions. - When adding new AST node validation, update both
validate.rs(the visitor) andresolve.rs(scope tracking) if the node introduces new tables or aliases.