AI Text-to-SQL Query Generation Development

We design and deploy artificial intelligence systems: from prototype to production-ready solutions. Our team combines expertise in machine learning, data engineering and MLOps to make AI work not in the lab, but in real business.
Showing 1 of 1All 1566 services
AI Text-to-SQL Query Generation Development
Medium
~5 days
Frequently Asked Questions

AI Development Areas

AI Solution Development Stages

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1284
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1197
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    902
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1119
  • image_logo-advance_0.webp
    B2B Advance company logo design
    586
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    853

AI SQL Generation from Text Queries (Text-to-SQL)

Text-to-SQL allows business analysts and managers to ask database questions directly without SQL knowledge. The key technical challenge is not SQL syntax generation (LLM handles that easily), but providing precise schema context: what tables exist, how they relate, what values are valid in enum fields.

Text-to-SQL System Architecture

from anthropic import Anthropic
import psycopg2
import json
from typing import Optional
from dataclasses import dataclass

client = Anthropic()

@dataclass
class QueryResult:
    sql: str
    explanation: str
    rows: list[dict]
    error: Optional[str] = None

class TextToSQLEngine:

    def __init__(self, connection_string: str):
        self.conn = psycopg2.connect(connection_string)
        self.schema_cache: dict = {}

    def get_schema(self, tables: list[str] = None) -> str:
        """Retrieves DDL schema from PostgreSQL"""

        query = """
        SELECT
            t.table_name,
            c.column_name,
            c.data_type,
            c.is_nullable,
            c.column_default,
            tc.constraint_type,
            kcu.column_name as fk_column,
            ccu.table_name as fk_table
        FROM information_schema.tables t
        JOIN information_schema.columns c ON t.table_name = c.table_name
        LEFT JOIN information_schema.key_column_usage kcu
            ON c.table_name = kcu.table_name AND c.column_name = kcu.column_name
        LEFT JOIN information_schema.table_constraints tc
            ON kcu.constraint_name = tc.constraint_name
        LEFT JOIN information_schema.constraint_column_usage ccu
            ON tc.constraint_name = ccu.constraint_name
        WHERE t.table_schema = 'public'
        """

        if tables:
            placeholders = ",".join(["%s"] * len(tables))
            query += f" AND t.table_name IN ({placeholders})"

        with self.conn.cursor() as cur:
            cur.execute(query, tables or [])
            rows = cur.fetchall()

        # Format as DDL
        tables_dict = {}
        for row in rows:
            table_name = row[0]
            if table_name not in tables_dict:
                tables_dict[table_name] = {"columns": [], "foreign_keys": []}

            col_def = f"  {row[1]} {row[2].upper()}"
            if row[3] == "NO":
                col_def += " NOT NULL"
            if row[4]:
                col_def += f" DEFAULT {row[4]}"
            if row[5] == "PRIMARY KEY":
                col_def += " PRIMARY KEY"

            tables_dict[table_name]["columns"].append(col_def)

            if row[5] == "FOREIGN KEY" and row[7]:
                tables_dict[table_name]["foreign_keys"].append(
                    f"  FOREIGN KEY ({row[6]}) REFERENCES {row[7]}"
                )

        ddl_parts = []
        for table, info in tables_dict.items():
            ddl = f"CREATE TABLE {table} (\n"
            ddl += ",\n".join(info["columns"])
            if info["foreign_keys"]:
                ddl += ",\n" + ",\n".join(info["foreign_keys"])
            ddl += "\n);"
            ddl_parts.append(ddl)

        return "\n\n".join(ddl_parts)

    def get_sample_values(self, important_columns: dict[str, list[str]]) -> str:
        """Gets sample values for enum/category fields"""
        samples = []

        with self.conn.cursor() as cur:
            for table_col, _ in important_columns.items():
                table, col = table_col.split(".")
                try:
                    cur.execute(
                        f"SELECT DISTINCT {col} FROM {table} LIMIT 10"
                    )
                    values = [str(row[0]) for row in cur.fetchall()]
                    samples.append(f"-- {table}.{col}: {', '.join(values)}")
                except Exception:
                    pass

        return "\n".join(samples)

    def generate_sql(self, question: str, context_tables: list[str] = None) -> QueryResult:
        """Generates SQL from text question"""

        schema = self.get_schema(context_tables)

        # Additional context: sample values for string fields
        sample_values = self._get_relevant_samples(question)

        response = client.messages.create(
            model="claude-sonnet-4-5",
            max_tokens=2048,
            system="""You are a SQL and PostgreSQL expert.
Generate precise, optimized SQL queries based on database schema.

Rules:
- Use only existing tables and columns from schema
- Prefer JOINs over subqueries where possible
- Add LIMIT 1000 for queries without aggregation
- For dates use PostgreSQL functions: DATE_TRUNC, NOW(), EXTRACT
- Always add ORDER BY for predictable results
- If question is ambiguous — choose most likely interpretation

Return JSON:
{
  "sql": "<SQL query>",
  "explanation": "<what query does, 1-2 sentences>",
  "assumptions": ["<assumption 1 if any>"]
}""",
            messages=[{
                "role": "user",
                "content": f"""Question: {question}

Database schema:
```sql
{schema}

{f"Sample values:{chr(10)}{sample_values}" if sample_values else ""}""" }] )

    text = response.content[0].text
    try:
        # Parse JSON response
        start = text.find("{")
        end = text.rfind("}") + 1
        data = json.loads(text[start:end])

        sql = data["sql"]
        explanation = data.get("explanation", "")

        # Execute query
        rows = self._execute_safe(sql)

        return QueryResult(sql=sql, explanation=explanation, rows=rows)

    except Exception as e:
        return QueryResult(sql="", explanation="", rows=[], error=str(e))

def _execute_safe(self, sql: str) -> list[dict]:
    """Executes SELECT queries only"""
    sql_upper = sql.strip().upper()
    if not sql_upper.startswith("SELECT") and not sql_upper.startswith("WITH"):
        raise ValueError("Only SELECT queries are allowed")

    with self.conn.cursor() as cur:
        cur.execute(sql)
        columns = [desc[0] for desc in cur.description]
        rows = cur.fetchall()
        return [dict(zip(columns, row)) for row in rows]

def _get_relevant_samples(self, question: str) -> str:
    """Simple heuristic to identify relevant enum fields"""
    # In real system — LLM determines needed fields
    return ""

### Self-Correcting Generator

```python
class SelfCorrectingTextToSQL:
    """Iteratively corrects SQL on execution errors"""

    def __init__(self, engine: TextToSQLEngine):
        self.engine = engine

    def query(self, question: str, max_attempts: int = 3) -> QueryResult:
        """Generates SQL with automatic error correction"""

        result = self.engine.generate_sql(question)
        if not result.error:
            return result

        # Iteratively correct
        messages = [{
            "role": "user",
            "content": f"Question: {question}\n\nGenerated query:\n```sql\n{result.sql}\n```\n\nError: {result.error}\n\nFix the query."
        }]

        for attempt in range(max_attempts - 1):
            response = client.messages.create(
                model="claude-sonnet-4-5",
                max_tokens=1024,
                system="You are a SQL expert. Fix SQL queries based on execution errors. Return only fixed SQL.",
                messages=messages,
            )

            fixed_sql = response.content[0].text.strip()
            if "```sql" in fixed_sql:
                fixed_sql = fixed_sql.split("```sql")[1].split("```")[0].strip()

            try:
                rows = self.engine._execute_safe(fixed_sql)
                return QueryResult(sql=fixed_sql, explanation="Auto-corrected", rows=rows)
            except Exception as e:
                messages.append({"role": "assistant", "content": response.content[0].text})
                messages.append({"role": "user", "content": f"Still error: {e}"})

        return QueryResult(sql=result.sql, rows=[], error="Max attempts reached", explanation="")

Natural Language Interface with History

class ConversationalDataAnalyst:
    """Conversational interface for data work"""

    def __init__(self, connection_string: str):
        self.engine = TextToSQLEngine(connection_string)
        self.history: list[dict] = []
        self.last_sql: str = ""

    def ask(self, question: str) -> str:
        """Answers question considering dialog history"""

        # Add previous query context
        context = ""
        if self.last_sql:
            context = f"\nPrevious query:\n```sql\n{self.last_sql}\n```"

        # Support clarifying questions
        if any(word in question.lower() for word in ["and also", "now", "add", "also"]):
            enhanced = f"Based on previous query, {question}"
        else:
            enhanced = question

        result = self.engine.generate_sql(enhanced + context)

        if result.error:
            return f"Query execution error: {result.error}"

        self.last_sql = result.sql
        self.history.append({"question": question, "sql": result.sql})

        # Format result
        if not result.rows:
            return "Query executed successfully, no data found."

        response_text = f"{result.explanation}\n\n"
        response_text += f"SQL: `{result.sql}`\n\n"
        response_text += f"Results ({len(result.rows)} rows):\n"

        # Results table
        if result.rows:
            headers = list(result.rows[0].keys())
            response_text += " | ".join(headers) + "\n"
            response_text += " | ".join(["---"] * len(headers)) + "\n"
            for row in result.rows[:10]:
                response_text += " | ".join(str(v) for v in row.values()) + "\n"
            if len(result.rows) > 10:
                response_text += f"... and {len(result.rows) - 10} more rows"

        return response_text

Practical Case: E-commerce Analytics

Task: product managers submitted tasks to analysts (2–5 day wait) because they didn't know SQL. Database: PostgreSQL, 23 tables, ~50M records.

Implementation:

  • Text-to-SQL interface in Slack: /data <question>
  • Allowlist of tables for product teams (without personal data)
  • Caching frequent questions

Metrics:

  • ad-hoc queries from product teams without analyst involvement: 0 → 23 per week
  • Time to answer simple question: 2 days → 30 seconds
  • Accuracy of generated SQL: 89% (no edits needed)
  • 11% of queries required iterative clarification via dialog

Timeline

  • Basic engine (schema extraction + SQL generation): 1 week
  • Self-correcting generator + history: 1-2 weeks
  • Conversational interface with Slack integration: 1 week
  • Query caching + performance optimization: 1 week