diff --git a/docs/docs/raw-queries.md b/docs/docs/raw-queries.md index 07f10dea5193..1b8f8f9c194a 100644 --- a/docs/docs/raw-queries.md +++ b/docs/docs/raw-queries.md @@ -56,6 +56,25 @@ When you want to refer to a column, you can use the `sql.ref()` function: await em.find(User, { foo: sql`bar` }); ``` +### `sql.now()` + +When you want to define a default value for a datetime column, you can use the `sql.now()` function. It resolves to `current_timestamp` SQL function, and accepts a `length` parameter. + +```ts +@Property({ default: sql.now() }) +createdAt: Date & Opt; +``` + +### `sql.lower()` and `sql.upper()` + +To convert a key to lowercase or uppercase, you can use the `sql.lower()` and `sql.upper()` functions + +```ts +const books = await orm.em.find(Book, { + [sql.upper('title')]: 'TITLE', +}); +``` + ### Aliasing To select a raw fragment, we need to alias it. For that, we can use ```sql`(select 1 + 1)`.as('')```. diff --git a/packages/core/src/metadata/MetadataDiscovery.ts b/packages/core/src/metadata/MetadataDiscovery.ts index 5202160904b8..213e38f02a58 100644 --- a/packages/core/src/metadata/MetadataDiscovery.ts +++ b/packages/core/src/metadata/MetadataDiscovery.ts @@ -21,9 +21,19 @@ import { EntitySchema } from './EntitySchema'; import { Cascade, type EventType, ReferenceKind } from '../enums'; import { MetadataError } from '../errors'; import type { Platform } from '../platforms'; -import { ArrayType, BigIntType, BlobType, EnumArrayType, JsonType, t, Type, Uint8ArrayType } from '../types'; +import { + ArrayType, + BigIntType, + BlobType, + EnumArrayType, + JsonType, + t, + Type, + Uint8ArrayType, + UnknownType, +} from '../types'; import { colors } from '../logging/colors'; -import { raw } from '../utils/RawQueryFragment'; +import { raw, RawQueryFragment } from '../utils/RawQueryFragment'; import type { Logger } from '../logging/Logger'; export class MetadataDiscovery { @@ -126,6 +136,8 @@ export class MetadataDiscovery { for (const meta of filtered) { for (const prop of Object.values(meta.properties)) { + this.initDefaultValue(prop); + this.inferTypeFromDefault(prop); this.initColumnType(prop); } } @@ -385,6 +397,17 @@ export class MetadataDiscovery { .forEach(k => delete (prop as Dictionary)[k]); }); + copy.props + .filter(prop => prop.default) + .forEach(prop => { + const raw = RawQueryFragment.getKnownFragment(prop.default as string); + + if (raw) { + prop.defaultRaw ??= this.platform.formatQuery(raw.sql, raw.params); + delete prop.default; + } + }); + ([ 'prototype', 'props', 'referencingProperties', 'propertyOrder', 'relations', 'concurrencyCheckKeys', 'checks', @@ -558,6 +581,7 @@ export class MetadataDiscovery { this.initNullability(prop); this.applyNamingStrategy(meta, prop); this.initDefaultValue(prop); + this.inferTypeFromDefault(prop); this.initVersionProperty(meta, prop); this.initCustomType(meta, prop); this.initColumnType(prop); @@ -1137,6 +1161,7 @@ export class MetadataDiscovery { const entity1 = new (meta.class as Constructor)(); const entity2 = new (meta.class as Constructor)(); + // we compare the two values by reference, this will discard things like `new Date()` or `Date.now()` if (this.config.get('discovery').inferDefaultValues && prop.default === undefined && entity1[prop.name] != null && entity1[prop.name] === entity2[prop.name] && entity1[prop.name] !== now) { prop.default ??= entity1[prop.name]; @@ -1162,6 +1187,12 @@ export class MetadataDiscovery { } let val = prop.default; + const raw = RawQueryFragment.getKnownFragment(val as string); + + if (raw) { + prop.defaultRaw = this.platform.formatQuery(raw.sql, raw.params); + return; + } if (prop.customType instanceof ArrayType && Array.isArray(prop.default)) { val = prop.customType.convertToDatabaseValue(prop.default, this.platform)!; @@ -1170,6 +1201,22 @@ export class MetadataDiscovery { prop.defaultRaw = typeof val === 'string' ? `'${val}'` : '' + val; } + private inferTypeFromDefault(prop: EntityProperty): void { + if ((prop.defaultRaw == null && prop.default == null) || prop.type !== 'any') { + return; + } + + switch (typeof prop.default) { + case 'string': prop.type = prop.runtimeType = 'string'; break; + case 'number': prop.type = prop.runtimeType = 'number'; break; + case 'boolean': prop.type = prop.runtimeType = 'boolean'; break; + } + + if (prop.defaultRaw?.startsWith('current_timestamp')) { + prop.type = prop.runtimeType = 'Date'; + } + } + private initVersionProperty(meta: EntityMetadata, prop: EntityProperty): void { if (prop.version) { this.initDefaultValue(prop); @@ -1265,7 +1312,7 @@ export class MetadataDiscovery { } } - if (prop.kind === ReferenceKind.SCALAR) { + if (prop.kind === ReferenceKind.SCALAR && !(mappedType instanceof UnknownType)) { prop.columnTypes ??= [mappedType.getColumnType(prop, this.platform)]; // use only custom types provided by user, we don't need to use the ones provided by ORM, diff --git a/packages/core/src/platforms/Platform.ts b/packages/core/src/platforms/Platform.ts index 83dc8ea89c91..a085e3c6c19a 100644 --- a/packages/core/src/platforms/Platform.ts +++ b/packages/core/src/platforms/Platform.ts @@ -380,6 +380,49 @@ export abstract class Platform { return value; } + formatQuery(sql: string, params: readonly any[]): string { + if (params.length === 0) { + return sql; + } + + // fast string replace without regexps + let j = 0; + let pos = 0; + let ret = ''; + + if (sql[0] === '?') { + if (sql[1] === '?') { + ret += this.quoteIdentifier(params[j++]); + pos = 2; + } else { + ret += this.quoteValue(params[j++]); + pos = 1; + } + } + + while (pos < sql.length) { + const idx = sql.indexOf('?', pos + 1); + + if (idx === -1) { + ret += sql.substring(pos, sql.length); + break; + } + + if (sql.substring(idx - 1, idx + 1) === '\\?') { + ret += sql.substring(pos, idx - 1) + '?'; + pos = idx + 1; + } else if (sql.substring(idx, idx + 2) === '??') { + ret += sql.substring(pos, idx) + this.quoteIdentifier(params[j++]); + pos = idx + 2; + } else { + ret += sql.substring(pos, idx) + this.quoteValue(params[j++]); + pos = idx + 1; + } + } + + return ret; + } + cloneEmbeddable(data: T): T { const copy = clone(data); // tag the copy so we know it should be stringified when quoting (so we know how to treat JSON arrays) diff --git a/packages/core/src/utils/RawQueryFragment.ts b/packages/core/src/utils/RawQueryFragment.ts index bb9f317f0225..b8278209df0e 100644 --- a/packages/core/src/utils/RawQueryFragment.ts +++ b/packages/core/src/utils/RawQueryFragment.ts @@ -1,6 +1,6 @@ import { inspect } from 'util'; import { Utils } from './Utils'; -import type { Dictionary, EntityKey, AnyString } from '../typings'; +import type { AnyString, Dictionary, EntityKey } from '../typings'; export class RawQueryFragment { @@ -178,4 +178,15 @@ export function sql(sql: readonly string[], ...values: unknown[]) { }, ''), values); } +export function createSqlFunction(func: string, key: string | ((alias: string) => string)): R { + if (typeof key === 'string') { + return raw(`${func}(${key})`); + } + + return raw(a => `${func}(${(key(a))})`); +} + sql.ref = (...keys: string[]) => raw('??', [keys.join('.')]); +sql.now = (length?: number) => raw('current_timestamp' + (length == null ? '' : `(${length})`)); +sql.lower = (key: string | ((alias: string) => string)) => createSqlFunction('lower', key); +sql.upper = (key: string | ((alias: string) => string)) => createSqlFunction('upper', key); diff --git a/packages/knex/src/AbstractSqlPlatform.ts b/packages/knex/src/AbstractSqlPlatform.ts index 0090a3eea5f6..f785dfec4d08 100644 --- a/packages/knex/src/AbstractSqlPlatform.ts +++ b/packages/knex/src/AbstractSqlPlatform.ts @@ -52,49 +52,6 @@ export abstract class AbstractSqlPlatform extends Platform { return escape(value, true, this.timezone); } - formatQuery(sql: string, params: readonly any[]): string { - if (params.length === 0) { - return sql; - } - - // fast string replace without regexps - let j = 0; - let pos = 0; - let ret = ''; - - if (sql[0] === '?') { - if (sql[1] === '?') { - ret += this.quoteIdentifier(params[j++]); - pos = 2; - } else { - ret += this.quoteValue(params[j++]); - pos = 1; - } - } - - while (pos < sql.length) { - const idx = sql.indexOf('?', pos + 1); - - if (idx === -1) { - ret += sql.substring(pos, sql.length); - break; - } - - if (sql.substring(idx - 1, idx + 1) === '\\?') { - ret += sql.substring(pos, idx - 1) + '?'; - pos = idx + 1; - } else if (sql.substring(idx, idx + 2) === '??') { - ret += sql.substring(pos, idx) + this.quoteIdentifier(params[j++]); - pos = idx + 2; - } else { - ret += sql.substring(pos, idx) + this.quoteValue(params[j++]); - pos = idx + 1; - } - } - - return ret; - } - override getSearchJsonPropertySQL(path: string, type: string, aliased: boolean): string { return this.getSearchJsonPropertyKey(path.split('->'), type, aliased); } diff --git a/packages/knex/src/query/QueryBuilderHelper.ts b/packages/knex/src/query/QueryBuilderHelper.ts index e87c764c3b9e..972958262e44 100644 --- a/packages/knex/src/query/QueryBuilderHelper.ts +++ b/packages/knex/src/query/QueryBuilderHelper.ts @@ -1,6 +1,7 @@ import type { Knex } from 'knex'; import { inspect } from 'util'; import { + ALIAS_REPLACEMENT, ALIAS_REPLACEMENT_RE, type Dictionary, type EntityData, @@ -260,11 +261,11 @@ export class QueryBuilderHelper { join.cond[`${alias}.${typeProperty}`] = join.prop.targetMeta!.discriminatorValue; } - Object.keys(join.cond).forEach(key => { - const needsPrefix = key.includes('.') || Utils.isOperator(key) || RawQueryFragment.isKnownFragment(key); - const newKey = needsPrefix ? key : `${join.alias}.${key}`; - conditions.push(this.processJoinClause(newKey, join.cond[key], params)); - }); + for (const key of Object.keys(join.cond)) { + const hasPrefix = key.includes('.') || Utils.isOperator(key) || RawQueryFragment.isKnownFragment(key); + const newKey = hasPrefix ? key : `${join.alias}.${key}`; + conditions.push(this.processJoinClause(newKey, join.cond[key], join.alias, params)); + } let sql = method + ' '; @@ -284,10 +285,10 @@ export class QueryBuilderHelper { }); } - private processJoinClause(key: string, value: unknown, params: Knex.Value[], operator = '$eq'): string { + private processJoinClause(key: string, value: unknown, alias: string, params: Knex.Value[], operator = '$eq'): string { if (Utils.isGroupOperator(key) && Array.isArray(value)) { const parts = value.map(sub => { - return this.wrapQueryGroup(Object.keys(sub).map(k => this.processJoinClause(k, sub[k], params))); + return this.wrapQueryGroup(Object.keys(sub).map(k => this.processJoinClause(k, sub[k], alias, params))); }); return this.wrapQueryGroup(parts, key); } @@ -302,13 +303,13 @@ export class QueryBuilderHelper { } if (Utils.isOperator(key, false) && Utils.isPlainObject(value)) { - const parts = Object.keys(value).map(k => this.processJoinClause(k, (value as Dictionary)[k], params, key)); + const parts = Object.keys(value).map(k => this.processJoinClause(k, (value as Dictionary)[k], alias, params, key)); return key === '$not' ? `not ${this.wrapQueryGroup(parts)}` : this.wrapQueryGroup(parts); } if (Utils.isPlainObject(value) && Object.keys(value).every(k => Utils.isOperator(k, false))) { - const parts = Object.keys(value).map(op => this.processJoinClause(key, (value as Dictionary)[op], params, op)); + const parts = Object.keys(value).map(op => this.processJoinClause(key, (value as Dictionary)[op], alias, params, op)); return this.wrapQueryGroup(parts); } @@ -345,7 +346,7 @@ export class QueryBuilderHelper { const rawField = RawQueryFragment.getKnownFragment(key); if (rawField) { - let sql = rawField.sql; + let sql = rawField.sql.replaceAll(ALIAS_REPLACEMENT, alias); params.push(...rawField.params as Knex.Value[]); params.push(...Utils.asArray(value) as Knex.Value[]); diff --git a/packages/knex/src/schema/SchemaHelper.ts b/packages/knex/src/schema/SchemaHelper.ts index 2baf4732ebf6..f5181606e5ca 100644 --- a/packages/knex/src/schema/SchemaHelper.ts +++ b/packages/knex/src/schema/SchemaHelper.ts @@ -1,5 +1,5 @@ import type { Knex } from 'knex'; -import { BigIntType, EnumType, Utils, type Connection, type Dictionary } from '@mikro-orm/core'; +import { BigIntType, EnumType, Utils, type Connection, type Dictionary, RawQueryFragment } from '@mikro-orm/core'; import type { AbstractSqlConnection } from '../AbstractSqlConnection'; import type { AbstractSqlPlatform } from '../AbstractSqlPlatform'; import type { CheckDef, Column, IndexDef, Table, TableDifference } from '../typings'; @@ -259,6 +259,12 @@ export abstract class SchemaHelper { return defaultValue; } + const raw = RawQueryFragment.getKnownFragment(defaultValue); + + if (raw) { + return this.platform.formatQuery(raw.sql, raw.params); + } + const genericValue = defaultValue.replace(/\(\d+\)/, '(?)').toLowerCase(); const norm = defaultValues[genericValue]; diff --git a/packages/postgresql/src/PostgreSqlSchemaHelper.ts b/packages/postgresql/src/PostgreSqlSchemaHelper.ts index 738931da85ca..9aeda2861bf5 100644 --- a/packages/postgresql/src/PostgreSqlSchemaHelper.ts +++ b/packages/postgresql/src/PostgreSqlSchemaHelper.ts @@ -395,8 +395,8 @@ export class PostgreSqlSchemaHelper extends SchemaHelper { } override normalizeDefaultValue(defaultValue: string, length: number) { - if (!defaultValue) { - return defaultValue; + if (!defaultValue || typeof defaultValue as unknown !== 'string') { + return super.normalizeDefaultValue(defaultValue, length, PostgreSqlSchemaHelper.DEFAULT_VALUES); } const match = defaultValue.match(/^'(.*)'::(.*)$/); diff --git a/packages/reflection/src/TsMorphMetadataProvider.ts b/packages/reflection/src/TsMorphMetadataProvider.ts index 94b51c17c451..9313ecb26ef5 100644 --- a/packages/reflection/src/TsMorphMetadataProvider.ts +++ b/packages/reflection/src/TsMorphMetadataProvider.ts @@ -59,8 +59,28 @@ export class TsMorphMetadataProvider extends MetadataProvider { return prop.type; } + private cleanUpTypeTags(type: string): string { + const genericTags = [/Opt<(.*?)>/, /Hidden<(.*?)>/]; + const intersectionTags = [ + '{ __optional?: 1 | undefined; }', + '{ __hidden?: 1 | undefined; }', + ]; + + for (const tag of genericTags) { + type = type.replace(tag, '$1'); + } + + for (const tag of intersectionTags) { + type = type.replace(' & ' + tag, ''); + type = type.replace(tag + ' & ', ''); + } + + return type; + } + private initPropertyType(meta: EntityMetadata, prop: EntityProperty): void { - const { type, optional } = this.readTypeFromSource(meta, prop); + const { type: typeRaw, optional } = this.readTypeFromSource(meta, prop); + const type = this.cleanUpTypeTags(typeRaw); prop.type = type; prop.runtimeType = type as 'string'; diff --git a/tests/EntityManager.postgre.test.ts b/tests/EntityManager.postgre.test.ts index 3cc668f2ca0d..9f5f6efce353 100644 --- a/tests/EntityManager.postgre.test.ts +++ b/tests/EntityManager.postgre.test.ts @@ -1784,7 +1784,7 @@ describe('EntityManagerPostgre', () => { const mock = mockLogger(orm, ['query', 'query-params']); const books1 = await orm.em.find(Book2, { - [raw('upper(title)')]: ['B1', 'B2'], + [sql.upper('title')]: ['B1', 'B2'], author: { [raw(a => `${a}.age::text`)]: { $ilike: '%2%' }, }, @@ -1794,7 +1794,7 @@ describe('EntityManagerPostgre', () => { orm.em.clear(); const books2 = await orm.em.find(Book2, { - [raw('upper(title)')]: raw('upper(?)', ['b2']), + [sql.upper('title')]: raw('upper(?)', ['b2']), }, { populate: ['perex'] }); expect(books2).toHaveLength(1); expect(mock.mock.calls[1][0]).toMatch(`select "b0".*, "b0".price * 1.19 as "price_taxed" from "book2" as "b0" where "b0"."author_id" is not null and upper(title) = upper('b2')`); @@ -1850,7 +1850,7 @@ describe('EntityManagerPostgre', () => { const ref2 = await orm.em.findOneOrFail(Author2, 2); const mock = mockLogger(orm, ['query', 'query-params']); - ref1.age = raw(`age * 2`); + ref1.age = sql`age * 2`; expect(() => ref1.age!++).toThrow(); expect(() => ref2.age = ref1.age).toThrow(); expect(() => JSON.stringify(ref1)).toThrow(); diff --git a/tests/QueryBuilder.test.ts b/tests/QueryBuilder.test.ts index 07ba6996076d..70fe8857f673 100644 --- a/tests/QueryBuilder.test.ts +++ b/tests/QueryBuilder.test.ts @@ -394,7 +394,7 @@ describe('QueryBuilder', () => { .leftJoin('a.books', 'b', { [sql`json_contains(b.meta, ${{ 'b.foo': 'bar' }})`]: [], [raw('json_contains(`b`.`meta`, ?) = ?', [{ 'b.foo': 'bar' }, false])]: [], - [raw('lower(??)', ['b.title'])]: '321', + [sql.lower(a => `${a}.title`)]: '321', }) .where({ 'b.title': 'test 123' }); expect(qb.getQuery()).toEqual('select `a`.*, `b`.* from `author2` as `a` ' + @@ -402,7 +402,7 @@ describe('QueryBuilder', () => { 'on `a`.`id` = `b`.`author_id` ' + 'and json_contains(b.meta, ?) ' + 'and json_contains(`b`.`meta`, ?) = ? ' + - 'and lower(`b`.`title`) = ? ' + + 'and lower(b.title) = ? ' + 'where `b`.`title` = ?'); expect(qb.getParams()).toEqual([{ 'b.foo': 'bar' }, { 'b.foo': 'bar' }, false, '321', 'test 123']); expect(qb.getFormattedQuery()).toEqual('select `a`.*, `b`.* from `author2` as `a` ' + @@ -410,7 +410,7 @@ describe('QueryBuilder', () => { 'on `a`.`id` = `b`.`author_id` ' + 'and json_contains(b.meta, \'{\\"b.foo\\":\\"bar\\"}\') ' + 'and json_contains(`b`.`meta`, \'{\\"b.foo\\":\\"bar\\"}\') = false ' + - 'and lower(`b`.`title`) = \'321\' ' + + 'and lower(b.title) = \'321\' ' + "where `b`.`title` = 'test 123'"); }); diff --git a/tests/entities-sql/Author2.ts b/tests/entities-sql/Author2.ts index fd517f137bbd..e6ae07fa749e 100644 --- a/tests/entities-sql/Author2.ts +++ b/tests/entities-sql/Author2.ts @@ -24,7 +24,7 @@ import { Opt, Hidden, Embeddable, - Embedded, + Embedded, sql, } from '@mikro-orm/core'; import { Book2 } from './Book2'; @@ -61,10 +61,10 @@ export class Author2 extends BaseEntity2 { static beforeDestroyCalled = 0; static afterDestroyCalled = 0; - @Property({ length: 3, defaultRaw: 'current_timestamp(3)' }) + @Property({ length: 3, default: sql.now(3) }) createdAt: Opt = new Date(); - @Property({ onUpdate: () => new Date(), length: 3, defaultRaw: 'current_timestamp(3)' }) + @Property({ onUpdate: () => new Date(), length: 3, default: sql.now(3) }) updatedAt: Opt = new Date(); @Property() diff --git a/tests/entities-sql/Book2.ts b/tests/entities-sql/Book2.ts index 2a106ed2e5a4..5cc9afc0407a 100644 --- a/tests/entities-sql/Book2.ts +++ b/tests/entities-sql/Book2.ts @@ -36,7 +36,7 @@ export class Book2 { @PrimaryKey({ name: 'uuid_pk', type: t.uuid }) uuid = v4(); - @Property({ defaultRaw: 'current_timestamp(3)', length: 3 }) + @Property({ default: sql.now(3), length: 3 }) createdAt = new Date(); @Index({ type: 'fulltext' }) diff --git a/tests/features/custom-types/GH725.test.ts b/tests/features/custom-types/GH725.test.ts index 5b7cd75fbcec..424f592806e9 100644 --- a/tests/features/custom-types/GH725.test.ts +++ b/tests/features/custom-types/GH725.test.ts @@ -1,4 +1,4 @@ -import { EntitySchema, MikroORM, Type, ValidationError } from '@mikro-orm/core'; +import { EntitySchema, MikroORM, sql, Type, ValidationError } from '@mikro-orm/core'; import type { AbstractSqlDriver } from '@mikro-orm/knex'; import { SqliteDriver } from '@mikro-orm/sqlite'; import { PostgreSqlDriver } from '@mikro-orm/postgresql'; @@ -19,10 +19,6 @@ export class DateTime { type Maybe = T | null | undefined; -export type TimestampTypeOptions = { - hasTimeZone: boolean; -}; - export class DateTimeType extends Type, Maybe> { override convertToDatabaseValue(value: unknown): Maybe { @@ -79,7 +75,7 @@ export const TestSchema = new EntitySchema({ defaultRaw: 'uuid_generate_v4()', }, createdAt: { - defaultRaw: 'now()', + default: sql.now(), type: DateTimeType, }, updatedAt: { diff --git a/tests/features/embeddables/GH3887.mysql.test.ts b/tests/features/embeddables/GH3887.mysql.test.ts index 51ac3b69134b..fa67cf678e95 100644 --- a/tests/features/embeddables/GH3887.mysql.test.ts +++ b/tests/features/embeddables/GH3887.mysql.test.ts @@ -1,9 +1,9 @@ -import { Embeddable, Embedded, Entity, PrimaryKey, Property, MikroORM } from '@mikro-orm/mysql'; +import { Embeddable, Embedded, Entity, PrimaryKey, Property, MikroORM, sql } from '@mikro-orm/mysql'; @Embeddable() class NestedTime { - @Property({ defaultRaw: 'current_timestamp' }) + @Property({ default: sql.now() }) timestamp!: Date; } @@ -11,7 +11,7 @@ class NestedTime { @Embeddable() class Time { - @Property({ defaultRaw: 'current_timestamp' }) + @Property({ default: sql.now() }) timestamp!: Date; @Embedded(() => NestedTime) diff --git a/tests/features/embeddables/GH3887.sqlite.test.ts b/tests/features/embeddables/GH3887.sqlite.test.ts index 10f41e391188..d779e68acad9 100644 --- a/tests/features/embeddables/GH3887.sqlite.test.ts +++ b/tests/features/embeddables/GH3887.sqlite.test.ts @@ -1,9 +1,9 @@ -import { Embeddable, Embedded, Entity, PrimaryKey, Property, MikroORM } from '@mikro-orm/better-sqlite'; +import { Embeddable, Embedded, Entity, PrimaryKey, Property, MikroORM, sql } from '@mikro-orm/better-sqlite'; @Embeddable() class NestedTime { - @Property({ defaultRaw: 'current_timestamp' }) + @Property({ default: sql.now() }) timestamp!: Date; } @@ -11,7 +11,7 @@ class NestedTime { @Embeddable() class Time { - @Property({ defaultRaw: 'current_timestamp' }) + @Property({ default: sql.now() }) timestamp!: Date; @Embedded(() => NestedTime) diff --git a/tests/features/schema-generator/GH4782.test.ts b/tests/features/schema-generator/GH4782.test.ts index 98a23a3ca0ed..5328d44475be 100644 --- a/tests/features/schema-generator/GH4782.test.ts +++ b/tests/features/schema-generator/GH4782.test.ts @@ -1,4 +1,4 @@ -import { MikroORM } from '@mikro-orm/mysql'; +import { MikroORM, sql } from '@mikro-orm/mysql'; import { Entity, PrimaryKey, Property } from '@mikro-orm/core'; @Entity({ tableName: 'user' }) @@ -15,7 +15,7 @@ class User1 { @PrimaryKey() id!: number; - @Property({ defaultRaw: 'current_timestamp(3)', columnType: 'timestamp(3)' }) + @Property({ default: sql.now(3), columnType: 'timestamp(3)' }) bar!: Date; } @@ -26,7 +26,7 @@ class User2 { @PrimaryKey() id!: number; - @Property({ defaultRaw: 'current_timestamp(3)', columnType: 'timestamp(3)' }) + @Property({ default: sql.now(3), columnType: 'timestamp(3)' }) bar!: Date; } @@ -37,7 +37,7 @@ class User3 { @PrimaryKey() id!: number; - @Property({ defaultRaw: 'current_timestamp(6)', columnType: 'timestamp(6)' }) + @Property({ default: sql.now(6), columnType: 'timestamp(6)' }) bar!: Date; } diff --git a/tests/features/schema-generator/__snapshots__/diffing-default-values.test.ts.snap b/tests/features/schema-generator/__snapshots__/diffing-default-values.test.ts.snap index b265759a7bf1..832fdcba7f67 100644 --- a/tests/features/schema-generator/__snapshots__/diffing-default-values.test.ts.snap +++ b/tests/features/schema-generator/__snapshots__/diffing-default-values.test.ts.snap @@ -4,7 +4,7 @@ exports[`diffing default values (GH #2385) string defaults do not produce additi "set names utf8mb4; set foreign_key_checks = 0; -create table \`foo1\` (\`id\` int unsigned not null auto_increment primary key, \`bar0\` varchar(255) not null default 'test', \`bar1\` varchar(255) not null default 'test', \`bar2\` datetime not null default now(), \`bar3\` datetime(6) not null default now(6), \`metadata\` json not null default '{"value":42}') default character set utf8mb4 engine = InnoDB; +create table \`foo1\` (\`id\` int unsigned not null auto_increment primary key, \`bar0\` varchar(255) not null default 'test', \`bar1\` varchar(255) not null default 'test', \`num\` int not null default 1, \`bool\` tinyint(1) not null default true, \`bar2\` datetime not null default current_timestamp, \`bar3\` datetime(6) not null default current_timestamp(6), \`metadata\` json not null default '{"value":42}') default character set utf8mb4 engine = InnoDB; set foreign_key_checks = 1; " @@ -14,7 +14,7 @@ exports[`diffing default values (GH #2385) string defaults do not produce additi "set names utf8mb4; set foreign_key_checks = 0; -create table \`foo0\` (\`id\` int unsigned not null auto_increment primary key, \`bar0\` varchar(255) not null default 'test', \`bar1\` varchar(255) not null default 'test', \`bar2\` datetime not null default now(), \`bar3\` datetime(6) not null default now(6)) default character set utf8mb4 engine = InnoDB; +create table \`foo0\` (\`id\` int unsigned not null auto_increment primary key, \`bar0\` varchar(255) not null default 'test', \`bar1\` varchar(255) not null default 'test', \`num\` int not null default 1, \`bool\` tinyint(1) not null default true, \`bar2\` datetime not null default current_timestamp, \`bar3\` datetime(6) not null default current_timestamp(6)) default character set utf8mb4 engine = InnoDB; set foreign_key_checks = 1; " @@ -24,7 +24,7 @@ exports[`diffing default values (GH #2385) string defaults do not produce additi "set names 'utf8'; set session_replication_role = 'replica'; -create table "foo2" ("id" serial primary key, "bar0" varchar(255) not null default 'test', "bar1" varchar(255) not null default 'test', "bar2" timestamptz not null default now(), "bar3" timestamptz(6) not null default now(), "metadata" jsonb not null default '{"value":42}'); +create table "foo2" ("id" serial primary key, "bar0" varchar(255) not null default 'test', "bar1" varchar(255) not null default 'test', "num" int not null default 1, "bool" boolean not null default true, "bar2" timestamptz not null default current_timestamp, "bar3" timestamptz(6) not null default current_timestamp, "metadata" jsonb not null default '{"value":42}'); set session_replication_role = 'origin'; " @@ -33,7 +33,7 @@ set session_replication_role = 'origin'; exports[`diffing default values (GH #2385) string defaults do not produce additional diffs [sqlite] 1`] = ` "pragma foreign_keys = off; -create table \`foo3\` (\`id\` integer not null primary key autoincrement, \`bar0\` text not null default 'test', \`bar1\` text not null default 'test', \`bar2\` datetime not null default now, \`metadata\` json not null default '{"value":43}'); +create table \`foo3\` (\`id\` integer not null primary key autoincrement, \`bar0\` text not null default 'test', \`bar1\` text not null default 'test', \`num\` integer not null default 1, \`bool\` integer not null default true, \`bar2\` datetime not null default current_timestamp, \`metadata\` json not null default '{"value":43}'); pragma foreign_keys = on; " diff --git a/tests/features/schema-generator/diffing-default-values.test.ts b/tests/features/schema-generator/diffing-default-values.test.ts index 5405a6c623ee..e492f45cdd58 100644 --- a/tests/features/schema-generator/diffing-default-values.test.ts +++ b/tests/features/schema-generator/diffing-default-values.test.ts @@ -1,40 +1,48 @@ -import { Entity, MikroORM, PrimaryKey, Property } from '@mikro-orm/core'; +import { Entity, MikroORM, Opt, PrimaryKey, Property, sql } from '@mikro-orm/core'; import { MySqlDriver } from '@mikro-orm/mysql'; import { MariaDbDriver } from '@mikro-orm/mariadb'; import { PostgreSqlDriver } from '@mikro-orm/postgresql'; import { SqliteDriver } from '@mikro-orm/sqlite'; +import { TsMorphMetadataProvider } from '@mikro-orm/reflection'; -export class Foo { +class Foo { @PrimaryKey() id!: number; @Property({ defaultRaw: "'test'" }) - bar0!: string; + bar0!: string & Opt; @Property({ default: 'test' }) - bar1!: string; + bar1!: string & Opt; + + @Property({ default: 1 }) + num!: number & Opt; + + @Property({ default: true }) + bool!: boolean & Opt; } @Entity() -export class Foo0 extends Foo { +class Foo0 extends Foo { - @Property({ defaultRaw: 'now()' }) - bar2!: Date; + @Property({ defaultRaw: sql.now() }) + bar2!: Opt; - @Property({ defaultRaw: 'now(6)', length: 6 }) - bar3!: Date; + @Property({ default: sql.now(6), length: 6 }) + bar3!: Date & Opt; } @Entity() -export class Foo1 extends Foo { +class Foo1 extends Foo { - @Property({ defaultRaw: 'now()' }) + @Property({ default: sql.now() }) bar2!: Date; - @Property({ defaultRaw: 'now(6)', length: 6 }) + // test that we can infer the Date type from default here too + @Property({ default: sql.now(6), type: 'any', length: 6 }) bar3!: Date; @Property({ type: 'json', default: JSON.stringify({ value: 42 }) }) @@ -43,12 +51,12 @@ export class Foo1 extends Foo { } @Entity() -export class Foo2 extends Foo { +class Foo2 extends Foo { - @Property({ defaultRaw: 'now()' }) + @Property({ default: sql.now() }) bar2!: Date; - @Property({ defaultRaw: 'now()', length: 6 }) + @Property({ default: sql.now(), length: 6 }) bar3!: Date; @Property({ type: 'json', default: JSON.stringify({ value: 42 }) }) @@ -57,9 +65,9 @@ export class Foo2 extends Foo { } @Entity() -export class Foo3 extends Foo { +class Foo3 extends Foo { - @Property({ defaultRaw: 'now' }) + @Property({ default: sql.now() }) bar2!: Date; @Property({ type: 'json', default: JSON.stringify({ value: 43 }) }) @@ -75,6 +83,8 @@ describe('diffing default values (GH #2385)', () => { dbName: 'mikro_orm_test_gh_2385', driver: MySqlDriver, port: 3308, + metadataProvider: TsMorphMetadataProvider, + metadataCache: { enabled: false }, }); await orm.schema.refreshDatabase(); expect(await orm.schema.getCreateSchemaSQL()).toMatchSnapshot(); diff --git a/tests/features/upsert/GH4020.test.ts b/tests/features/upsert/GH4020.test.ts index ccce51d4c4be..4b38dbbcecd6 100644 --- a/tests/features/upsert/GH4020.test.ts +++ b/tests/features/upsert/GH4020.test.ts @@ -1,4 +1,4 @@ -import { Entity, PrimaryKey, Property, SimpleLogger } from '@mikro-orm/core'; +import { Entity, PrimaryKey, Property, SimpleLogger, sql } from '@mikro-orm/core'; import { MikroORM } from '@mikro-orm/sqlite'; import { mockLogger } from '../../helpers'; @@ -11,7 +11,7 @@ export class GuildEntity { @Property() name!: string; - @Property({ defaultRaw: 'current_timestamp', index: true }) + @Property({ default: sql.now(), index: true }) created_at: Date = new Date(); } diff --git a/tests/features/upsert/GH4242-2.test.ts b/tests/features/upsert/GH4242-2.test.ts index 61534e84dff1..dba6138d9f35 100644 --- a/tests/features/upsert/GH4242-2.test.ts +++ b/tests/features/upsert/GH4242-2.test.ts @@ -1,4 +1,4 @@ -import { Entity, ManyToOne, PrimaryKey, Property, Ref, Reference, SimpleLogger, Unique } from '@mikro-orm/core'; +import { Entity, ManyToOne, PrimaryKey, Property, Ref, Reference, SimpleLogger, sql, Unique } from '@mikro-orm/core'; import { MikroORM } from '@mikro-orm/mysql'; import { mockLogger } from '../../helpers'; @@ -14,7 +14,7 @@ class B { @Property({ unique: true }) order!: number; - @Property({ defaultRaw: 'current_timestamp', onUpdate: () => new Date() }) + @Property({ default: sql.now(), onUpdate: () => new Date() }) updatedAt: Date = new Date(); } @@ -29,7 +29,7 @@ class D { @Property() tenantWorkflowId!: number; - @Property({ defaultRaw: 'current_timestamp', onUpdate: () => new Date() }) + @Property({ default: sql.now(), onUpdate: () => new Date() }) updatedAt: Date = new Date(); @Property({ nullable: true }) diff --git a/tests/features/upsert/GH4242.test.ts b/tests/features/upsert/GH4242.test.ts index c74a4870698d..4965da1169ff 100644 --- a/tests/features/upsert/GH4242.test.ts +++ b/tests/features/upsert/GH4242.test.ts @@ -1,4 +1,4 @@ -import { Entity, ManyToOne, PrimaryKey, Property, Ref, Reference, SimpleLogger, Unique } from '@mikro-orm/core'; +import { Entity, ManyToOne, PrimaryKey, Property, Ref, Reference, SimpleLogger, sql, Unique } from '@mikro-orm/core'; import { MikroORM } from '@mikro-orm/postgresql'; import { mockLogger } from '../../helpers'; @@ -14,7 +14,7 @@ class B { @Property({ unique: true }) order!: number; - @Property({ length: 6, defaultRaw: 'now()', onUpdate: () => new Date() }) + @Property({ length: 6, default: sql.now(), onUpdate: () => new Date() }) updatedAt: Date = new Date(); } @@ -29,7 +29,7 @@ class D { @Property() tenantWorkflowId!: number; - @Property({ length: 6, defaultRaw: 'now()', onUpdate: () => new Date() }) + @Property({ length: 6, default: sql.now(), onUpdate: () => new Date() }) updatedAt: Date = new Date(); @Property({ nullable: true }) diff --git a/tests/features/upsert/GH4786.test.ts b/tests/features/upsert/GH4786.test.ts index c6cd22e8f0cd..2863daa30179 100644 --- a/tests/features/upsert/GH4786.test.ts +++ b/tests/features/upsert/GH4786.test.ts @@ -6,7 +6,7 @@ import { OptionalProps, PrimaryKey, Property, - SimpleLogger, + SimpleLogger, sql, Unique, } from '@mikro-orm/core'; import { MikroORM } from '@mikro-orm/sqlite'; @@ -20,10 +20,10 @@ abstract class ApplicationEntity { @PrimaryKey() id!: number; - @Property({ name: 'created_at', defaultRaw: 'current_timestamp' }) + @Property({ name: 'created_at', default: sql.now() }) createdAt!: Date; - @Property({ name: 'updated_at', defaultRaw: 'current_timestamp', onUpdate: () => new Date() }) + @Property({ name: 'updated_at', default: sql.now(), onUpdate: () => new Date() }) updatedAt!: Date; } diff --git a/tests/issues/GH3965.test.ts b/tests/issues/GH3965.test.ts index 87945119c6cf..1083d23c6c1a 100644 --- a/tests/issues/GH3965.test.ts +++ b/tests/issues/GH3965.test.ts @@ -7,7 +7,9 @@ import { PrimaryKey, Cascade, Ref, - PrimaryKeyProp, Primary, + PrimaryKeyProp, + Primary, + sql, } from '@mikro-orm/core'; import { MikroORM } from '@mikro-orm/mysql'; import { randomUUID } from 'crypto'; @@ -21,7 +23,8 @@ export class Category { id!: string; @Property({ - defaultRaw: 'CURRENT_TIMESTAMP', + length: 3, + default: sql.now(3), }) createdAt?: Date; @@ -41,7 +44,8 @@ export class Article { id!: string; @Property({ - defaultRaw: 'CURRENT_TIMESTAMP', + length: 3, + default: sql.now(3), }) createdAt?: Date; @@ -67,7 +71,8 @@ export class ArticleAttribute { id!: string; @Property({ - defaultRaw: 'CURRENT_TIMESTAMP', + length: 3, + default: sql.now(3), }) createdAt?: Date; diff --git a/tests/issues/GH4062.test.ts b/tests/issues/GH4062.test.ts index 57b7e9643c51..a26b31aff9e8 100644 --- a/tests/issues/GH4062.test.ts +++ b/tests/issues/GH4062.test.ts @@ -1,4 +1,18 @@ -import { MikroORM, Cascade, Collection, Entity, EntityData, ManyToOne, OneToMany, PrimaryKey, PrimaryKeyProp, Property, Ref, SimpleLogger } from '@mikro-orm/mysql'; +import { + MikroORM, + Cascade, + Collection, + Entity, + EntityData, + ManyToOne, + OneToMany, + PrimaryKey, + PrimaryKeyProp, + Property, + Ref, + SimpleLogger, + sql, +} from '@mikro-orm/mysql'; import { mockLogger } from '../helpers'; @Entity() @@ -13,7 +27,7 @@ class Category { }) articles = new Collection
(this); - @Property({ defaultRaw: 'CURRENT_TIMESTAMP' }) + @Property({ default: sql.now() }) createdAt?: Date; } @@ -39,7 +53,7 @@ class Article { attributes = new Collection(this); @Property({ - defaultRaw: 'CURRENT_TIMESTAMP', + default: sql.now(), }) createdAt?: Date; @@ -63,7 +77,7 @@ class ArticleAttribute { [PrimaryKeyProp]?: ['id', ['id', 'category']]; @Property({ - defaultRaw: 'CURRENT_TIMESTAMP', + default: sql.now(), }) createdAt?: Date; diff --git a/tests/issues/GH4377.test.ts b/tests/issues/GH4377.test.ts index 2f1846992126..721aa5ca5cb0 100644 --- a/tests/issues/GH4377.test.ts +++ b/tests/issues/GH4377.test.ts @@ -1,4 +1,4 @@ -import { Cascade, Entity, OneToOne, PrimaryKey, PrimaryKeyProp, Property, Ref } from '@mikro-orm/core'; +import { Cascade, Entity, OneToOne, PrimaryKey, PrimaryKeyProp, Property, Ref, sql } from '@mikro-orm/core'; import { MikroORM } from '@mikro-orm/mysql'; import { randomUUID } from 'crypto'; @@ -25,9 +25,7 @@ class Root { @PrimaryKey() id!: string; - @Property({ - defaultRaw: 'CURRENT_TIMESTAMP', - }) + @Property({ default: sql.now() }) createdAt?: Date; @OneToOne(() => NonRoot, nonRoot => nonRoot.root, { diff --git a/tests/issues/GH4988.test.ts b/tests/issues/GH4988.test.ts index 4fef30e48e32..468d33738d78 100644 --- a/tests/issues/GH4988.test.ts +++ b/tests/issues/GH4988.test.ts @@ -1,4 +1,4 @@ -import { BigIntType, Collection, EntitySchema, Ref } from '@mikro-orm/core'; +import { BigIntType, Collection, EntitySchema, Ref, sql } from '@mikro-orm/core'; import { MikroORM } from '@mikro-orm/postgresql'; class ProductEntity { @@ -90,12 +90,12 @@ const companyProductsSchema = new EntitySchema({ createdAt: { type: 'timestamp', onCreate: () => new Date(), - defaultRaw: 'current_timestamp', + default: sql.now(), }, updatedAt: { type: 'timestamp', onUpdate: () => new Date(), - defaultRaw: 'current_timestamp', + default: sql.now(), }, }, });