> ## Documentation Index
> Fetch the complete documentation index at: https://docs.opper.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Natural language to SQL

> Turn plain-English questions into SQL you can run, using the gateway and a typed schema.

This is a clean fit for [structured output](/build/gateway/structured-output): you send the database schema and a question, and you get back a JSON object with the query. Because the output is schema-constrained, you can pull the `sql` field straight out and execute it. The example uses the OpenAI SDK pointed at the gateway, so the same code runs against any model.

The example uses an in-memory SQLite database so it runs as-is.

<div className="opp-term" style={{ borderRadius: "12px", overflow: "hidden", border: "1px solid #232a33", fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: "13.5px", lineHeight: "1.65", boxShadow: "0 8px 30px rgba(0,0,0,0.35)", margin: "1.5em 0" }}>
  <div className="opp-head" style={{ display: "flex", alignItems: "center", gap: "7px", padding: "11px 14px", background: "#11161c", borderBottom: "1px solid #232a33" }}>
    <span style={{ width: "11px", height: "11px", borderRadius: "50%", background: "#ff5f56", display: "inline-block" }} />

    <span style={{ width: "11px", height: "11px", borderRadius: "50%", background: "#ffbd2e", display: "inline-block" }} />

    <span style={{ width: "11px", height: "11px", borderRadius: "50%", background: "#27c93f", display: "inline-block" }} />

    <span style={{ marginLeft: "10px", color: "#6e7681", fontSize: "12px" }}>python app.py</span>
  </div>

  <div className="opp-body" style={{ padding: "16px 18px", background: "#0b0f14", color: "#c9d1d9" }}>
    <p data-role="user" style={{ margin: "0 0 16px" }}><span style={{ color: "#58a6ff", fontWeight: 600 }}>You ›</span> Who are the two highest-paid people in Engineering?</p>
    <p data-role="bot" style={{ margin: "0 0 16px" }}><span style={{ color: "#14cdcd", fontWeight: 600 }}>SQL ›</span> SELECT name, salary FROM employees WHERE department = 'Engineering' ORDER BY salary DESC LIMIT 2;</p>
    <p data-role="result" style={{ margin: 0 }}><span style={{ color: "#e3b341", fontWeight: 600 }}>→</span> <span style={{ color: "#6e7681" }}>\[('Dan', 151000), ('Alice', 145000)]</span><span className="opp-cursor" /></p>
  </div>
</div>

## The assistant

<CodeGroup>
  ```python app.py theme={null}
  import os, json, sqlite3
  from openai import OpenAI

  client = OpenAI(
      base_url="https://api.opper.ai/v3/compat",
      api_key=os.environ["OPPER_API_KEY"],
  )

  # A sample database
  db = sqlite3.connect(":memory:")
  db.executescript("""
  CREATE TABLE employees (id INTEGER, name TEXT, department TEXT, salary INTEGER, hire_date TEXT);
  INSERT INTO employees VALUES
   (1,'Alice','Engineering',145000,'2021-03-01'),
   (2,'Bjorn','Engineering',132000,'2022-07-15'),
   (3,'Chloe','Sales',98000,'2020-01-10'),
   (4,'Dan','Engineering',151000,'2019-11-20'),
   (5,'Eva','Sales',105000,'2023-02-05');
  """)

  def schema(con):
      rows = con.execute("SELECT sql FROM sqlite_master WHERE type='table'").fetchall()
      return "\n".join(r[0] for r in rows)

  QUERY_SCHEMA = {
      "type": "object",
      "properties": {
          "reasoning": {"type": "string", "description": "Brief reasoning about how to answer."},
          "sql": {"type": "string", "description": "A single valid SQLite query."},
      },
      "required": ["reasoning", "sql"],
  }

  def to_sql(question, db_schema):
      r = client.chat.completions.create(
          model="openai/gpt-5-mini",
          messages=[
              {"role": "system", "content": "Given a SQLite schema and a question, write one SQLite query that answers it. Return only the query in `sql`."},
              {"role": "user", "content": f"Schema:\n{db_schema}\n\nQuestion: {question}"},
          ],
          response_format={"type": "json_schema", "json_schema": {"name": "query", "schema": QUERY_SCHEMA}},
      )
      return json.loads(r.choices[0].message.content)

  question = "Who are the two highest-paid people in Engineering? Show name and salary."
  out = to_sql(question, schema(db))

  print("SQL:", out["sql"])
  print("Rows:", db.execute(out["sql"]).fetchall())
  ```

  ```typescript app.ts theme={null}
  import Database from "better-sqlite3";
  import OpenAI from "openai";

  const client = new OpenAI({
    baseURL: "https://api.opper.ai/v3/compat",
    apiKey: process.env.OPPER_API_KEY!,
  });

  // A sample database
  const db = new Database(":memory:");
  db.exec(`
  CREATE TABLE employees (id INTEGER, name TEXT, department TEXT, salary INTEGER, hire_date TEXT);
  INSERT INTO employees VALUES
   (1,'Alice','Engineering',145000,'2021-03-01'),
   (2,'Bjorn','Engineering',132000,'2022-07-15'),
   (3,'Chloe','Sales',98000,'2020-01-10'),
   (4,'Dan','Engineering',151000,'2019-11-20'),
   (5,'Eva','Sales',105000,'2023-02-05');
  `);

  const schema = () =>
    db.prepare("SELECT sql FROM sqlite_master WHERE type='table'").all().map((r: any) => r.sql).join("\n");

  const question = "Who are the two highest-paid people in Engineering? Show name and salary.";
  const r = await client.chat.completions.create({
    model: "openai/gpt-5-mini",
    messages: [
      { role: "system", content: "Given a SQLite schema and a question, write one SQLite query that answers it. Return only the query in `sql`." },
      { role: "user", content: `Schema:\n${schema()}\n\nQuestion: ${question}` },
    ],
    response_format: {
      type: "json_schema",
      json_schema: {
        name: "query",
        schema: {
          type: "object",
          properties: { reasoning: { type: "string" }, sql: { type: "string" } },
          required: ["reasoning", "sql"],
        },
      },
    },
  });

  const out = JSON.parse(r.choices[0].message.content!);
  console.log("SQL:", out.sql);
  console.log("Rows:", db.prepare(out.sql).all());
  ```
</CodeGroup>

Run it:

<CodeGroup>
  ```bash Python theme={null}
  pip install openai
  export OPPER_API_KEY="your-api-key"
  python app.py
  ```

  ```bash TypeScript theme={null}
  npm install openai better-sqlite3
  export OPPER_API_KEY="your-api-key"
  npx tsx app.ts
  ```
</CodeGroup>

## How it works

* The schema forces the model to return a `sql` string (plus its reasoning). With [structured output](/build/gateway/structured-output) you parse the response once and run `out["sql"]` directly, no regex.
* Passing the live `schema(db)` means the model writes queries against your real tables, not guessed ones.
* The `reasoning` field is optional but useful: it gives you a plain-English trace of why the query looks the way it does, which shows up on the call's trace.

## Make it more robust

* **Retry on error.** Catch SQLite exceptions, send the error message back as input, and ask for a corrected query.
* **Read-only safety.** Run the generated SQL against a read-only connection so a bad query can't modify data.
* **Score it.** Add an [Observe](/control-plane/observe) rule that checks the query ran and returned rows.

## What's next

<CardGroup cols={2}>
  <Card title="Structured output" icon="braces" href="/build/gateway/structured-output">
    JSON Schema, enums, and field descriptions.
  </Card>

  <Card title="Route" icon="signs-post" href="/control-plane/route">
    Set a default model per project without touching code.
  </Card>

  <Card title="Streaming" icon="bolt-lightning" href="/build/gateway/streaming">
    Stream the query as it generates.
  </Card>

  <Card title="Observe" icon="eye" href="/control-plane/observe">
    Score generated SQL automatically.
  </Card>
</CardGroup>
