Implementasi Text-to-SQL menggunakan Large Language Model (LLM) sering memicu silent failure di level backend. Query yang dihasilkan model seringkali lolos validasi sintaksis database engine tanpa melempar syntax error, namun mengembalikan empty set atau data korup secara logis. Masalah ini berakar pada kegagalan semantik: model membuat asumsi nama kolom (hallucinated column) atau relasi join yang tidak pernah ada di skema database riil.

Gejala: Silent Failure pada Runtime Eksekusi

Berbeda dengan kesalahan ketik query oleh developer yang langsung ditolak parser SQL engine dengan kode error deskriptif, query LLM sering menghasilkan kondisi logis valid yang salah sasaran. Kasus umum terjadi pada agregasi data analitik atau filter relasional.

Perhatikan contoh query yang digenerate oleh LLM saat diminta mengambil transaksi berhasil untuk pengguna aktif:

SELECT 
    u.id AS user_id,
    SUM(o.total_amount) AS revenue
FROM users u
JOIN orders o ON u.account_id = o.user_account_id
WHERE o.status = 'COMPLETED'
  AND u.is_deleted = FALSE
GROUP BY u.id;

Database mengembalikan status eksekusi sukses (HTTP 200 / zero exit code), namun hasilnya kosong. Dua anomali terjadi:

  • Tabel users menggunakan primary key id, bukan account_id. Join predicate u.account_id = o.user_account_id mengikat kolom yang salah atau bersifat nullable default.
  • Kolom soft delete pada tabel users sebenarnya bernama deleted_at (tipe data TIMESTAMP), bukan kolom boolean is_deleted. Database mengevaluasi predikat yang salah atau mengembalikan 0 rows affected tanpa memicu exception driver.

Analisis Root Cause: Absence of Semantic Pre-execution Validation

Akar masalah utama adalah ketiadaan layer verifikasi semantik deterministik sebelum query dikirim ke database connection pool. Arsitektur backend tipikal langsung meneruskan raw output model ke driver:

LLM Output --> Sanitize String --> DB Connection Pool --> Empty / Corrupted Result

Driver database relasional (seperti PostgreSQL atau MySQL client) hanya bertanggung jawab memastikan koneksi network dan validitas parsing dialek SQL native. Driver tidak melakukan pencocokan intensi natural language terhadap katalog metadata skema aktif aplikasi secara mendalam sebelum query diproses engine.

Ketika context prompt LLM menyertakan DDL skema yang besar, LLM cenderung melakukan halusinasi nama kolom berbasis konvensi umum (misal: is_active, account_id, created_by) alih-alih merujuk pada skema eksak yang didefinisikan.

Solusi: Deterministic Semantic Verification Layer Berbasis AST

Untuk mencegah query korup mencapai database, sistem memerlukan automated gatekeeper middleware. Solusi deterministik mengadopsi prinsip verifikasi skema AST (sebagaimana pendekatan pada tooling seperti Sqlsure): mengurai query menjadi Abstract Syntax Tree (AST), mengekstrak semua identifier (tabel, alias, kolom), lalu memvalidasinya terhadap katalog skema in-memory yang tersinkronisasi.

1. Isolasi AST Traversal Parser

Parser mengekstrak seluruh referensi kolom dan tabel tanpa mengeksekusi query. Berikut contoh implementasi validator semantik menggunakan TypeScript dan library AST parser standar:

import { Parser } from 'node-sql-parser';

interface SchemaCatalog {
  [table: string]: Set<string>;
}

// Metadata skema aktif dari database migration catalog
const activeSchema: SchemaCatalog = {
  users: new Set(['id', 'email', 'name', 'deleted_at', 'created_at']),
  orders: new Set(['id', 'user_id', 'total_amount', 'status', 'created_at'])
};

export class SemanticValidationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'SemanticValidationError';
  }
}

export function validateQuerySemantics(sql: string, catalog: SchemaCatalog): void {
  const parser = new Parser();
  let ast: any;

  try {
    ast = parser.astify(sql);
  } catch (err: any) {
    throw new SemanticValidationError(`Syntax Error: ${err.message}`);
  }

  const statements = Array.isArray(ast) ? ast : [ast];

  for (const stmt of statements) {
    if (stmt.type !== 'select') {
      throw new SemanticValidationError(`Forbidden operation: Only SELECT allowed`);
    }

    // Mapping alias ke nama tabel riil
    const tableAliasMap = new Map<string, string>();
    
    if (stmt.from) {
      for (const fromItem of stmt.from) {
        const tableName = fromItem.table;
        const alias = fromItem.as || tableName;
        
        if (!catalog[tableName]) {
          throw new SemanticValidationError(`Hallucinated table: '${tableName}'`);
        }
        tableAliasMap.set(alias, tableName);
      }
    }

    // Traversal kolom pada klausa WHERE dan JOIN
    const validateColumnRef = (colExpr: any) => {
      if (!colExpr || typeof colExpr !== 'object') return;

      if (colExpr.type === 'column_ref') {
        const table = tableAliasMap.get(colExpr.table) || colExpr.table;
        const column = colExpr.column;

        if (table && catalog[table]) {
          if (!catalog[table].has(column)) {
            throw new SemanticValidationError(
              `Hallucinated column '${column}' detected on table '${table}'`
            );
          }
        }
      }

      // Rekursif untuk nested binary expressions (AND, OR, join predicates)
      if (colExpr.left) validateColumnRef(colExpr.left);
      if (colExpr.right) validateColumnRef(colExpr.right);
    };

    if (stmt.where) validateColumnRef(stmt.where);
    if (stmt.from) {
      for (const fromItem of stmt.from) {
        if (fromItem.on) validateColumnRef(fromItem.on);
      }
    }
  }
}

2. Gatekeeper Middleware Sebelum Connection Pool

Integrasikan layer validasi ke dalam alur eksekusi service sebelum query dikirim ke database pool.

import { Pool } from 'pg';
import { validateQuerySemantics, SemanticValidationError } from './semanticValidator';
import { activeSchema } from './catalog';

const pool = new Pool({ connectionString: process.env.DATABASE_URL });

export async function executeAiQuery(generatedSql: string) {
  // Gatekeeper: Hentikan query sebelum menyentuh socket network DB
  try {
    validateQuerySemantics(generatedSql, activeSchema);
  } catch (error) {
    if (error instanceof SemanticValidationError) {
      // Log kegagalan dan trigger self-correction prompt ke LLM
      console.error(`[BLOCKED AI QUERY]: ${error.message}`);
      return {
        success: false,
        retryable: true,
        reason: error.message
      };
    }
    throw error;
  }

  // Safe path: Eksekusi query deterministik
  const client = await pool.connect();
  try {
    const result = await client.query(generatedSql);
    return { success: true, data: result.rows };
  } finally {
    client.release();
  }
}

Trade-off dan Limitasi

Penerapan deterministik AST validation memiliki sejumlah batasan teknis:

  • Overhead Latensi: Parsing AST menambah pemrosesan sebesar 1–5ms pada layer backend. Namun, angka ini jauh lebih efisien dibandingkan biaya eksekusi database round-trip untuk query yang menghasilkan data korup.
  • Query Dinamis Kompleks: Validasi untuk nested subquery berlapis dan correlated queries membutuhkan parser AST rekursif yang lebih komprehensif, mencakup WITH (CTE) definitions dan aliasing fungsional (seperti COALESCE atau window functions).
  • Sinkronisasi Skema: Katalog skema memory harus terhubung ke pipeline migrasi database aplikasi (CI/CD) agar validator tidak menolak kolom baru yang valid.

Memasukkan gatekeeper semantik deterministik mengeliminasi bug silent empty set dan melindungi database dari konsumsi resource akibat join predikat invalid.