update sqlc upsert article

This commit is contained in:
Adriano Caloiaro 2024-06-11 14:13:15 -07:00
parent 9ac2a7ae9a
commit 352a106ceb
No known key found for this signature in database
GPG key ID: C2BC56DE73CE3F75

View file

@ -52,12 +52,8 @@ And create a `SaveUser` query for saving user records. When a conflict happens o
```sql
-- name: SaveUser :one
INSERT INTO users (
id, name, email
) VALUES (
$1, $2, $3
) ON CONFLICT (id) DO UPDATE
SET name = $2, email = $3
INSERT INTO users (id, name, email) VALUES (@id, @name, @email) ON CONFLICT (id) DO UPDATE
SET name = @name, email = @email
RETURNING *;
```
@ -76,35 +72,16 @@ As you can see, the ID field is a `uuid.UUID`, not `*uuid.UUID`, so it's not pos
## The solution
So how do we perform upserts that account for the zero-value of our types? I've opted to create a function that provides default values when our type's zero-value is encountered. While the below function is explicitly written for `uuid` zero values, it can just as easily be ported to other types.
```sql
CREATE OR REPLACE FUNCTION public.uuid_if_empty(id uuid) RETURNS uuid
LANGUAGE plpgsql
AS $$BEGIN
IF id = uuid_nil() THEN
RETURN uuid_generate_v4();
ELSE
RETURN id;
END IF;
END$$;
```
So how do we perform upserts that account for the zero-value of our types? I've opted to combine `nullif` and `coalesce` to achieve the upsert effect. While the solution below specifically implements upserts for `UUID` types, the `nullif` call can be adapted to any type's zero value.
> Thanks to [mariusor](https://lobste.rs/~mariusor) on lobste.rs for notifying me about `uuid_nil()`
Now in our upsert statement, we use our new function instead of the raw value passed in
```sql
-- name: SaveUser :one
INSERT INTO users (
id, name, email
) VALUES (
-- Note how naming the `id` param changes the numeric value of positional args.
-- It may be preferable to use the sqlc.arg macro here to improve readability.
uuid_if_empty(sqlc.arg(id))::uuid, $1, $2
) ON CONFLICT (id) DO UPDATE
SET name = $2, email = $3
INSERT INTO users (id, name, email) VALUES (coalesce(nullif(@id, uuid_nil()), uuid_generate_v4()), @name, @email) ON CONFLICT (id) DO UPDATE
SET name = @name, email = @email
RETURNING *;
```
This statement coalesces to `uuid_generate_v4()` when `@id` has the UUID zero value, and uses `@id` as-is otherwise. Using `@id` when it is not the type's zero value is what allows the `ON CONFLICT` portion of the query to activate when we're updating existing records.
This turned out to be a very simple solution for my current problem. If there's a better or more idiomatic way to achieve the same result, please let me know in the comments!