惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Engineering at Meta
Engineering at Meta
人人都是产品经理
人人都是产品经理
大猫的无限游戏
大猫的无限游戏
博客园 - 三生石上(FineUI控件)
量子位
腾讯CDC
The Cloudflare Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
云风的 BLOG
云风的 BLOG
Vercel News
Vercel News
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
L
LangChain Blog
aimingoo的专栏
aimingoo的专栏
The Hacker News
The Hacker News
T
The Exploit Database - CXSecurity.com
B
Blog
S
SegmentFault 最新的问题
P
Privacy & Cybersecurity Law Blog
T
Threatpost
博客园 - 聂微东
T
Tailwind CSS Blog
The Last Watchdog
The Last Watchdog
C
Check Point Blog
N
Netflix TechBlog - Medium
D
DataBreaches.Net
爱范儿
爱范儿
IT之家
IT之家
S
Secure Thoughts
M
MIT News - Artificial intelligence
NISL@THU
NISL@THU
C
Cisco Blogs
TaoSecurity Blog
TaoSecurity Blog
有赞技术团队
有赞技术团队
A
Arctic Wolf
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
P
Proofpoint News Feed
Spread Privacy
Spread Privacy
Schneier on Security
Schneier on Security
Simon Willison's Weblog
Simon Willison's Weblog
G
GRAHAM CLULEY
雷峰网
雷峰网
Project Zero
Project Zero
博客园 - Franky
H
Heimdal Security Blog
A
About on SuperTechFans
Security Latest
Security Latest
Webroot Blog
Webroot Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Hugging Face - Blog
Hugging Face - Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More

Supabase Blog

AI Agents Know About Supabase. They Don't Always Use It Right. Custom OIDC Providers for Supabase Auth 100,000 GitHub stars Supabase docs over SSH Navigating Regional Network Blocks Supabase Joins the Stripe Projects Developer Preview Log Drains: Now available on Pro Supabase Storage: major performance, security, and reliability updates Supabase incident on February 12, 2026 Hydra joins Supabase X / Twitter OAuth 2.0 is now available for Supabase Auth BKND joins Supabase Supabase is now an official Claude connector Supabase PrivateLink is now available Introducing: Postgres Best Practices When to use Read Replicas vs. bigger compute Introducing TRAE SOLO integration with Supabase Supabase Security Retro: 2025 Sync Stripe Data to Your Supabase Database in One Click Building ChatGPT Apps with Supabase Edge Functions and mcp-use Own Your Observability: Supabase Metrics API Introducing iceberg-js: A JavaScript Client for Apache Iceberg Introducing Supabase for Platforms Adding Async Streaming to Postgres Foreign Data Wrappers Build "Sign in with Your App" using Supabase Auth Introducing Seven New Email Templates for Supabase Auth The new Supabase power for Kiro Introducing Supabase ETL Introducing Analytics Buckets Introducing Vector Buckets Snap, Inc. Launches Snap Cloud, Powered by Supabase Triplit joins Supabase Supabase Series E 1000 Y Combinator Founders Choose Supabase gm 👋 web3, welcome aboard to Sign in with Web3 (Solana, Ethereum) Announcing the Supabase Remote MCP Server Enterprise speed, enterprise standards with Bolt Cloud + Supabase PostgREST 13 Lovable Cloud + Supabase: The Default Platform for AI Builders Processing large jobs with Edge Functions, Cron, and Queues Defense in Depth for MCP Servers OrioleDB Patent: now freely available to the Postgres community Supabase Launch Week 15 Hackathon Winner Announcement The Vibe Coder's Guide to Supabase Environments Testing for Vibe Coders: From Zero to Production Confidence The Vibe Coding Master Checklist Vibe Coding: Best Practices for Prompting Supabase Auth: Build vs. Buy Top 10 Launches of Launch Week 15 Supabase Launch Week 15 Hackathon Storage: 10x Larger Uploads, 3x Cheaper Cached Egress, and 2x Egress Quota Persistent Storage and 97% Faster Cold Starts for Edge Functions Algolia Connector for Supabase New Observability Features in Supabase Improved Security Controls and A New Home for Security Introducing Branching 2.0 Stripe-To-Postgres Sync Engine as standalone Library Supabase Analytics Buckets with Iceberg Support Create a Supabase backend using Figma Make Introducing JWT Signing Keys Supabase UI: Platform Kit Build a Personalized AI Assistant with Postgres Announcing Multigres: Vitess for Postgres Building on open table formats Open Data Standards: Postgres, OTel, and Iceberg Simplifying back-end complexity with Supabase Data APIs PostgreSQL Event Triggers without superuser access Top 10 Launches of Launch Week 14 Supabase MCP Server Data API Routes to Nearest Read Replica Declarative Schemas for Simpler Database Management Realtime: Broadcast from Database Keeping Tabs on What's New in Supabase Studio Edge Functions: Deploy from the Dashboard + Deno 2.1 Automatic Embeddings in Postgres Introducing the Supabase UI Library Supabase Auth: Bring Your Own Clerk Postgres Language Server: Initial Release Migrating from Fauna to Supabase Migrating from the MongoDB Data API to Supabase Dedicated Poolers Postgres as a Graph Database: (Ab)using pgRouting AI Hackathon at Y Combinator Calendars in Postgres using Foreign Data Wrappers Supabase Launch Week 13 Hackathon Winners How to Hack the Base! Running Durable Workflows in Postgres using DBOS database.build v2: Bring-your-own-LLM Restore to a New Project Hack the Base! with Supabase Top 10 Launches of Launch Week 13 Supabase Queues High Performance Disk Supabase Cron Supabase CLI v2: Config as Code Supabase Edge Functions: Introducing Background Tasks, Ephemeral Storage, and WebSockets Supabase AI Assistant v2 OrioleDB Public Alpha Executing Dynamic JavaScript Code on Supabase with Edge Functions ClickHouse Partnership, improved Postgres Replication, and Disk Management
Type Constraints in 65 lines of SQL
Oliver Rice · 2023-02-17 · via Supabase Blog

Type Constraints in 65 lines of SQL

PostgreSQL has a rich and extensible type system. Beyond enums and composite types, we can:

  • apply data validation rules
  • override comparison operators like = / + / -
  • create custom aggregations
  • define casting rules between types

With a little effort, a user-defined type can feel indistinguishable from a built-in. In this article we focus on validation and ergonomics while quickly touching on a few other concepts.

To illustrate, we’ll create an semver data type to represent Semantic Versioning values. We’ll then add validation rules to make invalid states unrepresentable.

A (very) loose primer on SemVer:

SemVer is a specification for representing software versions that communicate information about backwards compatibility. The type is typically represented as a string with 5 components.

Where pre-release and metadata are optional.

The intent of each component is outside the scope of this article but, as an example, incrementing the major version number notifies users that the release includes at least one backwards incompatible change.

For a concise representation of the full spec, check out the grammar.

For our purposes, we’ll assume that the SemVer type is a critical component of the application that needs to be queried flexibly and efficiently.

Storing Components#

To that end, we’ll store each component of the version as a separate field on a composite type.


_10

create type semver_components as (

_10

major int,

_10

minor int,

_10

patch int,

_10

pre_release text[],

_10

build_metadata text[]

_10

);


We can create an instance of this type in SQL by casting a tuple as the semver_components type.


_10

select

_10

(1, 2, 3, array['beta', '1'], array['meta'])::semver_components

_10

-- returns: (1,2,3,{'beta','1'},{'meta'})


Unfortunately, our definition is far too permissive.


_10

select

_10

(null, -500, null, array['?'], array[''])::semver_components

_10

-- returns: (,-500,,{'?'},{''


Our data type has no problem accepting invalid components. To list a few of the SemVer rules we violated:

  • Major version must not be null
  • Minor version must be ≥ 0
  • Patch version must not be null
  • Pre-release elements must only include characters [A-z0-9]
  • Build metadata elements may not be empty strings

We need to add some validation rules to meet our “make invalid states unrepresentable” goal.

Validation#

Domains are Postgres’ solution for optionally layering constraints over a data type. Domains are to types what check constraints are to tables. If you’re not familiar with check constraints, you can think of them as equivalent to zod/pydantic in javascript/python.

Let's codify some SemVer rules, layer them on the semver_components type, and give the new domain a friendly name.


_18

create domain semver

_18

as semver_components

_18

check (

_18

-- major: non-null positive integer

_18

(value).major is not null and (value).major >= 0

_18

-- minor: non-null positive integer

_18

and (value).minor is not null and (value).minor >= 0

_18

-- patch: non-null positive integer

_18

and (value).patch is not null and (value).patch >= 0

_18

and semver_elements_match_regex(

_18

(value).pre_release,

_18

'^[A-z0-9]{1,255}$'

_18

)

_18

and semver_elements_match_regex(

_18

(value).build_metadata,

_18

'^[A-z0-9\.]{1,255}$'

_18

)

_18

);


which references a helper function:


_23

create or replace function semver_elements_match_regex(

_23

parts text[],

_23

regex text

_23

)

_23

returns bool

_23

language sql

_23

as $$

_23

-- validates that *parts* nullable array of non-empty strings

_23

-- where each element of *parts* matches *regex*

_23

select

_23

$1 is null

_23

or (

_23

(

_23

select (

_23

bool_and(pr_arr.elem is not null)

_23

and bool_and(pr_arr.elem ~ $2)

_23

)

_23

from

_23

unnest($1) pr_arr(elem)

_23

)

_23

and array_length($1, 1) > 0

_23

)

_23

$$;


Now, if we repeat our positive and negative test cases using the semver type (vs semver_components) we still accept valid states:


_10

-- Success Case

_10

select

_10

(1, 2, 3, array['beta', '1'], array['meta'])::semver,

_10

-- returns: (1,2,3,{'beta','1'},{'meta'})


while invalid states are rejected with an error:


_10

-- Failure Case

_10

select

_10

(null, -500, null, array['?'], array[''])::semver

_10

-- ERROR: value for domain semver violates check constraint "semver_check"

_10

-- SQL state: 23514


Testing#

Our validation doesn’t have to be called manually. The semver domain can be used anywhere you’d use the semver_components type and the validations are automatically applied.


_19

-- A table with a semver column

_19

create table package_version(

_19

id bigserial primary key,

_19

package_name text not null,

_19

package_semver semver not null -- semver column

_19

);

_19

_19

-- Insert some valid records

_19

insert into package_version( package_name, package_semver )

_19

values

_19

('supabase-js', (2, 2, 3, null, null)),

_19

('supabase-js', (2, 0, 0, array['rc', '1'], null)

_19

);

_19

_19

-- Attempt to insert an invalid record (major is null)

_19

insert into package_version( package_name, package_semver )

_19

values

_19

('invalid-js', (null, 1, 0, array['asdf'], null));

_19

-- ERROR: value for domain semver violates check constraint "semver_check"


Good stuff!

We’re 48 lines of SQL in and have solved for making invalid states unrepresentable. Now lets think about ergonomics.

Displaying#

Now that our data type is well constrained, you might notice that selecting values from a semver typed column returns a tuple, rather than the SemVer string we’re used to seeing.


_10

select

_10

*

_10

from

_10

package_version

_10

/*

_10

id | package_name | package_semver

_10

-------------------------------------

_10

1 | supabase-js | (2,2,3,,)

_10

2 | supabase-js | (2,0,0,"{rc,1}",)

_10

*/


For example: (2,0,0,"{rc,1}",) vs 2.0.0-rc.1

We could work around that problem with some custom casts, but I’d recommend keeping everything explicit with a function call.


_16

create or replace function semver_to_text(semver)

_16

returns text

_16

immutable

_16

language sql

_16

as $$

_16

select

_16

format('%s.%s.%s', $1.major, $1.minor, $1.patch)

_16

|| case

_16

when $1.pre_release is null then ''

_16

else format('-%s', array_to_string($1.pre_release, '.'))

_16

end

_16

|| case

_16

when $1.build_metadata is null then ''

_16

else format('+%s', array_to_string($1.build_metadata, '.'))

_16

end

_16

$$;


Which allows us to query the package_version table and retrieve a string representation of the data.


_12

select

_12

id,

_12

package_name,

_12

semver_to_text(package_semver) as ver -- cast as text

_12

from

_12

package_version

_12

/*

_12

id | package_name | ver

_12

------------------------------

_12

1 | supabase-js | 2.2.3

_12

2 | supabase-js | 2.0.0-rc.1

_12

*/


Or, better yet, use a generated column


_10

create table package_version(

_10

id bigserial primary key,

_10

package_name text not null,

_10

package_semver semver not null,

_10

semver_text text generated always as (semver_to_text(package_semver)) stored

_10

);


so the text representation is persisted along with the semver type and incurs no query/filter penalty.

Other Tricks#

Postgres provides all the tools you could want to make your data types/domains work with SQL as seamlessly as builtins.

For example, you could:

to name a few.

Aligning the right parts of your business’ logic with the database can dramatically improve throughput, decrease IO, and simplify application code.

Conclusion#

Admittedly, building performant and ergonomic custom data types in Postgres involves a lot of ceremony.

That said, in cases where:

  • the type’s data integrity is critical
  • the type is well specified
  • the type’s spec does not change (or changes infrequently)

Teaching Postgres to have first class support for your custom type can be transformative for data integrity and performance.