Creating Primary Keys in TimescaleDB with TypeORM

Using TypeORM1 with TimescaleDB2 hypertables requires some additional work besides the usual operations needed to use a PostgreSQL data source.

This is because:

  • TypeORM requires that each entity has a primary key (in Timescale this is quite optional);
  • Timescale requies the primary key to contain the partition key (usually the time range column).

To make this work, you can do the following:

  • Add an auto-generated ID column to the hyper-table;
  • Create a composite primary key on both the ID as well as the hypertable’s partition key.

This will satisfiy both TypeORM’s need to have a unique primary key across the whole table, as well as Timescale’s requirement that the primary key contains the table’s partition key.

Example

In Ping.ts (entity definition file):

@Entity('pings')
export class Ping {
    @PrimaryGeneratedColumn()
    id!: number;

    @CreateDateColumn({ primary: true })
    startedAt: Date = new Date();

    // ...other columns...
}

In the TypeORM migration file:

// Make sure the primary key contains all needed columns.
await queryRunner.query(`CREATE TABLE "pings" ("id" SERIAL NOT NULL, "started_at" TIMESTAMP NOT NULL DEFAULT now(), ...other columns..., CONSTRAINT "PK_005b7a421c5dcdf04f030f2ab92" PRIMARY KEY ("id", "started_at"))`);

// Create hypertable.
await queryRunner.query(`SELECT create_hypertable('pings', by_range('started_at'));`)

P.S. In case you’re wondering why the SQL code uses snake_case while the TypeScript code uses camelCase, this is achieved using the typeorm-naming-strategies package.

  1. This article presumes you are familiar with the TypeORM package. ↩︎
  2. This article presumes you are familiar with the Timescale extension for the PostgreSQL DB. ↩︎

Don't keep it to yourself!...