Skip to content

Type annotations for query parameters#2800

@kyleconroy

Description

@kyleconroy

I propose adding type annotations for query parameters. Users will no longer need to cast parameters to the desired type. This proposal builds on Andrew's query annotation
work for vet.

Unified syntax

We added the @sqlc-vet-disable annotation to disable vet rules on a per-query
basis. We can extend this syntax to support other per-query configuration
options.

The current syntax for query name and command are different, so we'll
standardize on the @ prefix. This will be the new, preferred syntax for name
and command.

-- name: GetAuthor :oneSELECT*FROM authors WHERE id = $1LIMIT1; -- becomes-- @name GetAuthor :oneSELECT*FROM authors WHERE id = $1LIMIT1;

The existing syntax will continue to work, but it will be an error to use both
annotations on a single query.

-- INVALID!-- name: GetAuthor :one-- @name GetAuthor :oneSELECT*FROM authors WHERE id = $1LIMIT1;

Query command

Today, queries must have a name and a command. With the new syntax, the
command option will default to exec.

-- These two annotations are the same-- @name DeleteAuthor :execDELETEFROM authors WHERE id = $1; -- @name DeleteAuthorDELETEFROM authors WHERE id = $1;

Validation

sqlc will delegate command validation to codegen plugins, allowing plugins to implement new commands without having
to merge anything into sqlc.

If you want to still validate those, you can simulate the current behavior by
using this vet rule.

rules: - name: validate-commandrule: | !(query.cmd in ["exec", "one", "many", "execrows", "execlastid", "execresult", "batchone", "batchmany", "batchexec"])

Type annotations for query parameters

The @param annotation supports passing type information without having to use
a cast.

-- @param name type

Casts are required today when sqlc infers an incorrect type, but these
casts are passed down to the engine itself, possibly hurting performance.
For example, this cast is required to get sqlc working correctly, but isn't needed at runtime.

-- @name ListAuthorsByIDs :manySELECT*FROM authors WHERE id = ANY(@ids::bigint[]);

Here's what it looks like with the new syntax. The type annotation is
engine-specific and is the same that you'd pass to CAST or CREATE TABLE.

-- @name ListAuthorsByIDs :many-- @param ids BIGINT[]SELECT*FROM authors WHERE id = ANY(@ids);

If a parameter has a type annotation, that will be used instead of inferring the
type from the query.

NULL values

sqlc will infer the nullability of parameters by default. You can force a parameter to be nullable using the ? operator, or not null using the ! operator.

-- @name CreateAuthor :one-- @param name! TEXT -- force NOT NULL-- @param bio? TEXT -- force NULLINSERT INTO authors ( name, bio ) VALUES ( @name, @bio ) RETURNING *;

Positional parameters

If your parameters do not have a given name, you can refer to them by number to add a type annotation and nullability.

For PostgreSQL:

-- @name CreateAuthor :one-- @param 1? TEXT-- @param 2? TEXTINSERT INTO authors ( name, bio ) VALUES ( $1, $2 ) RETURNING *;

And for MySQL or SQLite:

-- @name CreateAuthor :one-- @param 1? TEXT-- @param 2? TEXTINSERT INTO authors ( name, bio ) VALUES ( ?1, ?2 );

sqlc.arg / sqlc.narg

Using the proposed annotation syntax allows you to replace sqlc.arg() and sqlc.narg().

For example this query with sqlc.arg() and sqlc.narg()

-- name: UpdateAuthor :oneUPDATE author SET name = coalesce(sqlc.narg('name')::text, name), bio = coalesce(sqlc.narg('bio')::text, bio) WHERE id =sqlc.arg('id')::bigint RETURNING *;

is equivalent to this one without

-- name: UpdateAuthor :one-- @param name? TEXT-- @param bio? TEXT-- @param id! BIGINTUPDATE author SET name = coalesce(@name, name), bio = coalesce(@bio, bio) WHERE id = @id RETURNING *;

sqlc.arg and sqlc.narg will continue to work, but will likely be deprecated in favor of the @foo syntax.
You can use sqlc.arg() with the new @param annotation syntax (to avoid explicit casts), but not sqlc.narg(). This constraint
is intended to eliminate confusion about precedence of nullability directives.

So for example this will work

-- name: UpdateAuthor :one-- @param id! BIGINTUPDATE author SET name = coalesce(sqlc.narg('name')::text, name), bio = coalesce(sqlc.narg('bio')::text, bio) WHERE id =sqlc.arg('id') RETURNING *;

but this won't

-- name: UpdateAuthor :one-- @param name TEXT-- @param bio TEXTUPDATE author SET name = coalesce(sqlc.narg('name'), name), bio = coalesce(sqlc.narg('bio'), bio) WHERE id =sqlc.arg('id')::bigint RETURNING *;

To make this work, switch sqlc.narg to sqlc.arg and add a ? to the param annotation.

-- name: UpdateAuthor :one-- @param name? TEXT-- @param bio? TEXTUPDATE author SET name = coalesce(sqlc.arg('name'), name), bio = coalesce(sqlc.arg('bio'), bio) WHERE id =sqlc.arg('id')::bigint RETURNING *;

Why comments?

We're using comments instead of sqlc.* functions to avoid engine-specific parsing issues. For example, we've run into issues with the MySQL parser not support functions in certain parameter locations.

Full example

This is the normal example from the playground using the new syntax.

-- @name GetAuthor :one-- @param id! BIGINTSELECT*FROM authors WHERE id = @id LIMIT1; -- @name ListAuthors :manySELECT*FROM authors ORDER BY name; -- @name CreateAuthor :one-- @param name! TEXT-- @param bio TEXTINSERT INTO authors ( name, bio ) VALUES ( @name, @bio ) RETURNING *; -- @name GetAuthor-- @param id! BIGINTDELETEFROM authors WHERE id = @id;

Future

The plan is to use similar annotations to support type annotations for output columns and values, Go type overrides for parameters and outputs, and JSON unmarshal / marshal hints.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions