Complete "Foundations: How an AI Agent Perceives, Plans, and Acts" first to unlock this lesson.Go to previous lesson

Introduction

AI agents become far more useful when they can interact directly with relational databases on behalf of users — answering questions like "What were last quarter's top-selling products?" or "Update this customer's shipping address" without a human writing SQL by hand. The challenge is giving an agent real database power while keeping it from executing destructive or unsafe queries. This lesson walks through the practical building blocks: letting an agent read your schema, generate SQL, validate that SQL, and only then run it against a real database.

Granting schema awareness to the agent

An agent cannot write correct SQL if it does not know what tables and columns exist. The standard technique is to feed the model a condensed description of the database schema as part of the system prompt or as a retrieved context block. For a PostgreSQL database, you can dump tables and columns using the information_schema, then trim the output before passing it to the model.

sql
SELECT table_name, column_name, data_type
FROM information_schema.columns
WHERE table_schema = 'public'
ORDER BY table_name, ordinal_position;

The returned rows become a text summary the agent can reference whenever it constructs a query. Without this, the model will hallucinate column names and fail repeatedly.

flowchart showing agent receives user question, retrieves schema context from database metadata, generates SQL candidate, validates SQL, then either executes or rejects

Generating and validating SQL

Once the agent has schema context, you ask it to produce a single SQL statement in response to the user request. Wrap that output in a parser so you can inspect it before execution. A robust pipeline does three things in order: (1) reject anything that is not a single statement, (2) reject any statement whose leading verb is on a denylist (DROP, TRUNCATE, DELETE without WHERE, UPDATE without WHERE, GRANT, etc.), and (3) run the statement with a read-only or parameter-limited connection depending on intent.

python
import sqlparse
FORBIDDEN = {"drop", "truncate", "grant", "revoke"}
def safe_sql(raw: str) -> str:
parsed = sqlparse.parse(raw)
if len(parsed) != 1:
raise ValueError("Must be exactly one statement")
stmt = parsed[0]
first_token = stmt.get_type().lower()
head = stmt.token_first(skip_cm=True).value.lower()
if head in FORBIDDEN:
raise ValueError(f"Forbidden verb: {head}")
return str(stmt)

This validation layer runs *before* the SQL ever touches your database. It is the single most important safeguard in the whole pipeline.

Executing through a restricted connection

Even with validation, defense in depth matters. Connect to the database as a user whose permissions are already restricted: a read-only role for analytical questions, and a separate role with only the specific INSERT/UPDATE rights your app needs. Set a short statement timeout at the session level so runaway queries cannot lock a table for minutes.

sql
SET statement_timeout = '3s';
SET idle_in_transaction_session_timeout = '5s';

Pair these timeouts with a row limit appended to generated SELECTs (for example, always add LIMIT 100 unless the user explicitly opts out) so the agent cannot pull a million-row table into memory and stall.

block diagram comparing two database connection roles — a read-only analytics role and a scoped write role — showing which SQL statements each one is permitted to run

Common pitfalls

The most frequent failure is letting the agent echo raw SQL into chat without ever executing it — users then copy and run it themselves, and you lose audit trails. Another is trusting the model to "decide whether the query is safe" inside the same prompt; self-policing is unreliable, so always enforce validation in code. Finally, avoid passing the full schema of a huge database at once: the model burns context on tables it never needs, so filter to relevant tables per request or use embeddings to retrieve only the matching ones.

Summary

Connecting an agent to SQL is a layered problem. Start by exposing schema metadata so the model can write real SQL, then generate, parse, and validate every statement in code before execution. Back the validator with role-based permissions and connection-level timeouts so a mistake or a malicious prompt can never wipe a table. With those four layers — schema context, code-level validation, restricted roles, and timeouts — an agent can answer data questions and perform safe writes without putting your database at risk.

Lesson practice

Lesson Checkpoint

Review what you learned and get feedback on your work.

0/8
01Why is feeding schema metadata to the agent a prerequisite for accurate SQL generation?
02Which SQL verb should be explicitly rejected by the validation layer regardless of context?
03In the safe_sql Python function, what is the purpose of checking len(parsed) != 1?
04Why apply statement_timeout at the session level rather than only validating SQL in code?
05What problem occurs when you pass the full schema of a large database into the agent's prompt?
06Why is having the agent "self-police" its own SQL inside the prompt considered unreliable?
07What is the role of a read-only database user in this setup?
08Which order best describes the layered defense for agent-driven SQL?
Answered · 0/8