Queries
Bob can generate code for SQL queries. This is similar to sqlc.
To use this feature, write your SQL queries in .sql
files, and then point the driver to the folder containing these files using the queries
configuration option. For example:
sqlite:
dsn: file.db
queries:
- ./path/to/folder/containing/sql/files
- ./another/folder
Alongside a few common files, for each .sql
file found, it will generate two files:
file_name.bob.go
- This file contains the generated code for the queries in the SQL file.file_name.bob_test.go
- This file contains the generated test code for the queries in the SQL file.
Make sure to run the generated tests. This will ensure that the generated code is correct and that the queries are valid.
Using the generated code
Given the schema:
CREATE TABLE users (
id INT PRIMARY KEY NOT NULL,
name TEXT
);
And the query:
-- AllUsers
SELECT * FROM users WHERE id = ?;
The following code with be generated:
const allUsersSQL = `SELECT "users"."id", "users"."name" FROM "users" WHERE "id" = ?1`
func AllUsers(id int32) orm.ModQuery[...] {
// ...
}
type AllUsersRow struct {
ID int32 `db:"id"`
Name null.Val[string] `db:"name"`
}
See how SELECT *
is transformed into SELECT "users"."id", "users"."name"
. This is done to ensure that the generated code continues to work as expected even if the schema changes.
Retrieving related data
Bob supports retrieving related data by naming the returned columns in a specific way.
related_table__column_name
- This indicates ato-one
relationship.related_table.column_name
- This indicates ato-many
relationship.
When the All()
method is used on the query, the returned rows will be transformed and nested according to these relationships.
For example, given the following query:
-- Nested
SELECT
users.*,
--prefix:videos.
videos.*,
--prefix:videos.sponsor__
sponsors.*
FROM users
LEFT JOIN videos ON videos.user_id = users.id
INNER JOIN sponsors ON videos.sponsor_id = sponsors.id
WHERE users.id IN ($1);
Will generate the following code:
type AllNestedRow = []NestedRow_
type NestedRow_ = struct {
ID int32
EmailValidated null.Val[string]
PrimaryEmail null.Val[string]
ParentID null.Val[int32]
PartyID null.Val[int32]
Referrer null.Val[int32]
Videos []NestedRow_Videos
}
type NestedRow_Videos = struct {
ID null.Val[int32]
UserID null.Val[int32]
SponsorID null.Val[int32]
Sponsor *NestedRow_Videos_Sponsor
}
type NestedRow_Videos_Sponsor = struct {
ID null.Val[int32]
}
See how the --prefix
annotation is used to conveniently prefix the columns with the table name.
Making a query
To make a query, you can use the generated function:
query := AllUsers(1)
This will return a orm.ModQuery
object that you can use to execute the query with any of the expected finishers:
One(ctx, db) -> AllUsersRow
All(ctx, db) -> []AllUsersRow
Cursor(ctx, db) -> scan.ICursor[AllUsersRow]
Modifying a query
The generated query is a orm.ModQuery
object, which can also be used as a QueryMod
.
This opens up many use cases, since you can use the generated query as a base and add more mods to it.
// Also filter where name = "Bob"
query := sqlite.Select(
AllUsers(1),
psql.Quote("name").EQ(psql.Arg("Bob")),
)
Annotating queries
Each query has the following attributes that can be modified with annotations:
query_name
: The name of the query. This is used to generate the function name. Required.result_type_one
: The type of the result when usingOne()
. This is used to generate the result type. e.g.AllUsersRow
.result_type_all
: The type of the result when usingAll()
. This is used to generate the result type. e.g.[]AllUsersRow
.transformer
. The name of the slice transformer to use when usingAllx()
. If manually set theresult_type
will not be generated. Use placeholdersONETYPE
andALLTYPE
to indicate where the types should be placed. e.g.bob.SliceTransformer[ONETYPE, ALLTYPE]()
.
Each return column and parameter can also be annotated with the following attributes:
name
: The name of the column. This is used to generate the field name.type
: The type of the column. This is used to generate the field type.nullable
: This can benull
ornotnull
to specify if the column is nullable or not. If it is empty, the nullability will be inferred.
Any part of the annotation can be ommited. For example, instead of name:type:null
, all the following are valid annotations:
name
name:type
name::null
:type:null
::null
The other parts will be inferred from the context.
-- AllUsers *models.User:models.UserSlice:bob.SliceTransformer[ONETYPE, ALLTYPE]
SELECT id /* :big.Int:nnull */, name /* username */ FROM users WHERE id = ? /* ::notnull */;
Prefixing columns
If you want to prefix the columns with the table name, you can use the prefix
annotation:
--
SELECT
users.*,
-- Set a prefix for the next columns
--prefix:posts.
posts.id, -- "posts.id"
-- Change the prefix for the next columns
--prefix:posts.comments.
comments.*,
-- Remove the prefix
--prefix:
users.name -- "name"