Skip to content

feat(sql): implement TypeID specification 0.3.0 in typeid-sql #537

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions typeid/typeid-sql/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,27 @@ Then you can add in [the operator overload functions for typeid](https://github.
Some users have reported issues with the above operator when using Rails and ActiveRecord – we
recommend removing `COMMUTATOR` from the operator definition if you encounter issues.

## Tests

The tests in [`supabase/tests/`](./supabase/tests/) are using [pgTAP](https://supabase.com/docs/guides/database/extensions/pgtap) as test runner.

You need to install the development dependencies:
```sh
yarn install
```

Afterwards you can run the tests with the command:
```sh
yarn test
```

If you need to reset the database after changing the migrations in [`sql/`](./sql/), you can do so with the following command:
```sh
yarn test:reset
```

See also [Testing Your Database](https://supabase.com/docs/guides/database/testing) for more details.

## Future work (contributions welcome)

- Include examples not just for Postgres, but for other databases like MySQL as well.
Expand Down
6 changes: 6 additions & 0 deletions typeid/typeid-sql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,11 @@
"version": "0.1.0",
"devDependencies": {
"supabase": "^1.153.4"
},
"scripts": {
"test": "supabase start && supabase test db && supabase stop",
"test:start": "supabase start && supabase test db",
"test:stop": "supabase stop",
"test:reset": "supabase db reset"
}
}
27 changes: 16 additions & 11 deletions typeid/typeid-sql/sql/03_typeid.sql
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
create type "typeid" as ("type" varchar(63), "uuid" uuid);

-- Function that generates a random typeid of the given type.
-- This depends on the `uuid_generate_v7` function defined in `uuid_v7.sql`.
-- This depends on the `uuid_generate_v7` function defined in `01_uuidv7.sql`.
create or replace function typeid_generate(prefix text)
returns typeid
as $$
begin
if (prefix is null) or not (prefix ~ '^[a-z]{0,63}$') then
raise exception 'typeid prefix must match the regular expression [a-z]{0,63}';
if (prefix is null) or not (prefix ~ '^([a-z]([a-z_]{0,61}[a-z])?)?$') then
raise exception 'typeid prefix must match the regular expression ^([a-z]([a-z_]{0,61}[a-z])?)?$';
end if;
return (prefix, uuid_generate_v7())::typeid;
end
Expand All @@ -27,8 +27,8 @@ create or replace function typeid_generate_text(prefix text)
returns text
as $$
begin
if (prefix is null) or not (prefix ~ '^[a-z]{0,63}$') then
raise exception 'typeid prefix must match the regular expression [a-z]{0,63}';
if (prefix is null) or not (prefix ~ '^([a-z]([a-z_]{0,61}[a-z])?)?$') then
raise exception 'typeid prefix must match the regular expression ^([a-z]([a-z_]{0,61}[a-z])?)?$';
end if;
return typeid_print((prefix, uuid_generate_v7())::typeid);
end
Expand Down Expand Up @@ -73,21 +73,26 @@ as $$
declare
prefix text;
suffix text;
matches text[];
begin
if (typeid_str is null) then
return null;
end if;
if position('_' in typeid_str) = 0 then
return ('', base32_decode(typeid_str))::typeid;
end if;
prefix = split_part(typeid_str, '_', 1);
suffix = split_part(typeid_str, '_', 2);
matches = regexp_match(typeid_str, '^(.*)_(.*)$');
Copy link
Preview

Copilot AI Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex ^(.*)_(.*)$ uses greedy matching which could be inefficient for strings with multiple underscores. Consider using ^(.*?)_(.*)$ with non-greedy matching or ^([^_]+)_(.*)$ to match only the first underscore more efficiently.

Suggested change
matches = regexp_match(typeid_str, '^(.*)_(.*)$');
matches = regexp_match(typeid_str, '^([^_]+)_(.*)$');

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@loreto WDYT? We could already match the correct regular expression here (prefix + suffix) but would lose fidelity in the error messages afterwards.

if array_length(matches, 1) != 2 then
raise exception 'invalid typeid';
end if;
prefix = matches[1];
suffix = matches[2];
if prefix is null or prefix = '' then
raise exception 'typeid prefix cannot be empty with a delimiter';
end if;
-- prefix must match the regular expression [a-z]{0,63}
if not prefix ~ '^[a-z]{0,63}$' then
raise exception 'typeid prefix must match the regular expression [a-z]{0,63}';
if not prefix ~ '^([a-z]([a-z_]{0,61}[a-z])?)?$' then
raise exception 'typeid prefix must match the regular expression ^([a-z]([a-z_]{0,61}[a-z])?)?$';
end if;

return (prefix, base32_decode(suffix))::typeid;
Expand All @@ -109,8 +114,8 @@ begin
end if;
prefix = (tid).type;
suffix = base32_encode((tid).uuid);
if (prefix is null) or not (prefix ~ '^[a-z]{0,63}$') then
raise exception 'typeid prefix must match the regular expression [a-z]{0,63}';
if (prefix is null) or not (prefix ~ '^([a-z]([a-z_]{0,61}[a-z])?)?$') then
raise exception 'typeid prefix must match the regular expression ^([a-z]([a-z_]{0,61}[a-z])?)?$';
end if;
if prefix = '' then
return suffix;
Expand Down
51 changes: 37 additions & 14 deletions typeid/typeid-sql/supabase/tests/03_typed_text.test.sql
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
-- Start transaction and plan the tests.
BEGIN;
SELECT plan(7);
SELECT plan(9);

create table tests (
"tid" text CHECK(typeid_check_text(tid, 'generated'))
);

create table tests_underscores (
"tid" text CHECK(typeid_check_text(tid, 'ge_ne_ra_ted'))
);

-- -- Run tests for typeid_generate_text and typeid_check_text on tests table.

-- - name: generate-text
Expand All @@ -17,12 +22,40 @@ SELECT is(
'Generate typeid text with a specific prefix using typeid_generate_text'
);

-- - name: generate-text-underscore
-- typeid: "ge_ne_ra_ted_00000000000000000000000000"
-- description: "Generate a typeid with a specific prefix with multiple underscores using typeid_generate_text"
INSERT INTO tests_underscores (tid) VALUES (typeid_generate_text('ge_ne_ra_ted'));
SELECT is(
typeid_check_text((SELECT tid FROM tests_underscores), 'ge_ne_ra_ted'),
true,
'Generate typeid text with a specific prefix with multiple underscores using typeid_generate_text'
);

-- - name: generate-text-invalid-prefix
-- typeid: "12345_00000000000000000000000000"
-- description: "Attempt to generate a typeid with an invalid prefix"
SELECT throws_ok(
$$ INSERT INTO tests (tid) VALUES (typeid_generate_text('12345')); $$,
'typeid prefix must match the regular expression [a-z]{0,63}',
'typeid prefix must match the regular expression ^([a-z]([a-z_]{0,61}[a-z])?)?$',
'Generate typeid text with an invalid prefix should throw an error'
);

-- - name: generate-text-invalid-prefix-leading-underscore
-- typeid: "_generated_00000000000000000000000000"
-- description: "Attempt to generate a typeid with an invalid prefix with leading underscore"
SELECT throws_ok(
$$ INSERT INTO tests (tid) VALUES (typeid_generate_text('_generated')); $$,
'typeid prefix must match the regular expression ^([a-z]([a-z_]{0,61}[a-z])?)?$',
'Generate typeid text with an invalid prefix should throw an error'
);

-- - name: generate-text-invalid-prefix-trailing-underscore
-- typeid: "generated__00000000000000000000000000"
-- description: "Attempt to generate a typeid with an invalid prefix with trailing underscore"
SELECT throws_ok(
$$ INSERT INTO tests (tid) VALUES (typeid_generate_text('generated_')); $$,
'typeid prefix must match the regular expression ^([a-z]([a-z_]{0,61}[a-z])?)?$',
'Generate typeid text with an invalid prefix should throw an error'
);

Expand All @@ -43,7 +76,7 @@ SELECT is(
-- INSERT INTO tests (tid) VALUES ('12345_00000000000000000000000000');
SELECT throws_ok(
$$ INSERT into tests (tid) VALUES (typeid_parse('12345_00000000000000000000000000')); $$,
'typeid prefix must match the regular expression [a-z]{0,63}',
'typeid prefix must match the regular expression ^([a-z]([a-z_]{0,61}[a-z])?)?$',
'Parse invalid: prefix-underscore'
);

Expand All @@ -62,20 +95,10 @@ SELECT is(
-- description: "Attempt to generate a typeid with an invalid prefix"
SELECT throws_ok(
$$ INSERT INTO tests (tid) VALUES (typeid_generate_text('12345')); $$,
'typeid prefix must match the regular expression [a-z]{0,63}',
'typeid prefix must match the regular expression ^([a-z]([a-z_]{0,61}[a-z])?)?$',
'Generate typeid text with an invalid prefix should throw an error'
);

-- - name: check-text-valid
-- typeid: "generated_00000000000000000000000000"
-- description: "Check if a generated typeid text is valid"
INSERT INTO tests (tid) VALUES (typeid_generate_text('generated'));
SELECT is(
typeid_check_text((SELECT tid FROM tests limit 1), 'generated'),
true,
'Check if a generated typeid text is valid using typeid_check_text'
);


-- Finish the tests and clean up.
SELECT * FROM finish();
Expand Down
56 changes: 38 additions & 18 deletions typeid/typeid-sql/supabase/tests/03_typeid.test.sql
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
-- Start transaction and plan the tests.
BEGIN;
SELECT plan(41);
SELECT plan(44);

create table tests (
"tid" typeid
Expand Down Expand Up @@ -113,6 +113,26 @@ SELECT is(
'Print valid: valid-alphabet'
);

-- - name: valid-prefix-underscores
-- typeid: "pre_fix_0123456789abcdefghjkmnpqrs"
-- prefix: "pre_fix"
-- uuid: "0110c853-1d09-52d8-d73e-1194e95b5f19"
SELECT is(
typeid_parse('pre_fix_0123456789abcdefghjkmnpqrs'),
('pre_fix', '0110c853-1d09-52d8-d73e-1194e95b5f19')::typeid,
'Parse valid: valid-prefix-underscores'
);
SELECT is(
typeid_print(('pre_fix', '0110c853-1d09-52d8-d73e-1194e95b5f19')),
'pre_fix_0123456789abcdefghjkmnpqrs',
'Print valid: valid-alphabet-underscores'
);
SELECT is(
typeid_print(('pre_____fix', '0110c853-1d09-52d8-d73e-1194e95b5f19')),
'pre_____fix_0123456789abcdefghjkmnpqrs',
'Print valid: valid-alphabet-underscores'
);

-- - name: valid-uuidv7
-- typeid: "prefix_01h455vb4pex5vsknk084sn02q"
-- prefix: "prefix"
Expand All @@ -135,13 +155,13 @@ SELECT is(
-- description: "The prefix should be lowercase with no uppercase letters"
SELECT throws_ok(
$$ INSERT into tests (tid) VALUES (typeid_parse('PREFIX_00000000000000000000000000')); $$,
'typeid prefix must match the regular expression [a-z]{0,63}',
'typeid prefix must match the regular expression ^([a-z]([a-z_]{0,61}[a-z])?)?$',
'Parse invalid: prefix-uppercase'
);

SELECT throws_ok(
$$ INSERT into tests (tid) VALUES (typeid_generate('PREFIX')); $$,
'typeid prefix must match the regular expression [a-z]{0,63}',
'typeid prefix must match the regular expression ^([a-z]([a-z_]{0,61}[a-z])?)?$',
'Parse invalid: prefix-uppercase'
);

Expand All @@ -150,13 +170,13 @@ SELECT throws_ok(
-- description: "The prefix can't have numbers, it needs to be alphabetic"
SELECT throws_ok(
$$ INSERT into tests (tid) VALUES (typeid_parse('12345_00000000000000000000000000')); $$,
'typeid prefix must match the regular expression [a-z]{0,63}',
'typeid prefix must match the regular expression ^([a-z]([a-z_]{0,61}[a-z])?)?$',
'Parse invalid: prefix-numeric'
);

SELECT throws_ok(
$$ INSERT into tests (tid) VALUES (typeid_generate('12345')); $$,
'typeid prefix must match the regular expression [a-z]{0,63}',
'typeid prefix must match the regular expression ^([a-z]([a-z_]{0,61}[a-z])?)?$',
'Parse invalid: prefix-numeric'
);

Expand All @@ -165,28 +185,28 @@ SELECT throws_ok(
-- description: "The prefix can't have symbols, it needs to be alphabetic"
SELECT throws_ok(
$$ INSERT into tests (tid) VALUES (typeid_parse('pre.fix_00000000000000000000000000')); $$,
'typeid prefix must match the regular expression [a-z]{0,63}',
'typeid prefix must match the regular expression ^([a-z]([a-z_]{0,61}[a-z])?)?$',
'Parse invalid: prefix-period'
);

SELECT throws_ok(
$$ INSERT into tests (tid) VALUES (typeid_generate('pre.fix')); $$,
'typeid prefix must match the regular expression [a-z]{0,63}',
'typeid prefix must match the regular expression ^([a-z]([a-z_]{0,61}[a-z])?)?$',
'Parse invalid: prefix-period'
);

-- - name: prefix-underscore
-- typeid: "pre_fix_00000000000000000000000000"
-- description: "The prefix can't have symbols, it needs to be alphabetic"
-- description: "The prefix can't have leading or trailing underscores"
SELECT throws_ok(
$$ INSERT into tests (tid) VALUES (typeid_parse('pre_fix_00000000000000000000000000')); $$,
'typeid suffix must be 26 characters',
$$ INSERT into tests (tid) VALUES (typeid_generate('_prefix')); $$,
'typeid prefix must match the regular expression ^([a-z]([a-z_]{0,61}[a-z])?)?$',
'Parse invalid: prefix-underscore'
);

SELECT throws_ok(
$$ INSERT into tests (tid) VALUES (typeid_generate('pre_fix')); $$,
'typeid prefix must match the regular expression [a-z]{0,63}',
$$ INSERT into tests (tid) VALUES (typeid_generate('prefix_')); $$,
'typeid prefix must match the regular expression ^([a-z]([a-z_]{0,61}[a-z])?)?$',
'Parse invalid: prefix-underscore'
);

Expand All @@ -195,13 +215,13 @@ SELECT throws_ok(
-- description: "The prefix can only have ascii letters"
SELECT throws_ok(
$$ INSERT into tests (tid) VALUES (typeid_parse('préfix_00000000000000000000000000')); $$,
'typeid prefix must match the regular expression [a-z]{0,63}',
'typeid prefix must match the regular expression ^([a-z]([a-z_]{0,61}[a-z])?)?$',
'Parse invalid: prefix-non-ascii'
);

SELECT throws_ok(
$$ INSERT into tests (tid) VALUES (typeid_generate('préfix')); $$,
'typeid prefix must match the regular expression [a-z]{0,63}',
'typeid prefix must match the regular expression ^([a-z]([a-z_]{0,61}[a-z])?)?$',
'Parse invalid: prefix-non-ascii'
);

Expand All @@ -210,13 +230,13 @@ SELECT throws_ok(
-- description: "The prefix can't have any spaces"
SELECT throws_ok(
$$ INSERT into tests (tid) VALUES (typeid_parse(' prefix_00000000000000000000000000')); $$,
'typeid prefix must match the regular expression [a-z]{0,63}',
'typeid prefix must match the regular expression ^([a-z]([a-z_]{0,61}[a-z])?)?$',
'Parse invalid: prefix-spaces'
);

SELECT throws_ok(
$$ INSERT into tests (tid) VALUES (typeid_generate(' prefix')); $$,
'typeid prefix must match the regular expression [a-z]{0,63}',
'typeid prefix must match the regular expression ^([a-z]([a-z_]{0,61}[a-z])?)?$',
'Parse invalid: prefix-spaces'
);

Expand All @@ -226,13 +246,13 @@ SELECT throws_ok(
-- description: "The prefix can't be 64 characters, it needs to be 63 characters or less"
SELECT throws_ok(
$$ INSERT into tests (tid) VALUES (typeid_parse('abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl_00000000000000000000000000')); $$,
'typeid prefix must match the regular expression [a-z]{0,63}',
'typeid prefix must match the regular expression ^([a-z]([a-z_]{0,61}[a-z])?)?$',
'Parse invalid: prefix-64-chars'
);

SELECT throws_ok(
$$ INSERT into tests (tid) VALUES (typeid_generate('abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl')); $$,
'typeid prefix must match the regular expression [a-z]{0,63}',
'typeid prefix must match the regular expression ^([a-z]([a-z_]{0,61}[a-z])?)?$',
'Parse invalid: prefix-64-chars'
);

Expand Down
Loading