From 2bf1a0adf61a49dedcd17aa6f9db3198696099f9 Mon Sep 17 00:00:00 2001 From: nickk Date: Wed, 29 Oct 2025 09:37:34 -0500 Subject: [PATCH 01/10] Fix pg-native Pool transactions (#1708) Co-authored-by: Andrii Sherman Co-authored-by: Sukairo-02 --- drizzle-orm/src/node-postgres/session.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/drizzle-orm/src/node-postgres/session.ts b/drizzle-orm/src/node-postgres/session.ts index e5fb6ba7b7..803ab29b90 100644 --- a/drizzle-orm/src/node-postgres/session.ts +++ b/drizzle-orm/src/node-postgres/session.ts @@ -14,7 +14,8 @@ import { fillPlaceholders, type Query, type SQL, sql } from '~/sql/sql.ts'; import { tracer } from '~/tracing.ts'; import { type Assume, mapResultRow } from '~/utils.ts'; -const { Pool, types } = pg; +const { Pool, native, types } = pg; +const NativePool = native?.Pool; export type NodePgClient = pg.Pool | PoolClient | Client; @@ -249,7 +250,7 @@ export class NodePgSession< transaction: (tx: NodePgTransaction) => Promise, config?: PgTransactionConfig | undefined, ): Promise { - const session = this.client instanceof Pool // eslint-disable-line no-instanceof/no-instanceof + const session = (this.client instanceof Pool || (NativePool && this.client instanceof NativePool)) // eslint-disable-line no-instanceof/no-instanceof ? new NodePgSession(await this.client.connect(), this.dialect, this.schema, this.options) : this; const tx = new NodePgTransaction(this.dialect, session, this.schema); @@ -262,7 +263,7 @@ export class NodePgSession< await tx.execute(sql`rollback`); throw error; } finally { - if (this.client instanceof Pool) { // eslint-disable-line no-instanceof/no-instanceof + if (this.client instanceof Pool || (NativePool && this.client instanceof NativePool)) { // eslint-disable-line no-instanceof/no-instanceof (session.client as PoolClient).release(); } } From ce85ad2b0cc22b829ec35bacd32163746c110453 Mon Sep 17 00:00:00 2001 From: Sukairo-02 Date: Wed, 29 Oct 2025 19:22:36 +0200 Subject: [PATCH 02/10] Build fix --- drizzle-orm/src/node-postgres/session.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drizzle-orm/src/node-postgres/session.ts b/drizzle-orm/src/node-postgres/session.ts index 803ab29b90..8a757fe22e 100644 --- a/drizzle-orm/src/node-postgres/session.ts +++ b/drizzle-orm/src/node-postgres/session.ts @@ -14,8 +14,8 @@ import { fillPlaceholders, type Query, type SQL, sql } from '~/sql/sql.ts'; import { tracer } from '~/tracing.ts'; import { type Assume, mapResultRow } from '~/utils.ts'; -const { Pool, native, types } = pg; -const NativePool = native?.Pool; +const { Pool, types } = pg; +const NativePool = ( pg).native ? (<{ Pool: typeof Pool }> ( pg).native).Pool : undefined; export type NodePgClient = pg.Pool | PoolClient | Client; From 01c8043b4eceb4df68e310e4c502d1171b56d9b8 Mon Sep 17 00:00:00 2001 From: Angelelz <34462576+Angelelz@users.noreply.github.com> Date: Wed, 29 Oct 2025 16:52:10 -0400 Subject: [PATCH 03/10] Feat: Allow subqueries in select fields (#1674) --------- Co-authored-by: Sukairo-02 --- drizzle-orm/src/gel-core/dialect.ts | 17 +++++ drizzle-orm/src/mysql-core/dialect.ts | 17 +++++ drizzle-orm/src/operations.ts | 5 +- drizzle-orm/src/pg-core/dialect.ts | 17 +++++ .../src/query-builders/select.types.ts | 11 +-- drizzle-orm/src/singlestore-core/dialect.ts | 17 +++++ drizzle-orm/src/sqlite-core/dialect.ts | 18 +++++ drizzle-orm/src/utils.ts | 19 ++++- drizzle-orm/type-tests/mysql/subquery.ts | 63 ++++++++++----- drizzle-orm/type-tests/pg/subquery.ts | 63 ++++++++++----- drizzle-orm/type-tests/sqlite/subquery.ts | 63 ++++++++++----- integration-tests/tests/gel/gel.test.ts | 74 +++++++++++++++++- integration-tests/tests/mysql/mysql-common.ts | 76 +++++++++++++++++++ integration-tests/tests/pg/pg-common.ts | 72 ++++++++++++++++++ .../tests/singlestore/singlestore-common.ts | 76 +++++++++++++++++++ .../tests/sqlite/sqlite-common.ts | 72 ++++++++++++++++++ 16 files changed, 617 insertions(+), 63 deletions(-) diff --git a/drizzle-orm/src/gel-core/dialect.ts b/drizzle-orm/src/gel-core/dialect.ts index 5b40dfdb01..a7f5924adc 100644 --- a/drizzle-orm/src/gel-core/dialect.ts +++ b/drizzle-orm/src/gel-core/dialect.ts @@ -241,6 +241,23 @@ export class GelDialect { // } else { chunk.push(field); // } + } else if (is(field, Subquery)) { + const entries = Object.entries(field._.selectedFields) as [string, SQL.Aliased | Column | SQL][]; + + if (entries.length === 1) { + const entry = entries[0]![1]; + + const fieldDecoder = is(entry, SQL) + ? entry.decoder + : is(entry, Column) + ? { mapFromDriverValue: (v: any) => entry.mapFromDriverValue(v) } + : entry.sql.decoder; + + if (fieldDecoder) { + field._.sql.decoder = fieldDecoder; + } + } + chunk.push(field); } if (i < columnsLen - 1) { diff --git a/drizzle-orm/src/mysql-core/dialect.ts b/drizzle-orm/src/mysql-core/dialect.ts index 0014dd303f..0b2b709fc0 100644 --- a/drizzle-orm/src/mysql-core/dialect.ts +++ b/drizzle-orm/src/mysql-core/dialect.ts @@ -222,6 +222,23 @@ export class MySqlDialect { } else { chunk.push(field); } + } else if (is(field, Subquery)) { + const entries = Object.entries(field._.selectedFields) as [string, SQL.Aliased | Column | SQL][]; + + if (entries.length === 1) { + const entry = entries[0]![1]; + + const fieldDecoder = is(entry, SQL) + ? entry.decoder + : is(entry, Column) + ? { mapFromDriverValue: (v: any) => entry.mapFromDriverValue(v) } + : entry.sql.decoder; + + if (fieldDecoder) { + field._.sql.decoder = fieldDecoder; + } + } + chunk.push(field); } if (i < columnsLen - 1) { diff --git a/drizzle-orm/src/operations.ts b/drizzle-orm/src/operations.ts index 135e01fefb..f352bbf351 100644 --- a/drizzle-orm/src/operations.ts +++ b/drizzle-orm/src/operations.ts @@ -1,5 +1,6 @@ import type { AnyColumn, Column } from './column.ts'; import type { SQL } from './sql/sql.ts'; +import type { Subquery } from './subquery.ts'; import type { Table } from './table.ts'; export type RequiredKeyOnly = T extends AnyColumn<{ @@ -25,7 +26,7 @@ export type OptionalKeyOnly SQLWrapper export type SelectedFieldsFlat = Record< string, - TColumn | SQL | SQL.Aliased + TColumn | SQL | SQL.Aliased | Subquery >; export type SelectedFieldsFlatFull = Record< @@ -40,5 +41,5 @@ export type SelectedFields = Recor export type SelectedFieldsOrdered = { path: string[]; - field: TColumn | SQL | SQL.Aliased; + field: TColumn | SQL | SQL.Aliased | Subquery; }[]; diff --git a/drizzle-orm/src/pg-core/dialect.ts b/drizzle-orm/src/pg-core/dialect.ts index 59ec42b169..43f676b19a 100644 --- a/drizzle-orm/src/pg-core/dialect.ts +++ b/drizzle-orm/src/pg-core/dialect.ts @@ -246,6 +246,23 @@ export class PgDialect { } else { chunk.push(field); } + } else if (is(field, Subquery)) { + const entries = Object.entries(field._.selectedFields) as [string, SQL.Aliased | Column | SQL][]; + + if (entries.length === 1) { + const entry = entries[0]![1]; + + const fieldDecoder = is(entry, SQL) + ? entry.decoder + : is(entry, Column) + ? { mapFromDriverValue: (v: any) => entry.mapFromDriverValue(v) } + : entry.sql.decoder; + + if (fieldDecoder) { + field._.sql.decoder = fieldDecoder; + } + } + chunk.push(field); } if (i < columnsLen - 1) { diff --git a/drizzle-orm/src/query-builders/select.types.ts b/drizzle-orm/src/query-builders/select.types.ts index 6ba4462789..57c59b4fdd 100644 --- a/drizzle-orm/src/query-builders/select.types.ts +++ b/drizzle-orm/src/query-builders/select.types.ts @@ -4,7 +4,7 @@ import type { SelectedFields } from '~/operations.ts'; import type { ColumnsSelection, SQL, View } from '~/sql/sql.ts'; import type { Subquery } from '~/subquery.ts'; import type { Table } from '~/table.ts'; -import type { Assume, DrizzleTypeError, Equal, IsAny, Simplify } from '~/utils.ts'; +import type { Assume, DrizzleTypeError, Equal, FromSingleKeyObject, IsAny, IsUnion, Not, Simplify } from '~/utils.ts'; export type JoinType = 'inner' | 'left' | 'right' | 'full' | 'cross'; @@ -44,10 +44,6 @@ export type SelectResult< : TSelectMode extends 'single' ? SelectResultFields : ApplyNotNullMapToJoins, TNullabilityMap>; -type IsUnion = (T extends any ? (U extends T ? false : true) : never) extends false ? false : true; - -type Not = T extends true ? false : true; - type SelectPartialResult> = TNullability extends TNullability ? { [Key in keyof TFields]: TFields[Key] extends infer TField @@ -61,6 +57,11 @@ type SelectPartialResult, TNullability[TField['_']['tableName']]> : never : TField extends SQL | SQL.Aliased ? SelectResultField + : TField extends Subquery ? FromSingleKeyObject< + TField['_']['selectedFields'], + TField['_']['selectedFields'] extends { [key: string]: infer TValue } ? SelectResultField : never, + 'You can only select one column in the subquery' + > : TField extends Record ? TField[keyof TField] extends AnyColumn<{ tableName: infer TTableName extends string }> | SQL | SQL.Aliased ? Not> extends true diff --git a/drizzle-orm/src/singlestore-core/dialect.ts b/drizzle-orm/src/singlestore-core/dialect.ts index 359809b0e9..31aab80d68 100644 --- a/drizzle-orm/src/singlestore-core/dialect.ts +++ b/drizzle-orm/src/singlestore-core/dialect.ts @@ -221,6 +221,23 @@ export class SingleStoreDialect { } else { chunk.push(field); } + } else if (is(field, Subquery)) { + const entries = Object.entries(field._.selectedFields) as [string, SQL.Aliased | Column | SQL][]; + + if (entries.length === 1) { + const entry = entries[0]![1]; + + const fieldDecoder = is(entry, SQL) + ? entry.decoder + : is(entry, Column) + ? { mapFromDriverValue: (v: any) => entry.mapFromDriverValue(v) } + : entry.sql.decoder; + + if (fieldDecoder) { + field._.sql.decoder = fieldDecoder; + } + } + chunk.push(field); } if (i < columnsLen - 1) { diff --git a/drizzle-orm/src/sqlite-core/dialect.ts b/drizzle-orm/src/sqlite-core/dialect.ts index 2f4a0c366d..27c414ca7e 100644 --- a/drizzle-orm/src/sqlite-core/dialect.ts +++ b/drizzle-orm/src/sqlite-core/dialect.ts @@ -170,6 +170,10 @@ export abstract class SQLiteDialect { new SQL( query.queryChunks.map((c) => { if (is(c, Column)) { + if (c.columnType === 'SQLiteNumericBigInt') { + return chunk.push(sql`cast(${sql.identifier(this.casing.getColumnCasing(c))} as text)`); + } + return sql.identifier(this.casing.getColumnCasing(c)); } return c; @@ -200,6 +204,20 @@ export abstract class SQLiteDialect { chunk.push(sql`${sql.identifier(tableName)}.${sql.identifier(this.casing.getColumnCasing(field))}`); } } + } else if (is(field, Subquery)) { + const entries = Object.entries(field._.selectedFields) as [string, SQL.Aliased | Column | SQL][]; + + if (entries.length === 1) { + const entry = entries[0]![1]; + + const fieldDecoder = is(entry, SQL) + ? entry.decoder + : is(entry, Column) + ? { mapFromDriverValue: (v: any) => entry.mapFromDriverValue(v) } + : entry.sql.decoder; + if (fieldDecoder) field._.sql.decoder = fieldDecoder; + } + chunk.push(field); } if (i < columnsLen - 1) { diff --git a/drizzle-orm/src/utils.ts b/drizzle-orm/src/utils.ts index a83965001a..6f7659485f 100644 --- a/drizzle-orm/src/utils.ts +++ b/drizzle-orm/src/utils.ts @@ -27,6 +27,8 @@ export function mapResultRow( decoder = field; } else if (is(field, SQL)) { decoder = field.decoder; + } else if (is(field, Subquery)) { + decoder = field._.sql.decoder; } else { decoder = field.sql.decoder; } @@ -81,7 +83,7 @@ export function orderSelectedFields( } const newPath = pathPrefix ? [...pathPrefix, name] : [name]; - if (is(field, Column) || is(field, SQL) || is(field, SQL.Aliased)) { + if (is(field, Column) || is(field, SQL) || is(field, SQL.Aliased) || is(field, Subquery)) { result.push({ path: newPath, field }); } else if (is(field, Table)) { result.push(...orderSelectedFields(field[Table.Symbol.Columns], newPath)); @@ -146,6 +148,21 @@ export type Simplify = } & {}; +export type Not = T extends true ? false : true; + +export type IsNever = [T] extends [never] ? true : false; + +export type IsUnion = (T extends any ? (U extends T ? false : true) : never) extends false ? false + : true; + +export type SingleKeyObject = IsNever extends true ? never + : IsUnion extends true ? DrizzleTypeError + : T; + +export type FromSingleKeyObject = IsNever extends true ? never + : IsUnion extends true ? DrizzleTypeError + : Result; + export type SimplifyMappedType = [T] extends [unknown] ? T : never; export type ShallowRecord = SimplifyMappedType<{ [P in K]: T }>; diff --git a/drizzle-orm/type-tests/mysql/subquery.ts b/drizzle-orm/type-tests/mysql/subquery.ts index 1a3b64ea24..e223bf8cc7 100644 --- a/drizzle-orm/type-tests/mysql/subquery.ts +++ b/drizzle-orm/type-tests/mysql/subquery.ts @@ -1,35 +1,42 @@ import { Expect } from 'type-tests/utils.ts'; import { alias, int, mysqlTable, serial, text } from '~/mysql-core/index.ts'; import { and, eq } from '~/sql/expressions/index.ts'; +import { count } from '~/sql/functions/aggregate.ts'; import { sql } from '~/sql/sql.ts'; import type { DrizzleTypeError, Equal } from '~/utils.ts'; import { db } from './db.ts'; -const names = mysqlTable('names', { +const users = mysqlTable('names', { id: serial('id').primaryKey(), name: text('name'), + managerId: int('author_id'), +}); + +const posts = mysqlTable('posts', { + id: serial('id').primaryKey(), authorId: int('author_id'), + title: text('title'), }); const n1 = db .select({ - id: names.id, - name: names.name, - authorId: names.authorId, + id: users.id, + name: users.name, + authorId: users.managerId, count1: sql`count(1)::int`.as('count1'), }) - .from(names) - .groupBy(names.id, names.name, names.authorId) + .from(users) + .groupBy(users.id, users.name, users.managerId) .as('n1'); const n2 = db .select({ - id: names.id, - authorId: names.authorId, + id: users.id, + authorId: users.managerId, totalCount: sql`count(1)::int`.as('totalCount'), }) - .from(names) - .groupBy(names.id, names.authorId) + .from(users) + .groupBy(users.id, users.managerId) .as('n2'); const result = await db @@ -54,16 +61,16 @@ Expect< > >; -const names2 = alias(names, 'names2'); +const names2 = alias(users, 'names2'); const sq1 = db .select({ - id: names.id, - name: names.name, + id: users.id, + name: users.name, id2: names2.id, }) - .from(names) - .leftJoin(names2, eq(names.name, names2.name)) + .from(users) + .leftJoin(names2, eq(users.name, names2.name)) .as('sq1'); const res = await db.select().from(sq1); @@ -80,11 +87,11 @@ Expect< >; { - const sq = db.select({ count: sql`count(1)::int` }).from(names).as('sq'); + const sq = db.select({ count: sql`count(1)::int` }).from(users).as('sq'); Expect ? true : false>; } -const sqUnion = db.select().from(names).union(db.select().from(names2)).as('sqUnion'); +const sqUnion = db.select().from(users).union(db.select().from(names2)).as('sqUnion'); const resUnion = await db.select().from(sqUnion); @@ -92,6 +99,26 @@ Expect< Equal<{ id: number; name: string | null; - authorId: number | null; + managerId: number | null; }[], typeof resUnion> >; + +const fromSubquery = await db.select({ + count: db.select({ count: count().as('c') }).from(posts).where(eq(posts.authorId, users.id)).as('count'), +}).from(users); + +Expect>; + +const fromSubquery2 = await db.select({ + name: db.select({ name: users.name }).from(users).where(eq(users.id, posts.authorId)).as('name'), +}).from(posts); + +Expect>; + +const errorSubquery = await db.select({ + name: db.select({ name: users.name, managerId: users.managerId }).from(users).where(eq(users.id, posts.authorId)).as( + 'name', + ), +}).from(posts); + +Expect }[]>>; diff --git a/drizzle-orm/type-tests/pg/subquery.ts b/drizzle-orm/type-tests/pg/subquery.ts index cdcbd08874..e24cd05212 100644 --- a/drizzle-orm/type-tests/pg/subquery.ts +++ b/drizzle-orm/type-tests/pg/subquery.ts @@ -1,35 +1,42 @@ import { Expect } from 'type-tests/utils.ts'; import { alias, integer, pgTable, serial, text } from '~/pg-core/index.ts'; import { and, eq } from '~/sql/expressions/index.ts'; +import { count } from '~/sql/functions/aggregate.ts'; import { sql } from '~/sql/sql.ts'; import type { DrizzleTypeError, Equal } from '~/utils.ts'; import { db } from './db.ts'; -const names = pgTable('names', { +const users = pgTable('names', { id: serial('id').primaryKey(), name: text('name'), + managerId: integer('author_id'), +}); + +const posts = pgTable('posts', { + id: serial('id').primaryKey(), authorId: integer('author_id'), + title: text('title'), }); const n1 = db .select({ - id: names.id, - name: names.name, - authorId: names.authorId, + id: users.id, + name: users.name, + authorId: users.managerId, count1: sql`count(1)::int`.as('count1'), }) - .from(names) - .groupBy(names.id, names.name, names.authorId) + .from(users) + .groupBy(users.id, users.name, users.managerId) .as('n1'); const n2 = db .select({ - id: names.id, - authorId: names.authorId, + id: users.id, + authorId: users.managerId, totalCount: sql`count(1)::int`.as('totalCount'), }) - .from(names) - .groupBy(names.id, names.authorId) + .from(users) + .groupBy(users.id, users.managerId) .as('n2'); const result = await db @@ -54,16 +61,16 @@ Expect< > >; -const names2 = alias(names, 'names2'); +const names2 = alias(users, 'names2'); const sq1 = db .select({ - id: names.id, - name: names.name, + id: users.id, + name: users.name, id2: names2.id, }) - .from(names) - .leftJoin(names2, eq(names.name, names2.name)) + .from(users) + .leftJoin(names2, eq(users.name, names2.name)) .as('sq1'); const res = await db.select().from(sq1); @@ -80,11 +87,11 @@ Expect< >; { - const sq = db.select({ count: sql`count(1)::int` }).from(names).as('sq'); + const sq = db.select({ count: sql`count(1)::int` }).from(users).as('sq'); Expect ? true : false>; } -const sqUnion = db.select().from(names).union(db.select().from(names2)).as('sqUnion'); +const sqUnion = db.select().from(users).union(db.select().from(names2)).as('sqUnion'); const resUnion = await db.select().from(sqUnion); @@ -92,6 +99,26 @@ Expect< Equal<{ id: number; name: string | null; - authorId: number | null; + managerId: number | null; }[], typeof resUnion> >; + +const fromSubquery = await db.select({ + count: db.select({ count: count().as('c') }).from(posts).where(eq(posts.authorId, users.id)).as('count'), +}).from(users); + +Expect>; + +const fromSubquery2 = await db.select({ + name: db.select({ name: users.name }).from(users).where(eq(users.id, posts.authorId)).as('name'), +}).from(posts); + +Expect>; + +const errorSubquery = await db.select({ + name: db.select({ name: users.name, managerId: users.managerId }).from(users).where(eq(users.id, posts.authorId)).as( + 'name', + ), +}).from(posts); + +Expect }[]>>; diff --git a/drizzle-orm/type-tests/sqlite/subquery.ts b/drizzle-orm/type-tests/sqlite/subquery.ts index 573cfdf804..4c8c3d4e45 100644 --- a/drizzle-orm/type-tests/sqlite/subquery.ts +++ b/drizzle-orm/type-tests/sqlite/subquery.ts @@ -1,35 +1,42 @@ import { Expect } from 'type-tests/utils.ts'; import { and, eq } from '~/sql/expressions/index.ts'; +import { count } from '~/sql/functions/aggregate.ts'; import { sql } from '~/sql/sql.ts'; import { alias, integer, sqliteTable, text } from '~/sqlite-core/index.ts'; import type { DrizzleTypeError, Equal } from '~/utils.ts'; import { db } from './db.ts'; -const names = sqliteTable('names', { +const users = sqliteTable('names', { id: integer('id').primaryKey(), name: text('name'), + managerId: integer('author_id'), +}); + +const posts = sqliteTable('posts', { + id: integer('id').primaryKey(), authorId: integer('author_id'), + title: text('title'), }); const n1 = db .select({ - id: names.id, - name: names.name, - authorId: names.authorId, + id: users.id, + name: users.name, + authorId: users.managerId, count1: sql`count(1)::int`.as('count1'), }) - .from(names) - .groupBy(names.id, names.name, names.authorId) + .from(users) + .groupBy(users.id, users.name, users.managerId) .as('n1'); const n2 = db .select({ - id: names.id, - authorId: names.authorId, + id: users.id, + authorId: users.managerId, totalCount: sql`count(1)::int`.as('totalCount'), }) - .from(names) - .groupBy(names.id, names.authorId) + .from(users) + .groupBy(users.id, users.managerId) .as('n2'); const result = db @@ -55,16 +62,16 @@ Expect< > >; -const names2 = alias(names, 'names2'); +const names2 = alias(users, 'names2'); const sq1 = db .select({ - id: names.id, - name: names.name, + id: users.id, + name: users.name, id2: names2.id, }) - .from(names) - .leftJoin(names2, eq(names.name, names2.name)) + .from(users) + .leftJoin(names2, eq(users.name, names2.name)) .as('sq1'); const res = db.select().from(sq1).all(); @@ -81,11 +88,11 @@ Expect< >; { - const sq = db.select({ count: sql`count(1)::int` }).from(names).as('sq'); + const sq = db.select({ count: sql`count(1)::int` }).from(users).as('sq'); Expect ? true : false>; } -const sqUnion = db.select().from(names).union(db.select().from(names2)).as('sqUnion'); +const sqUnion = db.select().from(users).union(db.select().from(names2)).as('sqUnion'); const resUnion = await db.select().from(sqUnion); @@ -93,6 +100,26 @@ Expect< Equal<{ id: number; name: string | null; - authorId: number | null; + managerId: number | null; }[], typeof resUnion> >; + +const fromSubquery = await db.select({ + count: db.select({ count: count().as('c') }).from(posts).where(eq(posts.authorId, users.id)).as('count'), +}).from(users); + +Expect>; + +const fromSubquery2 = await db.select({ + name: db.select({ name: users.name }).from(users).where(eq(users.id, posts.authorId)).as('name'), +}).from(posts); + +Expect>; + +const errorSubquery = await db.select({ + name: db.select({ name: users.name, managerId: users.managerId }).from(users).where(eq(users.id, posts.authorId)).as( + 'name', + ), +}).from(posts); + +Expect }[]>>; diff --git a/integration-tests/tests/gel/gel.test.ts b/integration-tests/tests/gel/gel.test.ts index 858a1d21ce..15b2e6e026 100644 --- a/integration-tests/tests/gel/gel.test.ts +++ b/integration-tests/tests/gel/gel.test.ts @@ -77,7 +77,7 @@ import createClient, { RelativeDuration, } from 'gel'; import { v4 as uuidV4 } from 'uuid'; -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, expectTypeOf, test, vi } from 'vitest'; import { Expect } from '~/utils'; import 'zx/globals'; import { TestCache, TestGlobalCache } from './cache'; @@ -1863,6 +1863,78 @@ describe('some', async () => { ]); }); + test('select from a many subquery', async (ctx) => { + const { db } = ctx.gel; + + await db.insert(citiesTable) + .values([{ id1: 1, name: 'Paris' }, { id1: 2, name: 'London' }]); + + await db.insert(users2Table).values([ + { id1: 1, name: 'John', cityId: 1 }, + { id1: 2, name: 'Jane', cityId: 2 }, + { id1: 3, name: 'Jack', cityId: 2 }, + ]); + + const res = await db.select({ + population: db.select({ count: count().as('count') }).from(users2Table).where( + eq(users2Table.cityId, citiesTable.id1), + ).as( + 'population', + ), + name: citiesTable.name, + }).from(citiesTable); + + expectTypeOf(res).toEqualTypeOf<{ + population: number; + name: string; + }[]>(); + + expect(res).toStrictEqual([{ + population: 1, + name: 'Paris', + }, { + population: 2, + name: 'London', + }]); + }); + + test('select from a one subquery', async (ctx) => { + const { db } = ctx.gel; + + await db.insert(citiesTable) + .values([{ id1: 1, name: 'Paris' }, { id1: 2, name: 'London' }]); + + await db.insert(users2Table).values([ + { id1: 1, name: 'John', cityId: 1 }, + { id1: 2, name: 'Jane', cityId: 2 }, + { id1: 3, name: 'Jack', cityId: 2 }, + ]); + + const res = await db.select({ + cityName: db.select({ name: citiesTable.name }).from(citiesTable).where(eq(users2Table.cityId, citiesTable.id1)) + .as( + 'cityName', + ), + name: users2Table.name, + }).from(users2Table); + + expectTypeOf(res).toEqualTypeOf<{ + cityName: string; + name: string; + }[]>(); + + expect(res).toStrictEqual([{ + cityName: 'Paris', + name: 'John', + }, { + cityName: 'London', + name: 'Jane', + }, { + cityName: 'London', + name: 'Jack', + }]); + }); + test('join subquery', async (ctx) => { const { db } = ctx.gel; diff --git a/integration-tests/tests/mysql/mysql-common.ts b/integration-tests/tests/mysql/mysql-common.ts index 9df31a2d57..fa2511017f 100644 --- a/integration-tests/tests/mysql/mysql-common.ts +++ b/integration-tests/tests/mysql/mysql-common.ts @@ -1623,6 +1623,82 @@ export function tests(driver?: string) { ]); }); + test('select from a many subquery', async (ctx) => { + const { db } = ctx.mysql; + + await db.insert(citiesTable) + .values([{ name: 'Paris' }, { name: 'London' }]); + + await db.insert(users2Table).values([ + { name: 'John', cityId: 1 }, + { name: 'Jane', cityId: 2 }, + { name: 'Jack', cityId: 2 }, + ]); + + const res = await db.select({ + population: db.select({ count: count().as('count') }).from(users2Table).where( + eq(users2Table.cityId, citiesTable.id), + ).as( + 'population', + ), + name: citiesTable.name, + }).from(citiesTable); + + expectTypeOf(res).toEqualTypeOf< + { + population: number; + name: string; + }[] + >(); + + expect(res).toStrictEqual([{ + population: 1, + name: 'Paris', + }, { + population: 2, + name: 'London', + }]); + }); + + test('select from a one subquery', async (ctx) => { + const { db } = ctx.mysql; + + await db.insert(citiesTable) + .values([{ name: 'Paris' }, { name: 'London' }]); + + await db.insert(users2Table).values([ + { name: 'John', cityId: 1 }, + { name: 'Jane', cityId: 2 }, + { name: 'Jack', cityId: 2 }, + ]); + + const res = await db.select({ + cityName: db.select({ name: citiesTable.name }).from(citiesTable).where(eq(users2Table.cityId, citiesTable.id)) + .as( + 'cityName', + ), + name: users2Table.name, + }).from(users2Table); + + expectTypeOf(res).toEqualTypeOf< + { + cityName: string; + name: string; + }[] + >(); + + expect(res).toStrictEqual([{ + cityName: 'Paris', + name: 'John', + }, { + cityName: 'London', + name: 'Jane', + }, { + cityName: 'London', + name: 'Jack', + }]); + }); + test('join subquery', async (ctx) => { const { db } = ctx.mysql; diff --git a/integration-tests/tests/pg/pg-common.ts b/integration-tests/tests/pg/pg-common.ts index 54a384fcf1..35a2ce721c 100644 --- a/integration-tests/tests/pg/pg-common.ts +++ b/integration-tests/tests/pg/pg-common.ts @@ -1741,6 +1741,78 @@ export function tests() { ]); }); + test('select from a many subquery', async (ctx) => { + const { db } = ctx.pg; + + await db.insert(citiesTable) + .values([{ name: 'Paris' }, { name: 'London' }]); + + await db.insert(users2Table).values([ + { name: 'John', cityId: 1 }, + { name: 'Jane', cityId: 2 }, + { name: 'Jack', cityId: 2 }, + ]); + + const res = await db.select({ + population: db.select({ count: count().as('count') }).from(users2Table).where( + eq(users2Table.cityId, citiesTable.id), + ).as( + 'population', + ), + name: citiesTable.name, + }).from(citiesTable); + + expectTypeOf(res).toEqualTypeOf<{ + population: number; + name: string; + }[]>(); + + expect(res).toStrictEqual([{ + population: 1, + name: 'Paris', + }, { + population: 2, + name: 'London', + }]); + }); + + test('select from a one subquery', async (ctx) => { + const { db } = ctx.pg; + + await db.insert(citiesTable) + .values([{ name: 'Paris' }, { name: 'London' }]); + + await db.insert(users2Table).values([ + { name: 'John', cityId: 1 }, + { name: 'Jane', cityId: 2 }, + { name: 'Jack', cityId: 2 }, + ]); + + const res = await db.select({ + cityName: db.select({ name: citiesTable.name }).from(citiesTable).where(eq(users2Table.cityId, citiesTable.id)) + .as( + 'cityName', + ), + name: users2Table.name, + }).from(users2Table); + + expectTypeOf(res).toEqualTypeOf<{ + cityName: string; + name: string; + }[]>(); + + expect(res).toStrictEqual([{ + cityName: 'Paris', + name: 'John', + }, { + cityName: 'London', + name: 'Jane', + }, { + cityName: 'London', + name: 'Jack', + }]); + }); + test('join subquery', async (ctx) => { const { db } = ctx.pg; diff --git a/integration-tests/tests/singlestore/singlestore-common.ts b/integration-tests/tests/singlestore/singlestore-common.ts index 896981619f..1d80d55e1a 100644 --- a/integration-tests/tests/singlestore/singlestore-common.ts +++ b/integration-tests/tests/singlestore/singlestore-common.ts @@ -1506,6 +1506,82 @@ export function tests(driver?: string) { ]); }); + test('select from a many subquery', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(citiesTable) + .values([{ name: 'Paris' }, { name: 'London' }]); + + await db.insert(users2Table).values([ + { name: 'John', cityId: 1 }, + { name: 'Jane', cityId: 2 }, + { name: 'Jack', cityId: 2 }, + ]); + + const res = await db.select({ + population: db.select({ count: count().as('count') }).from(users2Table).where( + eq(users2Table.cityId, citiesTable.id), + ).as( + 'population', + ), + name: citiesTable.name, + }).from(citiesTable); + + expectTypeOf(res).toEqualTypeOf< + { + population: number; + name: string; + }[] + >(); + + expect(res).toStrictEqual(expect.arrayContaining([{ + population: 1, + name: 'Paris', + }, { + population: 2, + name: 'London', + }])); + }); + + test('select from a one subquery', async (ctx) => { + const { db } = ctx.singlestore; + + await db.insert(citiesTable) + .values([{ name: 'Paris' }, { name: 'London' }]); + + await db.insert(users2Table).values([ + { name: 'John', cityId: 1 }, + { name: 'Jane', cityId: 2 }, + { name: 'Jack', cityId: 2 }, + ]); + + const res = await db.select({ + cityName: db.select({ name: citiesTable.name }).from(citiesTable).where(eq(users2Table.cityId, citiesTable.id)) + .as( + 'cityName', + ), + name: users2Table.name, + }).from(users2Table); + + expectTypeOf(res).toEqualTypeOf< + { + cityName: string; + name: string; + }[] + >(); + + expect(res).toStrictEqual(expect.arrayContaining([{ + cityName: 'Paris', + name: 'John', + }, { + cityName: 'London', + name: 'Jane', + }, { + cityName: 'London', + name: 'Jack', + }])); + }); + test('left join (all fields)', async (ctx) => { const { db } = ctx.singlestore; diff --git a/integration-tests/tests/sqlite/sqlite-common.ts b/integration-tests/tests/sqlite/sqlite-common.ts index 3af9c64b4d..786ac5b3d9 100644 --- a/integration-tests/tests/sqlite/sqlite-common.ts +++ b/integration-tests/tests/sqlite/sqlite-common.ts @@ -1159,6 +1159,78 @@ export function tests() { expect(inserted).toEqual({ id: 1, name: 'John' }); }); + test('select from a many subquery', async (ctx) => { + const { db } = ctx.sqlite; + + await db.insert(citiesTable) + .values([{ name: 'Paris' }, { name: 'London' }]); + + await db.insert(users2Table).values([ + { name: 'John', cityId: 1 }, + { name: 'Jane', cityId: 2 }, + { name: 'Jack', cityId: 2 }, + ]); + + const res = await db.select({ + population: db.select({ count: count().as('count') }).from(users2Table).where( + eq(users2Table.cityId, citiesTable.id), + ).as( + 'population', + ), + name: citiesTable.name, + }).from(citiesTable); + + expectTypeOf(res).toEqualTypeOf<{ + population: number; + name: string; + }[]>(); + + expect(res).toStrictEqual([{ + population: 1, + name: 'Paris', + }, { + population: 2, + name: 'London', + }]); + }); + + test('select from a one subquery', async (ctx) => { + const { db } = ctx.sqlite; + + await db.insert(citiesTable) + .values([{ name: 'Paris' }, { name: 'London' }]); + + await db.insert(users2Table).values([ + { name: 'John', cityId: 1 }, + { name: 'Jane', cityId: 2 }, + { name: 'Jack', cityId: 2 }, + ]); + + const res = await db.select({ + cityName: db.select({ name: citiesTable.name }).from(citiesTable).where(eq(users2Table.cityId, citiesTable.id)) + .as( + 'cityName', + ), + name: users2Table.name, + }).from(users2Table); + + expectTypeOf(res).toEqualTypeOf<{ + cityName: string; + name: string; + }[]>(); + + expect(res).toStrictEqual([{ + cityName: 'Paris', + name: 'John', + }, { + cityName: 'London', + name: 'Jane', + }, { + cityName: 'London', + name: 'Jack', + }]); + }); + test('join subquery', async (ctx) => { const { db } = ctx.sqlite; From 22e1986c4f86b2ab3cc184551f9417dba6be0d39 Mon Sep 17 00:00:00 2001 From: Jake Leventhal Date: Thu, 30 Oct 2025 13:38:23 -0400 Subject: [PATCH 04/10] Fix: Updated algorithm typo (#1676) * Fix: Updated algorithm typo (fixes #1601) --------- Co-authored-by: Andrii Sherman Co-authored-by: Sukairo-02 --- drizzle-kit/src/serializer/mysqlSerializer.ts | 2 +- drizzle-kit/src/serializer/singlestoreSerializer.ts | 2 +- drizzle-orm/src/mysql-core/indexes.ts | 8 ++++---- drizzle-orm/src/singlestore-core/indexes.ts | 8 ++++---- drizzle-orm/type-tests/mysql/tables.ts | 2 +- drizzle-orm/type-tests/singlestore/tables.ts | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/drizzle-kit/src/serializer/mysqlSerializer.ts b/drizzle-kit/src/serializer/mysqlSerializer.ts index aaa1acb823..322d8957f8 100644 --- a/drizzle-kit/src/serializer/mysqlSerializer.ts +++ b/drizzle-kit/src/serializer/mysqlSerializer.ts @@ -365,7 +365,7 @@ export const generateMySqlSnapshot = ( columns: indexColumns, isUnique: value.config.unique ?? false, using: value.config.using, - algorithm: value.config.algorythm, + algorithm: value.config.algorithm, lock: value.config.lock, }; }); diff --git a/drizzle-kit/src/serializer/singlestoreSerializer.ts b/drizzle-kit/src/serializer/singlestoreSerializer.ts index e65f53d258..edd09ec671 100644 --- a/drizzle-kit/src/serializer/singlestoreSerializer.ts +++ b/drizzle-kit/src/serializer/singlestoreSerializer.ts @@ -274,7 +274,7 @@ export const generateSingleStoreSnapshot = ( columns: indexColumns, isUnique: value.config.unique ?? false, using: value.config.using, - algorithm: value.config.algorythm, + algorithm: value.config.algorithm, lock: value.config.lock, }; }); diff --git a/drizzle-orm/src/mysql-core/indexes.ts b/drizzle-orm/src/mysql-core/indexes.ts index 5b73b1d309..39a803df74 100644 --- a/drizzle-orm/src/mysql-core/indexes.ts +++ b/drizzle-orm/src/mysql-core/indexes.ts @@ -19,9 +19,9 @@ interface IndexConfig { using?: 'btree' | 'hash'; /** - * If set, the index will be created as `create index ... algorythm { 'default' | 'inplace' | 'copy' }`. + * If set, the index will be created as `create index ... algorithm { 'default' | 'inplace' | 'copy' }`. */ - algorythm?: 'default' | 'inplace' | 'copy'; + algorithm?: 'default' | 'inplace' | 'copy'; /** * If set, adds locks to the index creation. @@ -67,8 +67,8 @@ export class IndexBuilder implements AnyIndexBuilder { return this; } - algorythm(algorythm: IndexConfig['algorythm']): this { - this.config.algorythm = algorythm; + algorithm(algorithm: IndexConfig['algorithm']): this { + this.config.algorithm = algorithm; return this; } diff --git a/drizzle-orm/src/singlestore-core/indexes.ts b/drizzle-orm/src/singlestore-core/indexes.ts index 3120cab1b6..00513cd80f 100644 --- a/drizzle-orm/src/singlestore-core/indexes.ts +++ b/drizzle-orm/src/singlestore-core/indexes.ts @@ -19,9 +19,9 @@ interface IndexConfig { using?: 'btree' | 'hash'; /** - * If set, the index will be created as `create index ... algorythm { 'default' | 'inplace' | 'copy' }`. + * If set, the index will be created as `create index ... algorithm { 'default' | 'inplace' | 'copy' }`. */ - algorythm?: 'default' | 'inplace' | 'copy'; + algorithm?: 'default' | 'inplace' | 'copy'; /** * If set, adds locks to the index creation. @@ -67,8 +67,8 @@ export class IndexBuilder implements AnyIndexBuilder { return this; } - algorythm(algorythm: IndexConfig['algorythm']): this { - this.config.algorythm = algorythm; + algorithm(algorithm: IndexConfig['algorithm']): this { + this.config.algorithm = algorithm; return this; } diff --git a/drizzle-orm/type-tests/mysql/tables.ts b/drizzle-orm/type-tests/mysql/tables.ts index 9874357fc9..6f6219d374 100644 --- a/drizzle-orm/type-tests/mysql/tables.ts +++ b/drizzle-orm/type-tests/mysql/tables.ts @@ -69,7 +69,7 @@ export const users = mysqlTable( uniqueClass: uniqueIndex('uniqueClass') .on(users.class, users.subClass) .lock('default') - .algorythm('copy') + .algorithm('copy') .using(`btree`), legalAge: check('legalAge', sql`${users.age1} > 18`), usersClassFK: foreignKey({ columns: [users.subClass], foreignColumns: [classes.subClass] }), diff --git a/drizzle-orm/type-tests/singlestore/tables.ts b/drizzle-orm/type-tests/singlestore/tables.ts index 7c8cb35a7b..a924648c41 100644 --- a/drizzle-orm/type-tests/singlestore/tables.ts +++ b/drizzle-orm/type-tests/singlestore/tables.ts @@ -66,7 +66,7 @@ export const users = singlestoreTable( uniqueClass: uniqueIndex('uniqueClass') .on(users.class, users.subClass) .lock('default') - .algorythm('copy') + .algorithm('copy') .using(`btree`), pk: primaryKey(users.age1, users.class), }), From 7722bd52ed7fb8f73c8dc5e12e17802cae7c76df Mon Sep 17 00:00:00 2001 From: Sukairo-02 Date: Fri, 31 Oct 2025 18:06:18 +0200 Subject: [PATCH 05/10] Version bump, added changelogs --- changelogs/drizzle-kit/0.31.7.md | 3 +++ changelogs/drizzle-orm/0.44.8.md | 3 +++ drizzle-kit/package.json | 2 +- drizzle-orm/package.json | 2 +- 4 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 changelogs/drizzle-kit/0.31.7.md create mode 100644 changelogs/drizzle-orm/0.44.8.md diff --git a/changelogs/drizzle-kit/0.31.7.md b/changelogs/drizzle-kit/0.31.7.md new file mode 100644 index 0000000000..b8d45a1519 --- /dev/null +++ b/changelogs/drizzle-kit/0.31.7.md @@ -0,0 +1,3 @@ +### Bug fixes + +- Fixed `algorythm` => `algorithm` typo \ No newline at end of file diff --git a/changelogs/drizzle-orm/0.44.8.md b/changelogs/drizzle-orm/0.44.8.md new file mode 100644 index 0000000000..a30720f36e --- /dev/null +++ b/changelogs/drizzle-orm/0.44.8.md @@ -0,0 +1,3 @@ +- Fixed pg-native Pool detection in node-postgres transactions +- Allowed subqueries in select fields +- Updated typo algorythm => algorithm \ No newline at end of file diff --git a/drizzle-kit/package.json b/drizzle-kit/package.json index 42ec228380..a87c8ed469 100644 --- a/drizzle-kit/package.json +++ b/drizzle-kit/package.json @@ -1,6 +1,6 @@ { "name": "drizzle-kit", - "version": "0.31.6", + "version": "0.31.7", "homepage": "https://orm.drizzle.team", "keywords": [ "drizzle", diff --git a/drizzle-orm/package.json b/drizzle-orm/package.json index 1c56429781..3551027aee 100644 --- a/drizzle-orm/package.json +++ b/drizzle-orm/package.json @@ -1,6 +1,6 @@ { "name": "drizzle-orm", - "version": "0.44.7", + "version": "0.44.8", "description": "Drizzle ORM package for SQL databases", "type": "module", "scripts": { From 82993ae8428a1a865505a87328be981d86d484a3 Mon Sep 17 00:00:00 2001 From: Sukairo-02 Date: Fri, 31 Oct 2025 18:09:24 +0200 Subject: [PATCH 06/10] Version fix --- changelogs/drizzle-orm/{0.44.8.md => 0.45.0.md} | 0 drizzle-orm/package.json | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename changelogs/drizzle-orm/{0.44.8.md => 0.45.0.md} (100%) diff --git a/changelogs/drizzle-orm/0.44.8.md b/changelogs/drizzle-orm/0.45.0.md similarity index 100% rename from changelogs/drizzle-orm/0.44.8.md rename to changelogs/drizzle-orm/0.45.0.md diff --git a/drizzle-orm/package.json b/drizzle-orm/package.json index 3551027aee..670b87a48f 100644 --- a/drizzle-orm/package.json +++ b/drizzle-orm/package.json @@ -1,6 +1,6 @@ { "name": "drizzle-orm", - "version": "0.44.8", + "version": "0.45.0", "description": "Drizzle ORM package for SQL databases", "type": "module", "scripts": { From 645a6f3e79d84f4f9b9e5498349aaaddfc5e8805 Mon Sep 17 00:00:00 2001 From: Sergey Reka <71607800+Sukairo-02@users.noreply.github.com> Date: Fri, 14 Nov 2025 12:36:01 +0200 Subject: [PATCH 07/10] Removed unneeded lefrover changes --- drizzle-orm/src/sqlite-core/dialect.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/drizzle-orm/src/sqlite-core/dialect.ts b/drizzle-orm/src/sqlite-core/dialect.ts index 27c414ca7e..401c4c796a 100644 --- a/drizzle-orm/src/sqlite-core/dialect.ts +++ b/drizzle-orm/src/sqlite-core/dialect.ts @@ -170,10 +170,6 @@ export abstract class SQLiteDialect { new SQL( query.queryChunks.map((c) => { if (is(c, Column)) { - if (c.columnType === 'SQLiteNumericBigInt') { - return chunk.push(sql`cast(${sql.identifier(this.casing.getColumnCasing(c))} as text)`); - } - return sql.identifier(this.casing.getColumnCasing(c)); } return c; From adf9bf1fb4074ae563024f1acd8a20a1c72136ac Mon Sep 17 00:00:00 2001 From: Sukairo-02 Date: Tue, 25 Nov 2025 16:57:47 +0200 Subject: [PATCH 08/10] Fixed `SQL` in `` (fixes #2388, rework of #2911 with handling in proper place) --- drizzle-orm/src/gel-core/dialect.ts | 3 +- drizzle-orm/src/mysql-core/dialect.ts | 3 +- drizzle-orm/src/pg-core/dialect.ts | 3 +- drizzle-orm/src/singlestore-core/dialect.ts | 3 +- drizzle-orm/src/sqlite-core/dialect.ts | 3 +- integration-tests/tests/gel/gel.test.ts | 37 ++++++++++++++++ integration-tests/tests/mysql/mysql-common.ts | 41 ++++++++++++++++++ integration-tests/tests/pg/pg-common.ts | 38 ++++++++++++++++ .../tests/singlestore/singlestore-common.ts | 39 +++++++++++++++++ .../tests/sqlite/sqlite-common.ts | 43 +++++++++++++++++++ 10 files changed, 208 insertions(+), 5 deletions(-) diff --git a/drizzle-orm/src/gel-core/dialect.ts b/drizzle-orm/src/gel-core/dialect.ts index a7f5924adc..6c1541286b 100644 --- a/drizzle-orm/src/gel-core/dialect.ts +++ b/drizzle-orm/src/gel-core/dialect.ts @@ -149,7 +149,8 @@ export class GelDialect { return sql.join(columnNames.flatMap((colName, i) => { const col = tableColumns[colName]!; - const value = set[colName] ?? sql.param(col.onUpdateFn!(), col); + const onUpdateFnResult = col.onUpdateFn?.(); + const value = set[colName] ?? (is(onUpdateFnResult, SQL) ? onUpdateFnResult : sql.param(onUpdateFnResult, col)); const res = sql`${sql.identifier(this.casing.getColumnCasing(col))} = ${value}`; if (i < setSize - 1) { diff --git a/drizzle-orm/src/mysql-core/dialect.ts b/drizzle-orm/src/mysql-core/dialect.ts index 0b2b709fc0..053ddc0ce4 100644 --- a/drizzle-orm/src/mysql-core/dialect.ts +++ b/drizzle-orm/src/mysql-core/dialect.ts @@ -144,7 +144,8 @@ export class MySqlDialect { return sql.join(columnNames.flatMap((colName, i) => { const col = tableColumns[colName]!; - const value = set[colName] ?? sql.param(col.onUpdateFn!(), col); + const onUpdateFnResult = col.onUpdateFn?.(); + const value = set[colName] ?? (is(onUpdateFnResult, SQL) ? onUpdateFnResult : sql.param(onUpdateFnResult, col)); const res = sql`${sql.identifier(this.casing.getColumnCasing(col))} = ${value}`; if (i < setSize - 1) { diff --git a/drizzle-orm/src/pg-core/dialect.ts b/drizzle-orm/src/pg-core/dialect.ts index 43f676b19a..be5ecdb237 100644 --- a/drizzle-orm/src/pg-core/dialect.ts +++ b/drizzle-orm/src/pg-core/dialect.ts @@ -160,7 +160,8 @@ export class PgDialect { return sql.join(columnNames.flatMap((colName, i) => { const col = tableColumns[colName]!; - const value = set[colName] ?? sql.param(col.onUpdateFn!(), col); + const onUpdateFnResult = col.onUpdateFn?.(); + const value = set[colName] ?? (is(onUpdateFnResult, SQL) ? onUpdateFnResult : sql.param(onUpdateFnResult, col)); const res = sql`${sql.identifier(this.casing.getColumnCasing(col))} = ${value}`; if (i < setSize - 1) { diff --git a/drizzle-orm/src/singlestore-core/dialect.ts b/drizzle-orm/src/singlestore-core/dialect.ts index 31aab80d68..b0791c35f3 100644 --- a/drizzle-orm/src/singlestore-core/dialect.ts +++ b/drizzle-orm/src/singlestore-core/dialect.ts @@ -143,7 +143,8 @@ export class SingleStoreDialect { return sql.join(columnNames.flatMap((colName, i) => { const col = tableColumns[colName]!; - const value = set[colName] ?? sql.param(col.onUpdateFn!(), col); + const onUpdateFnResult = col.onUpdateFn?.(); + const value = set[colName] ?? (is(onUpdateFnResult, SQL) ? onUpdateFnResult : sql.param(onUpdateFnResult, col)); const res = sql`${sql.identifier(this.casing.getColumnCasing(col))} = ${value}`; if (i < setSize - 1) { diff --git a/drizzle-orm/src/sqlite-core/dialect.ts b/drizzle-orm/src/sqlite-core/dialect.ts index 401c4c796a..317c8df12c 100644 --- a/drizzle-orm/src/sqlite-core/dialect.ts +++ b/drizzle-orm/src/sqlite-core/dialect.ts @@ -107,7 +107,8 @@ export abstract class SQLiteDialect { return sql.join(columnNames.flatMap((colName, i) => { const col = tableColumns[colName]!; - const value = set[colName] ?? sql.param(col.onUpdateFn!(), col); + const onUpdateFnResult = col.onUpdateFn?.(); + const value = set[colName] ?? (is(onUpdateFnResult, SQL) ? onUpdateFnResult : sql.param(onUpdateFnResult, col)); const res = sql`${sql.identifier(this.casing.getColumnCasing(col))} = ${value}`; if (i < setSize - 1) { diff --git a/integration-tests/tests/gel/gel.test.ts b/integration-tests/tests/gel/gel.test.ts index 15b2e6e026..3f0ec8a67c 100644 --- a/integration-tests/tests/gel/gel.test.ts +++ b/integration-tests/tests/gel/gel.test.ts @@ -540,6 +540,12 @@ describe('some', async () => { create required property age: int32; create required property city: str; };" --tls-security=${tlsSecurity} --dsn=${dsn}`; + + await $`gel query "CREATE TYPE default::users_on_update_sql { + create required property id1: int16; + create required property name: str; + create required property updated_at: datetime; + };" --tls-security=${tlsSecurity} --dsn=${dsn}`; }); afterEach(async () => { @@ -555,6 +561,7 @@ describe('some', async () => { await $`gel query "DELETE default::users1;" --tls-security=${tlsSecurity} --dsn=${dsn}`; await $`gel query "DELETE default::users2;" --tls-security=${tlsSecurity} --dsn=${dsn}`; await $`gel query "DELETE default::jsontest;" --tls-security=${tlsSecurity} --dsn=${dsn}`; + await $`gel query "DELETE default::users_on_update_sql;" --tls-security=${tlsSecurity} --dsn=${dsn}`; }); afterAll(async () => { @@ -601,6 +608,7 @@ describe('some', async () => { await $`gel query "DROP TYPE default::users_with_names" --tls-security=${tlsSecurity} --dsn=${dsn}`; await $`gel query "DROP MODULE mySchema;" --tls-security=${tlsSecurity} --dsn=${dsn}`; await $`gel query "DROP TYPE users_with_age;" --tls-security=${tlsSecurity} --dsn=${dsn}`; + await $`gel query "DROP TYPE default::users_on_update_sql;" --tls-security=${tlsSecurity} --dsn=${dsn}`; }); async function setupSetOperationTest(db: GelJsDatabase) { @@ -4801,6 +4809,35 @@ describe('some', async () => { expect(config2.enableRLS).toBeFalsy(); }); + test('test $onUpdateFn and $onUpdate works with sql value', async (ctx) => { + const { db } = ctx.gel; + + const users = gelTable('users_on_update_sql', { + id: integer('id1').notNull(), + name: text('name').notNull(), + updatedAt: timestamptz('updated_at').notNull().$onUpdate(() => sql`now()`), + }); + + const insertResp = await db.insert(users).values({ + id: 1, + name: 'John', + }).returning({ + updatedAt: users.updatedAt, + }); + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const now = Date.now(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + const updateResp = await db.update(users).set({ + name: 'John', + }).returning({ + updatedAt: users.updatedAt, + }); + + expect(new Date(insertResp[0]?.updatedAt.toISOString() ?? 0).getTime()).lessThan(now); + expect(new Date(updateResp[0]?.updatedAt.toISOString() ?? 0).getTime()).greaterThan(now); + }); + test('$count separate', async (ctx) => { const { db } = ctx.gel; diff --git a/integration-tests/tests/mysql/mysql-common.ts b/integration-tests/tests/mysql/mysql-common.ts index fa2511017f..bf72da46a5 100644 --- a/integration-tests/tests/mysql/mysql-common.ts +++ b/integration-tests/tests/mysql/mysql-common.ts @@ -3844,6 +3844,47 @@ export function tests(driver?: string) { await db.execute(sql`drop view ${newYorkers1}`); }); + test('test $onUpdateFn and $onUpdate works with sql value', async (ctx) => { + const { db } = ctx.mysql; + + const users = mysqlTable('users', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + updatedAt: timestamp('updated_at', { + fsp: 6, + }) + .notNull() + .$onUpdate(() => sql`current_timestamp`), + }); + + await db.execute(sql`drop table if exists ${users}`); + await db.execute( + sql` + create table ${users} ( + \`id\` serial primary key, + \`name\` text not null, + \`updated_at\` timestamp not null + ) + `, + ); + + await db.insert(users).values({ + name: 'John', + }); + const insertResp = await db.select({ updatedAt: users.updatedAt }).from(users); + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const now = Date.now(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + await db.update(users).set({ + name: 'John', + }); + const updateResp = await db.select({ updatedAt: users.updatedAt }).from(users); + + expect(insertResp[0]?.updatedAt.getTime() ?? 0).lessThan(now); + expect(updateResp[0]?.updatedAt.getTime() ?? 0).greaterThan(now); + }); + test('$count separate', async (ctx) => { const { db } = ctx.mysql; diff --git a/integration-tests/tests/pg/pg-common.ts b/integration-tests/tests/pg/pg-common.ts index 35a2ce721c..f929944a73 100644 --- a/integration-tests/tests/pg/pg-common.ts +++ b/integration-tests/tests/pg/pg-common.ts @@ -5547,6 +5547,44 @@ export function tests() { expect(config2.enableRLS).toBeFalsy(); }); + test('test $onUpdateFn and $onUpdate works with sql value', async (ctx) => { + const { db } = ctx.pg; + + const users = pgTable('users_on_update', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + updatedAt: timestamp('updated_at', { mode: 'date' }).notNull().$onUpdate(() => sql`now()`), + }); + + await db.execute( + sql` + create table ${users} ( + "id" serial primary key, + "name" text not null, + "updated_at" timestamp(3) + ) + `, + ); + + const insertResp = await db.insert(users).values({ + name: 'John', + }).returning({ + updatedAt: users.updatedAt, + }); + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const now = Date.now(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + const updateResp = await db.update(users).set({ + name: 'John', + }).returning({ + updatedAt: users.updatedAt, + }); + + expect(insertResp[0]?.updatedAt.getTime() ?? 0).lessThan(now); + expect(updateResp[0]?.updatedAt.getTime() ?? 0).greaterThan(now); + }); + test('$count separate', async (ctx) => { const { db } = ctx.pg; diff --git a/integration-tests/tests/singlestore/singlestore-common.ts b/integration-tests/tests/singlestore/singlestore-common.ts index 1d80d55e1a..ae8231def6 100644 --- a/integration-tests/tests/singlestore/singlestore-common.ts +++ b/integration-tests/tests/singlestore/singlestore-common.ts @@ -3854,6 +3854,45 @@ export function tests(driver?: string) { ]); }); + test('test $onUpdateFn and $onUpdate works with sql value', async (ctx) => { + const { db } = ctx.singlestore; + + const users = singlestoreTable('users', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + updatedAt: timestamp('updated_at') + .notNull() + .$onUpdate(() => sql`current_timestamp`), + }); + + await db.execute(sql`drop table if exists ${users}`); + await db.execute( + sql` + create table ${users} ( + \`id\` serial primary key, + \`name\` text not null, + \`updated_at\` timestamp not null + ) + `, + ); + + await db.insert(users).values({ + name: 'John', + }); + const insertResp = await db.select({ updatedAt: users.updatedAt }).from(users); + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const now = Date.now(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + await db.update(users).set({ + name: 'John', + }); + const updateResp = await db.select({ updatedAt: users.updatedAt }).from(users); + + expect(insertResp[0]?.updatedAt.getTime() ?? 0).lessThan(now); + expect(updateResp[0]?.updatedAt.getTime() ?? 0).greaterThan(now); + }); + test('all types', async (ctx) => { const { db } = ctx.singlestore; diff --git a/integration-tests/tests/sqlite/sqlite-common.ts b/integration-tests/tests/sqlite/sqlite-common.ts index 786ac5b3d9..542ef24ba2 100644 --- a/integration-tests/tests/sqlite/sqlite-common.ts +++ b/integration-tests/tests/sqlite/sqlite-common.ts @@ -3062,6 +3062,49 @@ export function tests() { } }); + test('test $onUpdateFn and $onUpdate works with sql value', async (ctx) => { + const { db } = ctx.sqlite; + + const users = sqliteTable('users', { + id: integer('id').primaryKey({ autoIncrement: true }), + name: text('name').notNull(), + updatedAt: integer('updated_at') + .notNull() + .$onUpdate(() => + sql`(strftime('%s', 'now') * 1000) + (strftime('%f', 'now') - strftime('%S', 'now')) * 1000` + ), + }); + + await db.run(sql`drop table if exists ${users}`); + await db.run( + sql` + create table ${users} ( + \`id\` integer primary key autoincrement, + \`name\` text not null, + \`updated_at\` integer not null + ) + `, + ); + + const insertResp = await db.insert(users).values({ + name: 'John', + }).returning({ + updatedAt: users.updatedAt, + }); + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const now = Date.now(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + const updateResp = await db.update(users).set({ + name: 'John', + }).returning({ + updatedAt: users.updatedAt, + }); + + expect(insertResp[0]?.updatedAt ?? 0).lessThan(now); + expect(updateResp[0]?.updatedAt ?? 0).greaterThan(now); + }); + test('$count separate', async (ctx) => { const { db } = ctx.sqlite; From 74b85ae259036cd4f1becc040387df538c2a8e32 Mon Sep 17 00:00:00 2001 From: Sukairo-02 Date: Tue, 25 Nov 2025 18:51:54 +0200 Subject: [PATCH 09/10] Fixed `bun-sql:postgresql` date, timestamp mappers not accounting for `Date` instances in driver response, updated changelogs --- changelogs/drizzle-orm/0.45.0.md | 4 +- drizzle-orm/src/pg-core/columns/date.ts | 12 +- drizzle-orm/src/pg-core/columns/timestamp.ts | 21 +- integration-tests/tests/bun/bun-sql.test.ts | 494 ++++++++++++++++++- 4 files changed, 522 insertions(+), 9 deletions(-) diff --git a/changelogs/drizzle-orm/0.45.0.md b/changelogs/drizzle-orm/0.45.0.md index a30720f36e..fb21990e7c 100644 --- a/changelogs/drizzle-orm/0.45.0.md +++ b/changelogs/drizzle-orm/0.45.0.md @@ -1,3 +1,5 @@ - Fixed pg-native Pool detection in node-postgres transactions - Allowed subqueries in select fields -- Updated typo algorythm => algorithm \ No newline at end of file +- Updated typo algorythm => algorithm +- Fixed `$onUpdate` not handling `SQL` values (fixes [#2388](https://github.com/drizzle-team/drizzle-orm/issues/2388)) +- Fixed `pg` mappers not handling `Date` instances in `bun-sql:postgresql` driver responses for `date`, `timestamp` types (fixes [#4493](https://github.com/drizzle-team/drizzle-orm/issues/4493)) \ No newline at end of file diff --git a/drizzle-orm/src/pg-core/columns/date.ts b/drizzle-orm/src/pg-core/columns/date.ts index 54f3d1a0ea..51ae993a62 100644 --- a/drizzle-orm/src/pg-core/columns/date.ts +++ b/drizzle-orm/src/pg-core/columns/date.ts @@ -37,8 +37,10 @@ export class PgDate> extends PgColu return 'date'; } - override mapFromDriverValue(value: string): Date { - return new Date(value); + override mapFromDriverValue(value: string | Date): Date { + if (typeof value === 'string') return new Date(value); + + return value; } override mapToDriverValue(value: Date): string { @@ -81,6 +83,12 @@ export class PgDateString> getSQLType(): string { return 'date'; } + + override mapFromDriverValue(value: Date | string): string { + if (typeof value === 'string') return value; + + return value.toISOString().slice(0, -14); + } } export interface PgDateConfig { diff --git a/drizzle-orm/src/pg-core/columns/timestamp.ts b/drizzle-orm/src/pg-core/columns/timestamp.ts index f5a5cb0a6d..647e3f20e7 100644 --- a/drizzle-orm/src/pg-core/columns/timestamp.ts +++ b/drizzle-orm/src/pg-core/columns/timestamp.ts @@ -58,9 +58,11 @@ export class PgTimestamp> exte return `timestamp${precision}${this.withTimezone ? ' with time zone' : ''}`; } - override mapFromDriverValue = (value: string): Date | null => { - return new Date(this.withTimezone ? value : value + '+0000'); - }; + override mapFromDriverValue(value: Date | string): Date { + if (typeof value === 'string') return new Date(this.withTimezone ? value : value + '+0000'); + + return value; + } override mapToDriverValue = (value: Date): string => { return value.toISOString(); @@ -121,6 +123,19 @@ export class PgTimestampString(), }); -let pgContainer: Docker.Container; +const en = pgEnum('en', ['enVal1', 'enVal2']); + +const allTypesTable = pgTable('all_types', { + serial: serial('serial'), + bigserial53: bigserial('bigserial53', { + mode: 'number', + }), + bigserial64: bigserial('bigserial64', { + mode: 'bigint', + }), + int: integer('int'), + bigint53: bigint('bigint53', { + mode: 'number', + }), + bigint64: bigint('bigint64', { + mode: 'bigint', + }), + bool: boolean('bool'), + char: char('char'), + cidr: cidr('cidr'), + date: date('date', { + mode: 'date', + }), + dateStr: date('date_str', { + mode: 'string', + }), + double: doublePrecision('double'), + enum: en('enum'), + inet: inet('inet'), + interval: interval('interval'), + json: json('json'), + jsonb: jsonb('jsonb'), + line: line('line', { + mode: 'abc', + }), + lineTuple: line('line_tuple', { + mode: 'tuple', + }), + macaddr: macaddr('macaddr'), + macaddr8: macaddr8('macaddr8'), + numeric: numeric('numeric'), + numericNum: numeric('numeric_num', { + mode: 'number', + }), + numericBig: numeric('numeric_big', { + mode: 'bigint', + }), + point: point('point', { + mode: 'xy', + }), + pointTuple: point('point_tuple', { + mode: 'tuple', + }), + real: real('real'), + smallint: smallint('smallint'), + smallserial: smallserial('smallserial'), + text: text('text'), + time: time('time'), + timestamp: timestamp('timestamp', { + mode: 'date', + }), + timestampTz: timestamp('timestamp_tz', { + mode: 'date', + withTimezone: true, + }), + timestampStr: timestamp('timestamp_str', { + mode: 'string', + }), + timestampTzStr: timestamp('timestamp_tz_str', { + mode: 'string', + withTimezone: true, + }), + uuid: uuid('uuid'), + varchar: varchar('varchar'), + arrint: integer('arrint').array(), + arrbigint53: bigint('arrbigint53', { + mode: 'number', + }).array(), + arrbigint64: bigint('arrbigint64', { + mode: 'bigint', + }).array(), + arrbool: boolean('arrbool').array(), + arrchar: char('arrchar').array(), + arrcidr: cidr('arrcidr').array(), + arrdate: date('arrdate', { + mode: 'date', + }).array(), + arrdateStr: date('arrdate_str', { + mode: 'string', + }).array(), + arrdouble: doublePrecision('arrdouble').array(), + arrenum: en('arrenum').array(), + arrinet: inet('arrinet').array(), + arrinterval: interval('arrinterval').array(), + arrjson: json('arrjson').array(), + arrjsonb: jsonb('arrjsonb').array(), + arrline: line('arrline', { + mode: 'abc', + }).array(), + arrlineTuple: line('arrline_tuple', { + mode: 'tuple', + }).array(), + arrmacaddr: macaddr('arrmacaddr').array(), + arrmacaddr8: macaddr8('arrmacaddr8').array(), + arrnumeric: numeric('arrnumeric').array(), + arrnumericNum: numeric('arrnumeric_num', { + mode: 'number', + }).array(), + arrnumericBig: numeric('arrnumeric_big', { + mode: 'bigint', + }).array(), + arrpoint: point('arrpoint', { + mode: 'xy', + }).array(), + arrpointTuple: point('arrpoint_tuple', { + mode: 'tuple', + }).array(), + arrreal: real('arrreal').array(), + arrsmallint: smallint('arrsmallint').array(), + arrtext: text('arrtext').array(), + arrtime: time('arrtime').array(), + arrtimestamp: timestamp('arrtimestamp', { + mode: 'date', + }).array(), + arrtimestampTz: timestamp('arrtimestamp_tz', { + mode: 'date', + withTimezone: true, + }).array(), + arrtimestampStr: timestamp('arrtimestamp_str', { + mode: 'string', + }).array(), + arrtimestampTzStr: timestamp('arrtimestamp_tz_str', { + mode: 'string', + withTimezone: true, + }).array(), + arruuid: uuid('arruuid').array(), + arrvarchar: varchar('arrvarchar').array(), +}); + +let pgContainer: Docker.Container | undefined; afterAll(async () => { await pgContainer?.stop().catch(console.error); @@ -209,7 +357,6 @@ let db: BunSQLDatabase; let client: BunSQL; beforeAll(async () => { - console.log('here'); const connectionString = process.env['PG_CONNECTION_STRING']; client = await retry(async () => { // @ts-expect-error @@ -937,7 +1084,7 @@ test('build query', async () => { }); }); -test.only('insert sql', async () => { +test('insert sql', async () => { await db.insert(usersTable).values({ name: sql`${'John'}` }); const result = await db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable); expect(result).toEqual([{ id: 1, name: 'John' }]); @@ -5176,3 +5323,344 @@ test('sql operator as cte', async () => { expect(result1).toEqual([{ userId: 1, data: { name: 'John' } }]); expect(result2).toEqual([{ userId: 2, data: { name: 'Jane' } }]); }); + +test('all types', async () => { + await db.execute(sql`CREATE TYPE "public"."en" AS ENUM('enVal1', 'enVal2');`); + await db.execute(sql` + CREATE TABLE "all_types" ( + "serial" serial NOT NULL, + "bigserial53" bigserial NOT NULL, + "bigserial64" bigserial, + "int" integer, + "bigint53" bigint, + "bigint64" bigint, + "bool" boolean, + "char" char, + "cidr" "cidr", + "date" date, + "date_str" date, + "double" double precision, + "enum" "en", + "inet" "inet", + "interval" interval, + "json" json, + "jsonb" jsonb, + "line" "line", + "line_tuple" "line", + "macaddr" "macaddr", + "macaddr8" "macaddr8", + "numeric" numeric, + "numeric_num" numeric, + "numeric_big" numeric, + "point" "point", + "point_tuple" "point", + "real" real, + "smallint" smallint, + "smallserial" "smallserial" NOT NULL, + "text" text, + "time" time, + "timestamp" timestamp, + "timestamp_tz" timestamp with time zone, + "timestamp_str" timestamp, + "timestamp_tz_str" timestamp with time zone, + "uuid" uuid, + "varchar" varchar, + "arrint" integer[], + "arrbigint53" bigint[], + "arrbigint64" bigint[], + "arrbool" boolean[], + "arrchar" char[], + "arrcidr" "cidr"[], + "arrdate" date[], + "arrdate_str" date[], + "arrdouble" double precision[], + "arrenum" "en"[], + "arrinet" "inet"[], + "arrinterval" interval[], + "arrjson" json[], + "arrjsonb" jsonb[], + "arrline" "line"[], + "arrline_tuple" "line"[], + "arrmacaddr" "macaddr"[], + "arrmacaddr8" "macaddr8"[], + "arrnumeric" numeric[], + "arrnumeric_num" numeric[], + "arrnumeric_big" numeric[], + "arrpoint" "point"[], + "arrpoint_tuple" "point"[], + "arrreal" real[], + "arrsmallint" smallint[], + "arrtext" text[], + "arrtime" time[], + "arrtimestamp" timestamp[], + "arrtimestamp_tz" timestamp with time zone[], + "arrtimestamp_str" timestamp[], + "arrtimestamp_tz_str" timestamp with time zone[], + "arruuid" uuid[], + "arrvarchar" varchar[] + ); + `); + + await db.insert(allTypesTable).values({ + serial: 1, + smallserial: 15, + bigint53: 9007199254740991, + bigint64: 5044565289845416380n, + bigserial53: 9007199254740991, + bigserial64: 5044565289845416380n, + bool: true, + char: 'c', + cidr: '2001:4f8:3:ba:2e0:81ff:fe22:d1f1/128', + inet: '192.168.0.1/24', + macaddr: '08:00:2b:01:02:03', + macaddr8: '08:00:2b:01:02:03:04:05', + date: new Date(1741743161623), + dateStr: new Date(1741743161623).toISOString(), + double: 15.35325689124218, + enum: 'enVal1', + int: 621, + interval: '2 months ago', + json: { + str: 'strval', + arr: ['str', 10], + }, + jsonb: { + str: 'strvalb', + arr: ['strb', 11], + }, + line: { + a: 1, + b: 2, + c: 3, + }, + lineTuple: [1, 2, 3], + numeric: '475452353476', + numericNum: 9007199254740991, + numericBig: 5044565289845416380n, + point: { + x: 24.5, + y: 49.6, + }, + pointTuple: [57.2, 94.3], + real: 1.048596, + smallint: 10, + text: 'TEXT STRING', + time: '13:59:28', + timestamp: new Date(1741743161623), + timestampTz: new Date(1741743161623), + timestampStr: new Date(1741743161623).toISOString(), + timestampTzStr: new Date(1741743161623).toISOString(), + uuid: 'b77c9eef-8e28-4654-88a1-7221b46d2a1c', + varchar: 'C4-', + arrbigint53: [9007199254740991], + arrbigint64: [5044565289845416380n], + arrbool: [true], + arrchar: ['c'], + arrcidr: ['2001:4f8:3:ba:2e0:81ff:fe22:d1f1/128'], + arrinet: ['192.168.0.1/24'], + arrmacaddr: ['08:00:2b:01:02:03'], + arrmacaddr8: ['08:00:2b:01:02:03:04:05'], + arrdate: [new Date(1741743161623)], + arrdateStr: [new Date(1741743161623).toISOString()], + arrdouble: [15.35325689124218], + arrenum: ['enVal1'], + arrint: [621], + arrinterval: ['2 months ago'], + arrjson: [{ + str: 'strval', + arr: ['str', 10], + }], + arrjsonb: [{ + str: 'strvalb', + arr: ['strb', 11], + }], + arrline: [{ + a: 1, + b: 2, + c: 3, + }], + arrlineTuple: [[1, 2, 3]], + arrnumeric: ['475452353476'], + arrnumericNum: [9007199254740991], + arrnumericBig: [5044565289845416380n], + arrpoint: [{ + x: 24.5, + y: 49.6, + }], + arrpointTuple: [[57.2, 94.3]], + arrreal: [1.048596], + arrsmallint: [10], + arrtext: ['TEXT STRING'], + arrtime: ['13:59:28'], + arrtimestamp: [new Date(1741743161623)], + arrtimestampTz: [new Date(1741743161623)], + arrtimestampStr: [new Date(1741743161623).toISOString()], + arrtimestampTzStr: [new Date(1741743161623).toISOString()], + arruuid: ['b77c9eef-8e28-4654-88a1-7221b46d2a1c'], + arrvarchar: ['C4-'], + }); + + const rawRes = await db.select().from(allTypesTable); + + type ExpectedType = { + serial: number; + bigserial53: number; + bigserial64: bigint; + int: number | null; + bigint53: number | null; + bigint64: bigint | null; + bool: boolean | null; + char: string | null; + cidr: string | null; + date: Date | null; + dateStr: string | null; + double: number | null; + enum: 'enVal1' | 'enVal2' | null; + inet: string | null; + interval: string | null; + json: unknown; + jsonb: unknown; + line: { + a: number; + b: number; + c: number; + } | null; + lineTuple: [number, number, number] | null; + macaddr: string | null; + macaddr8: string | null; + numeric: string | null; + numericNum: number | null; + numericBig: bigint | null; + point: { + x: number; + y: number; + } | null; + pointTuple: [number, number] | null; + real: number | null; + smallint: number | null; + smallserial: number; + text: string | null; + time: string | null; + timestamp: Date | null; + timestampTz: Date | null; + timestampStr: string | null; + timestampTzStr: string | null; + uuid: string | null; + varchar: string | null; + arrint: number[] | null; + arrbigint53: number[] | null; + arrbigint64: bigint[] | null; + arrbool: boolean[] | null; + arrchar: string[] | null; + arrcidr: string[] | null; + arrdate: Date[] | null; + arrdateStr: string[] | null; + arrdouble: number[] | null; + arrenum: ('enVal1' | 'enVal2')[] | null; + arrinet: string[] | null; + arrinterval: string[] | null; + arrjson: unknown[] | null; + arrjsonb: unknown[] | null; + arrline: { + a: number; + b: number; + c: number; + }[] | null; + arrlineTuple: [number, number, number][] | null; + arrmacaddr: string[] | null; + arrmacaddr8: string[] | null; + arrnumeric: string[] | null; + arrnumericNum: number[] | null; + arrnumericBig: bigint[] | null; + arrpoint: { x: number; y: number }[] | null; + arrpointTuple: [number, number][] | null; + arrreal: number[] | null; + arrsmallint: number[] | null; + arrtext: string[] | null; + arrtime: string[] | null; + arrtimestamp: Date[] | null; + arrtimestampTz: Date[] | null; + arrtimestampStr: string[] | null; + arrtimestampTzStr: string[] | null; + arruuid: string[] | null; + arrvarchar: string[] | null; + }[]; + + const expectedRes: ExpectedType = [ + { + serial: 1, + bigserial53: 9007199254740991, + bigserial64: 5044565289845416380n, + int: 621, + bigint53: 9007199254740991, + bigint64: 5044565289845416380n, + bool: true, + char: 'c', + cidr: '2001:4f8:3:ba:2e0:81ff:fe22:d1f1/128', + date: new Date('2025-03-12T00:00:00.000Z'), + dateStr: '2025-03-12', + double: 15.35325689124218, + enum: 'enVal1', + inet: '192.168.0.1/24', + interval: '-2 mons', + json: { str: 'strval', arr: ['str', 10] }, + jsonb: { arr: ['strb', 11], str: 'strvalb' }, + line: { a: 1, b: 2, c: 3 }, + lineTuple: [1, 2, 3], + macaddr: '08:00:2b:01:02:03', + macaddr8: '08:00:2b:01:02:03:04:05', + numeric: '475452353476', + numericNum: 9007199254740991, + numericBig: 5044565289845416380n, + point: { x: 24.5, y: 49.6 }, + pointTuple: [57.2, 94.3], + real: 1.048596, + smallint: 10, + smallserial: 15, + text: 'TEXT STRING', + time: '13:59:28', + timestamp: new Date('2025-03-12T01:32:41.623Z'), + timestampTz: new Date('2025-03-12T01:32:41.623Z'), + timestampStr: '2025-03-12 01:32:41.623', + timestampTzStr: '2025-03-12 01:32:41.623+00', + uuid: 'b77c9eef-8e28-4654-88a1-7221b46d2a1c', + varchar: 'C4-', + arrint: [621], + arrbigint53: [9007199254740991], + arrbigint64: [5044565289845416380n], + arrbool: [true], + arrchar: ['c'], + arrcidr: ['2001:4f8:3:ba:2e0:81ff:fe22:d1f1/128'], + arrdate: [new Date('2025-03-12T00:00:00.000Z')], + arrdateStr: ['2025-03-12'], + arrdouble: [15.35325689124218], + arrenum: ['enVal1'], + arrinet: ['192.168.0.1/24'], + arrinterval: ['-2 mons'], + arrjson: [{ str: 'strval', arr: ['str', 10] }], + arrjsonb: [{ arr: ['strb', 11], str: 'strvalb' }], + arrline: [{ a: 1, b: 2, c: 3 }], + arrlineTuple: [[1, 2, 3]], + arrmacaddr: ['08:00:2b:01:02:03'], + arrmacaddr8: ['08:00:2b:01:02:03:04:05'], + arrnumeric: ['475452353476'], + arrnumericNum: [9007199254740991], + arrnumericBig: [5044565289845416380n], + arrpoint: [{ x: 24.5, y: 49.6 }], + arrpointTuple: [[57.2, 94.3]], + arrreal: [1.048596], + arrsmallint: [10], + arrtext: ['TEXT STRING'], + arrtime: ['13:59:28'], + arrtimestamp: [new Date('2025-03-12T01:32:41.623Z')], + arrtimestampTz: [new Date('2025-03-12T01:32:41.623Z')], + arrtimestampStr: ['2025-03-12 01:32:41.623'], + arrtimestampTzStr: ['2025-03-12 01:32:41.623+00'], + arruuid: ['b77c9eef-8e28-4654-88a1-7221b46d2a1c'], + arrvarchar: ['C4-'], + }, + ]; + + Expect>; + expect(rawRes).toStrictEqual(expectedRes); +}); From 5abed3dd5c618a0179832b0b34e2ff7c08d69408 Mon Sep 17 00:00:00 2001 From: Sukairo-02 Date: Tue, 25 Nov 2025 20:20:07 +0200 Subject: [PATCH 10/10] Credited author of copied tests in changelog --- changelogs/drizzle-orm/0.45.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/drizzle-orm/0.45.0.md b/changelogs/drizzle-orm/0.45.0.md index fb21990e7c..f7ba9e87da 100644 --- a/changelogs/drizzle-orm/0.45.0.md +++ b/changelogs/drizzle-orm/0.45.0.md @@ -1,5 +1,5 @@ - Fixed pg-native Pool detection in node-postgres transactions - Allowed subqueries in select fields - Updated typo algorythm => algorithm -- Fixed `$onUpdate` not handling `SQL` values (fixes [#2388](https://github.com/drizzle-team/drizzle-orm/issues/2388)) +- Fixed `$onUpdate` not handling `SQL` values (fixes [#2388](https://github.com/drizzle-team/drizzle-orm/issues/2388), tests implemented by [L-Mario564](https://github.com/L-Mario564) in [#2911](https://github.com/drizzle-team/drizzle-orm/pull/2911)) - Fixed `pg` mappers not handling `Date` instances in `bun-sql:postgresql` driver responses for `date`, `timestamp` types (fixes [#4493](https://github.com/drizzle-team/drizzle-orm/issues/4493)) \ No newline at end of file