Annotating queries¶
The inputs to Squiller are plain SQL files, with annotations in comments. This means that you can run the exact same files through e.g. sqlite
or psql
, and confirm that the queries are valid. For every annotated query, Squiller generates a corresponding function in the target language that executes that query. If needed, it also generates types for the inputs and outputs of the function.
Annotations¶
Squiller ignores all content, until it encounters the marker @query
or @begin
in a comment. This marks the following query as an annotated query, that it will generate code for. Following the marker is the query signature, which specifies its name, arguments, argument types, and result type, similar to function signatures in other languages. Let’s look at an example:
-- Return how many users with the given name exist.
-- @query count_users_with_name(name: str) ->1 int
select
count(*)
from
users
where
name = :name;
In this example, the signature is
@query count_users_with_name(name: str) ->1 int
The name of the query is count_users_with_name
, and this name will be used for the generated function. The query takes one argument, name
, of type str
. This will become an argument of the generated function, and that function will bind the provided value to the :name
query parameter.
After the name and arguments, is an arrow, and then the result type. The arrow includes a cardinality:
->?
for a query that returns zero or one rows.->1
for a query that returns exactly one row.->*
for a query that returns zero or more rows.
The exact types that these arrows map to depends on the target, but generally they translate as follows:
->? T
maps toOption<T>
.->1 T
maps to justT
.->* T
maps toIterator<T>
.
Query parameters¶
Squiller supports named query parameters with :name
syntax. This is one of the syntaxes supported by SQLite, and it allows for named parameters which is less error-prone than position-based parameters. For databases that use a different syntax, such as PostgreSQL, Squiller substitutes the correct syntax in the SQL string literal in the generated code.
Documentation comments¶
Squiller preserves any comments immediately preceding the @query
marker, up to the first blank line before that marker, as documentation comments. These are included in the output. For example, in Rust they are included as ///
-style documentation comments, in Python as docstrings.
Tuple result types¶
The result type can be a tuple. In this case, the number of columns that the query returns should match the arity of the tuple. Squiller does not verify this. For example, for the following query it would generate code that fails at runtime, because it tries to access a non-existent third column:
-- @query incorrect_result_type() ->1 (str, str, i32)
select name, email from users;
Struct result types¶
Because a type such as (str, str, i32)
is a bit meaningless, Squiller also supports struct types. Struct types must start with an uppercase ascii letter. The fields of the struct, and their types, are extracted from the query body. This means that type annotations are needed in the body:
-- @query get_all_users() ->* User
select
name /* :str */,
email /* :str */,
karma /* :i32 */
from
users;
In this example the fields are name: str
, email: str
, and karma: i32
.
When using struct types, every column that the query selects, should have a type annotation, because Squiller generates code that reads the columns by index. Squiller does not verify that every column is annotated, because it does not do the advanced parsing of the query that would be necessary for this.
Every comment between the @query
marker and the terminating ;
that starts with a :
is considered a type annotation, and turns into a struct field. The identifier that immediately precedes the annotation becomes the name of the field, so it can be used with as
to control the name:
-- @query get_all_users() ->* User
select
users.name /* :str */,
users.email /* :str */,
sum(karma_earned) as karma /* :i32 */
from
users, karma_history
where
users.id = karma_history.user_id
group by
users.id, users.name, users.email;
As before, this example has fields name: str
, email: str
, and karma: i32
.
Struct arguments¶
Like in result types, structs can be used in arguments. (Unlike tuples, which can only be used in result types.) Struct types can only be used for queries that take a single argument. The name of that argument is preserved in the generated function. As with result types, the fields are extracted from the query body, so all query parameters need a type annotation:
-- @query insert_user(user: User) ->1 i64
insert into
users (name, email, karma)
values
(:name /* :str */, :email /* :str */, :initial_karma /* :i32 */)
returning
id;
Nullable types¶
All primitive types can be made optional or nullable by appending a ?
. Primitive types are all types except for structs and tuples, so structs and tuples cannot be made nullable. This is because structs and tuples map to an entire row in SQL, not to individual columns. To specify optionality at the row level, use a ->?
result type arrow instead of ->1
.
Note, this means that the following two queries would have the same signature in the generated code, even though they have different signatures in SQL:
-- @query select_longest_email_length_1() ->1 i64?
select
max(length(email))
from
users;
-- @query select_longest_email_length_2() ->? i64
select
length(email)
from
users
order by
length(email) desc
limit
1;
Note also that annotating the first query with ->? i64
would result in a runtime error when the users
table is empty (because null cannot be decoded into i64
), and annotating the second query with ->1 i64?
would result in a runtime error when the users
table is empty as well (because it expects at least one row).
Multiple statements¶
You can create functions that execute multiple SQL statements by using a @begin
marker to start an annotated query, instead of @query
. This can be useful for e.g. migrations.
- With a
@query
marker, the query comprises only the statement that follows it, until the terminating semicolon. - With a
@begin
marker, all statements between@begin
and@end
are included.@end
is only valid directly after a semicolon.
Let’s look at an example:
-- Set up the initial schema.
-- @begin init_schema()
create table if not exists
users
( id integer primary key autoincrement
, name text not null
, email text not null
);
create index ix_users_email on users (email);
-- @end init_schema
In long documents, for clarity it is recommended to repeat the name of the query after the @end
marker, but this is not required.
When a multi-statement query has a result type, the result type applies to the final statement in the query. Every other statement must not return any rows. Query parameters are allowed in all statements.
Note: The @begin
and @end
markers are unrelated to the SQL statements BEGIN
and COMMIT
. Squiller never starts transactions implicitly.