Skip to content

Commit

Permalink
feat(postgres): add support for interval type
Browse files Browse the repository at this point in the history
Closes #5181
  • Loading branch information
B4nan committed Jan 30, 2024
1 parent 1bf13b4 commit 659a613
Show file tree
Hide file tree
Showing 10 changed files with 157 additions and 17 deletions.
16 changes: 5 additions & 11 deletions packages/core/src/metadata/MetadataDiscovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
t,
Type,
Uint8ArrayType,
UnknownType,
UnknownType, IntervalType,
} from '../types';
import { colors } from '../logging/colors';
import { raw, RawQueryFragment } from '../utils/RawQueryFragment';
Expand Down Expand Up @@ -1276,16 +1276,10 @@ export class MetadataDiscovery {

const mappedType = this.getMappedType(prop);

if (prop.fieldNames?.length === 1 && !prop.customType && mappedType instanceof BigIntType) {
prop.customType = new BigIntType();
}

if (prop.fieldNames?.length === 1 && !prop.customType && mappedType instanceof DoubleType) {
prop.customType = new DoubleType();
}

if (prop.fieldNames?.length === 1 && !prop.customType && mappedType instanceof DecimalType) {
prop.customType = new DecimalType();
if (prop.fieldNames?.length === 1 && !prop.customType) {
[BigIntType, DoubleType, DecimalType, IntervalType]
.filter(type => mappedType instanceof type)
.forEach(type => prop.customType = new type());
}

if (prop.customType && !prop.columnTypes) {
Expand Down
17 changes: 15 additions & 2 deletions packages/core/src/platforms/Platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { Configuration } from '../utils/Configuration';
import type { IDatabaseDriver } from '../drivers/IDatabaseDriver';
import {
ArrayType, BigIntType, BlobType, Uint8ArrayType, BooleanType, DateType, DecimalType, DoubleType, JsonType, SmallIntType, TimeType,
TinyIntType, Type, UuidType, StringType, IntegerType, FloatType, DateTimeType, TextType, EnumType, UnknownType, MediumIntType,
TinyIntType, Type, UuidType, StringType, IntegerType, FloatType, DateTimeType, TextType, EnumType, UnknownType, MediumIntType, IntervalType,
} from '../types';
import { parseJsonSafe, Utils } from '../utils/Utils';
import { ReferenceKind } from '../enums';
Expand Down Expand Up @@ -203,6 +203,10 @@ export abstract class Platform {
return `varchar(${column.length ?? 255})`;
}

getIntervalTypeDeclarationSQL(column: { length?: number }): string {
return 'interval' + (column.length ? `(${column.length})` : '');
}

getTextTypeDeclarationSQL(_column: { length?: number }): string {
return `text`;
}
Expand Down Expand Up @@ -250,8 +254,9 @@ export abstract class Platform {
}

switch (this.extractSimpleType(type)) {
case 'string': return Type.getType(StringType);
case 'string':
case 'varchar': return Type.getType(StringType);
case 'interval': return Type.getType(IntervalType);
case 'text': return Type.getType(TextType);
case 'number': return Type.getType(IntegerType);
case 'bigint': return Type.getType(BigIntType);
Expand Down Expand Up @@ -344,6 +349,14 @@ export abstract class Platform {
return parseJsonSafe(value);
}

convertIntervalToJSValue(value: string): unknown {
return value;
}

convertIntervalToDatabaseValue(value: unknown): unknown {
return value;
}

parseDate(value: string | number): Date {
return new Date(value);
}
Expand Down
19 changes: 19 additions & 0 deletions packages/core/src/types/IntervalType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Type } from './Type';
import type { Platform } from '../platforms';
import type { EntityProperty } from '../typings';

export class IntervalType extends Type<string | null | undefined, string | null | undefined> {

override getColumnType(prop: EntityProperty, platform: Platform) {
return platform.getIntervalTypeDeclarationSQL(prop);
}

override convertToJSValue(value: string | null | undefined, platform: Platform): string | null | undefined {
return platform.convertIntervalToJSValue(value!) as string;
}

override convertToDatabaseValue(value: string | null | undefined, platform: Platform): string | null | undefined {
return platform.convertIntervalToDatabaseValue(value) as string;
}

}
4 changes: 3 additions & 1 deletion packages/core/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ import { DecimalType } from './DecimalType';
import { StringType } from './StringType';
import { UuidType } from './UuidType';
import { TextType } from './TextType';
import { IntervalType } from './IntervalType';
import { UnknownType } from './UnknownType';

export {
Type, DateType, TimeType, DateTimeType, BigIntType, BlobType, Uint8ArrayType, ArrayType, EnumArrayType, EnumType,
JsonType, IntegerType, SmallIntType, TinyIntType, MediumIntType, FloatType, DoubleType, BooleanType, DecimalType,
StringType, UuidType, TextType, UnknownType, TransformContext,
StringType, UuidType, TextType, UnknownType, TransformContext, IntervalType,
};

export const types = {
Expand All @@ -50,6 +51,7 @@ export const types = {
string: StringType,
uuid: UuidType,
text: TextType,
interval: IntervalType,
unknown: UnknownType,
};

Expand Down
3 changes: 2 additions & 1 deletion packages/knex/src/schema/DatabaseTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type EntityMetadata,
type EntityProperty,
EntitySchema,
IntervalType,
type NamingStrategy,
ReferenceKind,
t,
Expand Down Expand Up @@ -97,7 +98,7 @@ export class DatabaseTable {
}
}

if (mappedType instanceof DateTimeType) {
if (mappedType instanceof DateTimeType || mappedType instanceof IntervalType) {
const match = prop.columnTypes[idx].match(/\w+\((\d+)\)/);

if (match) {
Expand Down
3 changes: 2 additions & 1 deletion packages/postgresql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@
"dependencies": {
"@mikro-orm/knex": "6.0.6",
"pg": "8.11.3",
"postgres-date": "2.1.0"
"postgres-date": "2.1.0",
"postgres-interval": "4.0.2"
},
"devDependencies": {
"@mikro-orm/core": "^6.0.6"
Expand Down
3 changes: 2 additions & 1 deletion packages/postgresql/src/PostgreSqlConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export class PostgreSqlConnection extends AbstractSqlConnection {
override getConnectionOptions(): Knex.PgConnectionConfig {
const ret = super.getConnectionOptions() as Knex.PgConnectionConfig;
const types = new TypeOverrides();
[1082, 1114, 1184].forEach(oid => types.setTypeParser(oid, str => str)); // date, timestamp, timestamptz type
// date, timestamp, timestamptz, interval type
[1082, 1114, 1184, 1186].forEach(oid => types.setTypeParser(oid, str => str));
ret.types = types as any;

return ret;
Expand Down
13 changes: 13 additions & 0 deletions packages/postgresql/src/PostgreSqlPlatform.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Client } from 'pg';
import parseDate from 'postgres-date';
import PostgresInterval, { type IPostgresInterval } from 'postgres-interval';
import { raw, ALIAS_REPLACEMENT, JsonProperty, Utils, type EntityProperty, Type, type SimpleColumnMeta, type Dictionary } from '@mikro-orm/core';
import { AbstractSqlPlatform, type IndexDef } from '@mikro-orm/knex';
import { PostgreSqlSchemaHelper } from './PostgreSqlSchemaHelper';
Expand Down Expand Up @@ -49,6 +50,18 @@ export class PostgreSqlPlatform extends AbstractSqlPlatform {
return 6; // timestamptz actually means timestamptz(6)
}

override convertIntervalToJSValue(value: string): unknown {
return PostgresInterval(value);
}

override convertIntervalToDatabaseValue(value: IPostgresInterval): unknown {
if (Utils.isObject(value) && 'toPostgres' in value && typeof value.toPostgres === 'function') {
return value.toPostgres();
}

return value;
}

override getTimeTypeDeclarationSQL(): string {
return 'time(0)';
}
Expand Down
88 changes: 88 additions & 0 deletions tests/features/custom-types/IntervalType.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Dictionary, Entity, MikroORM, PrimaryKey, Property } from '@mikro-orm/core';
import { PostgreSqlDriver } from '@mikro-orm/postgresql';
import { SqliteDriver } from '@mikro-orm/sqlite';
import { mockLogger } from '../../helpers';

@Entity()
class Something {

@PrimaryKey()
id!: number;

@Property({
type: 'interval',
nullable: true,
})
durationBuggy?: Dictionary | string;

}

test('interval columns (postgres)', async () => {
const orm = await MikroORM.init({
entities: [Something],
driver: PostgreSqlDriver,
dbName: 'mikro_orm_interval_type',
});
await orm.schema.refreshDatabase();

await expect(orm.schema.getCreateSchemaSQL()).resolves.toMatch('"duration_buggy" interval null');

const diff = await orm.schema.getUpdateSchemaSQL({ wrap: false });
expect(diff).toBe('');

const mock = mockLogger(orm);
orm.em.create(Something, { durationBuggy: '1s' });
await orm.em.flush();
await orm.em.flush(); // to check for extra updates
orm.em.clear();
const r = await orm.em.findOneOrFail(Something, { id: 1 });
(r.durationBuggy as Dictionary).seconds = 5;
await orm.em.flush();
await orm.em.flush(); // to check for extra updates

expect(mock.mock.calls).toHaveLength(7);
expect(mock.mock.calls[0][0]).toMatch('begin');
expect(mock.mock.calls[1][0]).toMatch('insert into "something" ("duration_buggy") values (\'1s\') returning "id"');
expect(mock.mock.calls[2][0]).toMatch('commit');
expect(mock.mock.calls[3][0]).toMatch('select "s0".* from "something" as "s0" where "s0"."id" = 1 limit 1');
expect(mock.mock.calls[4][0]).toMatch('begin');
expect(mock.mock.calls[5][0]).toMatch('update "something" set "duration_buggy" = \'5 seconds\' where "id" = 1');
expect(mock.mock.calls[6][0]).toMatch('commit');

await orm.close(true);
});

test('interval columns (sqlite)', async () => {
const orm = await MikroORM.init({
entities: [Something],
driver: SqliteDriver,
dbName: ':memory:',
});
await orm.schema.refreshDatabase();

await expect(orm.schema.getCreateSchemaSQL()).resolves.toMatch('`duration_buggy` interval null');

const diff = await orm.schema.getUpdateSchemaSQL({ wrap: false });
expect(diff).toBe('');

const mock = mockLogger(orm);
orm.em.create(Something, { durationBuggy: '1s' });
await orm.em.flush();
await orm.em.flush(); // to check for extra updates
orm.em.clear();
const r = await orm.em.findOneOrFail(Something, { id: 1 });
r.durationBuggy = '5s';
await orm.em.flush();
await orm.em.flush(); // to check for extra updates

expect(mock.mock.calls).toHaveLength(7);
expect(mock.mock.calls[0][0]).toMatch('begin');
expect(mock.mock.calls[1][0]).toMatch('insert into `something` (`duration_buggy`) values (\'1s\') returning `id`');
expect(mock.mock.calls[2][0]).toMatch('commit');
expect(mock.mock.calls[3][0]).toMatch('select `s0`.* from `something` as `s0` where `s0`.`id` = 1 limit 1');
expect(mock.mock.calls[4][0]).toMatch('begin');
expect(mock.mock.calls[5][0]).toMatch('update `something` set `duration_buggy` = \'5s\' where `id` = 1');
expect(mock.mock.calls[6][0]).toMatch('commit');

await orm.close(true);
});
8 changes: 8 additions & 0 deletions yarn.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 659a613

Please sign in to comment.