Databases
Dusty doesn't come with a database layer. You use a client library for your database, create its connection pool when the program starts, and keep the pool in the app, where every handler can reach it.
The examples on this page use PostgreSQL with pg.zig, and come from examples/todo. Other database clients fit in the same way.
Setup
Add pg.zig to the project:
And import the module in build.zig:
const pg = b.dependency("pg", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("pg", pg.module("pg"));
The Pool in the App
The app holds the pool:
main creates the pool with the same io as the server:
const url = init.environ_map.get("DATABASE_URL") orelse "postgres://todo:todo@127.0.0.1:5432/todo";
// One connection keeps a debug build quick to start. Size it for the load
// in a real app.
const pool = try pg.Pool.initUri(io, init.gpa, try std.Uri.parse(url), .{ .size = 1 });
defer pool.deinit();
And then the app and the server:
var app: App = .{ .pool = pool };
var server = http.Server(App).init(init.gpa, io, .{}, &app);
defer server.deinit();
With the same io, the database calls run on the same I/O as the server. On
zio, a handler waiting for the database doesn't block a thread. The
pool is created before the server, so it's closed after the server.
Queries in Handlers
A handler takes *App and runs its queries on app.pool:
fn listTodos(app: *App, res: *http.Response) !void {
var result = try app.pool.query("SELECT id, title FROM todos ORDER BY id", .{});
defer result.deinit();
var body = res.writer();
while (try result.next()) |row| {
const id = try row.get(i32, 0);
const title = try row.get([]const u8, 1);
try body.interface.print("{d}: {s}\n", .{ id, title });
}
try body.end();
res.content_type = .text;
}
All requests share the one pool, and they can run at the same time. Each query takes a connection
from the pool and gives it back when it's done. When all the connections are busy, a query waits
until one is free, and after 10 seconds it fails with error.Timeout. Make the pool about as big as
the number of requests you expect to use the database at once.
See the pg.zig documentation for the rest of its API.
Errors
A query error that the handler returns becomes a 500, like any other error. A handler can return
error.NotFound when the row it's looking for doesn't exist:
fn delete(app: *App, id: http.Param("id", i32), res: *http.Response) !void {
const deleted = try app.pool.exec("DELETE FROM todos WHERE id = $1", .{id.value});
if (deleted != 1) return error.NotFound;
try res.render(.html, templates.Count, .{ try app.countActive(), true });
}
See Handlers for how errors become responses, and how to change the error pages.
Queries in the App
As the app grows, you can move the queries into methods on the app, and keep the handlers short. The todo app has a method that reads to-dos into a list:
fn queryTodos(self: *App, arena: std.mem.Allocator, sql: []const u8, args: anytype) ![]Todo {
var result = try self.pool.query(sql, args);
defer result.deinit();
var list: std.ArrayList(Todo) = .empty;
while (try result.next()) |row| {
try list.append(arena, .{
.id = try row.get(i32, 0),
.title = try arena.dupe(u8, try row.get([]const u8, 1)),
.done = try row.get(bool, 2),
});
}
return list.items;
}
The caller passes req.arena, so the list lives until the response is sent. The title is copied,
because a string from row.get is only valid until the next row. queryTodo returns the first
to-do from the same list, and a handler loads a to-do with one call:
fn edit(app: *App, id: http.Param("id", i32), req: *http.Request, res: *http.Response) !void {
const todo = try app.queryTodo(req.arena, "SELECT id, title, done FROM todos WHERE id = $1", .{id.value}) orelse return error.NotFound;
try res.render(.html, templates.TodoEdit, .{todo});
}
Database Clients
These clients use std.Io:
- pg.zig for PostgreSQL
- myzql for MySQL
- redis.zig for Redis
- memcached.zig for Memcached
Next Steps
- Deployment covers running the server and the database client on zio.
- Handlers covers how errors become responses.