Skip to content

Instantly share code, notes, and snippets.

@peshkov3
Created February 19, 2026 07:44
Show Gist options
  • Select an option

  • Save peshkov3/8c06b3e898f3367ae77a45ed2c0e04a2 to your computer and use it in GitHub Desktop.

Select an option

Save peshkov3/8c06b3e898f3367ae77a45ed2c0e04a2 to your computer and use it in GitHub Desktop.
UUID v7 for PostgreSQL + TypeORM — decorator, migration, and entity usage
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,
});
}
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()`);
}
}
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