Skip to content

Query Predict

Query Predict (qp) is Emi’s approach to hand-written SQL with generated, type-safe Go bindings. You write plain .sql files; Emi parses them, predicts the shape of each result row and the parameters they need, and generates Go code with a typed row struct and a prepared-statement builder. There is no schema introspection, no auto-migration and no SQL synthesis — the SQL you write is the SQL that runs.

This keeps the full power of SQL in your hands while still giving you compile-time safety over the columns and parameters a query touches.

CommandDescription
emi qp:dirScans a directory for .sql files and generates QueryPredict Go code for each.
emi qp:genGenerates the QueryPredict Go files from a Query Predict YAML definition.
emi qp:sqlCompiles a single query into executable Go and transforms the metadata into pure SQL.

Point qp:dir at a directory of .sql files and it generates one Go file per query into the output directory. The generated file name is derived from the SQL file name.

Terminal window
emi qp:dir --path ./queries-source --output ./output

qp:gen reads a Query Predict document that bundles several queries together and lets you set the Go package name:

Terminal window
emi qp:gen --path ./queries.yaml --output ./output
package: queries
queries:
- name: basicSelect
query: select name, id from users;
- name: countUsers
query: select count(*) as 'count' from users
FlagDescription
—pathPath of the directory (qp:dir) or the Query Predict YAML/SQL file.
—outputDirectory the generated Go files are written to.
—tagsComma separated compile features to add or remove.

For a query like select name, id from users; Emi emits a typed row struct, the original SQL as a constant, and a context/prepare helper:

const BasicSelectSQL = `select name, id from users;`
type BasicSelectRow struct {
Name string
Id string
}
type BasicSelectContext struct {
Filter string
Having string
Restriction string
Params map[string]interface{}
Placeholders []any
}
func BasicSelectPrepreSql(ctx BasicSelectContext) (string, error) {
// ...substitutes useval(), filter(), restriction() and having into the SQL
}

Query Predict understands a small set of marker functions inside your SQL. They let you keep dynamic behaviour in the query while still generating predictable, typed code.

HelperPurpose
field(expr, ‘type’, ‘goName’?)Annotate a selected expression so Emi knows its Go type (and optionally the Go field name) when it cannot be inferred.
useval(‘name’)A named value substituted from ctx.Params at runtime, safely escaped.
filter()Placeholder replaced by the Filter clause from the context (defaults to 1).
restriction()Placeholder replaced by the Restriction clause from the context (defaults to 1).

An example combining them:

SELECT
u.user_id as user_id,
field(u.user_name, 'string') as UserName,
field(COUNT(o.order_id), 'int64') AS total_orders
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE filter()
GROUP BY u.user_id, u.user_name
HAVING MAX(o.total) > 100
ORDER BY total_orders DESC
LIMIT useval('limit')

Here field(..., 'int64') tells the generator the aggregate column is an int64, filter() is swapped for whatever filter the caller supplies, and useval('limit') is replaced by the limit entry of ctx.Params.