Created
February 19, 2026 07:44
-
-
Save peshkov3/8c06b3e898f3367ae77a45ed2c0e04a2 to your computer and use it in GitHub Desktop.
UUID v7 for PostgreSQL + TypeORM — decorator, migration, and entity usage
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { PrimaryColumn } from 'typeorm'; | |
| /** | |
| * TypeORM decorator that generates UUID v7 primary keys using a PostgreSQL function. | |
| * UUID v7 is time-sortable, giving better B-tree index performance than UUID v4. | |
| * | |
| * Requires the `uuid_generate_v7()` function to exist in your database — see the | |
| * companion migration file. | |
| */ | |
| export function PrimaryGeneratedUuidV7Column(options?: { primaryKeyConstraintName?: string }): PropertyDecorator { | |
| return PrimaryColumn({ | |
| type: 'uuid', | |
| default: () => 'uuid_generate_v7()', | |
| nullable: false, | |
| ...options, | |
| }); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { MigrationInterface, QueryRunner } from 'typeorm'; | |
| /** | |
| * Creates the uuid_generate_v7() PostgreSQL function and alters existing tables | |
| * to use it as the default for their UUID primary key columns. | |
| * | |
| * UUID v7 (RFC 9562) embeds a Unix timestamp in the first 48 bits, making the | |
| * values monotonically increasing and significantly improving B-tree insert | |
| * performance compared to random UUID v4. | |
| */ | |
| export class AddUuidV7Function1234567890000 implements MigrationInterface { | |
| public async up(queryRunner: QueryRunner): Promise<void> { | |
| await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS pgcrypto`); | |
| await queryRunner.query(` | |
| CREATE OR REPLACE FUNCTION uuid_generate_v7() | |
| RETURNS uuid | |
| AS $$ | |
| DECLARE | |
| unix_ts_ms bytea; | |
| uuid_bytes bytea; | |
| BEGIN | |
| unix_ts_ms = substring(int8send(floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint) FROM 3); | |
| uuid_bytes = unix_ts_ms || gen_random_bytes(10); | |
| uuid_bytes = set_byte(uuid_bytes, 6, (b'0111' || get_byte(uuid_bytes, 6)::bit(4))::bit(8)::int); | |
| uuid_bytes = set_byte(uuid_bytes, 8, (b'10' || get_byte(uuid_bytes, 8)::bit(6))::bit(8)::int); | |
| RETURN encode(uuid_bytes, 'hex')::uuid; | |
| END | |
| $$ | |
| LANGUAGE plpgsql | |
| VOLATILE; | |
| `); | |
| // List every table whose PK should switch to v7 | |
| const tables = [ | |
| 'orders', | |
| 'products', | |
| 'customers', | |
| // ... add your tables here | |
| ]; | |
| for (const table of tables) { | |
| await queryRunner.query( | |
| `ALTER TABLE "${table}" ALTER COLUMN "id" SET DEFAULT uuid_generate_v7()`, | |
| ); | |
| } | |
| } | |
| public async down(queryRunner: QueryRunner): Promise<void> { | |
| const tables = [ | |
| 'orders', | |
| 'products', | |
| 'customers', | |
| ]; | |
| for (const table of tables) { | |
| await queryRunner.query( | |
| `ALTER TABLE "${table}" ALTER COLUMN "id" SET DEFAULT uuid_generate_v4()`, | |
| ); | |
| } | |
| await queryRunner.query(`DROP FUNCTION IF EXISTS uuid_generate_v7()`); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { | |
| Column, | |
| CreateDateColumn, | |
| DeleteDateColumn, | |
| Entity, | |
| Index, | |
| JoinColumn, | |
| ManyToOne, | |
| UpdateDateColumn, | |
| } from 'typeorm'; | |
| import { PrimaryGeneratedUuidV7Column } from './primary-generated-uuid-v7.decorator'; | |
| @Entity({ name: 'products' }) | |
| @Index('unq_product_name_category', ['name', 'categoryId'], { unique: true, where: 'deleted_at IS NULL' }) | |
| export class ProductEntity { | |
| @PrimaryGeneratedUuidV7Column() | |
| id: string; | |
| @Column({ type: 'varchar', length: 255 }) | |
| name: string; | |
| @Column({ type: 'uuid', name: 'category_id', nullable: false }) | |
| categoryId: string; | |
| @Column('numeric', { precision: 32, scale: 12, default: 0, nullable: false }) | |
| price: number; | |
| @CreateDateColumn() | |
| createdAt: string; | |
| @UpdateDateColumn() | |
| updatedAt: string; | |
| @DeleteDateColumn() | |
| deletedAt: string; | |
| @ManyToOne('CategoryEntity', (category: any) => category.products, { onDelete: 'CASCADE' }) | |
| @JoinColumn([{ name: 'category_id', referencedColumnName: 'id' }]) | |
| category: any; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment