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 يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. كيف تعمل وكلاء NL-to-SQL؟
  2. فهم المخطط وحقنه
  3. إنشاء استعلامات SQL والتحقق منها
  4. التعامل مع أسئلة قواعد البيانات الملتبسة
← العودة إلى AI Agents