diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index 846323470..000000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,22 +0,0 @@ -module.exports = { - root: true, - extends: ['plugin:@typescript-eslint/base'], - parser: '@typescript-eslint/parser', - plugins: ['import', 'unused-imports'], - rules: { - '@typescript-eslint/consistent-type-imports': [ - 'error', - { - disallowTypeAnnotations: false, - fixStyle: 'inline-type-imports', - }, - ], - 'import/no-cycle': 'error', - 'import/no-self-import': 'error', - 'import/no-empty-named-blocks': 'error', - 'unused-imports/no-unused-imports': 'error', - 'import/no-useless-path-segments': 'error', - 'import/newline-after-import': 'error', - 'import/no-duplicates': 'error', - }, -}; diff --git a/.eslintrc.yaml b/.eslintrc.yaml new file mode 100644 index 000000000..26abdb39c --- /dev/null +++ b/.eslintrc.yaml @@ -0,0 +1,19 @@ +root: true +extends: + - 'plugin:@typescript-eslint/base' +parser: '@typescript-eslint/parser' +plugins: + - import + - unused-imports +rules: + '@typescript-eslint/consistent-type-imports': + - error + - disallowTypeAnnotations: false + fixStyle: inline-type-imports + import/no-cycle: error + import/no-self-import: error + import/no-empty-named-blocks: error + unused-imports/no-unused-imports: error + import/no-useless-path-segments: error + import/newline-after-import: error + import/no-duplicates: error diff --git a/.prettierrc.js b/.prettierrc.js deleted file mode 100644 index 34b31db14..000000000 --- a/.prettierrc.js +++ /dev/null @@ -1,36 +0,0 @@ -module.exports = { - useTabs: true, - tabWidth: 4, - singleQuote: true, - arrowParens: 'always', - trailingComma: 'all', - semi: true, - printWidth: 100, - bracketSpacing: true, - importOrder: [ - '^source-map-support', - '', - '^~\\/', - '^(\\.\\.?|@\\/)(.*\\/[^.\\/]+|\\/?)$', - '\\.[^.\\/]+$', - ], - // require.resolve is required (pun unintended) because of pnpm - may be fixed in the future by Prettier team - plugins: [require.resolve('@trivago/prettier-plugin-sort-imports')], - importOrderSeparation: true, - overrides: [ - { - files: ['**/*.yml', '**/*.yaml'], - options: { - useTabs: false, - tabWidth: 2, - }, - }, - { - files: ['**/*.md'], - options: { - useTabs: true, - tabWidth: 2, - }, - }, - ], -}; diff --git a/README.md b/README.md index 509f6be1e..81149c2da 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ export const users = pgTable('users', { id: serial('id').primaryKey(), fullName: text('full_name').notNull(), phone: varchar('phone', { length: 20 }).notNull(), - role: text<'user' | 'admin'>('role').default('user').notNull(), + role: text('role', { enum: ['user', 'admin'] }).default('user').notNull(), cityId: integer('city_id').references(() => cities.id), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), diff --git a/changelogs/drizzle-orm/0.23.6.md b/changelogs/drizzle-orm/0.23.6.md new file mode 100644 index 000000000..fcc5a92e5 --- /dev/null +++ b/changelogs/drizzle-orm/0.23.6.md @@ -0,0 +1,6 @@ +- 🐛 Fixed referencing the selected aliased field in the same query +- 🐛 Fixed decimal column data type in MySQL +- 🐛 Fixed mode autocompletion for integer column in SQLite +- 🐛 Fixed extra parentheses in the generated SQL for the `IN` operator (#382) +- 🐛 Fixed regression in `pgEnum.enumValues` type (#358) +- 🎉 Allowed readonly arrays to be passed to `pgEnum` diff --git a/changelogs/drizzle-orm/0.23.7.md b/changelogs/drizzle-orm/0.23.7.md new file mode 100644 index 000000000..0af0ea478 --- /dev/null +++ b/changelogs/drizzle-orm/0.23.7.md @@ -0,0 +1 @@ +- 🎉 Added `INSERT IGNORE` support for MySQL (#305) diff --git a/changelogs/drizzle-orm/0.23.8.md b/changelogs/drizzle-orm/0.23.8.md new file mode 100644 index 000000000..0f9e7bec1 --- /dev/null +++ b/changelogs/drizzle-orm/0.23.8.md @@ -0,0 +1 @@ +- 🎉 Fixed dates timezone differences for timestamps in Postgres and MySQL (contributed by @AppelBoomHD via #288) diff --git a/changelogs/drizzle-zod/0.2.1.md b/changelogs/drizzle-zod/0.2.1.md new file mode 100644 index 000000000..bdc9f26b2 --- /dev/null +++ b/changelogs/drizzle-zod/0.2.1.md @@ -0,0 +1 @@ +- 🐛 Fix insert schemas generation diff --git a/drizzle-orm/package.json b/drizzle-orm/package.json index 4a1bc2acf..cd3182515 100644 --- a/drizzle-orm/package.json +++ b/drizzle-orm/package.json @@ -1,6 +1,6 @@ { "name": "drizzle-orm", - "version": "0.23.5", + "version": "0.23.8", "description": "Drizzle ORM package for SQL databases", "scripts": { "build": "tsc && resolve-tspaths && cp ../README.md package.json dist/", @@ -126,6 +126,7 @@ "sql.js": "^1.8.0", "sqlite3": "^5.1.2", "vite-tsconfig-paths": "^4.0.7", - "vitest": "^0.29.8" + "vitest": "^0.29.8", + "zod": "^3.20.2" } } diff --git a/drizzle-orm/src/mysql-core/columns/decimal.ts b/drizzle-orm/src/mysql-core/columns/decimal.ts index 28b88f446..99cd5a558 100644 --- a/drizzle-orm/src/mysql-core/columns/decimal.ts +++ b/drizzle-orm/src/mysql-core/columns/decimal.ts @@ -15,8 +15,8 @@ export interface MySqlDecimalHKT extends ColumnHKTBase { export type MySqlDecimalBuilderInitial = MySqlDecimalBuilder<{ name: TName; - data: number; - driverParam: number | string; + data: string; + driverParam: string; notNull: false; hasDefault: false; }>; diff --git a/drizzle-orm/src/mysql-core/columns/enum.ts b/drizzle-orm/src/mysql-core/columns/enum.ts index 52aacfb24..c51d90fa8 100644 --- a/drizzle-orm/src/mysql-core/columns/enum.ts +++ b/drizzle-orm/src/mysql-core/columns/enum.ts @@ -42,9 +42,9 @@ export class MySqlEnumColumnBuilder } export class MySqlEnumColumn - extends MySqlColumn + extends MySqlColumn { - readonly values: string[] = this.config.values; + readonly values: readonly string[] = this.config.values; getSQLType(): string { return `enum(${this.values.map((value) => `'${value}'`).join(',')})`; @@ -53,8 +53,9 @@ export class MySqlEnumColumn export function mysqlEnum>( name: TName, - values: Writable, -): MySqlEnumColumnBuilderInitial> { + values: T | Writable, +): MySqlEnumColumnBuilderInitial>; +export function mysqlEnum(name: string, values: string[]) { if (values.length === 0) throw Error(`You have an empty array for "${name}" enum values`); return new MySqlEnumColumnBuilder(name, values); diff --git a/drizzle-orm/src/mysql-core/columns/timestamp.ts b/drizzle-orm/src/mysql-core/columns/timestamp.ts index 3e379a5cf..8c39a5b41 100644 --- a/drizzle-orm/src/mysql-core/columns/timestamp.ts +++ b/drizzle-orm/src/mysql-core/columns/timestamp.ts @@ -50,7 +50,11 @@ export class MySqlTimestamp } override mapFromDriverValue(value: string): Date { - return new Date(value); + return new Date(value + '+0000'); + } + + override mapToDriverValue(value: Date): string { + return value.toISOString().slice(0, 19).replace('T', ' '); } } diff --git a/drizzle-orm/src/mysql-core/db.ts b/drizzle-orm/src/mysql-core/db.ts index 815ea2d62..e8ee170ac 100644 --- a/drizzle-orm/src/mysql-core/db.ts +++ b/drizzle-orm/src/mysql-core/db.ts @@ -41,7 +41,7 @@ export class MySqlDatabase { return new Proxy( new WithSubquery(qb.getSQL(), qb.getSelectedFields() as SelectedFields, alias, true), - new SelectionProxyHandler({ alias, sqlAliasedBehavior: 'subquery_selection', sqlBehavior: 'error' }), + new SelectionProxyHandler({ alias, sqlAliasedBehavior: 'alias', sqlBehavior: 'error' }), ) as WithSubqueryWithSelection; }, }; diff --git a/drizzle-orm/src/mysql-core/dialect.ts b/drizzle-orm/src/mysql-core/dialect.ts index 5b2c214c8..81d33e6f2 100644 --- a/drizzle-orm/src/mysql-core/dialect.ts +++ b/drizzle-orm/src/mysql-core/dialect.ts @@ -292,7 +292,7 @@ export class MySqlDialect { return sql`${withSql}select ${selection} from ${table}${joinsSql}${whereSql}${groupBySql}${havingSql}${orderBySql}${limitSql}${offsetSql}${lockingClausesSql}`; } - buildInsertQuery({ table, values, onConflict, returning }: MySqlInsertConfig): SQL { + buildInsertQuery({ table, values, ignore, onConflict, returning }: MySqlInsertConfig): SQL { const isSingleValue = values.length === 1; const valuesSqlList: ((SQLChunk | SQL)[] | SQL)[] = []; const columns: Record = table[Table.Symbol.Columns]; @@ -319,13 +319,15 @@ export class MySqlDialect { const valuesSql = sql.fromList(valuesSqlList); + const ignoreSql = ignore ? sql` ignore` : undefined; + const returningSql = returning ? sql` returning ${this.buildSelection(returning, { isSingleTable: true })}` : undefined; const onConflictSql = onConflict ? sql` on duplicate key ${onConflict}` : undefined; - return sql`insert into ${table} ${insertOrder} values ${valuesSql}${onConflictSql}`; + return sql`insert${ignoreSql} into ${table} ${insertOrder} values ${valuesSql}${onConflictSql}`; } sqlToQuery(sql: SQL): Query { diff --git a/drizzle-orm/src/mysql-core/query-builders/insert.ts b/drizzle-orm/src/mysql-core/query-builders/insert.ts index e769eb5f5..a37094a9f 100644 --- a/drizzle-orm/src/mysql-core/query-builders/insert.ts +++ b/drizzle-orm/src/mysql-core/query-builders/insert.ts @@ -19,6 +19,7 @@ import type { MySqlUpdateSetSource } from './update'; export interface MySqlInsertConfig { table: TTable; values: Record[]; + ignore: boolean; onConflict?: SQL; returning?: SelectedFieldsOrdered; } @@ -32,12 +33,19 @@ export type MySqlInsertValue = Simplify< >; export class MySqlInsertBuilder { + private shouldIgnore: boolean = false; + constructor( private table: TTable, private session: MySqlSession, private dialect: MySqlDialect, ) {} + ignore(): this { + this.shouldIgnore = true; + return this; + } + values(value: MySqlInsertValue): MySqlInsert; values(values: MySqlInsertValue[]): MySqlInsert; /** @@ -68,7 +76,7 @@ export class MySqlInsertBuilder, TAlias> { return new Proxy( new Subquery(this.getSQL(), this.config.fields, alias), - new SelectionProxyHandler({ alias, sqlAliasedBehavior: 'subquery_selection', sqlBehavior: 'error' }), + new SelectionProxyHandler({ alias, sqlAliasedBehavior: 'alias', sqlBehavior: 'error' }), ) as SubqueryWithSelection, TAlias>; } } diff --git a/drizzle-orm/src/pg-core/README.md b/drizzle-orm/src/pg-core/README.md index d82c4ff67..ce19b69e7 100644 --- a/drizzle-orm/src/pg-core/README.md +++ b/drizzle-orm/src/pg-core/README.md @@ -382,7 +382,7 @@ const publicUsersTable = pgTable('users', { id: serial('id').primaryKey(), name: text('name').notNull(), verified: boolean('verified').notNull().default(false), - jsonb: jsonb('jsonb'), + jsonb: jsonb('jsonb').$type(), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), }); @@ -394,7 +394,7 @@ const usersTable = mySchema.table('users', { id: serial('id').primaryKey(), name: text('name').notNull(), verified: boolean('verified').notNull().default(false), - jsonb: jsonb('jsonb'), + jsonb: jsonb('jsonb').$type(), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), }); ``` @@ -639,7 +639,7 @@ await db.update(users) .set({ name: 'Mr. Dan' }) .where(eq(users.name, 'Dan')); -const updatedUser: InferModel = await db.delete(users) +const updatedUser: InferModel = await db.update(users) .set({ name: 'Mr. Dan' }) .where(eq(users.name, 'Dan')) .returning(); diff --git a/drizzle-orm/src/pg-core/columns/enum.ts b/drizzle-orm/src/pg-core/columns/enum.ts index 44ec05cab..36c9a7838 100644 --- a/drizzle-orm/src/pg-core/columns/enum.ts +++ b/drizzle-orm/src/pg-core/columns/enum.ts @@ -27,7 +27,7 @@ export interface PgEnum { (name: TName): PgEnumColumnBuilderInitial; readonly enumName: string; - readonly enumValues: string[]; + readonly enumValues: TValues; /** @internal */ [isPgEnumSym]: true; } @@ -71,7 +71,7 @@ export class PgEnumColumn extends PgColumn>( enumName: string, - values: Writable, + values: T | Writable, ): PgEnum> { const enumInstance = Object.assign( (name: TName): PgEnumColumnBuilderInitial> => diff --git a/drizzle-orm/src/pg-core/columns/timestamp.ts b/drizzle-orm/src/pg-core/columns/timestamp.ts index 0c139e534..98ec54ce3 100644 --- a/drizzle-orm/src/pg-core/columns/timestamp.ts +++ b/drizzle-orm/src/pg-core/columns/timestamp.ts @@ -61,7 +61,11 @@ export class PgTimestamp extends PgColumn { - return new Date(value); + return new Date(this.withTimezone ? value : value + '+0000'); + }; + + override mapToDriverValue = (value: Date): string => { + return this.withTimezone ? value.toUTCString() : value.toISOString(); }; } diff --git a/drizzle-orm/src/pg-core/db.ts b/drizzle-orm/src/pg-core/db.ts index 4599cfa30..adde7abfe 100644 --- a/drizzle-orm/src/pg-core/db.ts +++ b/drizzle-orm/src/pg-core/db.ts @@ -30,7 +30,7 @@ export class PgDatabase { return new Proxy( new WithSubquery(qb.getSQL(), qb.getSelectedFields() as SelectedFields, alias, true), - new SelectionProxyHandler({ alias, sqlAliasedBehavior: 'subquery_selection', sqlBehavior: 'error' }), + new SelectionProxyHandler({ alias, sqlAliasedBehavior: 'alias', sqlBehavior: 'error' }), ) as WithSubqueryWithSelection; }, }; diff --git a/drizzle-orm/src/pg-core/query-builders/select.ts b/drizzle-orm/src/pg-core/query-builders/select.ts index 2d7c021ce..72de4bd87 100644 --- a/drizzle-orm/src/pg-core/query-builders/select.ts +++ b/drizzle-orm/src/pg-core/query-builders/select.ts @@ -175,7 +175,7 @@ export abstract class PgSelectQueryBuilder< on = on( new Proxy( this.config.fields, - new SelectionProxyHandler({ sqlAliasedBehavior: 'alias', sqlBehavior: 'sql' }), + new SelectionProxyHandler({ sqlAliasedBehavior: 'sql', sqlBehavior: 'sql' }), ) as TSelection, ); } @@ -315,7 +315,7 @@ export abstract class PgSelectQueryBuilder< ): SubqueryWithSelection, TAlias> { return new Proxy( new Subquery(this.getSQL(), this.config.fields, alias), - new SelectionProxyHandler({ alias, sqlAliasedBehavior: 'subquery_selection', sqlBehavior: 'error' }), + new SelectionProxyHandler({ alias, sqlAliasedBehavior: 'alias', sqlBehavior: 'error' }), ) as SubqueryWithSelection, TAlias>; } } diff --git a/drizzle-orm/src/sql/expressions/conditions.ts b/drizzle-orm/src/sql/expressions/conditions.ts index 19f4ab6fd..432757f30 100644 --- a/drizzle-orm/src/sql/expressions/conditions.ts +++ b/drizzle-orm/src/sql/expressions/conditions.ts @@ -172,7 +172,7 @@ export function inArray( if (values.length === 0) { throw new Error('inArray requires at least one value'); } - return sql`${column} in (${values.map((v) => bindIfParam(v, column))})`; + return sql`${column} in ${values.map((v) => bindIfParam(v, column))}`; } return sql`${column} in ${bindIfParam(values, column)}`; diff --git a/drizzle-orm/src/sqlite-core/README.md b/drizzle-orm/src/sqlite-core/README.md index 37750b4d9..7c14d8297 100644 --- a/drizzle-orm/src/sqlite-core/README.md +++ b/drizzle-orm/src/sqlite-core/README.md @@ -461,9 +461,7 @@ const users = sqliteTable('users', { createdAt: integer('created_at', { mode: 'timestamp' }), }); -type NewUser = InferModel; - -const db = drizzle(...); +type NewUser = InferModel; const newUser: NewUser = { name: 'Andrew', @@ -472,10 +470,10 @@ const newUser: NewUser = { db.insert(users).values(newUser).run(); -const insertedUsers/*: NewUser[]*/ = db.insert(users).values(newUsers).returning().all(); +const insertedUsers/*: NewUser[]*/ = db.insert(users).values(newUser).returning().all(); const insertedUsersIds/*: { insertedId: number }[]*/ = db.insert(users) - .values(newUsers) + .values(newUser) .returning({ insertedId: users.id }) .all(); ``` diff --git a/drizzle-orm/src/sqlite-core/columns/integer.ts b/drizzle-orm/src/sqlite-core/columns/integer.ts index 4fe80d776..99e2bd835 100644 --- a/drizzle-orm/src/sqlite-core/columns/integer.ts +++ b/drizzle-orm/src/sqlite-core/columns/integer.ts @@ -8,7 +8,7 @@ import type { } from '~/column-builder'; import { sql } from '~/sql'; import type { OnConflict } from '~/sqlite-core/utils'; -import type { Assume } from '~/utils'; +import type { Assume, Equal, Or } from '~/utils'; import type { AnySQLiteTable } from '../table'; import { SQLiteColumn, SQLiteColumnBuilder } from './common'; @@ -155,10 +155,11 @@ export interface IntegerConfig< mode: TMode; } -export function integer( +export function integer( name: TName, config?: IntegerConfig, -): TMode extends 'number' ? SQLiteIntegerBuilderInitial : SQLiteTimestampBuilderInitial; +): Or, Equal> extends true ? SQLiteTimestampBuilderInitial + : SQLiteIntegerBuilderInitial; export function integer(name: string, config?: IntegerConfig) { if (config?.mode === 'timestamp' || config?.mode === 'timestamp_ms') { return new SQLiteTimestampBuilder(name, config.mode); diff --git a/drizzle-orm/src/sqlite-core/db.ts b/drizzle-orm/src/sqlite-core/db.ts index b8ab71d1a..3ef32fdaf 100644 --- a/drizzle-orm/src/sqlite-core/db.ts +++ b/drizzle-orm/src/sqlite-core/db.ts @@ -35,7 +35,7 @@ export class BaseSQLiteDatabase; }, }; diff --git a/drizzle-orm/src/sqlite-core/query-builders/select.ts b/drizzle-orm/src/sqlite-core/query-builders/select.ts index 96631ac37..923baaad4 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/select.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/select.ts @@ -197,7 +197,7 @@ export abstract class SQLiteSelectQueryBuilder< on = on( new Proxy( this.config.fields, - new SelectionProxyHandler({ sqlAliasedBehavior: 'alias', sqlBehavior: 'sql' }), + new SelectionProxyHandler({ sqlAliasedBehavior: 'sql', sqlBehavior: 'sql' }), ) as TSelection, ); } @@ -332,7 +332,7 @@ export abstract class SQLiteSelectQueryBuilder< ): SubqueryWithSelection, TAlias> { return new Proxy( new Subquery(this.getSQL(), this.config.fields, alias), - new SelectionProxyHandler({ alias, sqlAliasedBehavior: 'subquery_selection', sqlBehavior: 'error' }), + new SelectionProxyHandler({ alias, sqlAliasedBehavior: 'alias', sqlBehavior: 'error' }), ) as SubqueryWithSelection, TAlias>; } } diff --git a/drizzle-orm/src/subquery.ts b/drizzle-orm/src/subquery.ts index 783634332..f2bd1a184 100644 --- a/drizzle-orm/src/subquery.ts +++ b/drizzle-orm/src/subquery.ts @@ -39,8 +39,25 @@ export class SelectionProxyHandler | View> { private config: { + /** + * Table alias for the columns + */ alias?: string; - sqlAliasedBehavior: 'sql' | 'alias' | 'subquery_selection'; + /** + * What to do when a field is an instance of `SQL.Aliased` and it's not a selection field (from a subquery) + * + * `sql` - return the underlying SQL expression + * + * `alias` - return the field alias + */ + sqlAliasedBehavior: 'sql' | 'alias'; + /** + * What to do when a field is an instance of `SQL` and it doesn't have an alias declared + * + * `sql` - return the underlying SQL expression + * + * `error` - return a DrizzleTypeError on type level and throw an error on runtime + */ sqlBehavior: 'sql' | 'error'; }; @@ -61,10 +78,8 @@ export class SelectionProxyHandler>>; +} + +{ + const a = ['a', 'b', 'c'] as const; + const b = pgEnum('test', a); + const c = z.enum(b.enumValues); +} + +{ + const b = pgEnum('test', ['a', 'b', 'c']); + const c = z.enum(b.enumValues); +} diff --git a/drizzle-orm/type-tests/sqlite/tables.ts b/drizzle-orm/type-tests/sqlite/tables.ts index 5b0fb48e6..66294c645 100644 --- a/drizzle-orm/type-tests/sqlite/tables.ts +++ b/drizzle-orm/type-tests/sqlite/tables.ts @@ -232,3 +232,14 @@ Expect< > >; } + +{ + const test = sqliteTable('test', { + col1: integer('col1').default(1), + col2: integer('col2', { mode: 'number' }).default(1), + col3: integer('col3', { mode: 'timestamp' }).default(new Date()), + col4: integer('col4', { mode: 'timestamp_ms' }).default(new Date()), + // @ts-expect-error + col5: integer('col4', { mode: undefined }).default(new Date()), + }); +} diff --git a/drizzle-zod/package.json b/drizzle-zod/package.json index 1de82c154..7e3443cfd 100644 --- a/drizzle-zod/package.json +++ b/drizzle-zod/package.json @@ -1,6 +1,6 @@ { "name": "drizzle-zod", - "version": "0.2.0", + "version": "0.2.1", "description": "Generate Zod schemas from Drizzle ORM schemas", "scripts": { "build": "tsc && resolve-tspaths && cp README.md package.json dist/", diff --git a/integration-tests/tests/better-sqlite.test.ts b/integration-tests/tests/better-sqlite.test.ts index d490967d2..a78fea241 100644 --- a/integration-tests/tests/better-sqlite.test.ts +++ b/integration-tests/tests/better-sqlite.test.ts @@ -1226,6 +1226,16 @@ test.serial('prefixed table', (t) => { db.run(sql`drop table ${users}`); }); +test.serial('orderBy with aliased column', (t) => { + const { db } = t.context; + + const query = db.select({ + test: sql`something`.as('test'), + }).from(users2Table).orderBy((fields) => fields.test).toSQL(); + + t.deepEqual(query.sql, 'select something as "test" from "users2" order by "test"'); +}); + test.serial('transaction', (t) => { const { db } = t.context; diff --git a/integration-tests/tests/libsql.test.ts b/integration-tests/tests/libsql.test.ts index 06ce1da3b..f94e4b9ac 100644 --- a/integration-tests/tests/libsql.test.ts +++ b/integration-tests/tests/libsql.test.ts @@ -1185,6 +1185,16 @@ test.serial('prefixed table', async (t) => { await db.run(sql`drop table ${users}`); }); +test.serial('orderBy with aliased column', (t) => { + const { db } = t.context; + + const query = db.select({ + test: sql`something`.as('test'), + }).from(users2Table).orderBy((fields) => fields.test).toSQL(); + + t.deepEqual(query.sql, 'select something as "test" from "users2" order by "test"'); +}); + test.serial('transaction', async (t) => { const { db } = t.context; diff --git a/integration-tests/tests/mysql-schema.test.ts b/integration-tests/tests/mysql-schema.test.ts index 5ece5896a..164fdbd8a 100644 --- a/integration-tests/tests/mysql-schema.test.ts +++ b/integration-tests/tests/mysql-schema.test.ts @@ -82,7 +82,10 @@ async function createDockerDB(ctx: Context): Promise { const port = await getPort({ port: 3306 }); const image = 'mysql:8'; - await docker.pull(image); + const pullStream = await docker.pull(image); + await new Promise((resolve, reject) => + docker.modem.followProgress(pullStream, (err) => (err ? reject(err) : resolve(err))) + ); ctx.mysqlContainer = await docker.createContainer({ Image: image, @@ -139,7 +142,7 @@ test.beforeEach(async (t) => { sql`create table \`mySchema\`.\`userstest\` ( \`id\` serial primary key, \`name\` text not null, - \`verified\` boolean not null default false, + \`verified\` boolean not null default false, \`jsonb\` json, \`created_at\` timestamp not null default now() )`, @@ -165,7 +168,7 @@ test.beforeEach(async (t) => { \`date\` date, \`date_as_string\` date, \`time\` time, - \`datetime\` datetime, + \`datetime\` datetime, \`datetime_as_string\` datetime, \`year\` year )`, @@ -182,7 +185,7 @@ test.serial('select all fields', async (t) => { t.assert(result[0]!.createdAt instanceof Date); // not timezone based timestamp, thats why it should not work here - // t.assert(Math.abs(result[0]!.createdAt.getTime() - now) < 1000); + // t.assert(Math.abs(result[0]!.createdAt.getTime() - now) < 2000); t.deepEqual(result, [{ id: 1, name: 'John', verified: false, jsonb: null, createdAt: result[0]!.createdAt }]); }); @@ -248,7 +251,7 @@ test.serial('update with returning all fields', async (t) => { t.assert(users[0]!.createdAt instanceof Date); // not timezone based timestamp, thats why it should not work here - // t.assert(Math.abs(users[0]!.createdAt.getTime() - now) < 1000); + // t.assert(Math.abs(users[0]!.createdAt.getTime() - now) < 2000); t.deepEqual(users, [{ id: 1, name: 'Jane', verified: false, jsonb: null, createdAt: users[0]!.createdAt }]); }); @@ -462,6 +465,38 @@ test.serial('insert with onDuplicate', async (t) => { t.deepEqual(res, [{ id: 1, name: 'John1' }]); }); +test.serial('insert conflict', async (t) => { + const { db } = t.context; + + await db.insert(usersTable) + .values({ name: 'John' }); + + await t.throwsAsync( + () => db.insert(usersTable).values({ id: 1, name: 'John1' }), + { + code: 'ER_DUP_ENTRY', + message: "Duplicate entry '1' for key 'userstest.PRIMARY'", + }, + ); +}); + +test.serial('insert conflict with ignore', async (t) => { + const { db } = t.context; + + await db.insert(usersTable) + .values({ name: 'John' }); + + await db.insert(usersTable) + .ignore() + .values({ id: 1, name: 'John1' }); + + const res = await db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable).where( + eq(usersTable.id, 1), + ); + + t.deepEqual(res, [{ id: 1, name: 'John' }]); +}); + test.serial('insert sql', async (t) => { const { db } = t.context; @@ -655,7 +690,7 @@ test.serial('select from tables with same name from different schema using alias sql`create table \`userstest\` ( \`id\` serial primary key, \`name\` text not null, - \`verified\` boolean not null default false, + \`verified\` boolean not null default false, \`jsonb\` json, \`created_at\` timestamp not null default now() )`, diff --git a/integration-tests/tests/mysql.custom.test.ts b/integration-tests/tests/mysql.custom.test.ts index d7bda09b3..ab5ab788b 100644 --- a/integration-tests/tests/mysql.custom.test.ts +++ b/integration-tests/tests/mysql.custom.test.ts @@ -194,7 +194,7 @@ test.beforeEach(async (t) => { sql`create table \`userstest\` ( \`id\` serial primary key, \`name\` text not null, - \`verified\` boolean not null default false, + \`verified\` boolean not null default false, \`jsonb\` json, \`created_at\` timestamp not null default now() )`, @@ -205,7 +205,7 @@ test.beforeEach(async (t) => { \`date\` date, \`date_as_string\` date, \`time\` time, - \`datetime\` datetime, + \`datetime\` datetime, \`datetime_as_string\` datetime, \`year\` year )`, @@ -230,7 +230,7 @@ test.serial('select all fields', async (t) => { t.assert(result[0]!.createdAt instanceof Date); // not timezone based timestamp, thats why it should not work here - // t.assert(Math.abs(result[0]!.createdAt.getTime() - now) < 1000); + // t.assert(Math.abs(result[0]!.createdAt.getTime() - now) < 2000); t.deepEqual(result, [{ id: 1, name: 'John', verified: false, jsonb: null, createdAt: result[0]!.createdAt }]); }); @@ -296,7 +296,7 @@ test.serial('update with returning all fields', async (t) => { t.assert(users[0]!.createdAt instanceof Date); // not timezone based timestamp, thats why it should not work here - // t.assert(Math.abs(users[0]!.createdAt.getTime() - now) < 1000); + // t.assert(Math.abs(users[0]!.createdAt.getTime() - now) < 2000); t.deepEqual(users, [{ id: 1, name: 'Jane', verified: false, jsonb: null, createdAt: users[0]!.createdAt }]); }); @@ -510,6 +510,38 @@ test.serial('insert with onDuplicate', async (t) => { t.deepEqual(res, [{ id: 1, name: 'John1' }]); }); +test.serial('insert conflict', async (t) => { + const { db } = t.context; + + await db.insert(usersTable) + .values({ name: 'John' }); + + await t.throwsAsync( + () => db.insert(usersTable).values({ id: 1, name: 'John1' }), + { + code: 'ER_DUP_ENTRY', + message: "Duplicate entry '1' for key 'userstest.PRIMARY'", + }, + ); +}); + +test.serial('insert conflict with ignore', async (t) => { + const { db } = t.context; + + await db.insert(usersTable) + .values({ name: 'John' }); + + await db.insert(usersTable) + .ignore() + .values({ id: 1, name: 'John1' }); + + const res = await db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable).where( + eq(usersTable.id, 1), + ); + + t.deepEqual(res, [{ id: 1, name: 'John' }]); +}); + test.serial('insert sql', async (t) => { const { db } = t.context; diff --git a/integration-tests/tests/mysql.test.ts b/integration-tests/tests/mysql.test.ts index e7acc94f5..c5bb93529 100644 --- a/integration-tests/tests/mysql.test.ts +++ b/integration-tests/tests/mysql.test.ts @@ -173,7 +173,7 @@ test.beforeEach(async (t) => { sql`create table \`userstest\` ( \`id\` serial primary key, \`name\` text not null, - \`verified\` boolean not null default false, + \`verified\` boolean not null default false, \`jsonb\` json, \`created_at\` timestamp not null default now() )`, @@ -205,7 +205,7 @@ test.serial('select all fields', async (t) => { t.assert(result[0]!.createdAt instanceof Date); // not timezone based timestamp, thats why it should not work here - // t.assert(Math.abs(result[0]!.createdAt.getTime() - now) < 1000); + // t.assert(Math.abs(result[0]!.createdAt.getTime() - now) < 2000); t.deepEqual(result, [{ id: 1, name: 'John', verified: false, jsonb: null, createdAt: result[0]!.createdAt }]); }); @@ -271,7 +271,7 @@ test.serial('update with returning all fields', async (t) => { t.assert(users[0]!.createdAt instanceof Date); // not timezone based timestamp, thats why it should not work here - // t.assert(Math.abs(users[0]!.createdAt.getTime() - now) < 1000); + // t.assert(Math.abs(users[0]!.createdAt.getTime() - now) < 2000); t.deepEqual(users, [{ id: 1, name: 'Jane', verified: false, jsonb: null, createdAt: users[0]!.createdAt }]); }); @@ -485,6 +485,38 @@ test.serial('insert with onDuplicate', async (t) => { t.deepEqual(res, [{ id: 1, name: 'John1' }]); }); +test.serial('insert conflict', async (t) => { + const { db } = t.context; + + await db.insert(usersTable) + .values({ name: 'John' }); + + await t.throwsAsync( + () => db.insert(usersTable).values({ id: 1, name: 'John1' }), + { + code: 'ER_DUP_ENTRY', + message: "Duplicate entry '1' for key 'userstest.PRIMARY'", + }, + ); +}); + +test.serial('insert conflict with ignore', async (t) => { + const { db } = t.context; + + await db.insert(usersTable) + .values({ name: 'John' }); + + await db.insert(usersTable) + .ignore() + .values({ id: 1, name: 'John1' }); + + const res = await db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable).where( + eq(usersTable.id, 1), + ); + + t.deepEqual(res, [{ id: 1, name: 'John' }]); +}); + test.serial('insert sql', async (t) => { const { db } = t.context; @@ -1271,6 +1303,35 @@ test.serial('prefixed table', async (t) => { await db.execute(sql`drop table ${users}`); }); +test.serial('orderBy with aliased column', (t) => { + const { db } = t.context; + + const query = db.select({ + test: sql`something`.as('test'), + }).from(users2Table).orderBy((fields) => fields.test).toSQL(); + + t.deepEqual(query.sql, 'select something as `test` from `users2` order by `test`'); +}); + +test.serial('timestamp timezone', async (t) => { + const { db } = t.context; + + const date = new Date(Date.parse('2020-01-01T12:34:56+07:00')); + + await db.insert(usersTable).values({ name: 'With default times' }); + await db.insert(usersTable).values({ + name: 'Without default times', + createdAt: date, + }); + const users = await db.select().from(usersTable); + + // check that the timestamps are set correctly for default times + t.assert(Math.abs(users[0]!.createdAt.getTime() - new Date().getTime()) < 2000); + + // check that the timestamps are set correctly for non default times + t.assert(Math.abs(users[1]!.createdAt.getTime() - date.getTime()) < 2000); +}); + test.serial('transaction', async (t) => { const { db } = t.context; diff --git a/integration-tests/tests/pg-schema.test.ts b/integration-tests/tests/pg-schema.test.ts index 84095b63c..1b4973c44 100644 --- a/integration-tests/tests/pg-schema.test.ts +++ b/integration-tests/tests/pg-schema.test.ts @@ -70,7 +70,10 @@ async function createDockerDB(ctx: Context): Promise { const port = await getPort({ port: 5432 }); const image = 'postgres:14'; - await docker.pull(image); + const pullStream = await docker.pull(image); + await new Promise((resolve, reject) => + docker.modem.followProgress(pullStream, (err) => (err ? reject(err) : resolve(err))) + ); ctx.pgContainer = await docker.createContainer({ Image: image, diff --git a/integration-tests/tests/pg.test.ts b/integration-tests/tests/pg.test.ts index fc53ecc75..ef480a927 100644 --- a/integration-tests/tests/pg.test.ts +++ b/integration-tests/tests/pg.test.ts @@ -4,10 +4,13 @@ import type { TestFn } from 'ava'; import anyTest from 'ava'; import Docker from 'dockerode'; import { + and, asc, eq, gt, + gte, inArray, + lt, name, placeholder, type SQL, @@ -18,7 +21,7 @@ import { import type { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { drizzle } from 'drizzle-orm/node-postgres'; import { migrate } from 'drizzle-orm/node-postgres/migrator'; -import { type AnyPgColumn, pgEnum, pgTableCreator, varchar } from 'drizzle-orm/pg-core'; +import { type AnyPgColumn, pgEnum, pgTableCreator, uuid as pgUuid, varchar } from 'drizzle-orm/pg-core'; import { alias, boolean, @@ -1703,6 +1706,100 @@ test.serial('select from enum', async (t) => { await db.execute(sql`drop type ${name(categoryEnum.enumName)}`); }); +test.serial('orderBy with aliased column', (t) => { + const { db } = t.context; + + const query = db.select({ + test: sql`something`.as('test'), + }).from(users2Table).orderBy((fields) => fields.test).toSQL(); + + t.deepEqual(query.sql, 'select something as "test" from "users2" order by "test"'); +}); + +test.serial('select from sql', async (t) => { + const { db } = t.context; + + const metricEntry = pgTable('metric_entry', { + id: pgUuid('id').notNull(), + createdAt: timestamp('created_at').notNull(), + }); + + await db.execute(sql`drop table if exists ${metricEntry}`); + await db.execute(sql`create table ${metricEntry} (id uuid not null, created_at timestamp not null)`); + + const metricId = uuid(); + + const intervals = db.$with('intervals').as( + db + .select({ + startTime: sql`(date'2023-03-01'+ x * '1 day'::interval)`.as('start_time'), + endTime: sql`(date'2023-03-01'+ (x+1) *'1 day'::interval)`.as('end_time'), + }) + .from(sql`generate_series(0, 29, 1) as t(x)`), + ); + + await t.notThrowsAsync(() => + db + .with(intervals) + .select({ + startTime: intervals.startTime, + endTime: intervals.endTime, + count: sql`count(${metricEntry})`, + }) + .from(metricEntry) + .rightJoin( + intervals, + and( + eq(metricEntry.id, metricId), + gte(metricEntry.createdAt, intervals.startTime), + lt(metricEntry.createdAt, intervals.endTime), + ), + ) + .groupBy(intervals.startTime, intervals.endTime) + .orderBy(asc(intervals.startTime)) + ); +}); + +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() - new Date().getTime()) < 2000); + t.assert(Math.abs(users[0]!.createdAt.getTime() - new Date().getTime()) < 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('transaction', async (t) => { const { db } = t.context; diff --git a/integration-tests/tests/planetscale-serverless/mysql.test.ts b/integration-tests/tests/planetscale-serverless/mysql.test.ts index 7a4b74a05..697db78a1 100644 --- a/integration-tests/tests/planetscale-serverless/mysql.test.ts +++ b/integration-tests/tests/planetscale-serverless/mysql.test.ts @@ -118,7 +118,7 @@ test.serial('select all fields', async (t) => { t.assert(result[0]!.createdAt instanceof Date); // not timezone based timestamp, thats why it should not work here - // t.assert(Math.abs(result[0]!.createdAt.getTime() - now) < 1000); + // t.assert(Math.abs(result[0]!.createdAt.getTime() - now) < 2000); t.deepEqual(result, [{ id: 1, name: 'John', verified: false, jsonb: null, createdAt: result[0]!.createdAt }]); }); @@ -186,7 +186,7 @@ test.serial('update with returning all fields', async (t) => { t.assert(users[0]!.createdAt instanceof Date); // not timezone based timestamp, thats why it should not work here - // t.assert(Math.abs(users[0]!.createdAt.getTime() - now) < 1000); + // t.assert(Math.abs(users[0]!.createdAt.getTime() - now) < 2000); t.deepEqual(users, [{ id: 1, name: 'Jane', verified: false, jsonb: null, createdAt: users[0]!.createdAt }]); }); diff --git a/integration-tests/tests/postgres.js.test.ts b/integration-tests/tests/postgres.js.test.ts index c1b0e0292..d377f307c 100644 --- a/integration-tests/tests/postgres.js.test.ts +++ b/integration-tests/tests/postgres.js.test.ts @@ -3,8 +3,22 @@ import 'dotenv/config'; import type { TestFn } from 'ava'; import anyTest from 'ava'; import Docker from 'dockerode'; -import { DrizzleError, Name, name, placeholder, type SQL, sql, type SQLWrapper } from 'drizzle-orm'; -import { asc, eq, gt, inArray } from 'drizzle-orm/expressions'; +import { + and, + asc, + DrizzleError, + eq, + gt, + gte, + inArray, + lt, + Name, + name, + placeholder, + type SQL, + sql, + type SQLWrapper, +} from 'drizzle-orm'; import { alias, type AnyPgColumn, @@ -21,6 +35,7 @@ import { serial, text, timestamp, + uuid as pgUuid, varchar, } from 'drizzle-orm/pg-core'; import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; @@ -1481,6 +1496,61 @@ test.serial('select from enum', async (t) => { await db.execute(sql`drop type ${name(categoryEnum.enumName)}`); }); +test.serial('orderBy with aliased column', (t) => { + const { db } = t.context; + + const query = db.select({ + test: sql`something`.as('test'), + }).from(users2Table).orderBy((fields) => fields.test).toSQL(); + + t.deepEqual(query.sql, 'select something as "test" from "users2" order by "test"'); +}); + +test.serial('select from sql', async (t) => { + const { db } = t.context; + + const metricEntry = pgTable('metric_entry', { + id: pgUuid('id').notNull(), + createdAt: timestamp('created_at').notNull(), + }); + + await db.execute(sql`drop table if exists ${metricEntry}`); + await db.execute(sql`create table ${metricEntry} (id uuid not null, created_at timestamp not null)`); + + const metricId = uuid(); + + const intervals = db.$with('intervals').as( + db + .select({ + startTime: sql`(date'2023-03-01'+ x * '1 day'::interval)`.as('start_time'), + endTime: sql`(date'2023-03-01'+ (x+1) *'1 day'::interval)`.as('end_time'), + }) + .from(sql`generate_series(0, 29, 1) as t(x)`), + ); + + await t.notThrowsAsync(() => + db + .with(intervals) + .select({ + startTime: intervals.startTime, + endTime: intervals.endTime, + count: sql`count(${metricEntry})`, + }) + .from(metricEntry) + .rightJoin( + intervals, + and( + eq(metricEntry.id, metricId), + gte(metricEntry.createdAt, intervals.startTime), + lt(metricEntry.createdAt, intervals.endTime), + ), + ) + .groupBy(intervals.startTime, intervals.endTime) + .orderBy(asc(intervals.startTime)) + ); + // beta +}); + test.serial('transaction', async (t) => { const { db } = t.context; diff --git a/integration-tests/tests/sql.js.test.ts b/integration-tests/tests/sql.js.test.ts index 34d849a23..777a697cc 100644 --- a/integration-tests/tests/sql.js.test.ts +++ b/integration-tests/tests/sql.js.test.ts @@ -1150,6 +1150,16 @@ test.serial('prefixed table', (t) => { db.run(sql`drop table ${users}`); }); +test.serial('orderBy with aliased column', (t) => { + const { db } = t.context; + + const query = db.select({ + test: sql`something`.as('test'), + }).from(users2Table).orderBy((fields) => fields.test).toSQL(); + + t.deepEqual(query.sql, 'select something as "test" from "users2" order by "test"'); +}); + test.serial('transaction', (t) => { const { db } = t.context; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f19e0be61..6ef7cc7c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -114,6 +114,9 @@ importers: vitest: specifier: ^0.29.8 version: 0.29.8 + zod: + specifier: ^3.20.2 + version: 3.21.4 drizzle-zod: devDependencies: