Ejen AI · Pelajaran

Menjana dan Mengesahkan Pertanyaan SQL

Corak gesaan untuk SQL yang selamat: mod SELECT sahaja dan pertanyaan berparameter.

Pelajaran 3 daripada 413 langkah

Menjana dan Mengesahkan Pertanyaan SQL ialah pelajaran Ejen AI percuma di CoddyKit. Ini ialah pelajaran 3 daripada 4. Anda boleh membaca keseluruhan pelajaran di bawah secara percuma — kemudian berlatih secara praktikal dalam pelayar menggunakan penyunting kod terbina dalam dan tutor kecerdasan buatan 24/7. Pelajaran ini merupakan sebahagian daripada laluan pembelajaran Ejen AI, dan kemajuan anda disegerakkan merentas web serta aplikasi CoddyKit. Kursus Ejen AI merangkumi sejumlah 4 pelajaran.

Matlamat Penjanaan SQL

Menjana pertanyaan SQL hanyalah separuh daripada tugas. Sebelum melaksanakannya terhadap pangkalan data sebenar, anda perlu mengesahkan bahawa pertanyaan itu selamat, betul dari segi sintaks, dan melakukan tepat seperti yang dimaksudkan pengguna.

Pelajaran ini merangkumi penguatkuasaan SELECT sahaja, penghuraian, pelaksanaan selamat dan pengesahan pelan EXPLAIN.

Penguatkuasaan Mod SELECT Sahaja

Perkara paling berbahaya yang boleh dilakukan oleh ejen NL-ke-SQL ialah melaksanakan pernyataan yang merosakkan. Sentiasa kuatkuasakan mod SELECT sahaja tanpa mengira perkara yang dikembalikan oleh LLM.

Pemeriksaan rentetan naif tidak mencukupi — gunakan penghuraik SQL yang betul.

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

Senarai Sekatan Kata Kunci sebagai Pertahanan Berlapis

Walaupun menggunakan sqlparse, tambahkan senarai sekatan kata kunci sebagai pertahanan sekunder. Sesetengah suntikan SQL boleh memperdaya penghuraik. Memeriksa kata kunci berbahaya sebelum pelaksanaan menambah lapisan keselamatan tambahan.

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

Menghuraikan SQL dengan sqlparse

sqlparse memecahkan rentetan SQL kepada token dan menghuraikannya tanpa melaksanakannya. Anda boleh memeriksa struktur pertanyaan, mengekstrak nama jadual dan menyemak isu sintaks.

Pasang dengan 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']

Mengesahkan Kewujudan Jadual dalam Skema

Selepas mengekstrak nama jadual daripada SQL yang dijana, semak silang nama tersebut dengan skema yang diketahui. Jika LLM mereka-reka nama jadual, tolak pertanyaan itu sebelum pelaksanaan dan bukannya menerima ralat pangkalan data yang sukar difahami.

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)

Pelaksanaan Berparameter

Jangan gunakan pemformatan rentetan untuk menyisipkan nilai yang diberikan pengguna ke dalam SQL. Walaupun LLM menjana pertanyaan tersebut, sebarang nilai penapis yang diberikan pengguna hendaklah dihantar sebagai parameter untuk mencegah suntikan 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)

Pelan EXPLAIN Sebelum Pelaksanaan

Untuk pertanyaan yang mahal terhadap jadual besar, jalankan EXPLAIN sebelum pertanyaan sebenar. Jika perancang menunjukkan imbasan seluruh jadual yang mengandungi sejuta baris, beri amaran kepada pengguna atau tolak pertanyaan tersebut.

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

Penguatkuasaan Had Baris

LLM mungkin menjana SELECT * FROM logs tanpa LIMIT, yang berpotensi mengembalikan berjuta-juta baris. Sentiasa kuatkuasakan bilangan baris maksimum — sama ada dengan menambahkan LIMIT pada pertanyaan atau mendapatkan set hasil yang terhad.

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

Mengekstrak SQL Bersih daripada Keluaran LLM

LLM sering memulangkan SQL yang dibungkus dalam blok kod markdown (```sql ... ```) atau bersama teks penerangan. Anda perlu mengekstrak SQL mentah sebelum menghuraikan atau melaksanakannya.

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

Aliran Pengesahan Lengkap

Rantaikan semua langkah pengesahan ke dalam satu fungsi yang menerima keluaran mentah LLM dan mengembalikan rentetan SQL yang selamat serta boleh dilaksanakan, atau mencetuskan ralat dengan mesej yang jelas untuk pemulihan.

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)

Pengguna Pangkalan Data Baca Sahaja

Pengesahan pada peringkat kod penting tetapi tidak mencukupi. Sebagai lapisan pertahanan terakhir, sambung ke pangkalan data menggunakan akaun pengguna baca sahaja yang hanya mempunyai keistimewaan SELECT. Walaupun pertanyaan berniat jahat berjaya melepasi semua pemeriksaan, pangkalan data akan menolaknya.

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

Semakan Pengetahuan

Apakah pendekatan pertahanan berlapis yang betul untuk pengesahan SQL dalam ejen NL-ke-SQL?

Imbas Kembali: Menjana dan Mengesahkan SQL

Penjanaan SQL yang selamat memerlukan aliran pengesahan lengkap: ekstrak SQL bersih daripada keluaran LLM, kuatkuasakan SELECT sahaja menggunakan sqlparse, gunakan senarai sekatan kata kunci, sahkan nama jadual dengan skema sebenar, kuatkuasakan had baris, dan gunakan pengguna pangkalan data baca sahaja sebagai perlindungan terakhir.

Pertanyaan berparameter melindungi daripada suntikan apabila nilai yang diberikan pengguna terlibat. Pemeriksaan pelan EXPLAIN menghalang pertanyaan yang tidak dijangka mahal daripada dijalankan terhadap data pengeluaran.

Percuma untuk bermula

Pelajari Ejen AI dengan tutor kecerdasan buatan — percuma

Tulis dan jalankan kod sebenar dalam pelayar anda, dapatkan bantuan segera daripada tutor kecerdasan buatan yang tersedia 24/7, dan sambung semula dari tempat anda berhenti di web atau dalam aplikasi.

Kursus
60
Pelajaran
239

Soalan Lazim

Adakah pelajaran “Menjana dan Mengesahkan Pertanyaan SQL” percuma?

Ya — teks penuh “Menjana dan Mengesahkan Pertanyaan SQL” boleh dibaca secara percuma di web ini. Untuk berlatih secara interaktif menggunakan penyunting kod terbina dalam dan tutor kecerdasan buatan 24/7, serta membuka kunci baki kursus Ejen AI, tingkat taraf kepada CoddyKit PRO. Kursus Ejen AI merangkumi sejumlah 4 pelajaran.

Apakah yang akan saya pelajari dalam “Menjana dan Mengesahkan Pertanyaan SQL”?

Corak gesaan untuk SQL yang selamat: mod SELECT sahaja dan pertanyaan berparameter. Anda berlatih Ejen AI menggunakan kod praktikal yang dijalankan terus dalam pelayar, manakala tutor kecerdasan buatan 24/7 menjawab soalan anda semasa anda mengikuti pelajaran.

Adakah saya memerlukan pengalaman untuk memulakan Ejen AI?

Tiada pengalaman terdahulu diperlukan. Pembelajaran Ejen AI di CoddyKit disusun untuk pelajar daripada peringkat pemula hingga lanjutan, jadi anda boleh bermula di sini atau dari awal dan belajar mengikut kadar anda sendiri. Ini ialah pelajaran 3 daripada 4.

Berapa lamakah pelajaran “Menjana dan Mengesahkan Pertanyaan SQL” diambil?

Kebanyakan pelajaran CoddyKit mengambil masa kira-kira 5–10 minit. Setiap pelajaran ringkas dan interaktif, jadi anda boleh membuat kemajuan secara berterusan dan menyambung tepat dari tempat anda berhenti di web atau aplikasi.

Bolehkah saya menulis dan menjalankan kod dalam pelajaran Ejen AI ini?

Ya. Setiap pelajaran Ejen AI menyertakan penyunting kod terbina dalam, jadi anda boleh menulis dan menjalankan kod sebenar terus dalam pelayar serta menerima maklum balas kecerdasan buatan serta-merta — tanpa memerlukan persediaan setempat.

Semua pelajaran dalam kursus ini

  1. Cara Ejen NL-ke-SQL Berfungsi
  2. Memahami dan Menyuntik Skema
  3. Menjana dan Mengesahkan Pertanyaan SQL
  4. Mengendalikan Soalan Pangkalan Data yang Kabur
← Kembali ke Ejen AI