├── .github └── pull_request_template.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── devbox.json ├── devbox.lock ├── example ├── compound.sql └── text.sql ├── package.json ├── sql ├── 01_uuidv7.sql ├── 02_base32.sql ├── 03_typeid.sql └── 04_operator.sql ├── supabase ├── .gitignore ├── config.toml ├── migrations │ ├── 01_uuidv7.sql │ ├── 02_base32.sql │ ├── 03_typeid.sql │ ├── 04_operator.sql │ ├── 05_compound_example.sql │ └── 06_text_example.sql ├── seed.sql └── tests │ ├── 01_uuidv7.test.sql │ ├── 02_base32.test.sql │ ├── 03_typed_text.test.sql │ ├── 03_typeid.test.sql │ ├── 04_custom_type.test.sql │ └── 05_operator_test.sql └── yarn.lock /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # ⚠️ Submit PRs in our opensource monorepo instead ⚠️ 2 | 3 | This repository is automatically published from our opensource monorepo: 4 | https://github.com/jetify-com/opensource 5 | 6 | If you want to contribute code changes to this project, please submit your 7 | PR via the monorepo. 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | node_modules 4 | .pnp.* 5 | 6 | .turbo 7 | build/** 8 | dist/** 9 | .next/** 10 | coverage/** -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement. Use the 63 | "Report to repository admins" functionality on GitHub to report. 64 | 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][mozilla coc]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][faq]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [mozilla coc]: https://github.com/mozilla/diversity 132 | [faq]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please describe the change you wish to 4 | make via a related issue, or a pull request. 5 | 6 | Please note we have a [code of conduct](CODE_OF_CONDUCT.md), please follow it in 7 | all your interactions with the project. 8 | 9 | ## Opening a Pull Request 10 | 11 | This project is published as a standalone repo from our 12 | [opensource monorepo](https://github.com/jetify-com/opensource). Pull requests 13 | should be sent to the monorepo instead, and they will automatically be published 14 | to this repo when merged. 15 | 16 | Contributions made to this project must be made under the terms of the 17 | [Apache 2 License](https://www.apache.org/licenses/LICENSE-2.0). 18 | By contributing to this project you agree to the terms stated in the 19 | [Community Contribution License](https://github.com/jetify-com/opensource/blob/main/CONTRIBUTING.md#community-contribution-license). 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TypeID SQL 2 | 3 | ### A SQL implementation of [TypeID](https://github.com/jetify-com/typeid) using PostgreSQL. 4 | 5 | ![License: Apache 2.0](https://img.shields.io/github/license/jetify-com/typeid-sql) 6 | 7 | TypeIDs are a modern, **type-safe**, globally unique identifier based on the upcoming 8 | UUIDv7 standard. They provide a ton of nice properties that make them a great choice 9 | as the primary identifiers for your data in a database, APIs, and distributed systems. 10 | Read more about TypeIDs in their [spec](https://github.com/jetify-com/typeid). 11 | 12 | This particular implementation demonstrates how to use TypeIDs in a postgres database. 13 | 14 | ## Installation 15 | 16 | To use typeids in your Postgres instance, you'll need define all the 17 | appropriate types and functions by running the SQL scripts in this repo. 18 | 19 | We recommend copying the SQL scripts into your migrations directory and using the 20 | migration tool of your choice. For example, [Flyway](https://flywaydb.org/), or 21 | [Liquibase](https://www.liquibase.org/). 22 | 23 | Note that this repo is using Supabase as a way to easily start a Postgres instance 24 | for development and testing, but you do **not** need to use Supabase for this 25 | implementation to work – simply use the Postgres instance of your choice. 26 | 27 | ## Usage 28 | Once you've installed the TypeID types and functions in your Postgres instance, 29 | you have two options on how to encode TypeIDs in your database. 30 | 31 | ### 1. Text-based encoding 32 | This encoding is more inefficient than the alternative, but it's very straight-forward 33 | to understand, it's easy to debug by simply inspecting the contents of your tables, and 34 | it works well with other tools you might be using to inspect your database. 35 | 36 | To use it: 37 | + Declare your `id` column using the `text` type. 38 | + Use the `typeid_generate_text` function to generate new default values. 39 | + Use the `typeid_check_text` to enforce all strings in the column are valid typeids. 40 | 41 | Example: 42 | 43 | ```sql 44 | -- Define a `users` table that uses `user_id` as its primary key. 45 | -- We use the `typeid_generate_text` function to randomly generate a new typeid of the 46 | -- correct type for each user. 47 | -- We also recommend adding the check constraint to the column 48 | CREATE TABLE users ( 49 | "id" text not null default typeid_generate_text('user') CHECK (typeid_check_text(id, 'user')), 50 | "name" text, 51 | "email" text 52 | ); 53 | 54 | -- Now we can insert new users and have the `id` column automatically generated. 55 | INSERT INTO users ("name", "email") VALUES ('Alice P. Hacker', 'alice@hacker.net'); 56 | SELECT id FROM users; 57 | -- Result: 58 | -- "user_01hfs6amkdfem8sb6b1xmg7tq7" 59 | 60 | -- Insert a user with a specific typeid that might have been generated elsewhere: 61 | INSERT INTO users ("id", "name", "email") 62 | VALUES ('user_01h455vb4pex5vsknk084sn02q', 'Ben Bitdiddle', 'ben@bitdiddle.com'); 63 | 64 | -- To retrieve the ids as encoded strings, just use the column: 65 | SELECT id AS id, "name", "email" FROM users; 66 | 67 | -- You can also use filter in a WHERE clause to filter by typeid: 68 | SELECT typeid_print(id) AS id, "name", "email" FROM users 69 | WHERE id = 'user_01h455vb4pex5vsknk084sn02q'; 70 | ``` 71 | 72 | ### 2. UUID-based encoding using compound types 73 | In this approach, we internally encode typeids as a `(prefix, uuid)` tuple. The 74 | sql files in this library provide a predefined `typeid` type to represent 75 | said tuples. 76 | 77 | The advantage of this approach is that it is a more efficient encoding because we 78 | store the uuid portion of the typeid using the native `uuid` type. 79 | 80 | The disadvanage is that it is harder to work with and debug. 81 | 82 | If performance is a primary concern of yours, also consider using the native 83 | [postgres extension](https://github.com/blitss/typeid-postgres) for typeid, 84 | which exposes typeids as a "built-in" type. 85 | 86 | To define a new typeid using this encoding, you can use the `typeid_check` function: 87 | ```sql 88 | -- Define a `user_id` type, which is a typeid with type prefix "user". 89 | -- Using `user_id` throughout our schema, gives us type safety by guaranteeing 90 | -- that the type prefix is always "user". 91 | CREATE DOMAIN user_id AS typeid CHECK (typeid_check(value, 'user')); 92 | ``` 93 | 94 | You can now use the newly defined type in your tables. The `typeid_generate` function 95 | makes it possible to automatically a new random typeid for each row: 96 | 97 | ```sql 98 | -- Define a `users` table that uses `user_id` as its primary key. 99 | -- We use the `typeid_generate` function to randomly generate a new typeid of the 100 | -- correct type for each user. 101 | CREATE TABLE users ( 102 | "id" user_id not null default typeid_generate('user'), 103 | "name" text, 104 | "email" text 105 | ); 106 | 107 | -- Now we can insert new users and have the `id` column automatically generated. 108 | INSERT INTO users ("name", "email") VALUES ('Alice P. Hacker', 'alice@hacker.net'); 109 | ``` 110 | #### Querying 111 | To make it easy to query typeid tuples using the standard string representation, we 112 | provide two convenience functions: `typeid_parse` and `typeid_print`, which convert 113 | to and from the standard string representation. 114 | 115 | Example: 116 | 117 | ```sql 118 | -- Insert a user with a specific typeid that might have been generated elsewhere: 119 | INSERT INTO users ("id", "name", "email") 120 | VALUES (typeid_parse('user_01h455vb4pex5vsknk084sn02q'), 'Ben Bitdiddle', 'ben@bitdiddle.com'); 121 | 122 | -- To retrieve the ids as encoded strings, use the `typeid_print` function: 123 | SELECT typeid_print(id) AS id, "name", "email" FROM users; 124 | 125 | -- You can also use `typeid_parse` in a WHERE clause to filter by typeid: 126 | SELECT typeid_print(id) AS id, "name", "email" FROM users 127 | WHERE id = typeid_parse('user_01h455vb4pex5vsknk084sn02q'); 128 | ``` 129 | 130 | #### (Optional) Operator overload 131 | 132 | If you'd like to be able to do the following: 133 | 134 | ```sql 135 | -- Query directly from the DB with a serialized typeid 136 | SELECT * FROM users u WHERE u.id = 'user_01h455vb4pex5vsknk084sn02q'; 137 | 138 | -- Result: 139 | -- "(user,018962e7-3a6d-7290-b088-5c4e3bdf918c)",Ben Bitdiddle,ben@bitdiddle.com 140 | ``` 141 | 142 | Then you can add in [the operator overload functions for typeid](https://github.com/jetify-com/typeid-sql/blob/main/sql/04_operator.sql). 143 | 144 | Some users have reported issues with the above operator when using Rails and ActiveRecord – we 145 | recommend removing `COMMUTATOR` from the operator definition if you encounter issues. 146 | 147 | ## Future work (contributions welcome) 148 | 149 | - Include examples not just for Postgres, but for other databases like MySQL as well. 150 | - Consider rewriting this library as a postgres extension. It would make it possible to 151 | use the standard typeid string representation without the need of extra functions. 152 | -------------------------------------------------------------------------------- /devbox.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/main/.schema/devbox.schema.json", 3 | "packages": ["nodejs@18.12.1"], 4 | "env": { 5 | "DEVBOX_COREPACK_ENABLED": "true", 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /devbox.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfile_version": "1", 3 | "packages": { 4 | "github:NixOS/nixpkgs/nixpkgs-unstable": { 5 | "resolved": "github:NixOS/nixpkgs/02032da4af073d0f6110540c8677f16d4be0117f?lastModified=1741037377&narHash=sha256-SvtvVKHaUX4Owb%2BPasySwZsoc5VUeTf1px34BByiOxw%3D" 6 | }, 7 | "nodejs@18.12.1": { 8 | "last_modified": "2022-12-17T09:19:40Z", 9 | "plugin_version": "0.0.2", 10 | "resolved": "github:NixOS/nixpkgs/80c24eeb9ff46aa99617844d0c4168659e35175f#nodejs", 11 | "source": "devbox-search", 12 | "version": "18.12.1", 13 | "systems": { 14 | "aarch64-darwin": { 15 | "outputs": [ 16 | { 17 | "name": "out", 18 | "path": "/nix/store/l5w036ax6dpla6djgf1hbdwp2mfz00z0-nodejs-18.12.1", 19 | "default": true 20 | }, 21 | { 22 | "name": "libv8", 23 | "path": "/nix/store/p1y9z8vyik3zaigp8xk1k2631lkc4ly4-nodejs-18.12.1-libv8" 24 | } 25 | ], 26 | "store_path": "/nix/store/l5w036ax6dpla6djgf1hbdwp2mfz00z0-nodejs-18.12.1" 27 | }, 28 | "aarch64-linux": { 29 | "outputs": [ 30 | { 31 | "name": "out", 32 | "path": "/nix/store/r35wij4y7rvnjk8ziydah0j2shhjasjw-nodejs-18.12.1", 33 | "default": true 34 | }, 35 | { 36 | "name": "libv8", 37 | "path": "/nix/store/lglrgvdl7jvjlfm8zaz1k115rj2gdiqm-nodejs-18.12.1-libv8" 38 | } 39 | ], 40 | "store_path": "/nix/store/r35wij4y7rvnjk8ziydah0j2shhjasjw-nodejs-18.12.1" 41 | }, 42 | "x86_64-darwin": { 43 | "outputs": [ 44 | { 45 | "name": "out", 46 | "path": "/nix/store/ibafg0ajk6nd2632lzyaiwd4p4la1h11-nodejs-18.12.1", 47 | "default": true 48 | }, 49 | { 50 | "name": "libv8", 51 | "path": "/nix/store/innps02az9w3iqv7jdlchpkgqarxzh8i-nodejs-18.12.1-libv8" 52 | } 53 | ], 54 | "store_path": "/nix/store/ibafg0ajk6nd2632lzyaiwd4p4la1h11-nodejs-18.12.1" 55 | }, 56 | "x86_64-linux": { 57 | "outputs": [ 58 | { 59 | "name": "out", 60 | "path": "/nix/store/7ppnyqmp1c92gkfrixbz99sw7q6ifkg1-nodejs-18.12.1", 61 | "default": true 62 | }, 63 | { 64 | "name": "libv8", 65 | "path": "/nix/store/a11acjk7jvrb9rlr99hyf48ipwcjz25n-nodejs-18.12.1-libv8" 66 | } 67 | ], 68 | "store_path": "/nix/store/7ppnyqmp1c92gkfrixbz99sw7q6ifkg1-nodejs-18.12.1" 69 | } 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /example/compound.sql: -------------------------------------------------------------------------------- 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 | 4 | -- In this example we'll define a users table that uses typeids to identify users. 5 | 6 | -- Define a `user_id` type, which is a typeid with type prefix "user". 7 | -- Using `user_id` throughout our schema, gives us type safety by guaranteeing 8 | -- that the type prefix is always "user". 9 | create domain user_id AS typeid check (typeid_check(value, 'user')); 10 | 11 | -- Define a `users` table that uses `user_id` as its primary key. 12 | -- We use the `typeid_generate` function to randomly generate a new typeid of the 13 | -- correct type for each user. 14 | create table users ( 15 | "id" user_id not null default typeid_generate('user'), 16 | "name" text, 17 | "email" text 18 | ); 19 | 20 | -- Now we can insert new uses and have the `id` column automatically generated. 21 | insert into users ("name", "email") values ('Alice P. Hacker', 'alice@hacker.net'); 22 | 23 | -- Or you can specify the typeid yourself: 24 | insert into users ("id", "name", "email") 25 | values (typeid_parse('user_01h455vb4pex5vsknk084sn02q'), 'Ben Bitdiddle', 'ben@bitdiddle.com'); 26 | 27 | -- To retrieve the ids as encoded strings, use the `typeid_print` function: 28 | select typeid_print(id) AS id, "name", "email" from users; -------------------------------------------------------------------------------- /example/text.sql: -------------------------------------------------------------------------------- 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'); 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typeid-sql", 3 | "version": "0.1.0", 4 | "devDependencies": { 5 | "supabase": "^1.153.4" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /sql/01_uuidv7.sql: -------------------------------------------------------------------------------- 1 | -- Function to generate new v7 UUIDs. 2 | -- In the future we might want use an extension: https://github.com/fboulnois/pg_uuidv7 3 | -- Or, once the UUIDv7 spec is finalized, it will probably make it into the 'uuid-ossp' extension 4 | -- and a custom function will no longer be necessary. 5 | create or replace function uuid_generate_v7() 6 | returns uuid 7 | as $$ 8 | declare 9 | unix_ts_ms bytea; 10 | uuid_bytes bytea; 11 | begin 12 | unix_ts_ms = substring(int8send(floor(extract(epoch from clock_timestamp()) * 1000)::bigint) from 3); 13 | uuid_bytes = uuid_send(gen_random_uuid()); 14 | uuid_bytes = overlay(uuid_bytes placing unix_ts_ms from 1 for 6); 15 | uuid_bytes = set_byte(uuid_bytes, 6, (b'0111' || get_byte(uuid_bytes, 6)::bit(4))::bit(8)::int); 16 | return encode(uuid_bytes, 'hex')::uuid; 17 | end 18 | $$ 19 | language plpgsql 20 | volatile; -------------------------------------------------------------------------------- /sql/02_base32.sql: -------------------------------------------------------------------------------- 1 | -- Functions to encode and decode UUIDs to and from base32. 2 | 3 | -- Encodes a UUID as a base32 string 4 | create or replace function base32_encode(id uuid) 5 | returns text 6 | as $$ 7 | declare 8 | bytes bytea; 9 | alphabet bytea = '0123456789abcdefghjkmnpqrstvwxyz'; 10 | output text = ''; 11 | begin 12 | bytes = uuid_send(id); 13 | 14 | -- 10 byte timestamp 15 | output = output || chr(get_byte(alphabet, (get_byte(bytes, 0) & 224) >> 5)); 16 | output = output || chr(get_byte(alphabet, (get_byte(bytes, 0) & 31))); 17 | output = output || chr(get_byte(alphabet, (get_byte(bytes, 1) & 248) >> 3)); 18 | output = output || chr(get_byte(alphabet, ((get_byte(bytes, 1) & 7) << 2) | ((get_byte(bytes, 2) & 192) >> 6))); 19 | output = output || chr(get_byte(alphabet, (get_byte(bytes, 2) & 62) >> 1)); 20 | output = output || chr(get_byte(alphabet, ((get_byte(bytes, 2) & 1) << 4) | ((get_byte(bytes, 3) & 240) >> 4))); 21 | output = output || chr(get_byte(alphabet, ((get_byte(bytes, 3) & 15) << 1) | ((get_byte(bytes, 4) & 128) >> 7))); 22 | output = output || chr(get_byte(alphabet, (get_byte(bytes, 4) & 124) >> 2)); 23 | output = output || chr(get_byte(alphabet, ((get_byte(bytes, 4) & 3) << 3) | ((get_byte(bytes, 5) & 224) >> 5))); 24 | output = output || chr(get_byte(alphabet, (get_byte(bytes, 5) & 31))); 25 | 26 | -- 16 bytes of entropy 27 | output = output || chr(get_byte(alphabet, (get_byte(bytes, 6) & 248) >> 3)); 28 | output = output || chr(get_byte(alphabet, ((get_byte(bytes, 6) & 7) << 2) | ((get_byte(bytes, 7) & 192) >> 6))); 29 | output = output || chr(get_byte(alphabet, (get_byte(bytes, 7) & 62) >> 1)); 30 | output = output || chr(get_byte(alphabet, ((get_byte(bytes, 7) & 1) << 4) | ((get_byte(bytes, 8) & 240) >> 4))); 31 | output = output || chr(get_byte(alphabet, ((get_byte(bytes, 8) & 15) << 1) | ((get_byte(bytes, 9) & 128) >> 7))); 32 | output = output || chr(get_byte(alphabet, (get_byte(bytes, 9) & 124) >> 2)); 33 | output = output || chr(get_byte(alphabet, ((get_byte(bytes, 9) & 3) << 3) | ((get_byte(bytes, 10) & 224) >> 5))); 34 | output = output || chr(get_byte(alphabet, (get_byte(bytes, 10) & 31))); 35 | output = output || chr(get_byte(alphabet, (get_byte(bytes, 11) & 248) >> 3)); 36 | output = output || chr(get_byte(alphabet, ((get_byte(bytes, 11) & 7) << 2) | ((get_byte(bytes, 12) & 192) >> 6))); 37 | output = output || chr(get_byte(alphabet, (get_byte(bytes, 12) & 62) >> 1)); 38 | output = output || chr(get_byte(alphabet, ((get_byte(bytes, 12) & 1) << 4) | ((get_byte(bytes, 13) & 240) >> 4))); 39 | output = output || chr(get_byte(alphabet, ((get_byte(bytes, 13) & 15) << 1) | ((get_byte(bytes, 14) & 128) >> 7))); 40 | output = output || chr(get_byte(alphabet, (get_byte(bytes, 14) & 124) >> 2)); 41 | output = output || chr(get_byte(alphabet, ((get_byte(bytes, 14) & 3) << 3) | ((get_byte(bytes, 15) & 224) >> 5))); 42 | output = output || chr(get_byte(alphabet, (get_byte(bytes, 15) & 31))); 43 | 44 | return output; 45 | end 46 | $$ 47 | language plpgsql 48 | immutable; 49 | 50 | 51 | create or replace function base32_decode(s text) 52 | returns uuid as $$ 53 | declare 54 | dec bytea = '\xFF FF FF FF FF FF FF FF FF FF'::bytea || 55 | '\xFF FF FF FF FF FF FF FF FF FF'::bytea || 56 | '\xFF FF FF FF FF FF FF FF FF FF'::bytea || 57 | '\xFF FF FF FF FF FF FF FF FF FF'::bytea || 58 | '\xFF FF FF FF FF FF FF FF 00 01'::bytea || 59 | '\x02 03 04 05 06 07 08 09 FF FF'::bytea || 60 | '\xFF FF FF FF FF FF FF FF FF FF'::bytea || 61 | '\xFF FF FF FF FF FF FF FF FF FF'::bytea || 62 | '\xFF FF FF FF FF FF FF FF FF FF'::bytea || 63 | '\xFF FF FF FF FF FF FF 0A 0B 0C'::bytea || 64 | '\x0D 0E 0F 10 11 FF 12 13 FF 14'::bytea || 65 | '\x15 FF 16 17 18 19 1A FF 1B 1C'::bytea || 66 | '\x1D 1E 1F FF FF FF FF FF FF FF'::bytea || 67 | '\xFF FF FF FF FF FF FF FF FF FF'::bytea || 68 | '\xFF FF FF FF FF FF FF FF FF FF'::bytea || 69 | '\xFF FF FF FF FF FF FF FF FF FF'::bytea || 70 | '\xFF FF FF FF FF FF FF FF FF FF'::bytea || 71 | '\xFF FF FF FF FF FF FF FF FF FF'::bytea || 72 | '\xFF FF FF FF FF FF FF FF FF FF'::bytea || 73 | '\xFF FF FF FF FF FF FF FF FF FF'::bytea || 74 | '\xFF FF FF FF FF FF FF FF FF FF'::bytea || 75 | '\xFF FF FF FF FF FF FF FF FF FF'::bytea || 76 | '\xFF FF FF FF FF FF FF FF FF FF'::bytea || 77 | '\xFF FF FF FF FF FF FF FF FF FF'::bytea || 78 | '\xFF FF FF FF FF FF FF FF FF FF'::bytea || 79 | '\xFF FF FF FF FF FF'::bytea; 80 | v bytea = convert_to(s, 'UTF8'); 81 | id bytea = '\x00000000000000000000000000000000'; 82 | begin 83 | if length(s) <> 26 then 84 | raise exception 'typeid suffix must be 26 characters'; 85 | end if; 86 | 87 | if get_byte(dec, get_byte(v, 0)) = 255 or 88 | get_byte(dec, get_byte(v, 1)) = 255 or 89 | get_byte(dec, get_byte(v, 2)) = 255 or 90 | get_byte(dec, get_byte(v, 3)) = 255 or 91 | get_byte(dec, get_byte(v, 4)) = 255 or 92 | get_byte(dec, get_byte(v, 5)) = 255 or 93 | get_byte(dec, get_byte(v, 6)) = 255 or 94 | get_byte(dec, get_byte(v, 7)) = 255 or 95 | get_byte(dec, get_byte(v, 8)) = 255 or 96 | get_byte(dec, get_byte(v, 9)) = 255 or 97 | get_byte(dec, get_byte(v, 10)) = 255 or 98 | get_byte(dec, get_byte(v, 11)) = 255 or 99 | get_byte(dec, get_byte(v, 12)) = 255 or 100 | get_byte(dec, get_byte(v, 13)) = 255 or 101 | get_byte(dec, get_byte(v, 14)) = 255 or 102 | get_byte(dec, get_byte(v, 15)) = 255 or 103 | get_byte(dec, get_byte(v, 16)) = 255 or 104 | get_byte(dec, get_byte(v, 17)) = 255 or 105 | get_byte(dec, get_byte(v, 18)) = 255 or 106 | get_byte(dec, get_byte(v, 19)) = 255 or 107 | get_byte(dec, get_byte(v, 20)) = 255 or 108 | get_byte(dec, get_byte(v, 21)) = 255 or 109 | get_byte(dec, get_byte(v, 22)) = 255 or 110 | get_byte(dec, get_byte(v, 23)) = 255 or 111 | get_byte(dec, get_byte(v, 24)) = 255 or 112 | get_byte(dec, get_byte(v, 25)) = 255 113 | then 114 | raise exception 'typeid suffix must only use characters from the base32 alphabet'; 115 | end if; 116 | 117 | if chr(get_byte(v, 0)) > '7' then 118 | raise exception 'typeid suffix must start with 0-7'; 119 | end if; 120 | -- Transform base32 to binary array 121 | -- 6 bytes timestamp (48 bits) 122 | id = set_byte(id, 0, (get_byte(dec, get_byte(v, 0)) << 5) | get_byte(dec, get_byte(v, 1))); 123 | id = set_byte(id, 1, (get_byte(dec, get_byte(v, 2)) << 3) | (get_byte(dec, get_byte(v, 3)) >> 2)); 124 | id = set_byte(id, 2, ((get_byte(dec, get_byte(v, 3)) & 3) << 6) | (get_byte(dec, get_byte(v, 4)) << 1) | (get_byte(dec, get_byte(v, 5)) >> 4)); 125 | id = set_byte(id, 3, ((get_byte(dec, get_byte(v, 5)) & 15) << 4) | (get_byte(dec, get_byte(v, 6)) >> 1)); 126 | id = set_byte(id, 4, ((get_byte(dec, get_byte(v, 6)) & 1) << 7) | (get_byte(dec, get_byte(v, 7)) << 2) | (get_byte(dec, get_byte(v, 8)) >> 3)); 127 | id = set_byte(id, 5, ((get_byte(dec, get_byte(v, 8)) & 7) << 5) | get_byte(dec, get_byte(v, 9))); 128 | 129 | -- 10 bytes of entropy (80 bits) 130 | id = set_byte(id, 6, (get_byte(dec, get_byte(v, 10)) << 3) | (get_byte(dec, get_byte(v, 11)) >> 2)); 131 | id = set_byte(id, 7, ((get_byte(dec, get_byte(v, 11)) & 3) << 6) | (get_byte(dec, get_byte(v, 12)) << 1) | (get_byte(dec, get_byte(v, 13)) >> 4)); 132 | id = set_byte(id, 8, ((get_byte(dec, get_byte(v, 13)) & 15) << 4) | (get_byte(dec, get_byte(v, 14)) >> 1)); 133 | id = set_byte(id, 9, ((get_byte(dec, get_byte(v, 14)) & 1) << 7) | (get_byte(dec, get_byte(v, 15)) << 2) | (get_byte(dec, get_byte(v, 16)) >> 3)); 134 | id = set_byte(id, 10, ((get_byte(dec, get_byte(v, 16)) & 7) << 5) | get_byte(dec, get_byte(v, 17))); 135 | id = set_byte(id, 11, (get_byte(dec, get_byte(v, 18)) << 3) | (get_byte(dec, get_byte(v, 19)) >> 2)); 136 | id = set_byte(id, 12, ((get_byte(dec, get_byte(v, 19)) & 3) << 6) | (get_byte(dec, get_byte(v, 20)) << 1) | (get_byte(dec, get_byte(v, 21)) >> 4)); 137 | id = set_byte(id, 13, ((get_byte(dec, get_byte(v, 21)) & 15) << 4) | (get_byte(dec, get_byte(v, 22)) >> 1)); 138 | id = set_byte(id, 14, ((get_byte(dec, get_byte(v, 22)) & 1) << 7) | (get_byte(dec, get_byte(v, 23)) << 2) | (get_byte(dec, get_byte(v, 24)) >> 3)); 139 | id = set_byte(id, 15, ((get_byte(dec, get_byte(v, 24)) & 7) << 5) | get_byte(dec, get_byte(v, 25))); 140 | return encode(id, 'hex')::uuid; 141 | end 142 | $$ 143 | language plpgsql 144 | immutable; 145 | -------------------------------------------------------------------------------- /sql/03_typeid.sql: -------------------------------------------------------------------------------- 1 | -- Implementation of typeids in SQL (Postgres). 2 | -- This file: 3 | -- + Defines a `typeid` type: a composite type consisting of a type prefix, 4 | -- and a UUID 5 | -- + Defines functions to generate and validate typeids in SQL. 6 | 7 | -- Create a `typeid` type. 8 | create type "typeid" as ("type" varchar(63), "uuid" uuid); 9 | 10 | -- Function that generates a random typeid of the given type. 11 | -- This depends on the `uuid_generate_v7` function defined in `uuid_v7.sql`. 12 | create or replace function typeid_generate(prefix text) 13 | returns typeid 14 | as $$ 15 | begin 16 | if (prefix is null) or not (prefix ~ '^[a-z]{0,63}$') then 17 | raise exception 'typeid prefix must match the regular expression [a-z]{0,63}'; 18 | end if; 19 | return (prefix, uuid_generate_v7())::typeid; 20 | end 21 | $$ 22 | language plpgsql 23 | volatile; 24 | 25 | -- Function that generates a type_id of given type, and returns the parsed typeid as text. 26 | create or replace function typeid_generate_text(prefix text) 27 | returns text 28 | as $$ 29 | begin 30 | if (prefix is null) or not (prefix ~ '^[a-z]{0,63}$') then 31 | raise exception 'typeid prefix must match the regular expression [a-z]{0,63}'; 32 | end if; 33 | return typeid_print((prefix, uuid_generate_v7())::typeid); 34 | end 35 | $$ 36 | language plpgsql 37 | volatile; 38 | 39 | -- Function that checks if a typeid is valid, for the given type prefix. 40 | create or replace function typeid_check(tid typeid, expected_type text) 41 | returns boolean 42 | as $$ 43 | declare 44 | prefix text; 45 | begin 46 | prefix = (tid).type; 47 | return prefix = expected_type; 48 | end 49 | $$ 50 | language plpgsql 51 | immutable; 52 | 53 | -- Function that checks if a typeid is valid, for the given type_id in text format and type prefix, returns boolean. 54 | create or replace function typeid_check_text(typeid_str text, expected_type text) 55 | returns boolean 56 | as $$ 57 | declare 58 | prefix text; 59 | tid typeid; 60 | begin 61 | tid = typeid_parse(typeid_str); 62 | prefix = (tid).type; 63 | return prefix = expected_type; 64 | end 65 | $$ 66 | language plpgsql 67 | immutable; 68 | 69 | -- Function that parses a string into a typeid. 70 | create or replace function typeid_parse(typeid_str text) 71 | returns typeid 72 | as $$ 73 | declare 74 | prefix text; 75 | suffix text; 76 | begin 77 | if (typeid_str is null) then 78 | return null; 79 | end if; 80 | if position('_' in typeid_str) = 0 then 81 | return ('', base32_decode(typeid_str))::typeid; 82 | end if; 83 | prefix = split_part(typeid_str, '_', 1); 84 | suffix = split_part(typeid_str, '_', 2); 85 | if prefix is null or prefix = '' then 86 | raise exception 'typeid prefix cannot be empty with a delimiter'; 87 | end if; 88 | -- prefix must match the regular expression [a-z]{0,63} 89 | if not prefix ~ '^[a-z]{0,63}$' then 90 | raise exception 'typeid prefix must match the regular expression [a-z]{0,63}'; 91 | end if; 92 | 93 | return (prefix, base32_decode(suffix))::typeid; 94 | end 95 | $$ 96 | language plpgsql 97 | immutable; 98 | 99 | -- Function that serializes a typeid into a string. 100 | create or replace function typeid_print(tid typeid) 101 | returns text 102 | as $$ 103 | declare 104 | prefix text; 105 | suffix text; 106 | begin 107 | if (tid is null) then 108 | return null; 109 | end if; 110 | prefix = (tid).type; 111 | suffix = base32_encode((tid).uuid); 112 | if (prefix is null) or not (prefix ~ '^[a-z]{0,63}$') then 113 | raise exception 'typeid prefix must match the regular expression [a-z]{0,63}'; 114 | end if; 115 | if prefix = '' then 116 | return suffix; 117 | end if; 118 | return (prefix || '_' || suffix); 119 | end 120 | $$ 121 | language plpgsql 122 | immutable; 123 | -------------------------------------------------------------------------------- /sql/04_operator.sql: -------------------------------------------------------------------------------- 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 findByPassedInTypeId(@Param("id") Mono 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 | 39 | -------------------------------------------------------------------------------- /supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | -------------------------------------------------------------------------------- /supabase/config.toml: -------------------------------------------------------------------------------- 1 | # A string used to distinguish different Supabase projects on the same host. Defaults to the working 2 | # directory name when running `supabase init`. 3 | project_id = "typeid-sql" 4 | 5 | [api] 6 | # Port to use for the API URL. 7 | port = 54321 8 | # Schemas to expose in your API. Tables, views and stored procedures in this schema will get API 9 | # endpoints. public and storage are always included. 10 | schemas = ["public", "storage", "graphql_public"] 11 | # Extra schemas to add to the search_path of every request. public is always included. 12 | extra_search_path = ["public", "extensions"] 13 | # The maximum number of rows returns from a view, table, or stored procedure. Limits payload size 14 | # for accidental or malicious requests. 15 | max_rows = 1000 16 | 17 | [db] 18 | # Port to use for the local database URL. 19 | port = 54322 20 | # The database major version to use. This has to be the same as your remote database's. Run `SHOW 21 | # server_version;` on the remote database to check. 22 | major_version = 15 23 | 24 | [studio] 25 | # Port to use for Supabase Studio. 26 | port = 54323 27 | 28 | # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they 29 | # are monitored, and you can view the emails that would have been sent from the web interface. 30 | [inbucket] 31 | # Port to use for the email testing server web interface. 32 | port = 54324 33 | smtp_port = 54325 34 | pop3_port = 54326 35 | 36 | [storage] 37 | # The maximum file size allowed (e.g. "5MB", "500KB"). 38 | file_size_limit = "50MiB" 39 | 40 | [auth] 41 | # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used 42 | # in emails. 43 | site_url = "http://localhost:3000" 44 | # A list of *exact* URLs that auth providers are permitted to redirect to post authentication. 45 | additional_redirect_urls = ["https://localhost:3000"] 46 | # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one 47 | # week). 48 | jwt_expiry = 3600 49 | # Allow/disallow new user signups to your project. 50 | enable_signup = true 51 | 52 | [auth.email] 53 | # Allow/disallow new user signups via email to your project. 54 | enable_signup = true 55 | # If enabled, a user will be required to confirm any email change on both the old, and new email 56 | # addresses. If disabled, only the new email is required to confirm. 57 | double_confirm_changes = true 58 | # If enabled, users need to confirm their email address before signing in. 59 | enable_confirmations = false 60 | 61 | # Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, 62 | # `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin`, `notion`, `twitch`, 63 | # `twitter`, `slack`, `spotify`, `workos`, `zoom`. 64 | [auth.external.apple] 65 | enabled = false 66 | client_id = "" 67 | secret = "" 68 | # Overrides the default auth redirectUrl. 69 | redirect_uri = "" 70 | # Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, 71 | # or any other third-party OIDC providers. 72 | url = "" 73 | 74 | [analytics] 75 | enabled = false 76 | port = 54327 77 | vector_port = 54328 78 | # Setup BigQuery project to enable log viewer on local development stack. 79 | # See: https://supabase.com/docs/guides/getting-started/local-development#enabling-local-logging 80 | gcp_project_id = "" 81 | gcp_project_number = "" 82 | gcp_jwt_path = "supabase/gcloud.json" 83 | -------------------------------------------------------------------------------- /supabase/migrations/01_uuidv7.sql: -------------------------------------------------------------------------------- 1 | ../../sql/01_uuidv7.sql -------------------------------------------------------------------------------- /supabase/migrations/02_base32.sql: -------------------------------------------------------------------------------- 1 | ../../sql/02_base32.sql -------------------------------------------------------------------------------- /supabase/migrations/03_typeid.sql: -------------------------------------------------------------------------------- 1 | ../../sql/03_typeid.sql -------------------------------------------------------------------------------- /supabase/migrations/04_operator.sql: -------------------------------------------------------------------------------- 1 | ../../sql/04_operator.sql -------------------------------------------------------------------------------- /supabase/migrations/05_compound_example.sql: -------------------------------------------------------------------------------- 1 | ../../example/compound.sql -------------------------------------------------------------------------------- /supabase/migrations/06_text_example.sql: -------------------------------------------------------------------------------- 1 | ../../example/text.sql -------------------------------------------------------------------------------- /supabase/seed.sql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetify-com/typeid-sql/c459e0cbfe8a5344d2b5b7a7b618c2118f2cd311/supabase/seed.sql -------------------------------------------------------------------------------- /supabase/tests/01_uuidv7.test.sql: -------------------------------------------------------------------------------- 1 | -- Start transaction and plan the tests. 2 | BEGIN; 3 | SELECT plan(1); 4 | 5 | -- Run the tests. 6 | SELECT isa_ok(uuid_generate_v7(), 'uuid', 'uuid_generate_v7()'); 7 | 8 | -- Finish the tests and clean up. 9 | SELECT * FROM finish(); 10 | ROLLBACK; 11 | 12 | -------------------------------------------------------------------------------- /supabase/tests/02_base32.test.sql: -------------------------------------------------------------------------------- 1 | -- Start transaction and plan the tests. 2 | BEGIN; 3 | SELECT plan(4); 4 | 5 | -- Run the tests. 6 | SELECT is(base32_encode('00000000-0000-0000-0000-000000000000'), 7 | '00000000000000000000000000', 8 | 'Encode nil uuid' ); 9 | SELECT is(base32_decode('00000000000000000000000000'), 10 | '00000000-0000-0000-0000-000000000000', 11 | 'Decode nil uuid' ); 12 | 13 | SELECT is(base32_encode('01890a5d-ac96-774b-bcce-b302099a8057'), 14 | '01h455vb4pex5vsknk084sn02q', 15 | 'Encode valid uuidv7' ); 16 | SELECT is(base32_decode('01h455vb4pex5vsknk084sn02q'), 17 | '01890a5d-ac96-774b-bcce-b302099a8057', 18 | 'Decode valid uuidv7' ); 19 | 20 | -- Finish the tests and clean up. 21 | SELECT * FROM finish(); 22 | ROLLBACK; 23 | 24 | -------------------------------------------------------------------------------- /supabase/tests/03_typed_text.test.sql: -------------------------------------------------------------------------------- 1 | -- Start transaction and plan the tests. 2 | BEGIN; 3 | SELECT plan(7); 4 | 5 | create table tests ( 6 | "tid" text CHECK(typeid_check_text(tid, 'generated')) 7 | ); 8 | -- -- Run tests for typeid_generate_text and typeid_check_text on tests table. 9 | 10 | -- - name: generate-text 11 | -- typeid: "generated_00000000000000000000000000" 12 | -- description: "Generate a typeid with a specific prefix using typeid_generate_text" 13 | INSERT INTO tests (tid) VALUES (typeid_generate_text('generated')); 14 | SELECT is( 15 | typeid_check_text((SELECT tid FROM tests), 'generated'), 16 | true, 17 | 'Generate typeid text with a specific prefix using typeid_generate_text' 18 | ); 19 | 20 | -- - name: generate-text-invalid-prefix 21 | -- typeid: "12345_00000000000000000000000000" 22 | -- description: "Attempt to generate a typeid with an invalid prefix" 23 | SELECT throws_ok( 24 | $$ INSERT INTO tests (tid) VALUES (typeid_generate_text('12345')); $$, 25 | 'typeid prefix must match the regular expression [a-z]{0,63}', 26 | 'Generate typeid text with an invalid prefix should throw an error' 27 | ); 28 | 29 | -- - name: check-text-valid 30 | -- typeid: "generated_00000000000000000000000000" 31 | -- description: "Check if a generated typeid text is valid" 32 | INSERT INTO tests (tid) VALUES (typeid_generate_text('generated')); 33 | SELECT is( 34 | typeid_check_text((SELECT tid FROM tests limit 35 | 1), 'generated'), 36 | true, 37 | 'Check if a generated typeid text is valid using typeid_check_text' 38 | ); 39 | 40 | -- - name: check-text-invalid-prefix 41 | -- typeid: "12345_00000000000000000000000000" 42 | -- description: "Check if a typeid text with an invalid prefix is invalid" 43 | -- INSERT INTO tests (tid) VALUES ('12345_00000000000000000000000000'); 44 | SELECT throws_ok( 45 | $$ INSERT into tests (tid) VALUES (typeid_parse('12345_00000000000000000000000000')); $$, 46 | 'typeid prefix must match the regular expression [a-z]{0,63}', 47 | 'Parse invalid: prefix-underscore' 48 | ); 49 | 50 | -- - name: generate-text 51 | -- typeid: "generated_00000000000000000000000000" 52 | -- description: "Generate a typeid with a specific prefix using typeid_generate_text" 53 | INSERT INTO tests (tid) VALUES (typeid_generate_text('generated')); 54 | SELECT is( 55 | typeid_check_text((SELECT tid FROM tests limit 1), 'generated'), 56 | true, 57 | 'Generate typeid text with a specific prefix using typeid_generate_text' 58 | ); 59 | 60 | -- - name: generate-text-invalid-prefix 61 | -- typeid: "12345_00000000000000000000000000" 62 | -- description: "Attempt to generate a typeid with an invalid prefix" 63 | SELECT throws_ok( 64 | $$ INSERT INTO tests (tid) VALUES (typeid_generate_text('12345')); $$, 65 | 'typeid prefix must match the regular expression [a-z]{0,63}', 66 | 'Generate typeid text with an invalid prefix should throw an error' 67 | ); 68 | 69 | -- - name: check-text-valid 70 | -- typeid: "generated_00000000000000000000000000" 71 | -- description: "Check if a generated typeid text is valid" 72 | INSERT INTO tests (tid) VALUES (typeid_generate_text('generated')); 73 | SELECT is( 74 | typeid_check_text((SELECT tid FROM tests limit 1), 'generated'), 75 | true, 76 | 'Check if a generated typeid text is valid using typeid_check_text' 77 | ); 78 | 79 | 80 | -- Finish the tests and clean up. 81 | SELECT * FROM finish(); 82 | ROLLBACK; 83 | -------------------------------------------------------------------------------- /supabase/tests/03_typeid.test.sql: -------------------------------------------------------------------------------- 1 | -- Start transaction and plan the tests. 2 | BEGIN; 3 | SELECT plan(41); 4 | 5 | create table tests ( 6 | "tid" typeid 7 | ); 8 | 9 | -- Run the 'valid' tests. 10 | 11 | -- - name: nil 12 | -- typeid: "00000000000000000000000000" 13 | -- prefix: "" 14 | -- uuid: "00000000-0000-0000-0000-000000000000" 15 | SELECT is( 16 | typeid_parse('00000000000000000000000000'), 17 | ('', '00000000-0000-0000-0000-000000000000')::typeid, 18 | 'Parse valid: nil' 19 | ); 20 | SELECT is( 21 | typeid_print(('', '00000000-0000-0000-0000-000000000000')), 22 | '00000000000000000000000000', 23 | 'Print valid: nil' 24 | ); 25 | 26 | -- - name: one 27 | -- typeid: "00000000000000000000000001" 28 | -- prefix: "" 29 | -- uuid: "00000000-0000-0000-0000-000000000001" 30 | SELECT is( 31 | typeid_parse('00000000000000000000000001'), 32 | ('', '00000000-0000-0000-0000-000000000001')::typeid, 33 | 'Parse valid: one' 34 | ); 35 | SELECT is( 36 | typeid_print(('', '00000000-0000-0000-0000-000000000001')), 37 | '00000000000000000000000001', 38 | 'Print valid: one' 39 | ); 40 | 41 | -- - name: ten 42 | -- typeid: "0000000000000000000000000a" 43 | -- prefix: "" 44 | -- uuid: "00000000-0000-0000-0000-00000000000a" 45 | SELECT is( 46 | typeid_parse('0000000000000000000000000a'), 47 | ('', '00000000-0000-0000-0000-00000000000a')::typeid, 48 | 'Parse valid: ten' 49 | ); 50 | SELECT is( 51 | typeid_print(('', '00000000-0000-0000-0000-00000000000a')), 52 | '0000000000000000000000000a', 53 | 'Print valid: ten' 54 | ); 55 | 56 | -- - name: sixteen 57 | -- typeid: "0000000000000000000000000g" 58 | -- prefix: "" 59 | -- uuid: "00000000-0000-0000-0000-000000000010" 60 | SELECT is( 61 | typeid_parse('0000000000000000000000000g'), 62 | ('', '00000000-0000-0000-0000-000000000010')::typeid, 63 | 'Parse valid: sixteen' 64 | ); 65 | SELECT is( 66 | typeid_print(('', '00000000-0000-0000-0000-000000000010')), 67 | '0000000000000000000000000g', 68 | 'Print valid: sixteen' 69 | ); 70 | 71 | -- - name: thirty-two 72 | -- typeid: "00000000000000000000000010" 73 | -- prefix: "" 74 | -- uuid: "00000000-0000-0000-0000-000000000020" 75 | SELECT is( 76 | typeid_parse('00000000000000000000000010'), 77 | ('', '00000000-0000-0000-0000-000000000020')::typeid, 78 | 'Parse valid: thirty-two' 79 | ); 80 | SELECT is( 81 | typeid_print(('', '00000000-0000-0000-0000-000000000020')), 82 | '00000000000000000000000010', 83 | 'Print valid: thirty-two' 84 | ); 85 | 86 | -- - name: max-valid 87 | -- typeid: "7zzzzzzzzzzzzzzzzzzzzzzzzz" 88 | -- prefix: "" 89 | -- uuid: "ffffffff-ffff-ffff-ffff-ffffffffffff" 90 | SELECT is( 91 | typeid_parse('7zzzzzzzzzzzzzzzzzzzzzzzzz'), 92 | ('', 'ffffffff-ffff-ffff-ffff-ffffffffffff')::typeid, 93 | 'Parse valid: max-valid' 94 | ); 95 | SELECT is( 96 | typeid_print(('', 'ffffffff-ffff-ffff-ffff-ffffffffffff')), 97 | '7zzzzzzzzzzzzzzzzzzzzzzzzz', 98 | 'Print valid: max-valid' 99 | ); 100 | 101 | -- - name: valid-alphabet 102 | -- typeid: "prefix_0123456789abcdefghjkmnpqrs" 103 | -- prefix: "prefix" 104 | -- uuid: "0110c853-1d09-52d8-d73e-1194e95b5f19" 105 | SELECT is( 106 | typeid_parse('prefix_0123456789abcdefghjkmnpqrs'), 107 | ('prefix', '0110c853-1d09-52d8-d73e-1194e95b5f19')::typeid, 108 | 'Parse valid: valid-alphabet' 109 | ); 110 | SELECT is( 111 | typeid_print(('prefix', '0110c853-1d09-52d8-d73e-1194e95b5f19')), 112 | 'prefix_0123456789abcdefghjkmnpqrs', 113 | 'Print valid: valid-alphabet' 114 | ); 115 | 116 | -- - name: valid-uuidv7 117 | -- typeid: "prefix_01h455vb4pex5vsknk084sn02q" 118 | -- prefix: "prefix" 119 | -- uuid: "01890a5d-ac96-774b-bcce-b302099a8057" 120 | SELECT is( 121 | typeid_parse('prefix_01h455vb4pex5vsknk084sn02q'), 122 | ('prefix', '01890a5d-ac96-774b-bcce-b302099a8057')::typeid, 123 | 'Parse valid: valid-uuidv7' 124 | ); 125 | SELECT is( 126 | typeid_print(('prefix', '01890a5d-ac96-774b-bcce-b302099a8057')), 127 | 'prefix_01h455vb4pex5vsknk084sn02q', 128 | 'Print valid: valid-uuidv7' 129 | ); 130 | 131 | -- Run the 'invalid' tests. 132 | 133 | -- - name: prefix-uppercase 134 | -- typeid: "PREFIX_00000000000000000000000000" 135 | -- description: "The prefix should be lowercase with no uppercase letters" 136 | SELECT throws_ok( 137 | $$ INSERT into tests (tid) VALUES (typeid_parse('PREFIX_00000000000000000000000000')); $$, 138 | 'typeid prefix must match the regular expression [a-z]{0,63}', 139 | 'Parse invalid: prefix-uppercase' 140 | ); 141 | 142 | SELECT throws_ok( 143 | $$ INSERT into tests (tid) VALUES (typeid_generate('PREFIX')); $$, 144 | 'typeid prefix must match the regular expression [a-z]{0,63}', 145 | 'Parse invalid: prefix-uppercase' 146 | ); 147 | 148 | -- - name: prefix-numeric 149 | -- typeid: "12345_00000000000000000000000000" 150 | -- description: "The prefix can't have numbers, it needs to be alphabetic" 151 | SELECT throws_ok( 152 | $$ INSERT into tests (tid) VALUES (typeid_parse('12345_00000000000000000000000000')); $$, 153 | 'typeid prefix must match the regular expression [a-z]{0,63}', 154 | 'Parse invalid: prefix-numeric' 155 | ); 156 | 157 | SELECT throws_ok( 158 | $$ INSERT into tests (tid) VALUES (typeid_generate('12345')); $$, 159 | 'typeid prefix must match the regular expression [a-z]{0,63}', 160 | 'Parse invalid: prefix-numeric' 161 | ); 162 | 163 | -- - name: prefix-period 164 | -- typeid: "pre.fix_00000000000000000000000000" 165 | -- description: "The prefix can't have symbols, it needs to be alphabetic" 166 | SELECT throws_ok( 167 | $$ INSERT into tests (tid) VALUES (typeid_parse('pre.fix_00000000000000000000000000')); $$, 168 | 'typeid prefix must match the regular expression [a-z]{0,63}', 169 | 'Parse invalid: prefix-period' 170 | ); 171 | 172 | SELECT throws_ok( 173 | $$ INSERT into tests (tid) VALUES (typeid_generate('pre.fix')); $$, 174 | 'typeid prefix must match the regular expression [a-z]{0,63}', 175 | 'Parse invalid: prefix-period' 176 | ); 177 | 178 | -- - name: prefix-underscore 179 | -- typeid: "pre_fix_00000000000000000000000000" 180 | -- description: "The prefix can't have symbols, it needs to be alphabetic" 181 | SELECT throws_ok( 182 | $$ INSERT into tests (tid) VALUES (typeid_parse('pre_fix_00000000000000000000000000')); $$, 183 | 'typeid suffix must be 26 characters', 184 | 'Parse invalid: prefix-underscore' 185 | ); 186 | 187 | SELECT throws_ok( 188 | $$ INSERT into tests (tid) VALUES (typeid_generate('pre_fix')); $$, 189 | 'typeid prefix must match the regular expression [a-z]{0,63}', 190 | 'Parse invalid: prefix-underscore' 191 | ); 192 | 193 | -- - name: prefix-non-ascii 194 | -- typeid: "préfix_00000000000000000000000000" 195 | -- description: "The prefix can only have ascii letters" 196 | SELECT throws_ok( 197 | $$ INSERT into tests (tid) VALUES (typeid_parse('préfix_00000000000000000000000000')); $$, 198 | 'typeid prefix must match the regular expression [a-z]{0,63}', 199 | 'Parse invalid: prefix-non-ascii' 200 | ); 201 | 202 | SELECT throws_ok( 203 | $$ INSERT into tests (tid) VALUES (typeid_generate('préfix')); $$, 204 | 'typeid prefix must match the regular expression [a-z]{0,63}', 205 | 'Parse invalid: prefix-non-ascii' 206 | ); 207 | 208 | -- - name: prefix-spaces 209 | -- typeid: " prefix_00000000000000000000000000" 210 | -- description: "The prefix can't have any spaces" 211 | SELECT throws_ok( 212 | $$ INSERT into tests (tid) VALUES (typeid_parse(' prefix_00000000000000000000000000')); $$, 213 | 'typeid prefix must match the regular expression [a-z]{0,63}', 214 | 'Parse invalid: prefix-spaces' 215 | ); 216 | 217 | SELECT throws_ok( 218 | $$ INSERT into tests (tid) VALUES (typeid_generate(' prefix')); $$, 219 | 'typeid prefix must match the regular expression [a-z]{0,63}', 220 | 'Parse invalid: prefix-spaces' 221 | ); 222 | 223 | -- - name: prefix-64-chars 224 | -- # 123456789 123456789 123456789 123456789 123456789 123456789 1234 225 | -- typeid: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl_00000000000000000000000000" 226 | -- description: "The prefix can't be 64 characters, it needs to be 63 characters or less" 227 | SELECT throws_ok( 228 | $$ INSERT into tests (tid) VALUES (typeid_parse('abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl_00000000000000000000000000')); $$, 229 | 'typeid prefix must match the regular expression [a-z]{0,63}', 230 | 'Parse invalid: prefix-64-chars' 231 | ); 232 | 233 | SELECT throws_ok( 234 | $$ INSERT into tests (tid) VALUES (typeid_generate('abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl')); $$, 235 | 'typeid prefix must match the regular expression [a-z]{0,63}', 236 | 'Parse invalid: prefix-64-chars' 237 | ); 238 | 239 | -- - name: separator-empty-prefix 240 | -- typeid: "_00000000000000000000000000" 241 | -- description: "If the prefix is empty, the separator should not be there" 242 | SELECT throws_ok( 243 | $$ INSERT into tests (tid) VALUES (typeid_parse('_00000000000000000000000000')); $$, 244 | 'typeid prefix cannot be empty with a delimiter', 245 | 'Parse invalid: separator-empty-prefix' 246 | ); 247 | 248 | -- - name: separator-empty 249 | -- typeid: "_" 250 | -- description: "A separator by itself should not be treated as the empty string" 251 | SELECT throws_ok( 252 | $$ INSERT into tests (tid) VALUES (typeid_parse('_')); $$, 253 | 'typeid prefix cannot be empty with a delimiter', 254 | 'Parse invalid: separator-empty' 255 | ); 256 | 257 | -- - name: suffix-short 258 | -- typeid: "prefix_1234567890123456789012345" 259 | -- description: "The suffix can't be 25 characters, it needs to be exactly 26 characters" 260 | SELECT throws_ok( 261 | $$ INSERT into tests (tid) VALUES (typeid_parse('prefix_1234567890123456789012345')); $$, 262 | 'typeid suffix must be 26 characters', 263 | 'Parse invalid: suffix-short' 264 | ); 265 | 266 | -- - name: suffix-long 267 | -- typeid: "prefix_123456789012345678901234567" 268 | -- description: "The suffix can't be 27 characters, it needs to be exactly 26 characters" 269 | SELECT throws_ok( 270 | $$ INSERT into tests (tid) VALUES (typeid_parse('prefix_123456789012345678901234567')); $$, 271 | 'typeid suffix must be 26 characters', 272 | 'Parse invalid: suffix-long' 273 | ); 274 | 275 | -- - name: suffix-spaces 276 | -- # This example has the right length, so that the failure is caused by the space 277 | -- # and not the suffix length 278 | -- typeid: "prefix_1234567890123456789012345 " 279 | -- description: "The suffix can't have any spaces" 280 | SELECT throws_ok( 281 | $$ INSERT into tests (tid) VALUES (typeid_parse('prefix_1234567890123456789012345 ')); $$, 282 | 'typeid suffix must only use characters from the base32 alphabet', 283 | 'Parse invalid: suffix-spaces' 284 | ); 285 | 286 | -- - name: suffix-uppercase 287 | -- # This example is picked because it would be valid in lowercase 288 | -- typeid: "prefix_0123456789ABCDEFGHJKMNPQRS" 289 | -- description: "The suffix should be lowercase with no uppercase letters" 290 | SELECT throws_ok( 291 | $$ INSERT into tests (tid) VALUES (typeid_parse('prefix_0123456789ABCDEFGHJKMNPQRS')); $$, 292 | 'typeid suffix must only use characters from the base32 alphabet', 293 | 'Parse invalid: suffix-uppercase' 294 | ); 295 | 296 | -- - name: suffix-hyphens 297 | -- # This example has the right length, so that the failure is caused by the hyphens 298 | -- # and not the suffix length 299 | -- typeid: "prefix_123456789-123456789-123456" 300 | -- description: "The suffix should be lowercase with no uppercase letters" 301 | SELECT throws_ok( 302 | $$ INSERT into tests (tid) VALUES (typeid_parse('prefix_123456789-123456789-123456')); $$, 303 | 'typeid suffix must only use characters from the base32 alphabet', 304 | 'Parse invalid: suffix-hyphens' 305 | ); 306 | 307 | -- - name: suffix-wrong-alphabet 308 | -- typeid: "prefix_ooooooiiiiiiuuuuuuulllllll" 309 | -- description: "The suffix should only have letters from the spec's alphabet" 310 | SELECT throws_ok( 311 | $$ INSERT into tests (tid) VALUES (typeid_parse('prefix_ooooooiiiiiiuuuuuuulllllll')); $$, 312 | 'typeid suffix must only use characters from the base32 alphabet', 313 | 'Parse invalid: suffix-wrong-alphabet' 314 | ); 315 | 316 | -- - name: suffix-ambiguous-crockford 317 | -- # This example would be valid if we were using the crockford disambiguation rules 318 | -- typeid: "prefix_i23456789ol23456789oi23456" 319 | -- description: "The suffix should not have any ambiguous characters from the crockford encoding" 320 | SELECT throws_ok( 321 | $$ INSERT into tests (tid) VALUES (typeid_parse('prefix_i23456789ol23456789oi23456')); $$, 322 | 'typeid suffix must only use characters from the base32 alphabet', 323 | 'Parse invalid: suffix-ambiguous-crockford' 324 | ); 325 | 326 | -- - name: suffix-hyphens-crockford 327 | -- # This example would be valid if we were using the crockford hyphenation rules 328 | -- typeid: "prefix_123456789-0123456789-0123456" 329 | -- description: "The suffix can't ignore hyphens as in the crockford encoding" 330 | SELECT throws_ok( 331 | $$ INSERT into tests (tid) VALUES (typeid_parse('prefix_123456789-0123456789-0123456')); $$, 332 | 'typeid suffix must be 26 characters', 333 | 'Parse invalid: suffix-hyphens-crockford' 334 | ); 335 | 336 | -- - name: suffix-overflow 337 | -- # This is the first suffix that overflows into 129 bits 338 | -- typeid: "prefix_8zzzzzzzzzzzzzzzzzzzzzzzzz" 339 | -- description: "The suffix should encode at most 128-bits" 340 | SELECT throws_ok( 341 | $$ INSERT into tests (tid) VALUES (typeid_parse('prefix_8zzzzzzzzzzzzzzzzzzzzzzzzz')); $$, 342 | 'typeid suffix must start with 0-7', 343 | 'Parse invalid: suffix-overflow' 344 | ); 345 | 346 | -- Finish the tests and clean up. 347 | SELECT * FROM finish(); 348 | ROLLBACK; 349 | 350 | -------------------------------------------------------------------------------- /supabase/tests/04_custom_type.test.sql: -------------------------------------------------------------------------------- 1 | -- Start transaction and plan the tests. 2 | BEGIN; 3 | SELECT plan(5); 4 | 5 | create domain test_id AS typeid check (typeid_check(value, 'test')); 6 | 7 | create table tests ( 8 | "tid" test_id not null default typeid_generate('test'), 9 | "name" text 10 | ); 11 | 12 | -- Run the tests. 13 | SELECT isnt_empty( 14 | $$ INSERT into tests (name) VALUES ('random id') RETURNING tid; $$, 15 | 'Can insert with a default generated id' 16 | ); 17 | 18 | SELECT isnt_empty( 19 | $$ INSERT into tests (tid) VALUES (('test', '00000000-0000-0000-0000-000000000000')) RETURNING tid; $$, 20 | 'Can insert with an id of the correct type' 21 | ); 22 | 23 | SELECT results_eq( 24 | $$ INSERT into tests (tid) VALUES (typeid_parse('test_01h455vb4pex5vsknk084sn02q')) RETURNING tid; $$, 25 | ARRAY[('test', '01890a5d-ac96-774b-bcce-b302099a8057')::test_id], 26 | 'Can insert using the typeid_parse function' 27 | ); 28 | 29 | SELECT results_eq( 30 | $$ SELECT typeid_print(tid) FROM tests where tid = typeid_parse('test_01h455vb4pex5vsknk084sn02q') $$, 31 | ARRAY['test_01h455vb4pex5vsknk084sn02q'], 32 | 'Can select using typeid_parse and typeid_print' 33 | ); 34 | 35 | SELECT throws_ok( 36 | $$ INSERT into tests (tid) VALUES (('user', '00000000-0000-0000-0000-000000000000')); $$, 37 | 'value for domain test_id violates check constraint "test_id_check"', 38 | 'Cannot insert typeid of wrong type' 39 | ); 40 | 41 | -- Finish the tests and clean up. 42 | SELECT * FROM finish(); 43 | ROLLBACK; 44 | 45 | -------------------------------------------------------------------------------- /supabase/tests/05_operator_test.sql: -------------------------------------------------------------------------------- 1 | -- Start transaction and plan the tests. 2 | BEGIN; 3 | SELECT plan(2); -- number of tests to run 4 | 5 | create domain test_id AS typeid check (typeid_check(value, 'test')); 6 | 7 | create table tests ( 8 | "tid" test_id not null default typeid_generate('test'), 9 | "name" text 10 | ); 11 | 12 | -- Run the tests. 13 | SELECT results_eq( 14 | $$ INSERT into tests (tid) VALUES (typeid_parse('test_01h455vb4pex5vsknk084sn02q')) RETURNING tid; $$, 15 | ARRAY[('test', '01890a5d-ac96-774b-bcce-b302099a8057')::test_id], 16 | 'Can insert using the typeid_parse function' 17 | ); 18 | 19 | SELECT results_eq( 20 | $$ SELECT typeid_print(tid) FROM tests where tid === 'test_01h455vb4pex5vsknk084sn02q' $$, 21 | ARRAY['test_01h455vb4pex5vsknk084sn02q'], 22 | 'Can select without needing to call typeid_parse() thanks to operator overload' 23 | ); 24 | 25 | -- Finish the tests and clean up. 26 | SELECT * FROM finish(); 27 | ROLLBACK; 28 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | agent-base@^7.0.2: 6 | version "7.1.1" 7 | resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317" 8 | integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== 9 | dependencies: 10 | debug "^4.3.4" 11 | 12 | bin-links@^4.0.3: 13 | version "4.0.3" 14 | resolved "https://registry.yarnpkg.com/bin-links/-/bin-links-4.0.3.tgz#9e4a3c5900830aee3d7f52178b65e01dcdde64a5" 15 | integrity sha512-obsRaULtJurnfox/MDwgq6Yo9kzbv1CPTk/1/s7Z/61Lezc8IKkFCOXNeVLXz0456WRzBQmSsDWlai2tIhBsfA== 16 | dependencies: 17 | cmd-shim "^6.0.0" 18 | npm-normalize-package-bin "^3.0.0" 19 | read-cmd-shim "^4.0.0" 20 | write-file-atomic "^5.0.0" 21 | 22 | chownr@^2.0.0: 23 | version "2.0.0" 24 | resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" 25 | integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== 26 | 27 | cmd-shim@^6.0.0: 28 | version "6.0.2" 29 | resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-6.0.2.tgz#435fd9e5c95340e61715e19f90209ed6fcd9e0a4" 30 | integrity sha512-+FFYbB0YLaAkhkcrjkyNLYDiOsFSfRjwjY19LXk/psmMx1z00xlCv7hhQoTGXXIKi+YXHL/iiFo8NqMVQX9nOw== 31 | 32 | data-uri-to-buffer@^4.0.0: 33 | version "4.0.1" 34 | resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" 35 | integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== 36 | 37 | debug@4, debug@^4.3.4: 38 | version "4.3.4" 39 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" 40 | integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== 41 | dependencies: 42 | ms "2.1.2" 43 | 44 | fetch-blob@^3.1.2, fetch-blob@^3.1.4: 45 | version "3.2.0" 46 | resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" 47 | integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== 48 | dependencies: 49 | node-domexception "^1.0.0" 50 | web-streams-polyfill "^3.0.3" 51 | 52 | formdata-polyfill@^4.0.10: 53 | version "4.0.10" 54 | resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" 55 | integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== 56 | dependencies: 57 | fetch-blob "^3.1.2" 58 | 59 | fs-minipass@^2.0.0: 60 | version "2.1.0" 61 | resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" 62 | integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== 63 | dependencies: 64 | minipass "^3.0.0" 65 | 66 | https-proxy-agent@^7.0.2: 67 | version "7.0.4" 68 | resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz#8e97b841a029ad8ddc8731f26595bad868cb4168" 69 | integrity sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg== 70 | dependencies: 71 | agent-base "^7.0.2" 72 | debug "4" 73 | 74 | imurmurhash@^0.1.4: 75 | version "0.1.4" 76 | resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" 77 | integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== 78 | 79 | minipass@^3.0.0: 80 | version "3.3.6" 81 | resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" 82 | integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== 83 | dependencies: 84 | yallist "^4.0.0" 85 | 86 | minipass@^5.0.0: 87 | version "5.0.0" 88 | resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" 89 | integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== 90 | 91 | minizlib@^2.1.1: 92 | version "2.1.2" 93 | resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" 94 | integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== 95 | dependencies: 96 | minipass "^3.0.0" 97 | yallist "^4.0.0" 98 | 99 | mkdirp@^1.0.3: 100 | version "1.0.4" 101 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" 102 | integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== 103 | 104 | ms@2.1.2: 105 | version "2.1.2" 106 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 107 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 108 | 109 | node-domexception@^1.0.0: 110 | version "1.0.0" 111 | resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" 112 | integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== 113 | 114 | node-fetch@^3.3.2: 115 | version "3.3.2" 116 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b" 117 | integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA== 118 | dependencies: 119 | data-uri-to-buffer "^4.0.0" 120 | fetch-blob "^3.1.4" 121 | formdata-polyfill "^4.0.10" 122 | 123 | npm-normalize-package-bin@^3.0.0: 124 | version "3.0.1" 125 | resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz#25447e32a9a7de1f51362c61a559233b89947832" 126 | integrity sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ== 127 | 128 | read-cmd-shim@^4.0.0: 129 | version "4.0.0" 130 | resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-4.0.0.tgz#640a08b473a49043e394ae0c7a34dd822c73b9bb" 131 | integrity sha512-yILWifhaSEEytfXI76kB9xEEiG1AiozaCJZ83A87ytjRiN+jVibXjedjCRNjoZviinhG+4UkalO3mWTd8u5O0Q== 132 | 133 | signal-exit@^4.0.1: 134 | version "4.1.0" 135 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" 136 | integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== 137 | 138 | supabase@^1.153.4: 139 | version "1.153.4" 140 | resolved "https://registry.yarnpkg.com/supabase/-/supabase-1.153.4.tgz#408ac4f5a612960645ed6517965602693729f933" 141 | integrity sha512-dok/T9lu7ndDd7pzJOmnwlgr8mDyXNmdHBXx3Axhb1Dwy8igSHVwfLket6cp9zCAVNRhzrw+exjHxsIrPairPg== 142 | dependencies: 143 | bin-links "^4.0.3" 144 | https-proxy-agent "^7.0.2" 145 | node-fetch "^3.3.2" 146 | tar "6.2.1" 147 | 148 | tar@6.2.1: 149 | version "6.2.1" 150 | resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" 151 | integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== 152 | dependencies: 153 | chownr "^2.0.0" 154 | fs-minipass "^2.0.0" 155 | minipass "^5.0.0" 156 | minizlib "^2.1.1" 157 | mkdirp "^1.0.3" 158 | yallist "^4.0.0" 159 | 160 | web-streams-polyfill@^3.0.3: 161 | version "3.3.3" 162 | resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" 163 | integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== 164 | 165 | write-file-atomic@^5.0.0: 166 | version "5.0.1" 167 | resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-5.0.1.tgz#68df4717c55c6fa4281a7860b4c2ba0a6d2b11e7" 168 | integrity sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw== 169 | dependencies: 170 | imurmurhash "^0.1.4" 171 | signal-exit "^4.0.1" 172 | 173 | yallist@^4.0.0: 174 | version "4.0.0" 175 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" 176 | integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== 177 | --------------------------------------------------------------------------------