AI-agenten · Les

SQL-query's genereren en valideren

Promptpatronen voor veilige SQL: alleen-SELECT-modus en geparametriseerde query's.

Les 3 van 413 stappen

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 — Blocked

Blokkeerlijst 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 True

SQL 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 500

Schone 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.

Gratis beginnen

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

  1. Hoe NL-to-SQL-agents werken
  2. Schema's begrijpen en injecteren
  3. SQL-query's genereren en valideren
  4. Onduidelijke databasevragen afhandelen
← Terug naar AI-agenten