0Pricing
AI Agents · Урок

Генерация и проверка SQL-запросов

Шаблоны запросов для безопасного SQL: режим только SELECT и параметризованные запросы.

«Генерация и проверка SQL-запросов» — бесплатный урок AI Agents на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения AI Agents, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс AI Agents содержит 4 уроков всего.

Цель генерации SQL

Генерация SQL-запроса — лишь половина задачи. Перед выполнением в реальной базе данных необходимо проверить, что запрос безопасен, синтаксически корректен и точно выполняет то, что имел в виду пользователь.

В этом уроке рассматриваются принудительный режим только SELECT, разбор, безопасное выполнение и проверка плана выполнения.

Обеспечение режима только SELECT

Самое опасное, что может сделать агент, преобразующий естественный язык в SQL, — выполнить деструктивный оператор. Всегда принудительно включайте режим только SELECT независимо от того, что возвращает LLM.

Наивной проверки строки недостаточно — используйте полноценный синтаксический анализатор SQL.

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

Блок-лист ключевых слов как многоуровневая защита

Даже используя sqlparse, добавьте блок-лист ключевых слов как дополнительный уровень защиты. Некоторые SQL-инъекции могут обмануть анализаторы. Проверка опасных ключевых слов перед выполнением добавляет ещё один уровень безопасности.

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 с помощью sqlparse

sqlparse разбивает строки SQL на токены и анализирует их, не выполняя. С его помощью можно изучить структуру запроса, извлечь имена таблиц и проверить наличие синтаксических ошибок.

Установите пакет с помощью команды 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']

Проверка наличия таблиц в схеме

Извлекая имена таблиц из сгенерированного SQL, сверяйте их с известной вам схемой. Если LLM сгенерировала имя несуществующей таблицы, отклоните запрос до выполнения, вместо того чтобы получать непонятную ошибку базы данных.

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)

Параметризованное выполнение

Никогда не используйте форматирование строк для подстановки в SQL значений, предоставленных пользователем. Хотя запрос генерирует LLM, любые значения фильтров, предоставленные пользователем, следует передавать как параметры, чтобы предотвратить SQL-инъекции.

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 перед выполнением

Для ресурсоёмких запросов к большим таблицам выполняйте EXPLAIN перед фактическим запросом. Если планировщик показывает полное сканирование таблицы с миллионом строк, предупредите пользователя или отклоните запрос.

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"]}')

Ограничение числа строк

LLM может сгенерировать SELECT * FROM logs без LIMIT, что потенциально приведёт к возврату миллионов строк. Всегда устанавливайте максимальное число строк — добавляя LIMIT к запросу или извлекая ограниченный набор результатов.

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

Извлечение чистого SQL из вывода LLM

LLM часто возвращают SQL, обёрнутый в блоки кода Markdown (```sql ... ```), или сопровождают его пояснительным текстом. Перед разбором или выполнением необходимо извлечь исходный SQL.

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))

Полный конвейер проверки

Объедините все этапы проверки в одну функцию, которая принимает необработанный вывод LLM и возвращает безопасную строку SQL, готовую к выполнению, либо выдаёт ошибку с понятным описанием, позволяющим восстановить работу.

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)

Пользователь базы данных только для чтения

Проверка на уровне кода важна, но недостаточна. В качестве последнего уровня защиты подключайтесь к базе данных с помощью учётной записи пользователя только для чтения, имеющей только права SELECT. Даже если вредоносный запрос обойдёт все проверки, база данных отклонит его.

# 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')
    )

Проверка знаний

Какой подход к проверке SQL обеспечивает многоуровневую защиту в агенте, преобразующем естественный язык в SQL?

Итоги: генерация и проверка SQL

Безопасная генерация SQL требует полного конвейера проверки: извлекайте чистый SQL из вывода LLM, обеспечивайте режим только SELECT с помощью sqlparse, применяйте блок-лист ключевых слов, проверяйте имена таблиц по реальной схеме, ограничивайте число строк и используйте пользователя базы данных только для чтения как последнюю меру защиты.

Параметризованные запросы защищают от инъекций при наличии значений, предоставленных пользователем. Проверка плана EXPLAIN предотвращает выполнение неожиданно ресурсоёмких запросов в рабочей базе данных.

Часто задаваемые вопросы

Урок «Генерация и проверка SQL-запросов» бесплатный?

Да — полный текст урока «Генерация и проверка SQL-запросов» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс AI Agents, подпишись на CoddyKit PRO. Курс AI Agents содержит 4 уроков всего.

Чему я научусь в уроке «Генерация и проверка SQL-запросов»?

Шаблоны запросов для безопасного SQL: режим только SELECT и параметризованные запросы. Ты практикуешь AI Agents с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать AI Agents?

Предыдущий опыт не требуется. AI Agents на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.

Сколько времени занимает урок «Генерация и проверка SQL-запросов»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке AI Agents?

Да. Каждый урок AI Agents включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Как работают агенты NL-to-SQL
  2. Понимание и внедрение схемы
  3. Генерация и проверка SQL-запросов
  4. Обработка неоднозначных вопросов к базе данных
← Назад к AI Agents