Agent SQL Read-Only Guard (Python Generator)
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.
Fill in
LIMIT for PostgreSQL and MySQL, TOP for SQL Server, FETCH FIRST for Oracle 12c and later and DB2.
sql_guard.py
import re
ALLOWED_OBJECTS = {name.strip().lower() for name in "vw_open_orders, vw_inventory_onhand, vw_supplier_scorecard".split(",") if name.strip()}
MAX_ROWS = 200
ROW_LIMIT_STYLE = "LIMIT"
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 guard_sql(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
try:
safe_sql = guard_sql(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 roleThis 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.
Guardrail review for agent database access
Optional: review roles, views and logging for your agent's ERP database access.
Optional. The tool and every export work without this. No spam.
How to use it
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
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.
- 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
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
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.
Frequently Asked Questions
Is checking for SELECT enough to make SQL safe?
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?
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?
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?
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.
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.
Related Tools
ERP MCP Server Python Tool Skeleton Generator
Get a runnable Python MCP server with one read-only ERP lookup tool, argument validation and errors that the model can understand.
Developer ToolsMCP and JSON-RPC Error Codes Reference
A lookup table of JSON-RPC 2.0 error codes and how an MCP server should signal protocol errors versus tool failures.
Developer ToolsPaginated OData Fetch Loop Python Generator
Build a Python loop that follows nextLink pages from an OData entity set and writes the result to CSV with a hard row cap.
Developer ToolsERP Agent API Integration Readiness Assessment
Answer ten questions about APIs, roles, limits and logging to see whether your ERP is ready for an AI agent or MCP integration.
More Developer Tools tools
See all- SAP Sales Order ABAP SELECT Generator (VBAK, VBAP, VBFA)
- SAP Sales Order Create BAPI ABAP Generator (BAPI_SALESORDER_CREATEFROMDAT2)
- SAP ABAP Open Items SELECT Generator
- SAP ACDOCA SQL Query Generator
- SAP QM Inspection Lot ABAP SELECT Generator
- SAP Quality Notification Pareto SQL Query Generator
- SAP QM Usage Decision BAPI Skeleton
- ABAP Custom Exception Class Generator
- ABAP Unit Test Class Generator