Writing¶
Inserts, updates, deletes and upserts, one row or many, on the Row the tables page declared. Transactions is what holds several of these together.
const made = try db.insert(User, c, .{ .email = "a@b.c", .name = "Ada", .age = 30 });
// made.id is the generated key
const changed = try db.update(User, c, .{
.set = .{ .age = 31 },
.where = .{ .id = made.id },
});
const gone = try db.delete(User, c, .{ .where = .{ .id = made.id } });
insert names a subset of the columns, because the ones the database
fills in — a generated key, a DEFAULT now() — are exactly the ones you have
nothing to say about. What comes back is the whole row, via RETURNING, so
there is no second query to fetch what the database just had in its hand.
A column nothing fills is not yours to leave out, and leaving it out does not compile. The subset is for the columns something fills: the integer key a sequence fills, a column with a .default in the marker, an optional one that gets null, and one named in the marker's .filled, which is how you tell nilo the database fills it by means of its own (a DEFAULT written in a step, gen_random_uuid(), a trigger). Leave out anything else and the insert is refused, naming the columns, rather than failing with NotNullViolated the first time it runs. That is how a column added in one release and missed by an insert in the next is found by the compiler instead of by a user (ADR 181). A table this program only reads, .managed = false, is not checked: its defaults are the database's, and the marker does not know them.
A Str column takes text in whatever shape you hold it. The Row says
email: Str; the insert takes a literal, a []const u8, a []u8 an
allocator handed back, or a Str off the request, and each is written the
same way. There is nothing to convert on the way in, and made.email is a
Str on the way out, the Scope's for as long as the request is
(ADR 116).
An optional column takes null and an ?T of the same shapes.
update and delete answer with the number of rows they touched, and both
require a condition. An update with no .where rewrites the table and a
delete with none empties it; each is reached by leaving something out rather
than by writing something down, so each is a compile error:
error: nilo: an update on User with no condition.
That rewrites every row in the table. If it is meant, `db.raw` says
so where somebody reading the code can see it.
Many rows at once¶
A loop of db.insert is a round trip per row, and inside a transaction it is
a round trip per row holding a pool connection. insertMany is one statement:
const Line = struct { sku: nilo.Str, qty: i32 };
fn receive(db: *sql.Db, c: *nilo.Ctx, body: []const Line) ![]Item {
return db.insertMany(Item, c, body);
}
The rows come back in the order they were sent. tx.insertMany is the same
call inside a transaction.
The rows are a slice of a named struct rather than a tuple of literals, because the statement is compiled from the element type. What it compiles to is one array parameter per column:
INSERT INTO "items" ("sku", "qty")
SELECT * FROM unnest($1::text[], $2::int4[])
RETURNING "id", "sku", "qty"
Two placeholders for any number of rows, which is what keeps the statement a
constant — the VALUES ($1,$2),($3,$4),… most libraries generate has the
batch size in it, so the SQL would be rebuilt per call and Postgres would
plan it again for every distinct size
(ADR 047).
It is one statement, so a batch that violates a constraint stores none of its rows — usually what was wanted, and the opposite of a loop of inserts with nothing around it. An empty batch runs the statement, stores nothing and answers with nothing.
Two columns cannot be batched, and both say so at compile time: a list column,
because unnest would flatten it into one row per element, and an enum that
has not declared what its Postgres type is called.
updateMany is the same trick joined against the table rather than selected
into it:
const Change = struct { id: i64, qty: i32 };
const changed = try db.updateMany(Item, c, changes);
UPDATE "items" AS t SET "qty" = v."qty"
FROM unnest($1::int8[], $2::int4[]) AS v("id", "qty")
WHERE t."id" = v."id"
RETURNING t."id", t."sku", t."qty"
Each row carries the Row's key and is found by it — that is why there is
no .where to write, and why a batch that does not carry the key is a compile
error. A key the table does not have matches nothing, so an answer shorter
than the batch tells you which landed.
Two things it does not promise, and both are properties of a join rather than
choices: the order rows come back in is the planner's, and a batch naming
the same key twice changes that row once, from whichever of the two Postgres
reached. Where either matters, db.update in a loop is the honest shape.
Giving back the rows instead of the count¶
A PATCH endpoint changes a row and answers with it. Written with update
that is two round trips, and the second one may read what somebody else
changed in between:
fn rename(db: *sql.Db, c: *nilo.Ctx, id: i64, body: Rename) !?User {
return db.updateReturningOne(User, c, .{
.set = .{ .name = body.name },
.where = .{ .id = id },
});
}
updateReturning is the same statement answering with the slice, for a .where
that means to match many rows. deleteReturning is the other half, for a delete
that has to report or log what it took. The clause they add is the SELECT list
this module already writes, so none of them costs a statement the compiler did
not settle.
updateReturningOne is the unwrap, not a narrower statement
(ADR 146).
The .where is yours: an UPDATE matching several rows updates all of them, and
this hands back the first. What it saves is if (changed.len == 0) null else
changed[0] at every call site — and !?User is already a 404 in the typed
layer, so the handler above is the whole endpoint.
db.rawOne is the same shape for a statement you wrote yourself. It adds no
LIMIT 1, unlike db.one: this module did not write the statement and has
nowhere honest to put one.
Writing a row that may already be there¶
The shape everybody writes first is a caught error and a second statement:
const user = db.insert(User, c, .{ .email = email, .name = name }) catch |err| switch (err) {
error.AlreadyExists => try db.updateReturning(User, c, .{ … }), // two round trips
else => return err,
};
That is two round trips, and there is a window between them: two requests can
both fail the insert, both run the update, and the second one wins whatever
order they arrive in. ON CONFLICT is one statement and has no window.
// Leave the row that is there alone. `null` means it was already there.
const made = try db.insertOrIgnore(User, c, .{ .email = email, .name = name }, .email);
// Or write these values over it. Either way a row comes back.
const user = try db.insertOrUpdate(User, c, .{
.email = email,
.name = name,
}, .email);
The last argument is the conflict target: the column the database has a
unique constraint or index on, written the way a key is. For a constraint
spanning two columns it is a tuple, .{ .tenant_id, .email }. It is not
required to be the Row's key — an email is the ordinary case and is usually
not — and nothing on this side can check that a constraint exists, because a
constraint is not a column and a Row cannot name one. Postgres refuses the
statement if there is none.
When it is the key, write .key
(ADR 151):
_ = try tx.insertOrIgnore(UserTag, c, .{ .user_id = id, .tag = tag_name }, .key);
A join table already names its composite key in nilo_table, and spelling the
tuple again at the call site is two copies that can disagree — a key that gains
a column and a call site that does not is a statement conflicting on the old
columns, which inserts a duplicate where it used to ignore one. A Row that also
has a column called key is a compile error naming both readings.
They are two calls rather than one call with an option, because the answer
is a different shape. DO NOTHING stores no row, and RETURNING on a row
that was not stored gives nothing back — so insertOrIgnore returns ?User
where insertOrUpdate returns User. It is the same reason one is not
select with a flag.
insertOrUpdate sets every column you passed except the conflict target and
the key. The target is the value the two rows were matched on. The key is
left out because a caller passing .id is filling in the insert half — nobody
means "renumber the row that is already there", and Postgres would do it
quietly, along with every foreign key pointing at that row. If that leaves
nothing to set, the compiler says so and names the call you wanted:
error: nilo: `db.insertOrUpdate` on User has nothing to set.
Every column it was given is either the conflict target or the key
`id`, and the update half writes neither.
`db.insertOrIgnore` is the statement with nothing to set, and says so.