Skip to main content

SQL validation library that blocks destructive queries from LLM-generated SQL

Project description

ProxQL

A 50-line firewall to stop your AI from dropping tables

PyPI License Python

InstallationQuick StartModesAPI ReferenceIntegrations


The Problem

You're building an AI agent that talks to your database. But what happens when:

  • 🔥 Your LLM hallucinates and runs DROP TABLE users
  • 🔓 It queries SELECT * FROM employees and leaks salaries
  • 💸 It writes a cartesian join that scans 10 billion rows

ProxQL validates every query before it touches your data.

Installation

pip install proxql

Quick Start

import proxql

# ✓ Safe queries pass
proxql.validate("SELECT * FROM users").is_safe  # True
proxql.is_safe("SELECT * FROM products")        # True

# ✗ Dangerous queries are blocked
result = proxql.validate("DROP TABLE users")
result.is_safe   # False
result.reason    # "Statement type 'DROP' is not allowed in read_only mode"

# ✗ Unauthorized tables are blocked
result = proxql.validate(
    "SELECT * FROM employees",
    allowed_tables=["products", "categories"]
)
result.is_safe   # False
result.reason    # "Table 'employees' is not in allowed tables list"

Modes

Mode Allowed Statements Use Case
read_only SELECT only Analytics, reporting, read-only agents
write_safe SELECT, INSERT, UPDATE CRUD operations (no destructive ops)
custom You define Full control over allowed/blocked statements

Read-Only Mode (Default)

import proxql

# Only SELECT statements pass
proxql.is_safe("SELECT * FROM users")           # True
proxql.is_safe("INSERT INTO logs VALUES (1)")   # False
proxql.is_safe("DELETE FROM users")             # False
proxql.is_safe("DROP TABLE users")              # False

Write-Safe Mode

from proxql import Validator

validator = Validator(mode="write_safe")

validator.validate("SELECT * FROM users").is_safe    # True
validator.validate("INSERT INTO users ...").is_safe  # True
validator.validate("UPDATE users SET ...").is_safe   # True
validator.validate("DELETE FROM users").is_safe      # False  (blocked)
validator.validate("DROP TABLE users").is_safe       # False  (blocked)
validator.validate("TRUNCATE TABLE users").is_safe   # False  (blocked)

Custom Mode

from proxql import Validator

# Allow only specific statements
validator = Validator(
    mode="custom",
    allowed_statements=["SELECT", "INSERT"],
)
validator.validate("SELECT * FROM users").is_safe  # True
validator.validate("INSERT INTO logs ...").is_safe # True
validator.validate("UPDATE users SET ...").is_safe # False

# Or block specific statements
validator = Validator(
    mode="custom",
    blocked_statements=["DROP", "TRUNCATE"],
)
validator.validate("SELECT * FROM users").is_safe  # True
validator.validate("DROP TABLE users").is_safe     # False

Table Allowlist

Restrict queries to specific tables:

from proxql import Validator

validator = Validator(
    mode="read_only",
    allowed_tables=["products", "categories", "reviews"]
)

validator.validate("SELECT * FROM products").is_safe      # True
validator.validate("SELECT * FROM employees").is_safe     # False

# Also detects tables in subqueries, CTEs, and JOINs
validator.validate("""
    SELECT * FROM (SELECT * FROM secret_table) AS t
""").is_safe  # False - secret_table detected in subquery

SQL Dialect Support

ProxQL uses sqlglot under the hood, supporting 20+ SQL dialects:

from proxql import Validator

# PostgreSQL
pg_validator = Validator(mode="read_only", dialect="postgres")
pg_validator.validate("SELECT * FROM users LIMIT 10 OFFSET 5")

# MySQL
mysql_validator = Validator(mode="read_only", dialect="mysql")
mysql_validator.validate("SELECT * FROM users LIMIT 5, 10")

# Snowflake, BigQuery, DuckDB, etc.

Supported dialects: postgres, mysql, sqlite, snowflake, bigquery, redshift, duckdb, presto, trino, spark, and more.

API Reference

proxql.validate(sql, *, mode, allowed_tables, dialect)

Validate a SQL query string.

proxql.validate(
    sql: str,                              # The SQL query to validate
    *,
    mode: str = "read_only",               # "read_only" | "write_safe" | "custom"
    allowed_tables: list[str] | None = None,  # Optional table whitelist
    dialect: str | None = None,            # SQL dialect (auto-detected if None)
) -> ValidationResult

proxql.is_safe(sql, **kwargs)

Convenience wrapper that returns just the boolean result.

proxql.is_safe("SELECT * FROM users")  # True
proxql.is_safe("DROP TABLE users")     # False

proxql.Validator

For repeated validations, create a Validator instance:

from proxql import Validator

validator = Validator(
    mode: str = "read_only",               # Validation mode
    allowed_tables: list[str] | None = None,  # Table whitelist
    allowed_statements: list[str] | None = None,  # For custom mode
    blocked_statements: list[str] | None = None,  # For custom mode
    dialect: str | None = None,            # SQL dialect
)

result = validator.validate(sql: str) -> ValidationResult

ValidationResult

@dataclass(frozen=True)
class ValidationResult:
    is_safe: bool                    # Whether the query passed validation
    reason: str | None = None        # Explanation if blocked
    statement_type: str | None = None  # SELECT, INSERT, DROP, etc.
    tables: list[str] = []           # Tables referenced in query

    def __bool__(self) -> bool:      # Can use in boolean context
        return self.is_safe

Integrations

LangChain

from langchain_community.utilities import SQLDatabase
from proxql import Validator

db = SQLDatabase.from_uri("postgresql://localhost/mydb")
validator = Validator(mode="read_only")

def safe_query(query: str) -> str:
    result = validator.validate(query)
    if not result.is_safe:
        raise ValueError(f"Query blocked: {result.reason}")
    return db.run(query)

# Use safe_query instead of db.run in your agent

Raw Database Drivers

import psycopg2
from proxql import Validator

conn = psycopg2.connect("...")
validator = Validator(mode="read_only", allowed_tables=["products"])

def execute_safe(cursor, query: str):
    result = validator.validate(query)
    if not result.is_safe:
        raise ValueError(f"Blocked: {result.reason}")
    return cursor.execute(query)

FastAPI Middleware

from fastapi import FastAPI, HTTPException
from proxql import Validator

app = FastAPI()
validator = Validator(mode="read_only")

@app.post("/query")
async def run_query(query: str):
    result = validator.validate(query)
    if not result.is_safe:
        raise HTTPException(400, f"Query blocked: {result.reason}")
    # Execute query...

Edge Cases Handled

ProxQL correctly detects:

  • Subqueries: SELECT * FROM (SELECT * FROM secret_table) AS t
  • CTEs: WITH temp AS (SELECT * FROM secret) SELECT * FROM temp
  • JOINs: SELECT * FROM a JOIN b ON ... — checks all tables
  • Multi-statement: SELECT 1; DROP TABLE users; — blocks if any unsafe
  • Comments: SELECT * /* DROP TABLE */ FROM users — comments ignored
  • Case sensitivity: drop TABLE Users normalized correctly

Why ProxQL?

"You wouldn't give a junior intern root access to production. Why are you giving it to a hallucinating AI?"

Every AI framework (LangChain, CrewAI, AutoGen) lets you connect to databases. None of them protect you from what the AI might do once connected.

ProxQL is the missing safety layer.

Contributing

git clone https://github.com/zeredbaron/proxql.git
cd proxql
pip install -e ".[dev]"
pytest

License

Apache License 2.0 — See LICENSE for details.


Built for the agentic future 🤖

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

proxql-0.1.0.tar.gz (14.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

proxql-0.1.0-py3-none-any.whl (15.4 kB view details)

Uploaded Python 3

File details

Details for the file proxql-0.1.0.tar.gz.

File metadata

  • Download URL: proxql-0.1.0.tar.gz
  • Upload date:
  • Size: 14.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.8

File hashes

Hashes for proxql-0.1.0.tar.gz
Algorithm Hash digest
SHA256 fba39b17dc6dec6e41f82c08f83b91df0e4d26ee7b2f25c52aeab98afbe72e6b
MD5 bbc85e0721b6052e1266478fbd85e43b
BLAKE2b-256 0aa7e6adc7a3c79a6328a03f508e2b33b0191ea33994f674f6a65a03a8c7283e

See more details on using hashes here.

File details

Details for the file proxql-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: proxql-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 15.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.8

File hashes

Hashes for proxql-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1d9c5d7d218011ccac4b92033786f069382d4cba769f47a629677f70aabd397c
MD5 3319e6f52460cf6f5322d167949f269d
BLAKE2b-256 e88bd62c5513e184969b270db715ea12418e5b1461242c25962ffec017e2657b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page