diff --git a/drizzle-orm/src/pg-core/columns/timestamp.ts b/drizzle-orm/src/pg-core/columns/timestamp.ts index 3060bfb3f..6df0c3be6 100644 --- a/drizzle-orm/src/pg-core/columns/timestamp.ts +++ b/drizzle-orm/src/pg-core/columns/timestamp.ts @@ -58,12 +58,12 @@ export class PgTimestamp> exte return `timestamp${precision}${this.withTimezone ? ' with time zone' : ''}`; } - override mapFromDriverValue = (value: string): Date => { + override mapFromDriverValue = (value: string): Date | null => { return new Date(this.withTimezone ? value : value + '+0000'); }; override mapToDriverValue = (value: Date): string => { - return this.withTimezone ? value.toUTCString() : value.toISOString(); + return value.toISOString(); }; } diff --git a/drizzle-orm/src/postgres-js/driver.ts b/drizzle-orm/src/postgres-js/driver.ts index 9726bef8f..ae1b48a21 100644 --- a/drizzle-orm/src/postgres-js/driver.ts +++ b/drizzle-orm/src/postgres-js/driver.ts @@ -20,6 +20,14 @@ export function drizzle = Record = {}, ): PostgresJsDatabase { + const transparentParser = (val: any) => val; + + // Override postgres.js default date parsers: https://github.com/porsager/postgres/discussions/761 + for (const type of ['1184', '1082', '1083', '1114']) { + client.options.parsers[type as any] = transparentParser; + client.options.serializers[type as any] = transparentParser; + } + const dialect = new PgDialect(); let logger; if (config.logger === true) { diff --git a/integration-tests/tests/awsdatapi.test.ts b/integration-tests/tests/awsdatapi.test.ts index 1f390eb70..1e2cbafad 100644 --- a/integration-tests/tests/awsdatapi.test.ts +++ b/integration-tests/tests/awsdatapi.test.ts @@ -9,7 +9,22 @@ import { asc, eq, name, placeholder, sql, TransactionRollbackError } from 'drizz import type { AwsDataApiPgDatabase } from 'drizzle-orm/aws-data-api/pg'; import { drizzle } from 'drizzle-orm/aws-data-api/pg'; import { migrate } from 'drizzle-orm/aws-data-api/pg/migrator'; -import { alias, boolean, integer, jsonb, pgTable, pgTableCreator, serial, text, timestamp } from 'drizzle-orm/pg-core'; +import { + alias, + boolean, + date, + integer, + interval, + jsonb, + pgTable, + pgTableCreator, + serial, + text, + time, + timestamp, +} from 'drizzle-orm/pg-core'; +import type { Equal } from './utils'; +import { Expect } from './utils'; dotenv.config(); @@ -858,6 +873,262 @@ test.serial('select from raw sql with mapped values', async (t) => { ]); }); +test.serial('all date and time columns', async (t) => { + const { db } = t.context; + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + dateString: date('date_string', { mode: 'string' }).notNull(), + time: time('time', { precision: 3 }).notNull(), + datetime: timestamp('datetime').notNull(), + datetimeWTZ: timestamp('datetime_wtz', { withTimezone: true }).notNull(), + datetimeString: timestamp('datetime_string', { mode: 'string' }).notNull(), + datetimeFullPrecision: timestamp('datetime_full_precision', { precision: 6, mode: 'string' }).notNull(), + datetimeWTZString: timestamp('datetime_wtz_string', { withTimezone: true, mode: 'string' }).notNull(), + interval: interval('interval').notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + date_string date not null, + time time(3) not null, + datetime timestamp not null, + datetime_wtz timestamp with time zone not null, + datetime_string timestamp not null, + datetime_full_precision timestamp(6) not null, + datetime_wtz_string timestamp with time zone not null, + interval interval not null + ) + `); + + const someDatetime = new Date('2022-01-01T00:00:00.123Z'); + const fullPrecision = '2022-01-01T00:00:00.123456'; + const someTime = '23:23:12.432'; + + await db.insert(table).values({ + dateString: '2022-01-01', + time: someTime, + datetime: someDatetime, + datetimeWTZ: someDatetime, + datetimeString: '2022-01-01T00:00:00.123Z', + datetimeFullPrecision: fullPrecision, + datetimeWTZString: '2022-01-01T00:00:00.123Z', + interval: '1 day', + }); + + const result = await db.select().from(table); + + Expect< + Equal<{ + id: number; + dateString: string; + time: string; + datetime: Date; + datetimeWTZ: Date; + datetimeString: string; + datetimeFullPrecision: string; + datetimeWTZString: string; + interval: string; + }[], typeof result> + >; + + Expect< + Equal<{ + dateString: string; + time: string; + datetime: Date; + datetimeWTZ: Date; + datetimeString: string; + datetimeFullPrecision: string; + datetimeWTZString: string; + interval: string; + id?: number | undefined; + }, typeof table.$inferInsert> + >; + + t.deepEqual(result, [ + { + id: 1, + dateString: '2022-01-01', + time: someTime, + datetime: someDatetime, + datetimeWTZ: someDatetime, + datetimeString: '2022-01-01 00:00:00.123', + datetimeFullPrecision: fullPrecision.replace('T', ' ').replace('Z', ''), + datetimeWTZString: '2022-01-01 00:00:00.123+00', + interval: '1 day', + }, + ]); + + await db.execute(sql`drop table if exists ${table}`); +}); + +test.serial('all date and time columns with timezone', async (t) => { + const { db } = t.context; + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + timestamp: timestamp('timestamp_string', { mode: 'string', withTimezone: true, precision: 6 }).notNull(), + timestampAsDate: timestamp('timestamp_date', { withTimezone: true, precision: 3 }).notNull(), + timestampTimeZones: timestamp('timestamp_date_2', { withTimezone: true, precision: 3 }).notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + timestamp_string timestamp(6) with time zone not null, + timestamp_date timestamp(3) with time zone not null, + timestamp_date_2 timestamp(3) with time zone not null + ) + `); + + const timestampString = '2022-01-01 00:00:00.123456-0200'; + const timestampDate = new Date(); + const timestampDateWTZ = new Date('2022-01-01 00:00:00.123 +0500'); + + const timestampString2 = '2022-01-01 00:00:00.123456-0400'; + const timestampDate2 = new Date(); + const timestampDateWTZ2 = new Date('2022-01-01 00:00:00.123 +0200'); + + await db.insert(table).values([ + { timestamp: timestampString, timestampAsDate: timestampDate, timestampTimeZones: timestampDateWTZ }, + { timestamp: timestampString2, timestampAsDate: timestampDate2, timestampTimeZones: timestampDateWTZ2 }, + ]); + + const result = await db.select().from(table); + const result2 = await db.execute<{ + id: number; + timestamp_string: string; + timestamp_date: string; + timestamp_date_2: string; + }>(sql`select * from ${table}`); + + // Whatever you put in, you get back when you're using the date mode + // But when using the string mode, postgres returns a string transformed into UTC + t.deepEqual(result, [ + { + id: 1, + timestamp: '2022-01-01 02:00:00.123456+00', + timestampAsDate: timestampDate, + timestampTimeZones: timestampDateWTZ, + }, + { + id: 2, + timestamp: '2022-01-01 04:00:00.123456+00', + timestampAsDate: timestampDate2, + timestampTimeZones: timestampDateWTZ2, + }, + ]); + + t.deepEqual(result2.records, [ + { + id: 1, + timestamp_string: '2022-01-01 02:00:00.123456+00', + timestamp_date: timestampDate.toISOString().replace('T', ' ').replace('Z', '') + '+00', + timestamp_date_2: timestampDateWTZ.toISOString().replace('T', ' ').replace('Z', '') + '+00', + }, + { + id: 2, + timestamp_string: '2022-01-01 04:00:00.123456+00', + timestamp_date: timestampDate2.toISOString().replace('T', ' ').replace('Z', '') + '+00', + timestamp_date_2: timestampDateWTZ2.toISOString().replace('T', ' ').replace('Z', '') + '+00', + }, + ]); + + t.deepEqual( + result[0]?.timestampTimeZones.getTime(), + new Date((result2.records?.[0] as any).timestamp_date_2 as any).getTime(), + ); + + await db.execute(sql`drop table if exists ${table}`); +}); + +test.serial('all date and time columns without timezone', async (t) => { + const { db } = t.context; + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + timestampString: timestamp('timestamp_string', { mode: 'string', precision: 6 }).notNull(), + timestampString2: timestamp('timestamp_string2', { precision: 3, mode: 'string' }).notNull(), + timestampDate: timestamp('timestamp_date', { precision: 3 }).notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + timestamp_string timestamp(6) not null, + timestamp_string2 timestamp(3) not null, + timestamp_date timestamp(3) not null + ) + `); + + const timestampString = '2022-01-01 00:00:00.123456'; + const timestampString2 = '2022-01-02 00:00:00.123 -0300'; + const timestampDate = new Date('2022-01-01 00:00:00.123Z'); + + const timestampString_2 = '2022-01-01 00:00:00.123456'; + const timestampString2_2 = '2022-01-01 00:00:00.123 -0300'; + const timestampDate2 = new Date('2022-01-01 00:00:00.123 +0200'); + + await db.insert(table).values([ + { timestampString, timestampString2, timestampDate }, + { timestampString: timestampString_2, timestampString2: timestampString2_2, timestampDate: timestampDate2 }, + ]); + + const result = await db.select().from(table); + const result2 = await db.execute<{ + id: number; + timestamp_string: string; + timestamp_string2: string; + timestamp_date: string; + }>(sql`select * from ${table}`); + + // Whatever you put in, you get back when you're using the date mode + // But when using the string mode, postgres returns a string transformed into UTC + t.deepEqual(result, [ + { + id: 1, + timestampString: timestampString, + timestampString2: '2022-01-02 00:00:00.123', + timestampDate: timestampDate, + }, + { + id: 2, + timestampString: timestampString_2, + timestampString2: '2022-01-01 00:00:00.123', + timestampDate: timestampDate2, + }, + ]); + + t.deepEqual(result2.records, [ + { + id: 1, + timestamp_string: timestampString, + timestamp_string2: '2022-01-02 00:00:00.123', + timestamp_date: timestampDate.toISOString().replace('T', ' ').replace('Z', ''), + }, + { + id: 2, + timestamp_string: timestampString_2, + timestamp_string2: '2022-01-01 00:00:00.123', + timestamp_date: timestampDate2.toISOString().replace('T', ' ').replace('Z', ''), + }, + ]); + + t.deepEqual((result2.records?.[0] as any).timestamp_string, '2022-01-01 00:00:00.123456'); + // need to add the 'Z', otherwise javascript assumes it's in local time + t.deepEqual(new Date((result2.records?.[0] as any).timestamp_date + 'Z' as any).getTime(), timestampDate.getTime()); + + await db.execute(sql`drop table if exists ${table}`); +}); + test.after.always(async (t) => { const ctx = t.context; await ctx.db.execute(sql`drop table "users"`); diff --git a/integration-tests/tests/neon-http.test.ts b/integration-tests/tests/neon-http.test.ts index 5112fd3b1..2fbfa6083 100644 --- a/integration-tests/tests/neon-http.test.ts +++ b/integration-tests/tests/neon-http.test.ts @@ -29,10 +29,12 @@ import { boolean, char, cidr, + date, getMaterializedViewConfig, getViewConfig, inet, integer, + interval, jsonb, macaddr, macaddr8, @@ -44,6 +46,7 @@ import { pgView, serial, text, + time, timestamp, uuid as pgUuid, varchar, @@ -2011,6 +2014,262 @@ test.serial('timestamp timezone', async (t) => { t.assert(Math.abs(users[1]!.createdAt.getTime() - date.getTime()) < 2000); }); +test.serial('all date and time columns', async (t) => { + const { db } = t.context; + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + dateString: date('date_string', { mode: 'string' }).notNull(), + time: time('time', { precision: 3 }).notNull(), + datetime: timestamp('datetime').notNull(), + datetimeWTZ: timestamp('datetime_wtz', { withTimezone: true }).notNull(), + datetimeString: timestamp('datetime_string', { mode: 'string' }).notNull(), + datetimeFullPrecision: timestamp('datetime_full_precision', { precision: 6, mode: 'string' }).notNull(), + datetimeWTZString: timestamp('datetime_wtz_string', { withTimezone: true, mode: 'string' }).notNull(), + interval: interval('interval').notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + date_string date not null, + time time(3) not null, + datetime timestamp not null, + datetime_wtz timestamp with time zone not null, + datetime_string timestamp not null, + datetime_full_precision timestamp(6) not null, + datetime_wtz_string timestamp with time zone not null, + interval interval not null + ) + `); + + const someDatetime = new Date('2022-01-01T00:00:00.123Z'); + const fullPrecision = '2022-01-01T00:00:00.123456'; + const someTime = '23:23:12.432'; + + await db.insert(table).values({ + dateString: '2022-01-01', + time: someTime, + datetime: someDatetime, + datetimeWTZ: someDatetime, + datetimeString: '2022-01-01T00:00:00.123Z', + datetimeFullPrecision: fullPrecision.replace('T', ' ').replace('Z', ''), + datetimeWTZString: '2022-01-01T00:00:00.123Z', + interval: '1 day', + }); + + const result = await db.select().from(table); + + Expect< + Equal<{ + id: number; + dateString: string; + time: string; + datetime: Date; + datetimeWTZ: Date; + datetimeString: string; + datetimeFullPrecision: string; + datetimeWTZString: string; + interval: string; + }[], typeof result> + >; + + Expect< + Equal<{ + dateString: string; + time: string; + datetime: Date; + datetimeWTZ: Date; + datetimeString: string; + datetimeFullPrecision: string; + datetimeWTZString: string; + interval: string; + id?: number | undefined; + }, typeof table.$inferInsert> + >; + + t.deepEqual(result, [ + { + id: 1, + dateString: '2022-01-01', + time: someTime, + datetime: someDatetime, + datetimeWTZ: someDatetime, + datetimeString: '2022-01-01 00:00:00.123', + datetimeFullPrecision: fullPrecision.replace('T', ' '), + datetimeWTZString: '2022-01-01 00:00:00.123+00', + interval: '1 day', + }, + ]); + + await db.execute(sql`drop table if exists ${table}`); +}); + +test.serial('all date and time columns with timezone', async (t) => { + const { db } = t.context; + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + timestamp: timestamp('timestamp_string', { mode: 'string', withTimezone: true, precision: 6 }).notNull(), + timestampAsDate: timestamp('timestamp_date', { withTimezone: true, precision: 3 }).notNull(), + timestampTimeZones: timestamp('timestamp_date_2', { withTimezone: true, precision: 3 }).notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + timestamp_string timestamp(6) with time zone not null, + timestamp_date timestamp(3) with time zone not null, + timestamp_date_2 timestamp(3) with time zone not null + ) + `); + + const timestampString = '2022-01-01 00:00:00.123456-0200'; + const timestampDate = new Date(); + const timestampDateWTZ = new Date('2022-01-01 00:00:00.123 +0500'); + + const timestampString2 = '2022-01-01 00:00:00.123456-0400'; + const timestampDate2 = new Date(); + const timestampDateWTZ2 = new Date('2022-01-01 00:00:00.123 +0200'); + + await db.insert(table).values([ + { timestamp: timestampString, timestampAsDate: timestampDate, timestampTimeZones: timestampDateWTZ }, + { timestamp: timestampString2, timestampAsDate: timestampDate2, timestampTimeZones: timestampDateWTZ2 }, + ]); + + const result = await db.select().from(table); + const result2 = await db.execute<{ + id: number; + timestamp_string: string; + timestamp_date: string; + timestamp_date_2: string; + }>(sql`select * from ${table}`); + + // Whatever you put in, you get back when you're using the date mode + // But when using the string mode, postgres returns a string transformed into UTC + t.deepEqual(result, [ + { + id: 1, + timestamp: '2022-01-01 02:00:00.123456+00', + timestampAsDate: timestampDate, + timestampTimeZones: timestampDateWTZ, + }, + { + id: 2, + timestamp: '2022-01-01 04:00:00.123456+00', + timestampAsDate: timestampDate2, + timestampTimeZones: timestampDateWTZ2, + }, + ]); + + t.deepEqual(result2.rows, [ + { + id: 1, + timestamp_string: '2022-01-01 02:00:00.123456+00', + timestamp_date: timestampDate.toISOString().replace('T', ' ').replace('Z', '') + '+00', + timestamp_date_2: timestampDateWTZ.toISOString().replace('T', ' ').replace('Z', '') + '+00', + }, + { + id: 2, + timestamp_string: '2022-01-01 04:00:00.123456+00', + timestamp_date: timestampDate2.toISOString().replace('T', ' ').replace('Z', '') + '+00', + timestamp_date_2: timestampDateWTZ2.toISOString().replace('T', ' ').replace('Z', '') + '+00', + }, + ]); + + t.deepEqual( + result[0]?.timestampTimeZones.getTime(), + new Date((result2.rows[0] as any).timestamp_date_2 as any).getTime(), + ); + + await db.execute(sql`drop table if exists ${table}`); +}); + +test.serial('all date and time columns without timezone', async (t) => { + const { db } = t.context; + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + timestampString: timestamp('timestamp_string', { mode: 'string', precision: 6 }).notNull(), + timestampString2: timestamp('timestamp_string2', { precision: 3, mode: 'string' }).notNull(), + timestampDate: timestamp('timestamp_date', { precision: 3 }).notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + timestamp_string timestamp(6) not null, + timestamp_string2 timestamp(3) not null, + timestamp_date timestamp(3) not null + ) + `); + + const timestampString = '2022-01-01 00:00:00.123456'; + const timestampString2 = '2022-01-02 00:00:00.123 -0300'; + const timestampDate = new Date('2022-01-01 00:00:00.123Z'); + + const timestampString_2 = '2022-01-01 00:00:00.123456'; + const timestampString2_2 = '2022-01-01 00:00:00.123 -0300'; + const timestampDate2 = new Date('2022-01-01 00:00:00.123 +0200'); + + await db.insert(table).values([ + { timestampString, timestampString2, timestampDate }, + { timestampString: timestampString_2, timestampString2: timestampString2_2, timestampDate: timestampDate2 }, + ]); + + const result = await db.select().from(table); + const result2 = await db.execute<{ + id: number; + timestamp_string: string; + timestamp_string2: string; + timestamp_date: string; + }>(sql`select * from ${table}`); + + // Whatever you put in, you get back when you're using the date mode + // But when using the string mode, postgres returns a string transformed into UTC + t.deepEqual(result, [ + { + id: 1, + timestampString: timestampString, + timestampString2: '2022-01-02 00:00:00.123', + timestampDate: timestampDate, + }, + { + id: 2, + timestampString: timestampString_2, + timestampString2: '2022-01-01 00:00:00.123', + timestampDate: timestampDate2, + }, + ]); + + t.deepEqual(result2.rows, [ + { + id: 1, + timestamp_string: timestampString, + timestamp_string2: '2022-01-02 00:00:00.123', + timestamp_date: timestampDate.toISOString().replace('T', ' ').replace('Z', ''), + }, + { + id: 2, + timestamp_string: timestampString_2, + timestamp_string2: '2022-01-01 00:00:00.123', + timestamp_date: timestampDate2.toISOString().replace('T', ' ').replace('Z', ''), + }, + ]); + + t.deepEqual((result2.rows[0] as any).timestamp_string, '2022-01-01 00:00:00.123456'); + // need to add the 'Z', otherwise javascript assumes it's in local time + t.deepEqual(new Date((result2.rows[0] as any).timestamp_date + 'Z' as any).getTime(), timestampDate.getTime()); + + await db.execute(sql`drop table if exists ${table}`); +}); + test.serial('transaction', async (t) => { const { db } = t.context; diff --git a/integration-tests/tests/pg-proxy.test.ts b/integration-tests/tests/pg-proxy.test.ts index bc494c662..c7e87bed7 100644 --- a/integration-tests/tests/pg-proxy.test.ts +++ b/integration-tests/tests/pg-proxy.test.ts @@ -25,11 +25,13 @@ import { boolean, char, cidr, + date, getMaterializedViewConfig, getTableConfig, getViewConfig, inet, integer, + interval, jsonb, macaddr, macaddr8, @@ -41,6 +43,7 @@ import { pgView, serial, text, + time, timestamp, unique, uniqueKeyName, @@ -58,7 +61,14 @@ import { Expect } from './utils.ts'; // eslint-disable-next-line drizzle/require-entity-kind class ServerSimulator { - constructor(private db: pg.Client) { } + constructor(private db: pg.Client) { + const { types } = pg; + + types.setTypeParser(types.builtins.TIMESTAMPTZ, (val) => val); + types.setTypeParser(types.builtins.TIMESTAMP, (val) => val); + types.setTypeParser(types.builtins.DATE, (val) => val); + types.setTypeParser(types.builtins.INTERVAL, (val) => val); + } async query(sql: string, params: any[], method: 'all' | 'execute') { if (method === 'all') { @@ -1066,7 +1076,7 @@ test.serial('migrator', async (t) => { await t.throwsAsync(async () => { await db.insert(usersMigratorTable).values({ name: 'John', email: 'email' }); }, { - message: 'relation "users12" does not exist' + message: 'relation "users12" does not exist', }); await migrate(db, async (queries) => { @@ -1109,10 +1119,11 @@ test.serial('insert via db.execute + returning', async (t) => { const { db } = t.context; const inserted = await db.execute<{ id: number; name: string }>( - sql`insert into ${usersTable} (${name( - usersTable.name.name, - ) - }) values (${'John'}) returning ${usersTable.id}, ${usersTable.name}`, + sql`insert into ${usersTable} (${ + name( + usersTable.name.name, + ) + }) values (${'John'}) returning ${usersTable.id}, ${usersTable.name}`, ); t.deepEqual(inserted, [{ id: 1, name: 'John' }]); }); @@ -2085,11 +2096,19 @@ test.serial('select from enum', async (t) => { await db.execute(sql`drop type if exists ${name(equipmentEnum.enumName)}`); await db.execute(sql`drop type if exists ${name(categoryEnum.enumName)}`); - await db.execute(sql`create type ${name(muscleEnum.enumName)} as enum ('abdominals', 'hamstrings', 'adductors', 'quadriceps', 'biceps', 'shoulders', 'chest', 'middle_back', 'calves', 'glutes', 'lower_back', 'lats', 'triceps', 'traps', 'forearms', 'neck', 'abductors')`,); + await db.execute( + sql`create type ${ + name(muscleEnum.enumName) + } as enum ('abdominals', 'hamstrings', 'adductors', 'quadriceps', 'biceps', 'shoulders', 'chest', 'middle_back', 'calves', 'glutes', 'lower_back', 'lats', 'triceps', 'traps', 'forearms', 'neck', 'abductors')`, + ); await db.execute(sql`create type ${name(forceEnum.enumName)} as enum ('isometric', 'isotonic', 'isokinetic')`); await db.execute(sql`create type ${name(levelEnum.enumName)} as enum ('beginner', 'intermediate', 'advanced')`); await db.execute(sql`create type ${name(mechanicEnum.enumName)} as enum ('compound', 'isolation')`); - await db.execute(sql`create type ${name(equipmentEnum.enumName)} as enum ('barbell', 'dumbbell', 'bodyweight', 'machine', 'cable', 'kettlebell')`,); + await db.execute( + sql`create type ${ + name(equipmentEnum.enumName) + } as enum ('barbell', 'dumbbell', 'bodyweight', 'machine', 'cable', 'kettlebell')`, + ); await db.execute(sql`create type ${name(categoryEnum.enumName)} as enum ('upper_body', 'lower_body', 'full_body')`); await db.execute(sql` create table ${exercises} ( @@ -2246,6 +2265,276 @@ test.serial('timestamp timezone', async (t) => { t.assert(Math.abs(users[1]!.createdAt.getTime() - date.getTime()) < 2000); }); +test.serial('all date and time columns', async (t) => { + const { db } = t.context; + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + dateString: date('date_string', { mode: 'string' }).notNull(), + time: time('time', { precision: 3 }).notNull(), + datetime: timestamp('datetime').notNull(), + datetimeWTZ: timestamp('datetime_wtz', { withTimezone: true }).notNull(), + datetimeString: timestamp('datetime_string', { mode: 'string' }).notNull(), + datetimeFullPrecision: timestamp('datetime_full_precision', { precision: 6, mode: 'string' }).notNull(), + datetimeWTZString: timestamp('datetime_wtz_string', { withTimezone: true, mode: 'string' }).notNull(), + interval: interval('interval').notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + date_string date not null, + time time(3) not null, + datetime timestamp not null, + datetime_wtz timestamp with time zone not null, + datetime_string timestamp not null, + datetime_full_precision timestamp(6) not null, + datetime_wtz_string timestamp with time zone not null, + interval interval not null + ) + `); + + const someDatetime = new Date('2022-01-01T00:00:00.123Z'); + const fullPrecision = '2022-01-01T00:00:00.123456Z'; + const someTime = '23:23:12.432'; + + await db.insert(table).values({ + dateString: '2022-01-01', + time: someTime, + datetime: someDatetime, + datetimeWTZ: someDatetime, + datetimeString: '2022-01-01T00:00:00.123Z', + datetimeFullPrecision: fullPrecision, + datetimeWTZString: '2022-01-01T00:00:00.123Z', + interval: '1 day', + }); + + const result = await db.select().from(table); + + Expect< + Equal<{ + id: number; + dateString: string; + time: string; + datetime: Date; + datetimeWTZ: Date; + datetimeString: string; + datetimeFullPrecision: string; + datetimeWTZString: string; + interval: string; + }[], typeof result> + >; + + Expect< + Equal<{ + dateString: string; + time: string; + datetime: Date; + datetimeWTZ: Date; + datetimeString: string; + datetimeFullPrecision: string; + datetimeWTZString: string; + interval: string; + id?: number | undefined; + }, typeof table.$inferInsert> + >; + + t.deepEqual(result, [ + { + id: 1, + dateString: '2022-01-01', + time: someTime, + datetime: someDatetime, + datetimeWTZ: someDatetime, + datetimeString: '2022-01-01 00:00:00.123', + datetimeFullPrecision: fullPrecision.replace('T', ' ').replace('Z', ''), + datetimeWTZString: '2022-01-01 00:00:00.123+00', + interval: '1 day', + }, + ]); + + await db.execute(sql`drop table if exists ${table}`); +}); + +test.serial('all date and time columns with timezone second case mode date', async (t) => { + const { db } = t.context; + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + timestamp: timestamp('timestamp_string', { mode: 'date', withTimezone: true, precision: 3 }).notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + timestamp_string timestamp(3) with time zone not null + ) + `); + + const insertedDate = new Date(); + + // 1. Insert date as new date + await db.insert(table).values([ + { timestamp: insertedDate }, + ]); + + // 2, Select as date and check that timezones are the same + // There is no way to check timezone in Date object, as it is always represented internally in UTC + const result = await db.select().from(table); + + t.deepEqual(result, [{ id: 1, timestamp: insertedDate }]); + + // 3. Compare both dates + t.deepEqual(insertedDate.getTime(), result[0]?.timestamp.getTime()); + + await db.execute(sql`drop table if exists ${table}`); +}); + +test.serial('all date and time columns with timezone third case mode date', async (t) => { + const { db } = t.context; + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + timestamp: timestamp('timestamp_string', { mode: 'date', withTimezone: true, precision: 3 }).notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + timestamp_string timestamp(3) with time zone not null + ) + `); + + const insertedDate = new Date('2022-01-01 20:00:00.123-04'); // used different time zones, internally is still UTC + const insertedDate2 = new Date('2022-01-02 04:00:00.123+04'); // They are both the same date in different time zones + + // 1. Insert date as new dates with different time zones + await db.insert(table).values([ + { timestamp: insertedDate }, + { timestamp: insertedDate2 }, + ]); + + // 2, Select and compare both dates + const result = await db.select().from(table); + + t.deepEqual(result[0]?.timestamp.getTime(), result[1]?.timestamp.getTime()); + + await db.execute(sql`drop table if exists ${table}`); +}); + +test.serial('all date and time columns without timezone first case mode string', async (t) => { + const { db } = t.context; + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + timestamp: timestamp('timestamp_string', { mode: 'string', precision: 6 }).notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + timestamp_string timestamp(6) not null + ) + `); + + // 1. Insert date in string format without timezone in it + await db.insert(table).values([ + { timestamp: '2022-01-01 02:00:00.123456' }, + ]); + + // 2, Select in string format and check that values are the same + const result = await db.select().from(table); + + t.deepEqual(result, [{ id: 1, timestamp: '2022-01-01 02:00:00.123456' }]); + + // 3. Select as raw query and check that values are the same + const result2 = await db.execute<{ + id: number; + timestamp_string: string; + }>(sql`select * from ${table}`); + + t.deepEqual([...result2], [{ id: 1, timestamp_string: '2022-01-01 02:00:00.123456' }]); + + await db.execute(sql`drop table if exists ${table}`); +}); + +test.serial('all date and time columns without timezone second case mode string', async (t) => { + const { db } = t.context; + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + timestamp: timestamp('timestamp_string', { mode: 'string', precision: 6 }).notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + timestamp_string timestamp(6) not null + ) + `); + + // 1. Insert date in string format with timezone in it + await db.insert(table).values([ + { timestamp: '2022-01-01T02:00:00.123456-02' }, + ]); + + // 2, Select as raw query and check that values are the same + const result = await db.execute<{ + id: number; + timestamp_string: string; + }>(sql`select * from ${table}`); + + t.deepEqual([...result], [{ id: 1, timestamp_string: '2022-01-01 02:00:00.123456' }]); + + await db.execute(sql`drop table if exists ${table}`); +}); + +test.serial('all date and time columns without timezone third case mode date', async (t) => { + const { db } = t.context; + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + timestamp: timestamp('timestamp_string', { mode: 'date', precision: 3 }).notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + timestamp_string timestamp(3) not null + ) + `); + + const insertedDate = new Date('2022-01-01 20:00:00.123+04'); + + // 1. Insert date as new date + await db.insert(table).values([ + { timestamp: insertedDate }, + ]); + + // 2, Select as raw query as string + const result = await db.execute<{ + id: number; + timestamp_string: string; + }>(sql`select * from ${table}`); + + // 3. Compare both dates using orm mapping - Need to add 'Z' to tell JS that it is UTC + t.deepEqual(new Date(result[0]!.timestamp_string + 'Z').getTime(), insertedDate.getTime()); + + await db.execute(sql`drop table if exists ${table}`); +}); + // TODO: implement transaction // test.serial('transaction', async (t) => { // const { db } = t.context; diff --git a/integration-tests/tests/pg.test.ts b/integration-tests/tests/pg.test.ts index 0d4ff6b76..3b31d7d60 100644 --- a/integration-tests/tests/pg.test.ts +++ b/integration-tests/tests/pg.test.ts @@ -37,6 +37,7 @@ import { boolean, char, cidr, + date, except, exceptAll, foreignKey, @@ -47,9 +48,11 @@ import { integer, intersect, intersectAll, + interval, jsonb, macaddr, macaddr8, + numeric, type PgColumn, pgEnum, pgMaterializedView, @@ -59,6 +62,7 @@ import { primaryKey, serial, text, + time, timestamp, union, unionAll, @@ -66,7 +70,6 @@ import { uniqueKeyName, uuid as pgUuid, varchar, - numeric, } from 'drizzle-orm/pg-core'; import getPort from 'get-port'; import pg from 'pg'; @@ -1663,7 +1666,7 @@ test.serial('with ... select', async (t) => { productUnits: 16, productSales: 160, }, - ]) + ]); }); test.serial('with ... update', async (t) => { @@ -1672,7 +1675,7 @@ test.serial('with ... update', async (t) => { const products = pgTable('products', { id: serial('id').primaryKey(), price: numeric('price').notNull(), - cheap: boolean('cheap').notNull().default(false) + cheap: boolean('cheap').notNull().default(false), }); await db.execute(sql`drop table if exists ${products}`); @@ -1697,26 +1700,26 @@ test.serial('with ... update', async (t) => { .as( db .select({ - value: sql`avg(${products.price})`.as('value') + value: sql`avg(${products.price})`.as('value'), }) - .from(products) + .from(products), ); const result = await db .with(averagePrice) .update(products) .set({ - cheap: true + cheap: true, }) .where(lt(products.price, sql`(select * from ${averagePrice})`)) .returning({ - id: products.id + id: products.id, }); t.deepEqual(result, [ { id: 1 }, { id: 4 }, - { id: 5 } + { id: 5 }, ]); }); @@ -1725,7 +1728,7 @@ test.serial('with ... insert', async (t) => { const users = pgTable('users', { username: text('username').notNull(), - admin: boolean('admin').notNull() + admin: boolean('admin').notNull(), }); await db.execute(sql`drop table if exists ${users}`); @@ -1736,22 +1739,22 @@ test.serial('with ... insert', async (t) => { .as( db .select({ - value: sql`count(*)`.as('value') + value: sql`count(*)`.as('value'), }) - .from(users) + .from(users), ); const result = await db .with(userCount) .insert(users) .values([ - { username: 'user1', admin: sql`((select * from ${userCount}) = 0)` } + { username: 'user1', admin: sql`((select * from ${userCount}) = 0)` }, ]) .returning({ - admin: users.admin + admin: users.admin, }); - t.deepEqual(result, [{ admin: true }]) + t.deepEqual(result, [{ admin: true }]); }); test.serial('with ... delete', async (t) => { @@ -1773,9 +1776,9 @@ test.serial('with ... delete', async (t) => { .as( db .select({ - value: sql`avg(${orders.amount})`.as('value') + value: sql`avg(${orders.amount})`.as('value'), }) - .from(orders) + .from(orders), ); const result = await db @@ -1783,13 +1786,13 @@ test.serial('with ... delete', async (t) => { .delete(orders) .where(gt(orders.amount, sql`(select * from ${averageAmount})`)) .returning({ - id: orders.id + id: orders.id, }); t.deepEqual(result, [ { id: 6 }, { id: 7 }, - { id: 8 } + { id: 8 }, ]); }); @@ -2384,6 +2387,458 @@ test.serial('select from enum', async (t) => { await db.execute(sql`drop type ${name(categoryEnum.enumName)}`); }); +test.serial('all date and time columns', async (t) => { + const { db } = t.context; + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + dateString: date('date_string', { mode: 'string' }).notNull(), + time: time('time', { precision: 3 }).notNull(), + datetime: timestamp('datetime').notNull(), + datetimeWTZ: timestamp('datetime_wtz', { withTimezone: true }).notNull(), + datetimeString: timestamp('datetime_string', { mode: 'string' }).notNull(), + datetimeFullPrecision: timestamp('datetime_full_precision', { precision: 6, mode: 'string' }).notNull(), + datetimeWTZString: timestamp('datetime_wtz_string', { withTimezone: true, mode: 'string' }).notNull(), + interval: interval('interval').notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + date_string date not null, + time time(3) not null, + datetime timestamp not null, + datetime_wtz timestamp with time zone not null, + datetime_string timestamp not null, + datetime_full_precision timestamp(6) not null, + datetime_wtz_string timestamp with time zone not null, + interval interval not null + ) + `); + + const someDatetime = new Date('2022-01-01T00:00:00.123Z'); + const fullPrecision = '2022-01-01T00:00:00.123456Z'; + const someTime = '23:23:12.432'; + + await db.insert(table).values({ + dateString: '2022-01-01', + time: someTime, + datetime: someDatetime, + datetimeWTZ: someDatetime, + datetimeString: '2022-01-01T00:00:00.123Z', + datetimeFullPrecision: fullPrecision, + datetimeWTZString: '2022-01-01T00:00:00.123Z', + interval: '1 day', + }); + + const result = await db.select().from(table); + + Expect< + Equal<{ + id: number; + dateString: string; + time: string; + datetime: Date; + datetimeWTZ: Date; + datetimeString: string; + datetimeFullPrecision: string; + datetimeWTZString: string; + interval: string; + }[], typeof result> + >; + + Expect< + Equal<{ + dateString: string; + time: string; + datetime: Date; + datetimeWTZ: Date; + datetimeString: string; + datetimeFullPrecision: string; + datetimeWTZString: string; + interval: string; + id?: number | undefined; + }, typeof table.$inferInsert> + >; + + t.deepEqual(result, [ + { + id: 1, + dateString: '2022-01-01', + time: someTime, + datetime: someDatetime, + datetimeWTZ: someDatetime, + datetimeString: '2022-01-01 00:00:00.123', + datetimeFullPrecision: fullPrecision.replace('T', ' ').replace('Z', ''), + datetimeWTZString: '2022-01-01 00:00:00.123+00', + interval: '1 day', + }, + ]); + + await db.execute(sql`drop table if exists ${table}`); +}); + +test.serial('all date and time columns with timezone second case mode date', async (t) => { + const { db } = t.context; + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + timestamp: timestamp('timestamp_string', { mode: 'date', withTimezone: true, precision: 3 }).notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + timestamp_string timestamp(3) with time zone not null + ) + `); + + const insertedDate = new Date(); + + // 1. Insert date as new date + await db.insert(table).values([ + { timestamp: insertedDate }, + ]); + + // 2, Select as date and check that timezones are the same + // There is no way to check timezone in Date object, as it is always represented internally in UTC + const result = await db.select().from(table); + + t.deepEqual(result, [{ id: 1, timestamp: insertedDate }]); + + // 3. Compare both dates + t.deepEqual(insertedDate.getTime(), result[0]?.timestamp.getTime()); + + await db.execute(sql`drop table if exists ${table}`); +}); + +test.serial('all date and time columns with timezone third case mode date', async (t) => { + const { db } = t.context; + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + timestamp: timestamp('timestamp_string', { mode: 'date', withTimezone: true, precision: 3 }).notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + timestamp_string timestamp(3) with time zone not null + ) + `); + + const insertedDate = new Date('2022-01-01 20:00:00.123-04'); // used different time zones, internally is still UTC + const insertedDate2 = new Date('2022-01-02 04:00:00.123+04'); // They are both the same date in different time zones + + // 1. Insert date as new dates with different time zones + await db.insert(table).values([ + { timestamp: insertedDate }, + { timestamp: insertedDate2 }, + ]); + + // 2, Select and compare both dates + const result = await db.select().from(table); + + t.deepEqual(result[0]?.timestamp.getTime(), result[1]?.timestamp.getTime()); + + await db.execute(sql`drop table if exists ${table}`); +}); + +test.serial('all date and time columns without timezone first case mode string', async (t) => { + const { db } = t.context; + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + timestamp: timestamp('timestamp_string', { mode: 'string', precision: 6 }).notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + timestamp_string timestamp(6) not null + ) + `); + + // 1. Insert date in string format without timezone in it + await db.insert(table).values([ + { timestamp: '2022-01-01 02:00:00.123456' }, + ]); + + // 2, Select in string format and check that values are the same + const result = await db.select().from(table); + + t.deepEqual(result, [{ id: 1, timestamp: '2022-01-01 02:00:00.123456' }]); + + // 3. Select as raw query and check that values are the same + const result2 = await db.execute<{ + id: number; + timestamp_string: string; + }>(sql`select * from ${table}`); + + t.deepEqual(result2.rows, [{ id: 1, timestamp_string: '2022-01-01 02:00:00.123456' }]); + + await db.execute(sql`drop table if exists ${table}`); +}); + +test.serial('all date and time columns without timezone second case mode string', async (t) => { + const { db } = t.context; + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + timestamp: timestamp('timestamp_string', { mode: 'string', precision: 6 }).notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + timestamp_string timestamp(6) not null + ) + `); + + // 1. Insert date in string format with timezone in it + await db.insert(table).values([ + { timestamp: '2022-01-01T02:00:00.123456-02' }, + ]); + + // 2, Select as raw query and check that values are the same + const result = await db.execute<{ + id: number; + timestamp_string: string; + }>(sql`select * from ${table}`); + + t.deepEqual(result.rows, [{ id: 1, timestamp_string: '2022-01-01 02:00:00.123456' }]); + + await db.execute(sql`drop table if exists ${table}`); +}); + +test.serial('all date and time columns without timezone third case mode date', async (t) => { + const { db } = t.context; + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + timestamp: timestamp('timestamp_string', { mode: 'date', precision: 3 }).notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + timestamp_string timestamp(3) not null + ) + `); + + const insertedDate = new Date('2022-01-01 20:00:00.123+04'); + + // 1. Insert date as new date + await db.insert(table).values([ + { timestamp: insertedDate }, + ]); + + // 2, Select as raw query as string + const result = await db.execute<{ + id: number; + timestamp_string: string; + }>(sql`select * from ${table}`); + + // 3. Compare both dates using orm mapping - Need to add 'Z' to tell JS that it is UTC + t.deepEqual(new Date(result.rows[0]!.timestamp_string + 'Z').getTime(), insertedDate.getTime()); + + await db.execute(sql`drop table if exists ${table}`); +}); + +test.serial('test mode string for timestamp with timezone', async (t) => { + const { db } = t.context; + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + timestamp: timestamp('timestamp_string', { mode: 'string', withTimezone: true, precision: 6 }).notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + timestamp_string timestamp(6) with time zone not null + ) + `); + + const timestampString = '2022-01-01 00:00:00.123456-0200'; + + // 1. Insert date in string format with timezone in it + await db.insert(table).values([ + { timestamp: timestampString }, + ]); + + // 2. Select date in string format and check that the values are the same + const result = await db.select().from(table); + + // 2.1 Notice that postgres will return the date in UTC, but it is exactly the same + t.deepEqual(result, [{ id: 1, timestamp: '2022-01-01 02:00:00.123456+00' }]); + + // 3. Select as raw query and checke that values are the same + const result2 = await db.execute<{ + id: number; + timestamp_string: string; + }>(sql`select * from ${table}`); + + // 3.1 Notice that postgres will return the date in UTC, but it is exactlt the same + t.deepEqual(result2.rows, [{ id: 1, timestamp_string: '2022-01-01 02:00:00.123456+00' }]); + + await db.execute(sql`drop table if exists ${table}`); +}); + +test.serial('test mode date for timestamp with timezone', async (t) => { + const { db } = t.context; + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + timestamp: timestamp('timestamp_string', { mode: 'date', withTimezone: true, precision: 3 }).notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + timestamp_string timestamp(3) with time zone not null + ) + `); + + const timestampString = new Date('2022-01-01 00:00:00.456-0200'); + + // 1. Insert date in string format with timezone in it + await db.insert(table).values([ + { timestamp: timestampString }, + ]); + + // 2. Select date in string format and check that the values are the same + const result = await db.select().from(table); + + // 2.1 Notice that postgres will return the date in UTC, but it is exactly the same + t.deepEqual(result, [{ id: 1, timestamp: timestampString }]); + + // 3. Select as raw query and checke that values are the same + const result2 = await db.execute<{ + id: number; + timestamp_string: string; + }>(sql`select * from ${table}`); + + // 3.1 Notice that postgres will return the date in UTC, but it is exactlt the same + t.deepEqual(result2.rows, [{ id: 1, timestamp_string: '2022-01-01 02:00:00.456+00' }]); + + await db.execute(sql`drop table if exists ${table}`); +}); + +test.serial('test mode string for timestamp with timezone in UTC timezone', async (t) => { + const { db } = t.context; + + // get current timezone from db + const timezone = await db.execute<{ TimeZone: string }>(sql`show timezone`); + + // set timezone to UTC + await db.execute(sql`set time zone 'UTC'`); + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + timestamp: timestamp('timestamp_string', { mode: 'string', withTimezone: true, precision: 6 }).notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + timestamp_string timestamp(6) with time zone not null + ) + `); + + const timestampString = '2022-01-01 00:00:00.123456-0200'; + + // 1. Insert date in string format with timezone in it + await db.insert(table).values([ + { timestamp: timestampString }, + ]); + + // 2. Select date in string format and check that the values are the same + const result = await db.select().from(table); + + // 2.1 Notice that postgres will return the date in UTC, but it is exactly the same + t.deepEqual(result, [{ id: 1, timestamp: '2022-01-01 02:00:00.123456+00' }]); + + // 3. Select as raw query and checke that values are the same + const result2 = await db.execute<{ + id: number; + timestamp_string: string; + }>(sql`select * from ${table}`); + + // 3.1 Notice that postgres will return the date in UTC, but it is exactlt the same + t.deepEqual(result2.rows, [{ id: 1, timestamp_string: '2022-01-01 02:00:00.123456+00' }]); + + await db.execute(sql`set time zone '${sql.raw(timezone.rows[0]!.TimeZone)}'`); + + await db.execute(sql`drop table if exists ${table}`); +}); + +test.serial('test mode string for timestamp with timezone in different timezone', async (t) => { + const { db } = t.context; + + // get current timezone from db + const timezone = await db.execute<{ TimeZone: string }>(sql`show timezone`); + + // set timezone to HST (UTC - 10) + await db.execute(sql`set time zone 'HST'`); + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + timestamp: timestamp('timestamp_string', { mode: 'string', withTimezone: true, precision: 6 }).notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + timestamp_string timestamp(6) with time zone not null + ) + `); + + const timestampString = '2022-01-01 00:00:00.123456-1000'; + + // 1. Insert date in string format with timezone in it + await db.insert(table).values([ + { timestamp: timestampString }, + ]); + + // 2. Select date in string format and check that the values are the same + const result = await db.select().from(table); + + t.deepEqual(result, [{ id: 1, timestamp: '2022-01-01 00:00:00.123456-10' }]); + + // 3. Select as raw query and checke that values are the same + const result2 = await db.execute<{ + id: number; + timestamp_string: string; + }>(sql`select * from ${table}`); + + t.deepEqual(result2.rows, [{ id: 1, timestamp_string: '2022-01-01 00:00:00.123456-10' }]); + + await db.execute(sql`set time zone '${sql.raw(timezone.rows[0]!.TimeZone)}'`); + + await db.execute(sql`drop table if exists ${table}`); +}); + test.serial('orderBy with aliased column', (t) => { const { db } = t.context; diff --git a/integration-tests/tests/postgres.js.test.ts b/integration-tests/tests/postgres.js.test.ts index 50424c71d..0fd0c45ea 100644 --- a/integration-tests/tests/postgres.js.test.ts +++ b/integration-tests/tests/postgres.js.test.ts @@ -26,9 +26,11 @@ import { import { alias, boolean, + date, getMaterializedViewConfig, getViewConfig, integer, + interval, jsonb, type PgColumn, pgEnum, @@ -38,6 +40,7 @@ import { pgView, serial, text, + time, timestamp, uuid as pgUuid, varchar, @@ -1864,6 +1867,500 @@ test.serial('select from sql', async (t) => { // beta }); +test.serial('timestamp timezone', async (t) => { + const { db } = t.context; + + const usersTableWithAndWithoutTimezone = pgTable('users_test_with_and_without_timezone', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: false }).notNull().defaultNow(), + }); + + await db.execute(sql`drop table if exists ${usersTableWithAndWithoutTimezone}`); + + await db.execute( + sql` + create table users_test_with_and_without_timezone ( + id serial not null primary key, + name text not null, + created_at timestamptz not null default now(), + updated_at timestamp not null default now() + ) + `, + ); + + const date = new Date(Date.parse('2020-01-01T00:00:00+04:00')); + + await db.insert(usersTableWithAndWithoutTimezone).values({ name: 'With default times' }); + await db.insert(usersTableWithAndWithoutTimezone).values({ + name: 'Without default times', + createdAt: date, + updatedAt: date, + }); + const users = await db.select().from(usersTableWithAndWithoutTimezone); + + // check that the timestamps are set correctly for default times + t.assert(Math.abs(users[0]!.updatedAt.getTime() - Date.now()) < 2000); + t.assert(Math.abs(users[0]!.createdAt.getTime() - Date.now()) < 2000); + + // check that the timestamps are set correctly for non default times + t.assert(Math.abs(users[1]!.updatedAt.getTime() - date.getTime()) < 2000); + t.assert(Math.abs(users[1]!.createdAt.getTime() - date.getTime()) < 2000); +}); + +test.serial('all date and time columns', async (t) => { + const { db } = t.context; + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + dateString: date('date_string', { mode: 'string' }).notNull(), + time: time('time', { precision: 3 }).notNull(), + datetime: timestamp('datetime').notNull(), + datetimeWTZ: timestamp('datetime_wtz', { withTimezone: true }).notNull(), + datetimeString: timestamp('datetime_string', { mode: 'string' }).notNull(), + datetimeFullPrecision: timestamp('datetime_full_precision', { precision: 6, mode: 'string' }).notNull(), + datetimeWTZString: timestamp('datetime_wtz_string', { withTimezone: true, mode: 'string' }).notNull(), + interval: interval('interval').notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + date_string date not null, + time time(3) not null, + datetime timestamp not null, + datetime_wtz timestamp with time zone not null, + datetime_string timestamp not null, + datetime_full_precision timestamp(6) not null, + datetime_wtz_string timestamp with time zone not null, + interval interval not null + ) + `); + + const someDatetime = new Date('2022-01-01T00:00:00.123Z'); + const fullPrecision = '2022-01-01T00:00:00.123456Z'; + const someTime = '23:23:12.432'; + + await db.insert(table).values({ + dateString: '2022-01-01', + time: someTime, + datetime: someDatetime, + datetimeWTZ: someDatetime, + datetimeString: '2022-01-01T00:00:00.123Z', + datetimeFullPrecision: fullPrecision, + datetimeWTZString: '2022-01-01T00:00:00.123Z', + interval: '1 day', + }); + + const result = await db.select().from(table); + + Expect< + Equal<{ + id: number; + dateString: string; + time: string; + datetime: Date; + datetimeWTZ: Date; + datetimeString: string; + datetimeFullPrecision: string; + datetimeWTZString: string; + interval: string; + }[], typeof result> + >; + + Expect< + Equal<{ + dateString: string; + time: string; + datetime: Date; + datetimeWTZ: Date; + datetimeString: string; + datetimeFullPrecision: string; + datetimeWTZString: string; + interval: string; + id?: number | undefined; + }, typeof table.$inferInsert> + >; + + t.deepEqual(result, [ + { + id: 1, + dateString: '2022-01-01', + time: someTime, + datetime: someDatetime, + datetimeWTZ: someDatetime, + datetimeString: '2022-01-01 00:00:00.123', + datetimeFullPrecision: fullPrecision.replace('T', ' ').replace('Z', ''), + datetimeWTZString: '2022-01-01 00:00:00.123+00', + interval: '1 day', + }, + ]); + + await db.execute(sql`drop table if exists ${table}`); +}); + +test.serial('all date and time columns with timezone second case mode date', async (t) => { + const { db } = t.context; + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + timestamp: timestamp('timestamp_string', { mode: 'date', withTimezone: true, precision: 3 }).notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + timestamp_string timestamp(3) with time zone not null + ) + `); + + const insertedDate = new Date(); + + // 1. Insert date as new date + await db.insert(table).values([ + { timestamp: insertedDate }, + ]); + + // 2, Select as date and check that timezones are the same + // There is no way to check timezone in Date object, as it is always represented internally in UTC + const result = await db.select().from(table); + + t.deepEqual(result, [{ id: 1, timestamp: insertedDate }]); + + // 3. Compare both dates + t.deepEqual(insertedDate.getTime(), result[0]?.timestamp.getTime()); + + await db.execute(sql`drop table if exists ${table}`); +}); + +test.serial('all date and time columns with timezone third case mode date', async (t) => { + const { db } = t.context; + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + timestamp: timestamp('timestamp_string', { mode: 'date', withTimezone: true, precision: 3 }).notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + timestamp_string timestamp(3) with time zone not null + ) + `); + + const insertedDate = new Date('2022-01-01 20:00:00.123-04'); // used different time zones, internally is still UTC + const insertedDate2 = new Date('2022-01-02 04:00:00.123+04'); // They are both the same date in different time zones + + // 1. Insert date as new dates with different time zones + await db.insert(table).values([ + { timestamp: insertedDate }, + { timestamp: insertedDate2 }, + ]); + + // 2, Select and compare both dates + const result = await db.select().from(table); + + t.deepEqual(result[0]?.timestamp.getTime(), result[1]?.timestamp.getTime()); + + await db.execute(sql`drop table if exists ${table}`); +}); + +test.serial('all date and time columns without timezone first case mode string', async (t) => { + const { db } = t.context; + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + timestamp: timestamp('timestamp_string', { mode: 'string', precision: 6 }).notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + timestamp_string timestamp(6) not null + ) + `); + + // 1. Insert date in string format without timezone in it + await db.insert(table).values([ + { timestamp: '2022-01-01 02:00:00.123456' }, + ]); + + // 2, Select in string format and check that values are the same + const result = await db.select().from(table); + + t.deepEqual(result, [{ id: 1, timestamp: '2022-01-01 02:00:00.123456' }]); + + // 3. Select as raw query and check that values are the same + const result2 = await db.execute<{ + id: number; + timestamp_string: string; + }>(sql`select * from ${table}`); + + t.deepEqual([...result2], [{ id: 1, timestamp_string: '2022-01-01 02:00:00.123456' }]); + + await db.execute(sql`drop table if exists ${table}`); +}); + +test.serial('all date and time columns without timezone second case mode string', async (t) => { + const { db } = t.context; + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + timestamp: timestamp('timestamp_string', { mode: 'string', precision: 6 }).notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + timestamp_string timestamp(6) not null + ) + `); + + // 1. Insert date in string format with timezone in it + await db.insert(table).values([ + { timestamp: '2022-01-01T02:00:00.123456-02' }, + ]); + + // 2, Select as raw query and check that values are the same + const result = await db.execute<{ + id: number; + timestamp_string: string; + }>(sql`select * from ${table}`); + + t.deepEqual([...result], [{ id: 1, timestamp_string: '2022-01-01 02:00:00.123456' }]); + + await db.execute(sql`drop table if exists ${table}`); +}); + +test.serial('all date and time columns without timezone third case mode date', async (t) => { + const { db } = t.context; + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + timestamp: timestamp('timestamp_string', { mode: 'date', precision: 3 }).notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + timestamp_string timestamp(3) not null + ) + `); + + const insertedDate = new Date('2022-01-01 20:00:00.123+04'); + + // 1. Insert date as new date + await db.insert(table).values([ + { timestamp: insertedDate }, + ]); + + // 2, Select as raw query as string + const result = await db.execute<{ + id: number; + timestamp_string: string; + }>(sql`select * from ${table}`); + + // 3. Compare both dates using orm mapping - Need to add 'Z' to tell JS that it is UTC + t.deepEqual(new Date(result[0]!.timestamp_string + 'Z').getTime(), insertedDate.getTime()); + + await db.execute(sql`drop table if exists ${table}`); +}); + +test.serial('test mode string for timestamp with timezone', async (t) => { + const { db } = t.context; + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + timestamp: timestamp('timestamp_string', { mode: 'string', withTimezone: true, precision: 6 }).notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + timestamp_string timestamp(6) with time zone not null + ) + `); + + const timestampString = '2022-01-01 00:00:00.123456-0200'; + + // 1. Insert date in string format with timezone in it + await db.insert(table).values([ + { timestamp: timestampString }, + ]); + + // 2. Select date in string format and check that the values are the same + const result = await db.select().from(table); + + // 2.1 Notice that postgres will return the date in UTC, but it is exactly the same + t.deepEqual(result, [{ id: 1, timestamp: '2022-01-01 02:00:00.123456+00' }]); + + // 3. Select as raw query and checke that values are the same + const result2 = await db.execute<{ + id: number; + timestamp_string: string; + }>(sql`select * from ${table}`); + + // 3.1 Notice that postgres will return the date in UTC, but it is exactlt the same + t.deepEqual([...result2], [{ id: 1, timestamp_string: '2022-01-01 02:00:00.123456+00' }]); + + await db.execute(sql`drop table if exists ${table}`); +}); + +test.serial('test mode date for timestamp with timezone', async (t) => { + const { db } = t.context; + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + timestamp: timestamp('timestamp_string', { mode: 'date', withTimezone: true, precision: 3 }).notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + timestamp_string timestamp(3) with time zone not null + ) + `); + + const timestampString = new Date('2022-01-01 00:00:00.456-0200'); + + // 1. Insert date in string format with timezone in it + await db.insert(table).values([ + { timestamp: timestampString }, + ]); + + // 2. Select date in string format and check that the values are the same + const result = await db.select().from(table); + + // 2.1 Notice that postgres will return the date in UTC, but it is exactly the same + t.deepEqual(result, [{ id: 1, timestamp: timestampString }]); + + // 3. Select as raw query and checke that values are the same + const result2 = await db.execute<{ + id: number; + timestamp_string: string; + }>(sql`select * from ${table}`); + + // 3.1 Notice that postgres will return the date in UTC, but it is exactlt the same + t.deepEqual([...result2], [{ id: 1, timestamp_string: '2022-01-01 02:00:00.456+00' }]); + + await db.execute(sql`drop table if exists ${table}`); +}); + +test.serial('test mode string for timestamp with timezone in UTC timezone', async (t) => { + const { db } = t.context; + + // get current timezone from db + const [timezone] = await db.execute<{ TimeZone: string }>(sql`show timezone`); + + // set timezone to UTC + await db.execute(sql`set time zone 'UTC'`); + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + timestamp: timestamp('timestamp_string', { mode: 'string', withTimezone: true, precision: 6 }).notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + timestamp_string timestamp(6) with time zone not null + ) + `); + + const timestampString = '2022-01-01 00:00:00.123456-0200'; + + // 1. Insert date in string format with timezone in it + await db.insert(table).values([ + { timestamp: timestampString }, + ]); + + // 2. Select date in string format and check that the values are the same + const result = await db.select().from(table); + + // 2.1 Notice that postgres will return the date in UTC, but it is exactly the same + t.deepEqual(result, [{ id: 1, timestamp: '2022-01-01 02:00:00.123456+00' }]); + + // 3. Select as raw query and checke that values are the same + const result2 = await db.execute<{ + id: number; + timestamp_string: string; + }>(sql`select * from ${table}`); + + // 3.1 Notice that postgres will return the date in UTC, but it is exactlt the same + t.deepEqual([...result2], [{ id: 1, timestamp_string: '2022-01-01 02:00:00.123456+00' }]); + + await db.execute(sql`set time zone '${sql.raw(timezone!.TimeZone)}'`); + + await db.execute(sql`drop table if exists ${table}`); +}); + +test.serial('test mode string for timestamp with timezone in different timezone', async (t) => { + const { db } = t.context; + + // get current timezone from db + const [timezone] = await db.execute<{ TimeZone: string }>(sql`show timezone`); + + // set timezone to HST (UTC - 10) + await db.execute(sql`set time zone 'HST'`); + + const table = pgTable('all_columns', { + id: serial('id').primaryKey(), + timestamp: timestamp('timestamp_string', { mode: 'string', withTimezone: true, precision: 6 }).notNull(), + }); + + await db.execute(sql`drop table if exists ${table}`); + + await db.execute(sql` + create table ${table} ( + id serial primary key, + timestamp_string timestamp(6) with time zone not null + ) + `); + + const timestampString = '2022-01-01 00:00:00.123456-1000'; + + // 1. Insert date in string format with timezone in it + await db.insert(table).values([ + { timestamp: timestampString }, + ]); + + // 2. Select date in string format and check that the values are the same + const result = await db.select().from(table); + + t.deepEqual(result, [{ id: 1, timestamp: '2022-01-01 00:00:00.123456-10' }]); + + // 3. Select as raw query and checke that values are the same + const result2 = await db.execute<{ + id: number; + timestamp_string: string; + }>(sql`select * from ${table}`); + + t.deepEqual([...result2], [{ id: 1, timestamp_string: '2022-01-01 00:00:00.123456-10' }]); + + await db.execute(sql`set time zone '${sql.raw(timezone!.TimeZone)}'`); + + await db.execute(sql`drop table if exists ${table}`); +}); + test.serial('transaction', async (t) => { const { db } = t.context;