# Agent SQL Read-Only Guard (Python Generator)

> Source: https://netray.co/tools/agent-sql-read-only-guard-python-generator
> Publisher: Netray (netray.co), AI and integration engineering for ERP and manufacturing systems.
> Section: Free Tools

## Short answer

Add a first line of defense before agent-written SQL reaches an ERP database: single statement, SELECT only, allow-listed tables and a row cap.

## Category

developer

## Audience

- AI engineer
- ERP developer
- Database administrator

## Tool

**Kind:** template

**Output:** code

**Language:** python

**Fields:**

#### Function name

**Key:** func_name

**Type:** text

**Default:** guard_sql

#### Allowed tables or views (comma separated)

**Key:** allowed

**Type:** text

**Default:** vw_open_orders, vw_inventory_onhand, vw_supplier_scorecard

#### Maximum rows returned

**Key:** max_rows

**Type:** number

**Default:** 200

#### Row limit style

**Key:** dialect

**Type:** select

**Options:**

- LIMIT
- TOP
- FETCH FIRST

**Default:** LIMIT

**Help:** LIMIT for PostgreSQL and MySQL, TOP for SQL Server, FETCH FIRST for Oracle 12c and later and DB2.

**Blocks:**

#### sql_guard.py

**Body:** import re

ALLOWED_OBJECTS = {name.strip().lower() for name in "{{allowed}}".split(",") if name.strip()}
MAX_ROWS = {{max_rows}}
ROW_LIMIT_STYLE = "{{dialect}}"

BLOCKED_WORDS = re.compile(
    r"\b(insert|update|delete|drop|alter|truncate|create|grant|revoke|merge|exec|execute|call|into|xp_cmdshell|pg_sleep|waitfor)\b",
    re.IGNORECASE,
)
OBJECT_REFS = re.compile(r"\b(?:from|join)\s+([A-Za-z0-9_.\[\]\"]+)", re.IGNORECASE)

class SqlRejected(Exception):
    pass

def {{func_name}}(sql):
    text = sql.strip().rstrip(";").strip()

    if ";" in text:
        raise SqlRejected("Only one statement is allowed.")
    if "--" in text or "/*" in text:
        raise SqlRejected("Comments are not allowed.")
    if not re.match(r"^(select|with)\b", text, re.IGNORECASE):
        raise SqlRejected("Only SELECT queries are allowed.")
    if BLOCKED_WORDS.search(text):
        raise SqlRejected("The query contains a blocked keyword.")

    for ref in OBJECT_REFS.findall(text):
        name = ref.replace("[", "").replace("]", "").replace("\"", "").lower()
        name = name.split(".")[-1]
        if name not in ALLOWED_OBJECTS:
            raise SqlRejected("Object not on the allow list: " + name)

    # Wrap so the row cap applies even if the model forgets a limit
    if ROW_LIMIT_STYLE == "TOP":
        return "SELECT TOP " + str(MAX_ROWS) + " * FROM (" + text + ") AS guarded"
    if ROW_LIMIT_STYLE == "FETCH FIRST":
        return "SELECT * FROM (" + text + ") guarded FETCH FIRST " + str(MAX_ROWS) + " ROWS ONLY"
    return "SELECT * FROM (" + text + ") AS guarded LIMIT " + str(MAX_ROWS)

#### Usage

**Body:** try:
    safe_sql = {{func_name}}(model_generated_sql)
except SqlRejected as problem:
    # Return the message to the model so it can correct the query
    result = "Query rejected: " + str(problem)
else:
    cursor.execute(safe_sql)  # connection must use a read-only database role

**Notes:** This guard is a convenience layer, not a security boundary. Pattern checks can be bypassed in ways a parser would catch. The real controls are a database role that only has SELECT on approved views, a read replica where possible, statement timeouts and query logging. A wrapped ORDER BY inside a subquery is not allowed by SQL Server without TOP, so test your dialect. Confirm syntax for your database version.

## Content

**Intro:** If an AI agent writes SQL against an ERP database, you need to decide what it may run before it reaches the database. This generator produces a Python function that rejects anything but a single SELECT on approved views and wraps the result with a row limit.

**Sections:**

#### How to use it

**Body:** List the views the agent may query, set a row cap and choose your database's limit syntax. Copy the function and call it on every generated statement before execution. Return the rejection message to the model so it can retry with a corrected query. Run the connection under a database login that has only SELECT on those views, because this code is only a first filter.

#### What the guard checks

**Body:** It enforces one statement, bans comments that can hide text, requires the statement to start with SELECT or WITH, blocks write and execution keywords, and checks every table or view after FROM and JOIN against the allow list. The final wrapper applies a row cap even when the model forgets a limit. Pointing the agent at purpose-built views also hides sensitive columns such as pay rates or customer bank details.

**Bullets:**

- Single statement only
- SELECT or WITH queries only
- Allow list of views, not base tables
- Row cap applied by wrapper
- Statement timeout set on the database side

#### Limits of regex validation

**Body:** String checks have blind spots: quoted identifiers, functions with side effects, vendor specific syntax and clever concatenation. For stronger assurance parse the SQL with a real parser library, execute through a read-only role on a replica, set a short statement timeout, and log queries with the requesting user. Treat the guard as one control among several.

#### Common mistakes

**Body:** Pointing the agent at the production schema with a powerful account, allowing base tables with personal or financial data, and returning raw database errors to the model are frequent. Another is forgetting that SELECT INTO can create tables on some databases, which is why INTO is blocked here.

**Cta:** Netray's ERPray answers questions over ERP data read-only, shows the query it ran and respects ERP roles. We can discuss guardrails for your own agent project.

**Faq:**

#### Is checking for SELECT enough to make SQL safe?

**Answer:** No. Pattern checks are easy to get wrong and some SELECT statements call functions with side effects or consume heavy resources. Use a read-only database login, approved views, timeouts and logging in addition to any code check.

#### Why use an allow list of views?

**Answer:** Views let you expose only the columns and rows the agent should see, rename cryptic ERP columns into plain names and hide sensitive data. An allow list also stops the model from joining to tables you never intended to open.

#### Why wrap the query to add a row limit?

**Answer:** Models often forget limits, and an unbounded query on a large transaction table can slow the ERP database. Wrapping applies the cap regardless of what the model wrote. Check that your dialect permits the wrapped form.

#### Should rejected queries be sent back to the model?

**Answer:** Yes, a short reason such as only SELECT allowed or object not permitted lets the model correct itself. Do not return database error text that reveals table names, paths or versions you did not intend to disclose.

## Search terms

text to sql guardrails python, validate sql select only python, llm sql injection prevention, read only sql agent erp, block drop delete in llm sql, sql allow list tables python, ai agent database read only, sql query validator python

---
Talk to an engineer: https://netray.co/contact?intent=erp_ai | Machine catalog: https://netray.co/agent/catalog.json
