UNPKG

drizzle-orm

Version:

Drizzle ORM package for SQL databases

836 lines • 27.5 kB
import { entityKind, is } from "../../entity.js"; import { GelViewBase } from "../view-base.js"; import { TypedQueryBuilder } from "../../query-builders/query-builder.js"; import { QueryPromise } from "../../query-promise.js"; import { SelectionProxyHandler } from "../../selection-proxy.js"; import { SQL, View } from "../../sql/sql.js"; import { Subquery } from "../../subquery.js"; import { Table } from "../../table.js"; import { tracer } from "../../tracing.js"; import { applyMixins, getTableColumns, getTableLikeName, haveSameKeys } from "../../utils.js"; import { orderSelectedFields } from "../../utils.js"; import { ViewBaseConfig } from "../../view-common.js"; import { extractUsedTable } from "../utils.js"; class GelSelectBuilder { static [entityKind] = "GelSelectBuilder"; fields; session; dialect; withList = []; distinct; constructor(config) { this.fields = config.fields; this.session = config.session; this.dialect = config.dialect; if (config.withList) { this.withList = config.withList; } this.distinct = config.distinct; } authToken; /** @internal */ setToken(token) { this.authToken = token; return this; } /** * Specify the table, subquery, or other target that you're * building a select query against. * * {@link https://www.postgresql.org/docs/current/sql-select.html#SQL-FROM | Postgres from documentation} */ from(source) { const isPartialSelect = !!this.fields; let fields; if (this.fields) { fields = this.fields; } else if (is(source, Subquery)) { fields = Object.fromEntries( Object.keys(source._.selectedFields).map((key) => [key, source[key]]) ); } else if (is(source, GelViewBase)) { fields = source[ViewBaseConfig].selectedFields; } else if (is(source, SQL)) { fields = {}; } else { fields = getTableColumns(source); } return new GelSelectBase({ table: source, fields, isPartialSelect, session: this.session, dialect: this.dialect, withList: this.withList, distinct: this.distinct }); } } class GelSelectQueryBuilderBase extends TypedQueryBuilder { static [entityKind] = "GelSelectQueryBuilder"; _; config; joinsNotNullableMap; tableName; isPartialSelect; session; dialect; cacheConfig = void 0; usedTables = /* @__PURE__ */ new Set(); constructor({ table, fields, isPartialSelect, session, dialect, withList, distinct }) { super(); this.config = { withList, table, fields: { ...fields }, distinct, setOperators: [] }; this.isPartialSelect = isPartialSelect; this.session = session; this.dialect = dialect; this._ = { selectedFields: fields, config: this.config }; this.tableName = getTableLikeName(table); this.joinsNotNullableMap = typeof this.tableName === "string" ? { [this.tableName]: true } : {}; for (const item of extractUsedTable(table)) this.usedTables.add(item); } /** @internal */ getUsedTables() { return [...this.usedTables]; } createJoin(joinType, lateral) { return (table, on) => { const baseTableName = this.tableName; const tableName = getTableLikeName(table); if (typeof tableName === "string" && this.config.joins?.some((join) => join.alias === tableName)) { throw new Error(`Alias "${tableName}" is already used in this query`); } for (const item of extractUsedTable(table)) this.usedTables.add(item); if (!this.isPartialSelect) { if (Object.keys(this.joinsNotNullableMap).length === 1 && typeof baseTableName === "string") { this.config.fields = { [baseTableName]: this.config.fields }; } if (typeof tableName === "string" && !is(table, SQL)) { const selection = is(table, Subquery) ? table._.selectedFields : is(table, View) ? table[ViewBaseConfig].selectedFields : table[Table.Symbol.Columns]; this.config.fields[tableName] = selection; } } if (typeof on === "function") { on = on( new Proxy( this.config.fields, new SelectionProxyHandler({ sqlAliasedBehavior: "sql", sqlBehavior: "sql" }) ) ); } if (!this.config.joins) { this.config.joins = []; } this.config.joins.push({ on, table, joinType, alias: tableName, lateral }); if (typeof tableName === "string") { switch (joinType) { case "left": { this.joinsNotNullableMap[tableName] = false; break; } case "right": { this.joinsNotNullableMap = Object.fromEntries( Object.entries(this.joinsNotNullableMap).map(([key]) => [key, false]) ); this.joinsNotNullableMap[tableName] = true; break; } case "cross": case "inner": { this.joinsNotNullableMap[tableName] = true; break; } case "full": { this.joinsNotNullableMap = Object.fromEntries( Object.entries(this.joinsNotNullableMap).map(([key]) => [key, false]) ); this.joinsNotNullableMap[tableName] = false; break; } } } return this; }; } /** * Executes a `left join` operation by adding another table to the current query. * * Calling this method associates each row of the table with the corresponding row from the joined table, if a match is found. If no matching row exists, it sets all columns of the joined table to null. * * See docs: {@link https://orm.drizzle.team/docs/joins#left-join} * * @param table the table to join. * @param on the `on` clause. * * @example * * ```ts * // Select all users and their pets * const usersWithPets: { user: User; pets: Pet | null; }[] = await db.select() * .from(users) * .leftJoin(pets, eq(users.id, pets.ownerId)) * * // Select userId and petId * const usersIdsAndPetIds: { userId: number; petId: number | null; }[] = await db.select({ * userId: users.id, * petId: pets.id, * }) * .from(users) * .leftJoin(pets, eq(users.id, pets.ownerId)) * ``` */ leftJoin = this.createJoin("left", false); /** * Executes a `left join lateral` operation by adding subquery to the current query. * * A `lateral` join allows the right-hand expression to refer to columns from the left-hand side. * * Calling this method associates each row of the table with the corresponding row from the joined table, if a match is found. If no matching row exists, it sets all columns of the joined table to null. * * See docs: {@link https://orm.drizzle.team/docs/joins#left-join-lateral} * * @param table the subquery to join. * @param on the `on` clause. */ leftJoinLateral = this.createJoin("left", true); /** * Executes a `right join` operation by adding another table to the current query. * * Calling this method associates each row of the joined table with the corresponding row from the main table, if a match is found. If no matching row exists, it sets all columns of the main table to null. * * See docs: {@link https://orm.drizzle.team/docs/joins#right-join} * * @param table the table to join. * @param on the `on` clause. * * @example * * ```ts * // Select all users and their pets * const usersWithPets: { user: User | null; pets: Pet; }[] = await db.select() * .from(users) * .rightJoin(pets, eq(users.id, pets.ownerId)) * * // Select userId and petId * const usersIdsAndPetIds: { userId: number | null; petId: number; }[] = await db.select({ * userId: users.id, * petId: pets.id, * }) * .from(users) * .rightJoin(pets, eq(users.id, pets.ownerId)) * ``` */ rightJoin = this.createJoin("right", false); /** * Executes an `inner join` operation, creating a new table by combining rows from two tables that have matching values. * * Calling this method retrieves rows that have corresponding entries in both joined tables. Rows without matching entries in either table are excluded, resulting in a table that includes only matching pairs. * * See docs: {@link https://orm.drizzle.team/docs/joins#inner-join} * * @param table the table to join. * @param on the `on` clause. * * @example * * ```ts * // Select all users and their pets * const usersWithPets: { user: User; pets: Pet; }[] = await db.select() * .from(users) * .innerJoin(pets, eq(users.id, pets.ownerId)) * * // Select userId and petId * const usersIdsAndPetIds: { userId: number; petId: number; }[] = await db.select({ * userId: users.id, * petId: pets.id, * }) * .from(users) * .innerJoin(pets, eq(users.id, pets.ownerId)) * ``` */ innerJoin = this.createJoin("inner", false); /** * Executes an `inner join lateral` operation, creating a new table by combining rows from two queries that have matching values. * * A `lateral` join allows the right-hand expression to refer to columns from the left-hand side. * * Calling this method retrieves rows that have corresponding entries in both joined tables. Rows without matching entries in either table are excluded, resulting in a table that includes only matching pairs. * * See docs: {@link https://orm.drizzle.team/docs/joins#inner-join-lateral} * * @param table the subquery to join. * @param on the `on` clause. */ innerJoinLateral = this.createJoin("inner", true); /** * Executes a `full join` operation by combining rows from two tables into a new table. * * Calling this method retrieves all rows from both main and joined tables, merging rows with matching values and filling in `null` for non-matching columns. * * See docs: {@link https://orm.drizzle.team/docs/joins#full-join} * * @param table the table to join. * @param on the `on` clause. * * @example * * ```ts * // Select all users and their pets * const usersWithPets: { user: User | null; pets: Pet | null; }[] = await db.select() * .from(users) * .fullJoin(pets, eq(users.id, pets.ownerId)) * * // Select userId and petId * const usersIdsAndPetIds: { userId: number | null; petId: number | null; }[] = await db.select({ * userId: users.id, * petId: pets.id, * }) * .from(users) * .fullJoin(pets, eq(users.id, pets.ownerId)) * ``` */ fullJoin = this.createJoin("full", false); /** * Executes a `cross join` operation by combining rows from two tables into a new table. * * Calling this method retrieves all rows from both main and joined tables, merging all rows from each table. * * See docs: {@link https://orm.drizzle.team/docs/joins#cross-join} * * @param table the table to join. * * @example * * ```ts * // Select all users, each user with every pet * const usersWithPets: { user: User; pets: Pet; }[] = await db.select() * .from(users) * .crossJoin(pets) * * // Select userId and petId * const usersIdsAndPetIds: { userId: number; petId: number; }[] = await db.select({ * userId: users.id, * petId: pets.id, * }) * .from(users) * .crossJoin(pets) * ``` */ crossJoin = this.createJoin("cross", false); /** * Executes a `cross join lateral` operation by combining rows from two queries into a new table. * * A `lateral` join allows the right-hand expression to refer to columns from the left-hand side. * * Calling this method retrieves all rows from both main and joined queries, merging all rows from each query. * * See docs: {@link https://orm.drizzle.team/docs/joins#cross-join-lateral} * * @param table the query to join. */ crossJoinLateral = this.createJoin("cross", true); createSetOperator(type, isAll) { return (rightSelection) => { const rightSelect = typeof rightSelection === "function" ? rightSelection(getGelSetOperators()) : rightSelection; if (!haveSameKeys(this.getSelectedFields(), rightSelect.getSelectedFields())) { throw new Error( "Set operator error (union / intersect / except): selected fields are not the same or are in a different order" ); } this.config.setOperators.push({ type, isAll, rightSelect }); return this; }; } /** * Adds `union` set operator to the query. * * Calling this method will combine the result sets of the `select` statements and remove any duplicate rows that appear across them. * * See docs: {@link https://orm.drizzle.team/docs/set-operations#union} * * @example * * ```ts * // Select all unique names from customers and users tables * await db.select({ name: users.name }) * .from(users) * .union( * db.select({ name: customers.name }).from(customers) * ); * // or * import { union } from 'drizzle-orm/gel-core' * * await union( * db.select({ name: users.name }).from(users), * db.select({ name: customers.name }).from(customers) * ); * ``` */ union = this.createSetOperator("union", false); /** * Adds `union all` set operator to the query. * * Calling this method will combine the result-set of the `select` statements and keep all duplicate rows that appear across them. * * See docs: {@link https://orm.drizzle.team/docs/set-operations#union-all} * * @example * * ```ts * // Select all transaction ids from both online and in-store sales * await db.select({ transaction: onlineSales.transactionId }) * .from(onlineSales) * .unionAll( * db.select({ transaction: inStoreSales.transactionId }).from(inStoreSales) * ); * // or * import { unionAll } from 'drizzle-orm/gel-core' * * await unionAll( * db.select({ transaction: onlineSales.transactionId }).from(onlineSales), * db.select({ transaction: inStoreSales.transactionId }).from(inStoreSales) * ); * ``` */ unionAll = this.createSetOperator("union", true); /** * Adds `intersect` set operator to the query. * * Calling this method will retain only the rows that are present in both result sets and eliminate duplicates. * * See docs: {@link https://orm.drizzle.team/docs/set-operations#intersect} * * @example * * ```ts * // Select course names that are offered in both departments A and B * await db.select({ courseName: depA.courseName }) * .from(depA) * .intersect( * db.select({ courseName: depB.courseName }).from(depB) * ); * // or * import { intersect } from 'drizzle-orm/gel-core' * * await intersect( * db.select({ courseName: depA.courseName }).from(depA), * db.select({ courseName: depB.courseName }).from(depB) * ); * ``` */ intersect = this.createSetOperator("intersect", false); /** * Adds `intersect all` set operator to the query. * * Calling this method will retain only the rows that are present in both result sets including all duplicates. * * See docs: {@link https://orm.drizzle.team/docs/set-operations#intersect-all} * * @example * * ```ts * // Select all products and quantities that are ordered by both regular and VIP customers * await db.select({ * productId: regularCustomerOrders.productId, * quantityOrdered: regularCustomerOrders.quantityOrdered * }) * .from(regularCustomerOrders) * .intersectAll( * db.select({ * productId: vipCustomerOrders.productId, * quantityOrdered: vipCustomerOrders.quantityOrdered * }) * .from(vipCustomerOrders) * ); * // or * import { intersectAll } from 'drizzle-orm/gel-core' * * await intersectAll( * db.select({ * productId: regularCustomerOrders.productId, * quantityOrdered: regularCustomerOrders.quantityOrdered * }) * .from(regularCustomerOrders), * db.select({ * productId: vipCustomerOrders.productId, * quantityOrdered: vipCustomerOrders.quantityOrdered * }) * .from(vipCustomerOrders) * ); * ``` */ intersectAll = this.createSetOperator("intersect", true); /** * Adds `except` set operator to the query. * * Calling this method will retrieve all unique rows from the left query, except for the rows that are present in the result set of the right query. * * See docs: {@link https://orm.drizzle.team/docs/set-operations#except} * * @example * * ```ts * // Select all courses offered in department A but not in department B * await db.select({ courseName: depA.courseName }) * .from(depA) * .except( * db.select({ courseName: depB.courseName }).from(depB) * ); * // or * import { except } from 'drizzle-orm/gel-core' * * await except( * db.select({ courseName: depA.courseName }).from(depA), * db.select({ courseName: depB.courseName }).from(depB) * ); * ``` */ except = this.createSetOperator("except", false); /** * Adds `except all` set operator to the query. * * Calling this method will retrieve all rows from the left query, except for the rows that are present in the result set of the right query. * * See docs: {@link https://orm.drizzle.team/docs/set-operations#except-all} * * @example * * ```ts * // Select all products that are ordered by regular customers but not by VIP customers * await db.select({ * productId: regularCustomerOrders.productId, * quantityOrdered: regularCustomerOrders.quantityOrdered, * }) * .from(regularCustomerOrders) * .exceptAll( * db.select({ * productId: vipCustomerOrders.productId, * quantityOrdered: vipCustomerOrders.quantityOrdered, * }) * .from(vipCustomerOrders) * ); * // or * import { exceptAll } from 'drizzle-orm/gel-core' * * await exceptAll( * db.select({ * productId: regularCustomerOrders.productId, * quantityOrdered: regularCustomerOrders.quantityOrdered * }) * .from(regularCustomerOrders), * db.select({ * productId: vipCustomerOrders.productId, * quantityOrdered: vipCustomerOrders.quantityOrdered * }) * .from(vipCustomerOrders) * ); * ``` */ exceptAll = this.createSetOperator("except", true); /** @internal */ addSetOperators(setOperators) { this.config.setOperators.push(...setOperators); return this; } /** * Adds a `where` clause to the query. * * Calling this method will select only those rows that fulfill a specified condition. * * See docs: {@link https://orm.drizzle.team/docs/select#filtering} * * @param where the `where` clause. * * @example * You can use conditional operators and `sql function` to filter the rows to be selected. * * ```ts * // Select all cars with green color * await db.select().from(cars).where(eq(cars.color, 'green')); * // or * await db.select().from(cars).where(sql`${cars.color} = 'green'`) * ``` * * You can logically combine conditional operators with `and()` and `or()` operators: * * ```ts * // Select all BMW cars with a green color * await db.select().from(cars).where(and(eq(cars.color, 'green'), eq(cars.brand, 'BMW'))); * * // Select all cars with the green or blue color * await db.select().from(cars).where(or(eq(cars.color, 'green'), eq(cars.color, 'blue'))); * ``` */ where(where) { if (typeof where === "function") { where = where( new Proxy( this.config.fields, new SelectionProxyHandler({ sqlAliasedBehavior: "sql", sqlBehavior: "sql" }) ) ); } this.config.where = where; return this; } /** * Adds a `having` clause to the query. * * Calling this method will select only those rows that fulfill a specified condition. It is typically used with aggregate functions to filter the aggregated data based on a specified condition. * * See docs: {@link https://orm.drizzle.team/docs/select#aggregations} * * @param having the `having` clause. * * @example * * ```ts * // Select all brands with more than one car * await db.select({ * brand: cars.brand, * count: sql<number>`cast(count(${cars.id}) as int)`, * }) * .from(cars) * .groupBy(cars.brand) * .having(({ count }) => gt(count, 1)); * ``` */ having(having) { if (typeof having === "function") { having = having( new Proxy( this.config.fields, new SelectionProxyHandler({ sqlAliasedBehavior: "sql", sqlBehavior: "sql" }) ) ); } this.config.having = having; return this; } groupBy(...columns) { if (typeof columns[0] === "function") { const groupBy = columns[0]( new Proxy( this.config.fields, new SelectionProxyHandler({ sqlAliasedBehavior: "alias", sqlBehavior: "sql" }) ) ); this.config.groupBy = Array.isArray(groupBy) ? groupBy : [groupBy]; } else { this.config.groupBy = columns; } return this; } orderBy(...columns) { if (typeof columns[0] === "function") { const orderBy = columns[0]( new Proxy( this.config.fields, new SelectionProxyHandler({ sqlAliasedBehavior: "alias", sqlBehavior: "sql" }) ) ); const orderByArray = Array.isArray(orderBy) ? orderBy : [orderBy]; if (this.config.setOperators.length > 0) { this.config.setOperators.at(-1).orderBy = orderByArray; } else { this.config.orderBy = orderByArray; } } else { const orderByArray = columns; if (this.config.setOperators.length > 0) { this.config.setOperators.at(-1).orderBy = orderByArray; } else { this.config.orderBy = orderByArray; } } return this; } /** * Adds a `limit` clause to the query. * * Calling this method will set the maximum number of rows that will be returned by this query. * * See docs: {@link https://orm.drizzle.team/docs/select#limit--offset} * * @param limit the `limit` clause. * * @example * * ```ts * // Get the first 10 people from this query. * await db.select().from(people).limit(10); * ``` */ limit(limit) { if (this.config.setOperators.length > 0) { this.config.setOperators.at(-1).limit = limit; } else { this.config.limit = limit; } return this; } /** * Adds an `offset` clause to the query. * * Calling this method will skip a number of rows when returning results from this query. * * See docs: {@link https://orm.drizzle.team/docs/select#limit--offset} * * @param offset the `offset` clause. * * @example * * ```ts * // Get the 10th-20th people from this query. * await db.select().from(people).offset(10).limit(10); * ``` */ offset(offset) { if (this.config.setOperators.length > 0) { this.config.setOperators.at(-1).offset = offset; } else { this.config.offset = offset; } return this; } /** * Adds a `for` clause to the query. * * Calling this method will specify a lock strength for this query that controls how strictly it acquires exclusive access to the rows being queried. * * See docs: {@link https://www.postgresql.org/docs/current/sql-select.html#SQL-FOR-UPDATE-SHARE} * * @param strength the lock strength. * @param config the lock configuration. */ for(strength, config = {}) { this.config.lockingClause = { strength, config }; return this; } /** @internal */ getSQL() { return this.dialect.buildSelectQuery(this.config); } toSQL() { const { typings: _typings, ...rest } = this.dialect.sqlToQuery(this.getSQL()); return rest; } as(alias) { const usedTables = []; usedTables.push(...extractUsedTable(this.config.table)); if (this.config.joins) { for (const it of this.config.joins) usedTables.push(...extractUsedTable(it.table)); } return new Proxy( new Subquery(this.getSQL(), this.config.fields, alias, false, [...new Set(usedTables)]), new SelectionProxyHandler({ alias, sqlAliasedBehavior: "alias", sqlBehavior: "error" }) ); } /** @internal */ getSelectedFields() { return new Proxy( this.config.fields, new SelectionProxyHandler({ alias: this.tableName, sqlAliasedBehavior: "alias", sqlBehavior: "error" }) ); } $dynamic() { return this; } } class GelSelectBase extends GelSelectQueryBuilderBase { static [entityKind] = "GelSelect"; /** @internal */ _prepare(name) { const { session, config, dialect, joinsNotNullableMap, cacheConfig, usedTables } = this; if (!session) { throw new Error("Cannot execute a query on a query builder. Please use a database instance instead."); } return tracer.startActiveSpan("drizzle.prepareQuery", () => { const fieldsList = orderSelectedFields(config.fields); const query = session.prepareQuery(dialect.sqlToQuery(this.getSQL()), fieldsList, name, true, void 0, { type: "select", tables: [...usedTables] }, cacheConfig); query.joinsNotNullableMap = joinsNotNullableMap; return query; }); } $withCache(config) { this.cacheConfig = config === void 0 ? { config: {}, enable: true, autoInvalidate: true } : config === false ? { enable: false } : { enable: true, autoInvalidate: true, ...config }; return this; } /** * Create a prepared statement for this query. This allows * the database to remember this query for the given session * and call it by name, rather than specifying the full query. * * {@link https://www.postgresql.org/docs/current/sql-prepare.html | Postgres prepare documentation} */ prepare(name) { return this._prepare(name); } execute = (placeholderValues) => { return tracer.startActiveSpan("drizzle.operation", () => { return this._prepare().execute(placeholderValues); }); }; } applyMixins(GelSelectBase, [QueryPromise]); function createSetOperator(type, isAll) { return (leftSelect, rightSelect, ...restSelects) => { const setOperators = [rightSelect, ...restSelects].map((select) => ({ type, isAll, rightSelect: select })); for (const setOperator of setOperators) { if (!haveSameKeys(leftSelect.getSelectedFields(), setOperator.rightSelect.getSelectedFields())) { throw new Error( "Set operator error (union / intersect / except): selected fields are not the same or are in a different order" ); } } return leftSelect.addSetOperators(setOperators); }; } const getGelSetOperators = () => ({ union, unionAll, intersect, intersectAll, except, exceptAll }); const union = createSetOperator("union", false); const unionAll = createSetOperator("union", true); const intersect = createSetOperator("intersect", false); const intersectAll = createSetOperator("intersect", true); const except = createSetOperator("except", false); const exceptAll = createSetOperator("except", true); export { GelSelectBase, GelSelectBuilder, GelSelectQueryBuilderBase, except, exceptAll, intersect, intersectAll, union, unionAll }; //# sourceMappingURL=select.js.map