diff --git a/changelogs/drizzle-orm/0.30.10.md b/changelogs/drizzle-orm/0.30.10.md new file mode 100644 index 000000000..18ed4f51c --- /dev/null +++ b/changelogs/drizzle-orm/0.30.10.md @@ -0,0 +1,17 @@ +## New Features + +### 🎉 `.if()` function added to all WHERE expressions + +#### Select all users after cursors if a cursor value was provided + +```ts +function getUsersAfter(cursor?: number) { + return db.select().from(users).where( + gt(users.id, cursor).if(cursor) + ); +} +``` + +## Bug Fixes + +- Fixed internal mappings for sessions `.all`, `.values`, `.execute` functions in AWS DataAPI diff --git a/drizzle-orm/package.json b/drizzle-orm/package.json index b26382698..771407a4b 100644 --- a/drizzle-orm/package.json +++ b/drizzle-orm/package.json @@ -1,6 +1,6 @@ { "name": "drizzle-orm", - "version": "0.30.9", + "version": "0.30.10", "description": "Drizzle ORM package for SQL databases", "type": "module", "scripts": { diff --git a/drizzle-orm/src/aws-data-api/pg/session.ts b/drizzle-orm/src/aws-data-api/pg/session.ts index fdb6d5d98..353a77cf3 100644 --- a/drizzle-orm/src/aws-data-api/pg/session.ts +++ b/drizzle-orm/src/aws-data-api/pg/session.ts @@ -24,7 +24,9 @@ import { getValueFromDataApi, toValueParam } from '../common/index.ts'; export type AwsDataApiClient = RDSDataClient; -export class AwsDataApiPreparedQuery extends PgPreparedQuery { +export class AwsDataApiPreparedQuery< + T extends PreparedQueryConfig & { values: AwsDataApiPgQueryResult }, +> extends PgPreparedQuery { static readonly [entityKind]: string = 'AwsDataApiPreparedQuery'; private rawQuery: ExecuteStatementCommand; @@ -56,18 +58,44 @@ export class AwsDataApiPreparedQuery extends PgPr async execute(placeholderValues: Record | undefined = {}): Promise { const { fields, joinsNotNullableMap, customResultMapper } = this; - const result = await this.values(placeholderValues) as AwsDataApiPgQueryResult; + const result = await this.values(placeholderValues); if (!fields && !customResultMapper) { - return result as T['execute']; + const { columnMetadata, rows } = result; + if (!columnMetadata) { + return result; + } + const mappedRows = rows.map((sourceRow) => { + const row: Record = {}; + for (const [index, value] of sourceRow.entries()) { + const metadata = columnMetadata[index]; + if (!metadata) { + throw new Error( + `Unexpected state: no column metadata found for index ${index}. Please report this issue on GitHub: https://github.com/drizzle-team/drizzle-orm/issues/new/choose`, + ); + } + if (!metadata.name) { + throw new Error( + `Unexpected state: no column name for index ${index} found in the column metadata. Please report this issue on GitHub: https://github.com/drizzle-team/drizzle-orm/issues/new/choose`, + ); + } + row[metadata.name] = value; + } + return row; + }); + return Object.assign(result, { rows: mappedRows }); } + return customResultMapper ? customResultMapper(result.rows!) - : result.rows!.map((row) => mapResultRow(fields!, row, joinsNotNullableMap)); + : result.rows!.map((row) => mapResultRow(fields!, row, joinsNotNullableMap)); } async all(placeholderValues?: Record | undefined): Promise { - const result = await this.execute(placeholderValues) as AwsDataApiPgQueryResult; - return result.rows; + const result = await this.execute(placeholderValues); + if (!this.fields && !this.customResultMapper) { + return (result as AwsDataApiPgQueryResult).rows; + } + return result; } async values(placeholderValues: Record = {}): Promise { @@ -80,20 +108,7 @@ export class AwsDataApiPreparedQuery extends PgPr this.options.logger?.logQuery(this.rawQuery.input.sql!, this.rawQuery.input.parameters); - const { fields, rawQuery, client, customResultMapper } = this; - if (!fields && !customResultMapper) { - const result = await client.send(rawQuery); - if (result.columnMetadata && result.columnMetadata.length > 0) { - const rows = this.mapResultRows(result.records ?? [], result.columnMetadata); - return { - ...result, - rows, - }; - } - return result; - } - - const result = await client.send(rawQuery); + const result = await this.client.send(this.rawQuery); const rows = result.records?.map((row) => { return row.map((field) => getValueFromDataApi(field)); }) ?? []; @@ -161,14 +176,20 @@ export class AwsDataApiSession< }; } - prepareQuery( + prepareQuery< + T extends PreparedQueryConfig & { + values: AwsDataApiPgQueryResult; + } = PreparedQueryConfig & { + values: AwsDataApiPgQueryResult; + }, + >( query: QueryWithTypings, fields: SelectedFieldsOrdered | undefined, name: string | undefined, isResponseInArrayMode: boolean, customResultMapper?: (rows: unknown[][]) => T['execute'], transactionId?: string, - ): PgPreparedQuery { + ): AwsDataApiPreparedQuery { return new AwsDataApiPreparedQuery( this.client, query.sql, @@ -183,7 +204,7 @@ export class AwsDataApiSession< } override execute(query: SQL): Promise { - return this.prepareQuery( + return this.prepareQuery }>( this.dialect.sqlToQuery(query), undefined, undefined, diff --git a/drizzle-orm/src/pg-proxy/driver.ts b/drizzle-orm/src/pg-proxy/driver.ts index 2c03dfb61..cdffa15c1 100644 --- a/drizzle-orm/src/pg-proxy/driver.ts +++ b/drizzle-orm/src/pg-proxy/driver.ts @@ -18,13 +18,15 @@ export type RemoteCallback = ( sql: string, params: any[], method: 'all' | 'execute', + typings?: any[], ) => Promise<{ rows: any[] }>; export function drizzle = Record>( callback: RemoteCallback, config: DrizzleConfig = {}, + _dialect: () => PgDialect = () => new PgDialect(), ): PgRemoteDatabase { - const dialect = new PgDialect(); + const dialect = _dialect(); let logger; if (config.logger === true) { logger = new DefaultLogger(); diff --git a/drizzle-orm/src/pg-proxy/session.ts b/drizzle-orm/src/pg-proxy/session.ts index d46ea037a..386d830f7 100644 --- a/drizzle-orm/src/pg-proxy/session.ts +++ b/drizzle-orm/src/pg-proxy/session.ts @@ -7,7 +7,8 @@ import type { SelectedFieldsOrdered } from '~/pg-core/query-builders/select.type import type { PgTransactionConfig, PreparedQueryConfig, QueryResultHKT } from '~/pg-core/session.ts'; import { PgPreparedQuery as PreparedQueryBase, PgSession } from '~/pg-core/session.ts'; import type { RelationalSchemaConfig, TablesRelationalConfig } from '~/relations.ts'; -import { fillPlaceholders, type Query } from '~/sql/sql.ts'; +import type { QueryWithTypings } from '~/sql/sql.ts'; +import { fillPlaceholders } from '~/sql/sql.ts'; import { tracer } from '~/tracing.ts'; import { type Assume, mapResultRow } from '~/utils.ts'; import type { RemoteCallback } from './driver.ts'; @@ -35,7 +36,7 @@ export class PgRemoteSession< } prepareQuery( - query: Query, + query: QueryWithTypings, fields: SelectedFieldsOrdered | undefined, name: string | undefined, isResponseInArrayMode: boolean, @@ -45,6 +46,7 @@ export class PgRemoteSession< this.client, query.sql, query.params, + query.typings, this.logger, fields, isResponseInArrayMode, @@ -80,6 +82,7 @@ export class PreparedQuery extends PreparedQueryB private client: RemoteCallback, private queryString: string, private params: unknown[], + private typings: any[] | undefined, private logger: Logger, private fields: SelectedFieldsOrdered | undefined, private _isResponseInArrayMode: boolean, @@ -91,7 +94,7 @@ export class PreparedQuery extends PreparedQueryB async execute(placeholderValues: Record | undefined = {}): Promise { return tracer.startActiveSpan('drizzle.execute', async (span) => { const params = fillPlaceholders(this.params, placeholderValues); - const { fields, client, queryString, joinsNotNullableMap, customResultMapper, logger } = this; + const { fields, client, queryString, joinsNotNullableMap, customResultMapper, logger, typings } = this; span?.setAttributes({ 'drizzle.query.text': queryString, @@ -102,7 +105,7 @@ export class PreparedQuery extends PreparedQueryB if (!fields && !customResultMapper) { return tracer.startActiveSpan('drizzle.driver.execute', async () => { - const { rows } = await client(queryString, params as any[], 'execute'); + const { rows } = await client(queryString, params as any[], 'execute', typings); return rows; }); @@ -114,7 +117,7 @@ export class PreparedQuery extends PreparedQueryB 'drizzle.query.params': JSON.stringify(params), }); - const { rows } = await client(queryString, params as any[], 'all'); + const { rows } = await client(queryString, params as any[], 'all', typings); return rows; }); diff --git a/drizzle-orm/src/sql/sql.ts b/drizzle-orm/src/sql/sql.ts index 306f82519..c680486cc 100644 --- a/drizzle-orm/src/sql/sql.ts +++ b/drizzle-orm/src/sql/sql.ts @@ -319,6 +319,16 @@ export class SQL implements SQLWrapper { this.shouldInlineParams = true; return this; } + + /** + * This method is used to conditionally include a part of the query. + * + * @param condition - Condition to check + * @returns itself if the condition is `true`, otherwise `undefined` + */ + if(condition: any | undefined): this | undefined { + return condition ? this : undefined; + } } export type GetDecoderResult = T extends Column ? T['_']['data'] : T extends diff --git a/integration-tests/tests/pg.test.ts b/integration-tests/tests/pg.test.ts index 3f7305cfa..660819050 100644 --- a/integration-tests/tests/pg.test.ts +++ b/integration-tests/tests/pg.test.ts @@ -18,11 +18,13 @@ import { getTableColumns, gt, gte, + ilike, inArray, lt, max, min, name, + or, placeholder, type SQL, sql, @@ -4125,3 +4127,201 @@ test.serial('test $onUpdateFn and $onUpdate works updating', async (t) => { t.assert(eachUser.updatedAt!.valueOf() > Date.now() - msDelay); } }); + +test.serial('test if method with sql operators', async (t) => { + const { db } = t.context; + + const users = pgTable('users', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + age: integer('age').notNull(), + city: text('city').notNull(), + }); + + await db.execute(sql`drop table if exists ${users}`); + + await db.execute(sql` + create table ${users} ( + id serial primary key, + name text not null, + age integer not null, + city text not null + ) + `); + + await db.insert(users).values([ + { id: 1, name: 'John', age: 20, city: 'New York' }, + { id: 2, name: 'Alice', age: 21, city: 'New York' }, + { id: 3, name: 'Nick', age: 22, city: 'London' }, + { id: 4, name: 'Lina', age: 23, city: 'London' }, + ]); + + const condition1 = true; + + const [result1] = await db.select().from(users).where(eq(users.id, 1).if(condition1)); + + t.deepEqual(result1, { id: 1, name: 'John', age: 20, city: 'New York' }); + + const condition2 = 1; + + const [result2] = await db.select().from(users).where(sql`${users.id} = 1`.if(condition2)); + + t.deepEqual(result2, { id: 1, name: 'John', age: 20, city: 'New York' }); + + const condition3 = 'non-empty string'; + + const result3 = await db.select().from(users).where( + or(eq(users.id, 1).if(condition3), eq(users.id, 2).if(condition3)), + ); + + t.deepEqual(result3, [{ id: 1, name: 'John', age: 20, city: 'New York' }, { + id: 2, + name: 'Alice', + age: 21, + city: 'New York', + }]); + + const condtition4 = false; + + const result4 = await db.select().from(users).where(eq(users.id, 1).if(condtition4)); + + t.deepEqual(result4, [ + { id: 1, name: 'John', age: 20, city: 'New York' }, + { id: 2, name: 'Alice', age: 21, city: 'New York' }, + { id: 3, name: 'Nick', age: 22, city: 'London' }, + { id: 4, name: 'Lina', age: 23, city: 'London' }, + ]); + + const condition5 = undefined; + + const result5 = await db.select().from(users).where(sql`${users.id} = 1`.if(condition5)); + + t.deepEqual(result5, [ + { id: 1, name: 'John', age: 20, city: 'New York' }, + { id: 2, name: 'Alice', age: 21, city: 'New York' }, + { id: 3, name: 'Nick', age: 22, city: 'London' }, + { id: 4, name: 'Lina', age: 23, city: 'London' }, + ]); + + const condition6 = null; + + const result6 = await db.select().from(users).where( + or(eq(users.id, 1).if(condition6), eq(users.id, 2).if(condition6)), + ); + + t.deepEqual(result6, [ + { id: 1, name: 'John', age: 20, city: 'New York' }, + { id: 2, name: 'Alice', age: 21, city: 'New York' }, + { id: 3, name: 'Nick', age: 22, city: 'London' }, + { id: 4, name: 'Lina', age: 23, city: 'London' }, + ]); + + const condition7 = { + term1: 0, + term2: 1, + }; + + const result7 = await db.select().from(users).where( + and(gt(users.age, 20).if(condition7.term1), eq(users.city, 'New York').if(condition7.term2)), + ); + + t.deepEqual(result7, [ + { id: 1, name: 'John', age: 20, city: 'New York' }, + { id: 2, name: 'Alice', age: 21, city: 'New York' }, + ]); + + const condition8 = { + term1: '', + term2: 'non-empty string', + }; + + const result8 = await db.select().from(users).where( + or(lt(users.age, 21).if(condition8.term1), eq(users.city, 'London').if(condition8.term2)), + ); + + t.deepEqual(result8, [ + { id: 3, name: 'Nick', age: 22, city: 'London' }, + { id: 4, name: 'Lina', age: 23, city: 'London' }, + ]); + + const condition9 = { + term1: 1, + term2: true, + }; + + const result9 = await db.select().from(users).where( + and(inArray(users.city, ['New York', 'London']).if(condition9.term1), ilike(users.name, 'a%').if(condition9.term2)), + ); + + t.deepEqual(result9, [ + { id: 2, name: 'Alice', age: 21, city: 'New York' }, + ]); + + const condition10 = { + term1: 4, + term2: 19, + }; + + const result10 = await db.select().from(users).where( + and( + sql`length(${users.name}) <= ${condition10.term1}`.if(condition10.term1), + gt(users.age, condition10.term2).if(condition10.term2 > 20), + ), + ); + + t.deepEqual(result10, [ + { id: 1, name: 'John', age: 20, city: 'New York' }, + { id: 3, name: 'Nick', age: 22, city: 'London' }, + { id: 4, name: 'Lina', age: 23, city: 'London' }, + ]); + + const condition11 = true; + + const result11 = await db.select().from(users).where( + or(eq(users.city, 'New York'), gte(users.age, 22))!.if(condition11), + ); + + t.deepEqual(result11, [ + { id: 1, name: 'John', age: 20, city: 'New York' }, + { id: 2, name: 'Alice', age: 21, city: 'New York' }, + { id: 3, name: 'Nick', age: 22, city: 'London' }, + { id: 4, name: 'Lina', age: 23, city: 'London' }, + ]); + + const condition12 = false; + + const result12 = await db.select().from(users).where( + and(eq(users.city, 'London'), gte(users.age, 23))!.if(condition12), + ); + + t.deepEqual(result12, [ + { id: 1, name: 'John', age: 20, city: 'New York' }, + { id: 2, name: 'Alice', age: 21, city: 'New York' }, + { id: 3, name: 'Nick', age: 22, city: 'London' }, + { id: 4, name: 'Lina', age: 23, city: 'London' }, + ]); + + const condition13 = true; + + const result13 = await db.select().from(users).where(sql`(city = 'New York' or age >= 22)`.if(condition13)); + + t.deepEqual(result13, [ + { id: 1, name: 'John', age: 20, city: 'New York' }, + { id: 2, name: 'Alice', age: 21, city: 'New York' }, + { id: 3, name: 'Nick', age: 22, city: 'London' }, + { id: 4, name: 'Lina', age: 23, city: 'London' }, + ]); + + const condition14 = false; + + const result14 = await db.select().from(users).where(sql`(city = 'London' and age >= 23)`.if(condition14)); + + t.deepEqual(result14, [ + { id: 1, name: 'John', age: 20, city: 'New York' }, + { id: 2, name: 'Alice', age: 21, city: 'New York' }, + { id: 3, name: 'Nick', age: 22, city: 'London' }, + { id: 4, name: 'Lina', age: 23, city: 'London' }, + ]); + + await db.execute(sql`drop table ${users}`); +});