Skip to content

Commit 16aef99

Browse files
authored
Improvements to typeid-sql (#203)
## Summary Improvements to typeid-sql: - Provide example files for both the text version and the compound version of typeid - Remove the UUIDv7 check, sometimes people want to store other UUIDs in typeid format - Rename the equality operator from `=` to `===` (and `!==`). Unfortunately some libraries give special meaning to `=`, and that breaks functionality like `supabase db diff` ## How was it tested? Ran supabase tests
1 parent ae84426 commit 16aef99

10 files changed

Lines changed: 77 additions & 62 deletions

File tree

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
-- Example of how to use typeids in your own tables.
1+
-- Example of how to use the compound version of typeids in your own tables.
2+
-- The compound version is a tuple of type (string, uuid)
3+
24
-- In this example we'll define a users table that uses typeids to identify users.
35

46
-- Define a `user_id` type, which is a typeid with type prefix "user".

example/text.sql

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-- Example of how to use text version of typeids in your own tables.
2+
-- In this example we'll define a members table that uses typeids.
3+
4+
-- Define a `member_id` type, which is a typeid with type prefix "member".
5+
-- Using `member_id` throughout our schema, gives us type safety by guaranteeing
6+
-- that the type prefix is always "member".
7+
create domain member_id AS text check (typeid_check_text(value, 'member'));
8+
9+
-- Define a `members` table that uses `member_id` as its primary key.
10+
-- We use the `typeid_generate_text` function to randomly generate a new typeid of the
11+
-- correct type for each member.
12+
create table members (
13+
"id" member_id not null default typeid_generate_text('member'),
14+
"name" text,
15+
"email" text
16+
);
17+
18+
CREATE UNIQUE INDEX members_pkey ON members USING btree (id);
19+
alter table "members" add constraint "members_pkey" PRIMARY KEY using index "members_pkey";
20+
21+
22+
-- Now we can insert new uses and have the `id` column automatically generated.
23+
insert into members ("name", "email") values ('Alice P. Hacker', 'alice@hacker.net');
24+
25+
-- Or you can specify the typeid yourself:
26+
insert into members ("id", "name", "email")
27+
values ('member_01h455vb4pex5vsknk084sn02q', 'Ben Bitdiddle', 'ben@bitdiddle.com');

sql/03_typeid.sql

Lines changed: 4 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
-- + Defines functions to generate and validate typeids in SQL.
66

77
-- Create a `typeid` type.
8-
-- Note that the "uuid" field should be a UUID v7.
98
create type "typeid" as ("type" varchar(63), "uuid" uuid);
109

1110
-- Function that generates a random typeid of the given type.
@@ -38,47 +37,30 @@ language plpgsql
3837
volatile;
3938

4039
-- Function that checks if a typeid is valid, for the given type prefix.
41-
-- It also enforces that the UUID is a v7 UUID.
42-
-- NOTE: we might want to make the version check optional.
4340
create or replace function typeid_check(tid typeid, expected_type text)
4441
returns boolean
4542
as $$
4643
declare
4744
prefix text;
48-
bytes bytea;
49-
ver int;
5045
begin
5146
prefix = (tid).type;
52-
bytes = uuid_send((tid).uuid);
53-
ver = (get_byte(bytes, 6) >> 4)::bit(4)::int;
54-
-- Check that:
55-
-- + The prefix matches the expected type
56-
-- + The UUID version is 7 OR it's the special "nil" UUID
57-
return prefix = expected_type AND (ver = 7 OR (tid).uuid = '00000000-0000-0000-0000-000000000000');
47+
return prefix = expected_type;
5848
end
5949
$$
6050
language plpgsql
6151
immutable;
6252

6353
-- Function that checks if a typeid is valid, for the given type_id in text format and type prefix, returns boolean.
64-
-- It also enforces that the UUID is a v7 UUID.
65-
create or replace function typeid_check_text(type_id text, expected_type text)
54+
create or replace function typeid_check_text(typeid_str text, expected_type text)
6655
returns boolean
6756
as $$
6857
declare
6958
prefix text;
7059
tid typeid;
71-
bytes bytea;
72-
ver int;
7360
begin
74-
tid = typeid_parse(type_id);
61+
tid = typeid_parse(typeid_str);
7562
prefix = (tid).type;
76-
bytes = uuid_send((tid).uuid);
77-
ver = (get_byte(bytes, 6) >> 4)::bit(4)::int;
78-
-- Check that:
79-
-- + The prefix matches the expected type
80-
-- + The UUID version is 7 OR it's the special "nil" UUID
81-
return prefix = expected_type AND (ver = 7 OR (tid).uuid = '00000000-0000-0000-0000-000000000000');
63+
return prefix = expected_type;
8264
end
8365
$$
8466
language plpgsql
@@ -138,39 +120,3 @@ end
138120
$$
139121
language plpgsql
140122
immutable;
141-
142-
-- Enables direct textual equality checks. This enables direct querying in pSQL without
143-
-- having to have clients know about db column internals- e.g. using the users table
144-
-- example in example.sql:
145-
--
146-
-- Query:
147-
-- SELECT * FROM users u WHERE u.id = 'user_01h455vb4pex5vsknk084sn02q'
148-
--
149-
-- Result:
150-
-- "(user,018962e7-3a6d-7290-b088-5c4e3bdf918c)",Ben Bitdiddle,ben@bitdiddle.com
151-
--
152-
-- Note: This also has the nice benefit of playing very well with generators
153-
-- such as Hibernate/JPA/JDBC/r2dbc, as you'll be able to do direct equality checks
154-
-- in repositories, such as for r2dbc:
155-
--
156-
-- @Query(value = "SELECT u.id, u.name, u.email FROM users u WHERE u.id = :id")
157-
-- Mono<UserEntity> findByPassedInTypeId(@Param("id") Mono<String> typeId); // user_01h455vb4pex5vsknk084sn02q
158-
--
159-
-- Note: This function only has to ever be declared once, and will work for any domains that use
160-
-- the original typeid type (e.g. this function gets called when querying for a user_id even though
161-
-- we never explicitly override the quality operator for a user_id.
162-
CREATE OR REPLACE FUNCTION compare_type_id_equality(lhs_id typeid, rhs_id VARCHAR)
163-
RETURNS BOOLEAN AS $$
164-
SELECT lhs_id = typeid_parse(rhs_id);
165-
$$ LANGUAGE SQL IMMUTABLE;
166-
167-
CREATE OPERATOR = (
168-
LEFTARG = typeid,
169-
RIGHTARG = VARCHAR,
170-
PROCEDURE = compare_type_id_equality,
171-
COMMUTATOR = =,
172-
NEGATOR = !=,
173-
HASHES,
174-
MERGES
175-
);
176-

sql/04_operator.sql

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
2+
-- Implementation of an equality operator that makes it easy to compare typeids stored
3+
-- as a compound tuple (prefix, uuid) against a typeid in text form.
4+
--
5+
-- This is useful so that clients can query using a textual representation of typeid.
6+
-- For example, using the users table in example.sql, you could write:
7+
--
8+
-- Query:
9+
-- SELECT * FROM users u WHERE u.id === 'user_01h455vb4pex5vsknk084sn02q'
10+
--
11+
-- Result:
12+
-- "(user,018962e7-3a6d-7290-b088-5c4e3bdf918c)",Ben Bitdiddle,ben@bitdiddle.com
13+
--
14+
-- Note: This also has the nice benefit of playing very well with generators
15+
-- such as Hibernate/JPA/JDBC/r2dbc, as you'll be able to do direct equality checks
16+
-- in repositories, such as for r2dbc:
17+
--
18+
-- @Query(value = "SELECT u.id, u.name, u.email FROM users u WHERE u.id === :id")
19+
-- Mono<UserEntity> findByPassedInTypeId(@Param("id") Mono<String> typeId); // user_01h455vb4pex5vsknk084sn02q
20+
--
21+
-- Note: This function only has to ever be declared once, and will work for any domains that use
22+
-- the original typeid type (e.g. this function gets called when querying for a user_id even though
23+
-- we never explicitly override the quality operator for a user_id.
24+
CREATE OR REPLACE FUNCTION typeid_eq_operator(lhs_id typeid, rhs_id VARCHAR)
25+
RETURNS BOOLEAN AS $$
26+
SELECT lhs_id = typeid_parse(rhs_id);
27+
$$ LANGUAGE SQL IMMUTABLE;
28+
29+
CREATE OPERATOR === (
30+
LEFTARG = typeid,
31+
RIGHTARG = VARCHAR,
32+
FUNCTION = typeid_eq_operator,
33+
COMMUTATOR = ===,
34+
NEGATOR = !==,
35+
HASHES,
36+
MERGES
37+
);
38+

supabase/migrations/04_example.sql

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../sql/04_operator.sql
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../example/compound.sql
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../example/text.sql
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ BEGIN;
33
SELECT plan(7);
44

55
create table tests (
6-
"tid" text Check(typeid_check_text(tid, 'generated'))
6+
"tid" text CHECK(typeid_check_text(tid, 'generated'))
77
);
88
-- -- Run tests for typeid_generate_text and typeid_check_text on tests table.
99

supabase/tests/05_operator_test.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ SELECT results_eq(
1717
);
1818

1919
SELECT results_eq(
20-
$$ SELECT typeid_print(tid) FROM tests where tid = 'test_01h455vb4pex5vsknk084sn02q' $$,
20+
$$ SELECT typeid_print(tid) FROM tests where tid === 'test_01h455vb4pex5vsknk084sn02q' $$,
2121
ARRAY['test_01h455vb4pex5vsknk084sn02q'],
2222
'Can select without needing to call typeid_parse() thanks to operator overload'
2323
);

0 commit comments

Comments
 (0)