SQL-query's genereren en valideren
Promptpatronen voor veilige SQL: alleen-SELECT-modus en geparametriseerde query's.
SQL-query's genereren en valideren is een gratis AI-agenten-les op CoddyKit. Dit is les 3 van 4. Je kunt de volledige les hieronder gratis lezen en daarna in de browser praktisch oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is. Deze les maakt deel uit van het leertraject AI-agenten. Je voortgang wordt gesynchroniseerd op het web en in de CoddyKit-app. De cursus AI-agenten bevat in totaal 4 lessen.
Het doel van SQL-generatie
Een SQL-query genereren is maar de helft van het werk. Voordat je de query op een echte database uitvoert, moet je valideren of deze veilig en syntactisch correct is en precies doet wat de gebruiker bedoelde.
Deze les behandelt het afdwingen van alleen SELECT, parseren, veilige uitvoering en controle van het uitleggingsplan.
De modus voor alleen SELECT afdwingen
Het gevaarlijkste wat een NL-to-SQL-agent kan doen, is een destructieve instructie uitvoeren. Dwing altijd de modus voor alleen SELECT af, ongeacht wat de LLM terugstuurt.
Een eenvoudige tekenreekscontrole is niet voldoende — gebruik een echte SQL-parser.
import sqlparse
def is_select_only(sql):
parsed = sqlparse.parse(sql)
if not parsed:
return False
for statement in parsed:
stmt_type = statement.get_type()
if stmt_type != 'SELECT':
print(f'Blocked statement type: {stmt_type}')
return False
return True
# Test
print(is_select_only('SELECT * FROM users')) # True
print(is_select_only('DROP TABLE users')) # False — BlockedBlokkeerlijst met trefwoorden als extra beveiligingslaag
Voeg zelfs met sqlparse een blokkeerlijst met trefwoorden toe als secundaire beveiliging. Sommige SQL-injecties kunnen parsers misleiden. Door vóór uitvoering op gevaarlijke trefwoorden te controleren, voeg je een extra beveiligingslaag toe.
DANGEROUS_KEYWORDS = [
'INSERT', 'UPDATE', 'DELETE', 'DROP', 'CREATE',
'ALTER', 'TRUNCATE', 'GRANT', 'REVOKE', 'EXEC',
'EXECUTE', 'CALL', 'MERGE'
]
def passes_blocklist(sql):
sql_upper = sql.upper()
for keyword in DANGEROUS_KEYWORDS:
# Check as whole word to avoid false positives like 'CREATED_AT'
import re
if re.search(r'\b' + keyword + r'\b', sql_upper):
raise ValueError(f'Blocked keyword detected: {keyword}')
return True
def validate_sql(sql):
if not is_select_only(sql):
raise ValueError('Only SELECT statements are allowed')
passes_blocklist(sql)
return TrueSQL parseren met sqlparse
sqlparse zet SQL-tekenreeksen om in tokens en parseert ze zonder ze uit te voeren. Je kunt de querystructuur inspecteren, tabelnamen ophalen en controleren op syntaxisproblemen.
Installeer het met pip install sqlparse.
import sqlparse
from sqlparse.sql import IdentifierList, Identifier
from sqlparse.tokens import Keyword, DML
def extract_table_names(sql):
parsed = sqlparse.parse(sql)[0]
tables = []
from_seen = False
for token in parsed.tokens:
if token.ttype is DML and token.value.upper() == 'SELECT':
continue
if token.ttype is Keyword and token.value.upper() in ('FROM', 'JOIN'):
from_seen = True
continue
if from_seen:
if isinstance(token, Identifier):
tables.append(token.get_name())
elif isinstance(token, IdentifierList):
for item in token.get_identifiers():
tables.append(item.get_name())
from_seen = False
return tables
print(extract_table_names('SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id'))
# ['users', 'orders']Controleren of tabellen in het schema bestaan
Vergelijk de tabelnamen die je uit de gegenereerde SQL hebt gehaald met je bekende schema. Als de LLM een tabelnaam heeft verzonnen, wijs je de query vóór uitvoering af in plaats van een cryptische databasefout te krijgen.
def validate_tables_exist(sql, known_tables):
used_tables = extract_table_names(sql)
invalid = [t for t in used_tables if t and t not in known_tables]
if invalid:
raise ValueError(
f'Query references non-existent tables: {invalid}. '
f'Available tables: {list(known_tables)[:10]}...'
)
return True
# Usage
known = set(build_schema_dict(conn).keys())
try:
validate_tables_exist(generated_sql, known)
except ValueError as e:
# Send error back to LLM for correction
corrected_sql = llm_fix_sql(generated_sql, str(e))
print('Corrected SQL:', corrected_sql)Uitvoering met parameters
Gebruik nooit tekenreeksopmaak om door de gebruiker aangeleverde waarden in SQL te plaatsen. Hoewel de LLM de query genereert, moeten alle filterwaarden van de gebruiker als parameters worden doorgegeven om SQL-injectie te voorkomen.
import sqlite3
conn = sqlite3.connect(':memory:')
conn.execute('CREATE TABLE orders (status TEXT, user_id INTEGER)')
conn.execute("INSERT INTO orders VALUES ('pending', 42)")
def safe_execute(conn, sql_template, params=()):
"""Execute with parameterized values."""
cur = conn.cursor()
cur.execute(sql_template, params) # driver handles escaping
columns = [d[0] for d in cur.description]
rows = cur.fetchmany(200)
return {'columns': columns, 'rows': rows}
sql = 'SELECT * FROM orders WHERE status = ? AND user_id = ?'
result = safe_execute(conn, sql, params=('pending', 42))
print(result)EXPLAIN-plan vóór uitvoering
Voer bij dure queries op grote tabellen EXPLAIN uit vóór de eigenlijke query. Als de planner een volledige tabelscan op een tabel met een miljoen rijen laat zien, waarschuw je de gebruiker of wijs je de query af.
def check_explain_plan(conn, sql):
explain_sql = f'EXPLAIN {sql}'
with conn.cursor() as cur:
cur.execute(explain_sql)
plan = '\n'.join(row[0] for row in cur.fetchall())
# Check for sequential scans on large tables
if 'Seq Scan' in plan:
print('WARNING: Query involves a sequential scan')
print(plan)
return {'safe': False, 'plan': plan, 'warning': 'Sequential scan detected'}
return {'safe': True, 'plan': plan}
# Use before executing
plan_result = check_explain_plan(conn, generated_sql)
if not plan_result['safe']:
print(f'Optimization hint: {plan_result["warning"]}')Een rijlimiet afdwingen
Een LLM kan SELECT * FROM logs genereren zonder een LIMIT, waardoor mogelijk miljoenen rijen worden teruggegeven. Dwing altijd een maximumaantal rijen af — bijvoorbeeld door LIMIT aan de query toe te voegen of door een begrensde resultatenset op te halen.
import re
MAX_ROWS = 500
def enforce_row_limit(sql, max_rows=MAX_ROWS):
sql_upper = sql.upper().rstrip().rstrip(';')
# Check if LIMIT already present
if re.search(r'\bLIMIT\b', sql_upper):
# Extract current limit and enforce maximum
match = re.search(r'LIMIT\s+(\d+)', sql_upper)
if match:
current = int(match.group(1))
if current > max_rows:
sql = re.sub(r'LIMIT\s+\d+', f'LIMIT {max_rows}', sql, flags=re.IGNORECASE)
else:
sql = sql.rstrip(';') + f' LIMIT {max_rows}'
return sql
print(enforce_row_limit('SELECT * FROM users'))
# SELECT * FROM users LIMIT 500Schone SQL uit LLM-uitvoer halen
LLM's geven SQL vaak terug in markdown-codeblokken (```sql ... ```) of met toelichtende tekst. Je moet de onbewerkte SQL eruit halen voordat je deze parseert of uitvoert.
import re
CODE_FENCE = chr(96) * 3 # three backticks, built at runtime to avoid template issues
def extract_sql(llm_response):
# Remove markdown code blocks ('''sql ... ''' or ''' ... ''')
pattern = CODE_FENCE + r'(?:sql)?\s*([\s\S]+?)' + CODE_FENCE
match = re.search(pattern, llm_response, re.IGNORECASE)
if match:
return match.group(1).strip()
# If no code block, look for SELECT statement
match = re.search(r'(SELECT\s+[\s\S]+?;)', llm_response, re.IGNORECASE)
if match:
return match.group(1).strip()
# Fallback: strip common preamble phrases
cleaned = re.sub(r'^(Here is|The SQL query is|Query:)[^\n]*\n', '',
llm_response, flags=re.IGNORECASE).strip()
return cleaned
if __name__ == '__main__':
demo_response = 'Here is the SQL query:\n' + CODE_FENCE + 'sql\nSELECT * FROM users;\n' + CODE_FENCE
print(extract_sql(demo_response))
Volledige validatieketen
Combineer alle validatiestappen in één functie die onbewerkte LLM-uitvoer ontvangt en een veilige, uitvoerbare SQL-tekenreeks teruggeeft of een foutmelding genereert met een beschrijvende boodschap voor herstel.
def validate_and_prepare_sql(llm_output, known_tables, max_rows=500):
# Step 1: extract raw SQL
sql = extract_sql(llm_output)
if not sql:
raise ValueError('No SQL found in LLM response')
# Step 2: type check
if not is_select_only(sql):
raise ValueError('Only SELECT queries allowed')
# Step 3: keyword blocklist
passes_blocklist(sql)
# Step 4: table existence check
validate_tables_exist(sql, known_tables)
# Step 5: row limit
sql = enforce_row_limit(sql, max_rows)
return sql
# Full flow
try:
safe_sql = validate_and_prepare_sql(llm_output, known_tables)
result = safe_execute(conn, safe_sql)
except ValueError as e:
corrected = llm_fix_sql(llm_output, str(e))
safe_sql = validate_and_prepare_sql(corrected, known_tables)
result = safe_execute(conn, safe_sql)Databasegebruiker met alleen-lezenrechten
Validatie op codeniveau is belangrijk, maar niet voldoende. Gebruik als laatste beveiligingslaag een databaseverbinding met een gebruikersaccount met alleen-lezenrechten dat uitsluitend SELECT-rechten heeft. Zelfs als een kwaadwillige query alle controles omzeilt, wijst de database deze af.
# Create read-only user in PostgreSQL:
# CREATE USER nl_to_sql_reader WITH PASSWORD 'secure_password';
# GRANT CONNECT ON DATABASE yourdb TO nl_to_sql_reader;
# GRANT USAGE ON SCHEMA public TO nl_to_sql_reader;
# GRANT SELECT ON ALL TABLES IN SCHEMA public TO nl_to_sql_reader;
import os
import psycopg2
def get_readonly_connection():
return psycopg2.connect(
host=os.getenv('DB_HOST'),
database=os.getenv('DB_NAME'),
user='nl_to_sql_reader', # read-only account
password=os.getenv('DB_READER_PASS')
)Kennistoets
Wat is de juiste gelaagde beveiligingsaanpak voor SQL-validatie in een NL-to-SQL-agent?
Samenvatting: SQL genereren en valideren
Veilige SQL-generatie vereist een volledige validatieketen: haal schone SQL uit de LLM-uitvoer, dwing alleen SELECT af met sqlparse, pas een blokkeerlijst met trefwoorden toe, controleer tabelnamen aan de hand van het echte schema, dwing rijlimieten af en gebruik een databasegebruiker met alleen-lezenrechten als laatste beveiliging.
Queries met parameters beschermen tegen injectie wanneer waarden van de gebruiker worden gebruikt. Controles van het EXPLAIN-plan voorkomen dat onverwacht dure queries op productiegegevens worden uitgevoerd.
Leer AI-agenten met een AI-tutor — gratis
Schrijf echte code en voer die uit in je browser, krijg direct hulp van een AI-tutor die 24/7 beschikbaar is en ga verder waar je gebleven bent op het web of in de app.
- Cursussen
- 60
- Lessen
- 239
Veelgestelde vragen
Is de les “SQL-query's genereren en valideren” gratis?
Ja — de volledige tekst van “SQL-query's genereren en valideren” kun je hier gratis op het web lezen. Als je interactief wilt oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is, en de rest van de cursus AI-agenten wilt ontgrendelen, kun je upgraden naar CoddyKit PRO. De cursus AI-agenten bevat in totaal 4 lessen.
Wat leer ik in “SQL-query's genereren en valideren”?
Promptpatronen voor veilige SQL: alleen-SELECT-modus en geparametriseerde query's. Je oefent met AI-agenten door code rechtstreeks in de browser uit te voeren. Een AI-begeleider die 24/7 beschikbaar is beantwoordt je vragen terwijl je de les doorwerkt.
Heb ik ervaring nodig om met AI-agenten te beginnen?
Ervaring vooraf is niet nodig. AI-agenten op CoddyKit is opgebouwd voor beginners tot gevorderden, zodat je hier of bij het begin kunt starten en in je eigen tempo kunt leren. Dit is les 3 van 4.
Hoe lang duurt de les “SQL-query's genereren en valideren”?
De meeste lessen van CoddyKit duren ongeveer 5–10 minuten. Elke les is kort en interactief, zodat je gestaag vooruitgaat en op het web en in de app precies verdergaat waar je was gebleven.
Kan ik code schrijven en uitvoeren in deze les over AI-agenten?
Ja. Elke les over AI-agenten bevat een ingebouwde code-editor, zodat je rechtstreeks in je browser echte code kunt schrijven en uitvoeren en direct feedback van AI krijgt — lokale installatie is niet nodig.
Alle lessen in deze cursus
- Hoe NL-to-SQL-agents werken
- Schema's begrijpen en injecteren
- SQL-query's genereren en valideren
- Onduidelijke databasevragen afhandelen