Text to SQL in Go: a safe natural-language query library

By Amtry · Updated 2026-11-11

QueryForge is an open-source Go library that turns a user’s sentence into a query you can run. The model never writes the SQL: it fills in a typed plan that is validated against your config, and Go code compiles the plan into a parameterized statement. The same plan can also compile to MongoDB and Elasticsearch, so one integration covers every database you query.

Install

go get github.com/awsaman-ai/queryforge

You need a config file that describes your data and, unless you use a local model, an API key in your environment. The config builder creates the file in your browser.

Describe your data in a config

The config is the vocabulary the model is allowed to use. Anything not listed here cannot appear in a query. This is a minimal one for an orders table, using a local model through Ollama:

{
  "entity": "Order",
  "model": { "provider": "ollama", "model": "qwen2.5" },
  "backends": { "sql": { "table": "orders" } },
  "fields": [
    { "name": "status", "type": "enum",
      "values": ["PLACED", "DELIVERED", "CANCELLED", "REFUNDED"],
      "operators": ["equals", "notEquals", "in", "notIn"],
      "mapping": { "sql": "status" } },
    { "name": "amount", "type": "number", "mapping": { "sql": "amount" } }
  ]
}

Every key is explained in the configuration documentation.

A working example

cfg, _ := qf.LoadConfig("orders.config.json")
engine := qf.New(cfg)

res, _ := engine.Translate(ctx,
    "cancelled orders over 200 dollars", "sql", nil)

fmt.Println(res.Query.SQL)
// SELECT … WHERE (status = $1 AND amount > $2) …
fmt.Println(res.Query.Args)  // [CANCELLED 200]

The library returns the SQL and its arguments. It never opens a connection, so you run the query with the driver you already use. With database/sql that is one line:

rows, err := db.QueryContext(ctx, res.Query.SQL, res.Query.Args...)

The result also carries the validated plan and a plain-language explanation of it, so you can show the user what was understood before you run anything.

Keep every query inside the caller’s tenant

Some filters are not the user’s to choose. Pass them as a scope map and they are AND-ed onto the query after the model answers, so the model never learns the field exists:

res, err := engine.Translate(ctx, "delivered orders over 500 dollars", "sql", qf.Scope{
    "subscriptionId": session.SubscriptionID,  // "SUB-42"
    "userId":         session.UserID,          // 9
    "enterpriseId":   session.EnterpriseIDs,   // []string{"E-1", "E-2"}
})
SELECT … FROM orders
WHERE (enterpriseId IN ($1, $2) AND subscriptionId = $3 AND userId = $4
       AND status = $5 AND amount > $6)
-- args: ["E-1", "E-2", "SUB-42", 9, "DELIVERED", 500]

Scope values are bound parameters like any other value, so an injection payload stays an argument. Set policy.requiredScope in the config and a query without the tenant filter is refused.

Choose your model

The default provider speaks any OpenAI-compatible /chat/completions endpoint. Others speak Anthropic’s native Messages API and Amazon Bedrock’s Converse API. Built-in presets include OpenAI, Anthropic, Gemini, Mistral, Groq and Ollama, and switching is a config change, not a code change.

Why not just prompt an LLM for SQL?

The reasoning is covered in more depth in Safe text-to-SQL.

What you get

Other languages

The same engine is available for Python (pip install queryforge-ai) and Java, and as an MCP server for AI assistants. See the install guide.

Next steps

The Go API is documented on pkg.go.dev, and the source is on GitHub under the Apache-2.0 license.

Try the playgroundDocumentationGitHub