diff --git a/.github/workflows/release-feature-branch.yaml b/.github/workflows/release-feature-branch.yaml index 3126e994a..f00ce0dcb 100644 --- a/.github/workflows/release-feature-branch.yaml +++ b/.github/workflows/release-feature-branch.yaml @@ -17,6 +17,7 @@ jobs: - drizzle-zod - drizzle-typebox - drizzle-valibot + - eslint-plugin-drizzle runs-on: ubuntu-20.04 permissions: contents: read @@ -91,8 +92,8 @@ jobs: is_version_published="$(npm view ${{ matrix.package }} versions --json | jq -r '.[] | select(. == "'$version'") | . == "'$version'"')" if [[ "$is_version_published" == "true" ]]; then - echo "\`${{ matrix.package }}@$version\` already published, adding tag \`$tag\`" >> $GITHUB_STEP_SUMMARY - npm dist-tag add ${{ matrix.package }}@$version $tag + echo "\`${{ matrix.package }}@ $version\` already published, adding tag \`$tag\`" >> $GITHUB_STEP_SUMMARY + npm dist-tag add ${{ matrix.package }}@ $version $tag else { echo "version=$version" diff --git a/.github/workflows/release-latest.yaml b/.github/workflows/release-latest.yaml index 3153adb32..181f9e65b 100644 --- a/.github/workflows/release-latest.yaml +++ b/.github/workflows/release-latest.yaml @@ -13,6 +13,7 @@ jobs: - drizzle-zod - drizzle-typebox - drizzle-valibot + - eslint-plugin-drizzle runs-on: ubuntu-20.04 services: postgres: @@ -82,8 +83,8 @@ jobs: is_version_published="$(npm view ${{ matrix.package }} versions --json | jq -r '.[] | select(. == "'$version'") | . == "'$version'"')" if [[ "$is_version_published" == "true" ]]; then - echo "\`${{ matrix.package }}@$version\` already published, adding tag \`latest\`" >> $GITHUB_STEP_SUMMARY - npm dist-tag add ${{ matrix.package }}@$version latest + echo "\`${{ matrix.package }}@ $version\` already published, adding tag \`latest\`" >> $GITHUB_STEP_SUMMARY + npm dist-tag add ${{ matrix.package }}@ $version latest elif [[ "$latest" != "$version" ]]; then echo "Latest: $latest" echo "Current: $version" diff --git a/.github/workflows/unpublish-release-feature-branch.yaml b/.github/workflows/unpublish-release-feature-branch.yaml index a821291e5..dd9f11420 100644 --- a/.github/workflows/unpublish-release-feature-branch.yaml +++ b/.github/workflows/unpublish-release-feature-branch.yaml @@ -12,6 +12,7 @@ jobs: - drizzle-zod - drizzle-typebox - drizzle-valibot + - eslint-plugin-drizzle runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v3 diff --git a/changelogs/drizzle-orm/0.29.1.md b/changelogs/drizzle-orm/0.29.1.md new file mode 100644 index 000000000..2b02b109a --- /dev/null +++ b/changelogs/drizzle-orm/0.29.1.md @@ -0,0 +1,276 @@ +# Fixes + +- Forward args correctly when using withReplica feature #1536. Thanks @Angelelz +- Fix selectDistinctOn not working with multiple columns #1466. Thanks @L-Mario564 + +# New Features/Helpers + +## 🎉 Detailed JSDoc for all query builders in all dialects - thanks @realmikesolo + +You can now access more information, hints, documentation links, etc. while developing and using JSDoc right in your IDE. Previously, we had them only for filter expressions, but now you can see them for all parts of the Drizzle query builder + +## 🎉 New helpers for aggregate functions in SQL - thanks @L-Mario564 + +> Remember, aggregation functions are often used with the GROUP BY clause of the SELECT statement. So if you are selecting using aggregating functions and other columns in one query, +be sure to use the `.groupBy` clause + +Here is a list of functions and equivalent using `sql` template + +**count** +```ts +await db.select({ value: count() }).from(users); +await db.select({ value: count(users.id) }).from(users); + +// It's equivalent to writing +await db.select({ + value: sql`count('*'))`.mapWith(Number) +}).from(users); +await db.select({ + value: sql`count(${users.id})`.mapWith(Number) +}).from(users); +``` + +**countDistinct** +```ts +await db.select({ value: countDistinct(users.id) }).from(users); + +// It's equivalent to writing +await db.select({ + value: sql`count(${users.id})`.mapWith(Number) +}).from(users); +``` + +**avg** +```ts +await db.select({ value: avg(users.id) }).from(users); + +// It's equivalent to writing +await db.select({ + value: sql`avg(${users.id})`.mapWith(String) +}).from(users); +``` + +**avgDistinct** +```ts +await db.select({ value: avgDistinct(users.id) }).from(users); + +// It's equivalent to writing +await db.select({ + value: sql`avg(distinct ${users.id})`.mapWith(String) +}).from(users); +``` + +**sum** +```ts +await db.select({ value: sum(users.id) }).from(users); + +// It's equivalent to writing +await db.select({ + value: sql`sum(${users.id})`.mapWith(String) +}).from(users); +``` + +**sumDistinct** +```ts +await db.select({ value: sumDistinct(users.id) }).from(users); + +// It's equivalent to writing +await db.select({ + value: sql`sum(distinct ${users.id})`.mapWith(String) +}).from(users); +``` + +**max** +```ts +await db.select({ value: max(users.id) }).from(users); + +// It's equivalent to writing +await db.select({ + value: sql`max(${expression})`.mapWith(users.id) +}).from(users); +``` + +**min** +```ts +await db.select({ value: min(users.id) }).from(users); + +// It's equivalent to writing +await db.select({ + value: sql`min(${users.id})`.mapWith(users.id) +}).from(users); +``` + +# New Packages +## 🎉 ESLint Drizzle Plugin + +For cases where it's impossible to perform type checks for specific scenarios, or where it's possible but error messages would be challenging to understand, we've decided to create an ESLint package with recommended rules. This package aims to assist developers in handling crucial scenarios during development + +> Big thanks to @Angelelz for initiating the development of this package and transferring it to the Drizzle Team's npm + +## Install + +```sh +[ npm | yarn | pnpm | bun ] install eslint eslint-plugin-drizzle +``` +You can install those packages for typescript support in your IDE +```sh +[ npm | yarn | pnpm | bun ] install @typescript-eslint/eslint-plugin @typescript-eslint/parser +``` + +## Usage + +Create a `.eslintrc.yml` file, add `drizzle` to the `plugins`, and specify the rules you want to use. You can find a list of all existing rules below + +```yml +root: true +parser: '@typescript-eslint/parser' +parserOptions: + project: './tsconfig.json' +plugins: + - drizzle +rules: + 'drizzle/enforce-delete-with-where': "error" + 'drizzle/enforce-update-with-where': "error" +``` + +### All config + +This plugin exports an [`all` config](src/configs/all.js) that makes use of all rules (except for deprecated ones). + +```yml +root: true +extends: + - "plugin:drizzle/all" +parser: '@typescript-eslint/parser' +parserOptions: + project: './tsconfig.json' +plugins: + - drizzle +``` + +At the moment, `all` is equivalent to `recommended` + +```yml +root: true +extends: + - "plugin:drizzle/recommended" +parser: '@typescript-eslint/parser' +parserOptions: + project: './tsconfig.json' +plugins: + - drizzle +``` + +## Rules + +**enforce-delete-with-where**: Enforce using `delete` with the`.where()` clause in the `.delete()` statement. Most of the time, you don't need to delete all rows in the table and require some kind of `WHERE` statements. + +**Error Message**: +``` +Without `.where(...)` you will delete all the rows in a table. If you didn't want to do it, please use `db.delete(...).where(...)` instead. Otherwise you can ignore this rule here +``` + +Optionally, you can define a `drizzleObjectName` in the plugin options that accept a `string` or `string[]`. This is useful when you have objects or classes with a delete method that's not from Drizzle. Such a `delete` method will trigger the ESLint rule. To avoid that, you can define the name of the Drizzle object that you use in your codebase (like db) so that the rule would only trigger if the delete method comes from this object: + +Example, config 1: +```json +"rules": { + "drizzle/enforce-delete-with-where": ["error"] +} +``` + +```ts +class MyClass { + public delete() { + return {} + } +} + +const myClassObj = new MyClass(); + +// ---> Will be triggered by ESLint Rule +myClassObj.delete() + +const db = drizzle(...) +// ---> Will be triggered by ESLint Rule +db.delete() +``` + +Example, config 2: +```json +"rules": { + "drizzle/enforce-delete-with-where": ["error", { "drizzleObjectName": ["db"] }], +} +``` +```ts +class MyClass { + public delete() { + return {} + } +} + +const myClassObj = new MyClass(); + +// ---> Will NOT be triggered by ESLint Rule +myClassObj.delete() + +const db = drizzle(...) +// ---> Will be triggered by ESLint Rule +db.delete() +``` + +**enforce-update-with-where**: Enforce using `update` with the`.where()` clause in the `.update()` statement. Most of the time, you don't need to update all rows in the table and require some kind of `WHERE` statements. + +**Error Message**: +``` +Without `.where(...)` you will update all the rows in a table. If you didn't want to do it, please use `db.update(...).set(...).where(...)` instead. Otherwise you can ignore this rule here +``` + +Optionally, you can define a `drizzleObjectName` in the plugin options that accept a `string` or `string[]`. This is useful when you have objects or classes with a delete method that's not from Drizzle. Such as `update` method will trigger the ESLint rule. To avoid that, you can define the name of the Drizzle object that you use in your codebase (like db) so that the rule would only trigger if the delete method comes from this object: + +Example, config 1: +```json +"rules": { + "drizzle/enforce-update-with-where": ["error"] +} +``` + +```ts +class MyClass { + public update() { + return {} + } +} + +const myClassObj = new MyClass(); + +// ---> Will be triggered by ESLint Rule +myClassObj.update() + +const db = drizzle(...) +// ---> Will be triggered by ESLint Rule +db.update() +``` + +Example, config 2: +```json +"rules": { + "drizzle/enforce-update-with-where": ["error", { "drizzleObjectName": ["db"] }], +} +``` +```ts +class MyClass { + public update() { + return {} + } +} + +const myClassObj = new MyClass(); + +// ---> Will NOT be triggered by ESLint Rule +myClassObj.update() + +const db = drizzle(...) +// ---> Will be triggered by ESLint Rule +db.update() +``` \ No newline at end of file diff --git a/changelogs/eslint-plugin-drizzle/0.2.0.md b/changelogs/eslint-plugin-drizzle/0.2.0.md new file mode 100644 index 000000000..c94ced11f --- /dev/null +++ b/changelogs/eslint-plugin-drizzle/0.2.0.md @@ -0,0 +1,4 @@ +# eslint-plugin-drizzle 0.1.0 + +- Initial release +- 2 rules available diff --git a/changelogs/eslint-plugin-drizzle/0.2.1.md b/changelogs/eslint-plugin-drizzle/0.2.1.md new file mode 100644 index 000000000..b29f78c3a --- /dev/null +++ b/changelogs/eslint-plugin-drizzle/0.2.1.md @@ -0,0 +1,4 @@ +# eslint-plugin-drizzle 0.2.1 + +- Update README.md +- Change error text message \ No newline at end of file diff --git a/drizzle-orm/package.json b/drizzle-orm/package.json index 6d2613d75..2a1658773 100644 --- a/drizzle-orm/package.json +++ b/drizzle-orm/package.json @@ -1,6 +1,6 @@ { "name": "drizzle-orm", - "version": "0.29.0", + "version": "0.29.1", "description": "Drizzle ORM package for SQL databases", "type": "module", "scripts": { diff --git a/drizzle-orm/src/mysql-core/db.ts b/drizzle-orm/src/mysql-core/db.ts index e3a07cee3..dc0b9bb31 100644 --- a/drizzle-orm/src/mysql-core/db.ts +++ b/drizzle-orm/src/mysql-core/db.ts @@ -2,7 +2,9 @@ import type { ResultSetHeader } from 'mysql2/promise'; import { entityKind } from '~/entity.ts'; import type { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; import type { ExtractTablesWithRelations, RelationalSchemaConfig, TablesRelationalConfig } from '~/relations.ts'; +import { SelectionProxyHandler } from '~/selection-proxy.ts'; import type { ColumnsSelection, SQLWrapper } from '~/sql/sql.ts'; +import { WithSubquery } from '~/subquery.ts'; import type { DrizzleTypeError } from '~/utils.ts'; import type { MySqlDialect } from './dialect.ts'; import { @@ -25,8 +27,6 @@ import type { } from './session.ts'; import type { WithSubqueryWithSelection } from './subquery.ts'; import type { MySqlTable } from './table.ts'; -import { WithSubquery } from '~/subquery.ts'; -import { SelectionProxyHandler } from '~/selection-proxy.ts'; export class MySqlDatabase< TQueryResult extends QueryResultHKT, @@ -76,6 +76,38 @@ export class MySqlDatabase< } } + /** + * Creates a subquery that defines a temporary named result set as a CTE. + * + * It is useful for breaking down complex queries into simpler parts and for reusing the result set in subsequent parts of the query. + * + * See docs: {@link https://orm.drizzle.team/docs/select#with-clause} + * + * @param alias The alias for the subquery. + * + * Failure to provide an alias will result in a DrizzleTypeError, preventing the subquery from being referenced in other queries. + * + * @example + * + * ```ts + * // Create a subquery with alias 'sq' and use it in the select query + * const sq = db.$with('sq').as(db.select().from(users).where(eq(users.id, 42))); + * + * const result = await db.with(sq).select().from(sq); + * ``` + * + * To select arbitrary SQL values as fields in a CTE and reference them in other CTEs or in the main query, you need to add aliases to them: + * + * ```ts + * // Select an arbitrary SQL value as a field in a CTE and reference it in the main query + * const sq = db.$with('sq').as(db.select({ + * name: sql`upper(${users.name})`.as('name'), + * }) + * .from(users)); + * + * const result = await db.with(sq).select({ name: sq.name }).from(sq); + * ``` + */ $with(alias: TAlias) { return { as( @@ -93,6 +125,25 @@ export class MySqlDatabase< }; } + /** + * Incorporates a previously defined CTE (using `$with`) into the main query. + * + * This method allows the main query to reference a temporary named result set. + * + * See docs: {@link https://orm.drizzle.team/docs/select#with-clause} + * + * @param queries The CTEs to incorporate into the main query. + * + * @example + * + * ```ts + * // Define a subquery 'sq' as a CTE using $with + * const sq = db.$with('sq').as(db.select().from(users).where(eq(users.id, 42))); + * + * // Incorporate the CTE 'sq' into the main query and select from it + * const result = await db.with(sq).select().from(sq); + * ``` + */ with(...queries: WithSubquery[]) { const self = this; @@ -128,12 +179,72 @@ export class MySqlDatabase< return { select, selectDistinct }; } + /** + * Creates a select query. + * + * Calling this method with no arguments will select all columns from the table. Pass a selection object to specify the columns you want to select. + * + * Use `.from()` method to specify which table to select from. + * + * See docs: {@link https://orm.drizzle.team/docs/select} + * + * @param fields The selection object. + * + * @example + * + * ```ts + * // Select all columns and all rows from the 'cars' table + * const allCars: Car[] = await db.select().from(cars); + * + * // Select specific columns and all rows from the 'cars' table + * const carsIdsAndBrands: { id: number; brand: string }[] = await db.select({ + * id: cars.id, + * brand: cars.brand + * }) + * .from(cars); + * ``` + * + * Like in SQL, you can use arbitrary expressions as selection fields, not just table columns: + * + * ```ts + * // Select specific columns along with expression and all rows from the 'cars' table + * const carsIdsAndLowerNames: { id: number; lowerBrand: string }[] = await db.select({ + * id: cars.id, + * lowerBrand: sql`lower(${cars.brand})`, + * }) + * .from(cars); + * ``` + */ select(): MySqlSelectBuilder; select(fields: TSelection): MySqlSelectBuilder; select(fields?: SelectedFields): MySqlSelectBuilder { return new MySqlSelectBuilder({ fields: fields ?? undefined, session: this.session, dialect: this.dialect }); } + /** + * Adds `distinct` expression to the select query. + * + * Calling this method will return only unique values. When multiple columns are selected, it returns rows with unique combinations of values in these columns. + * + * Use `.from()` method to specify which table to select from. + * + * See docs: {@link https://orm.drizzle.team/docs/select#distinct} + * + * @param fields The selection object. + * + * @example + * ```ts + * // Select all unique rows from the 'cars' table + * await db.selectDistinct() + * .from(cars) + * .orderBy(cars.id, cars.brand, cars.color); + * + * // Select all unique brands from the 'cars' table + * await db.selectDistinct({ brand: cars.brand }) + * .from(cars) + * .orderBy(cars.brand); + * ``` + */ selectDistinct(): MySqlSelectBuilder; selectDistinct( fields: TSelection, @@ -147,14 +258,73 @@ export class MySqlDatabase< }); } + /** + * Creates an update query. + * + * Calling this method without `.where()` clause will update all rows in a table. The `.where()` clause specifies which rows should be updated. + * + * Use `.set()` method to specify which values to update. + * + * See docs: {@link https://orm.drizzle.team/docs/update} + * + * @param table The table to update. + * + * @example + * + * ```ts + * // Update all rows in the 'cars' table + * await db.update(cars).set({ color: 'red' }); + * + * // Update rows with filters and conditions + * await db.update(cars).set({ color: 'red' }).where(eq(cars.brand, 'BMW')); + * ``` + */ update(table: TTable): MySqlUpdateBuilder { return new MySqlUpdateBuilder(table, this.session, this.dialect); } + /** + * Creates an insert query. + * + * Calling this method will create new rows in a table. Use `.values()` method to specify which values to insert. + * + * See docs: {@link https://orm.drizzle.team/docs/insert} + * + * @param table The table to insert into. + * + * @example + * + * ```ts + * // Insert one row + * await db.insert(cars).values({ brand: 'BMW' }); + * + * // Insert multiple rows + * await db.insert(cars).values([{ brand: 'BMW' }, { brand: 'Porsche' }]); + * ``` + */ insert(table: TTable): MySqlInsertBuilder { return new MySqlInsertBuilder(table, this.session, this.dialect); } + /** + * Creates a delete query. + * + * Calling this method without `.where()` clause will delete all rows in a table. The `.where()` clause specifies which rows should be deleted. + * + * See docs: {@link https://orm.drizzle.team/docs/delete} + * + * @param table The table to delete from. + * + * @example + * + * ```ts + * // Delete all rows in the 'cars' table + * await db.delete(cars); + * + * // Delete rows with filters and conditions + * await db.delete(cars).where(eq(cars.color, 'green')); + * ``` + */ delete(table: TTable): MySqlDeleteBase { return new MySqlDeleteBase(table, this.session, this.dialect); } @@ -194,36 +364,29 @@ export const withReplicas = < replicas: [Q, ...Q[]], getReplica: (replicas: Q[]) => Q = () => replicas[Math.floor(Math.random() * replicas.length)]!, ): MySQLWithReplicas => { - const select: Q['select'] = (...args: any) => getReplica(replicas).select(args); - const selectDistinct: Q['selectDistinct'] = (...args: any) => getReplica(replicas).selectDistinct(args); - const $with: Q['with'] = (...args: any) => getReplica(replicas).with(args); - - const update: Q['update'] = (...args: any) => primary.update(args); - const insert: Q['insert'] = (...args: any) => primary.insert(args); - const $delete: Q['delete'] = (...args: any) => primary.delete(args); - const execute: Q['execute'] = (...args: any) => primary.execute(args); - const transaction: Q['transaction'] = (...args: any) => primary.transaction(args); - - return new Proxy( - { - ...primary, - update, - insert, - delete: $delete, - execute, - transaction, - $primary: primary, - select, - selectDistinct, - with: $with, - }, - { - get(target, prop, _receiver) { - if (prop === 'query') { - return getReplica(replicas).query; - } - return target[prop as keyof typeof target]; - }, + const select: Q['select'] = (...args: []) => getReplica(replicas).select(...args); + const selectDistinct: Q['selectDistinct'] = (...args: []) => getReplica(replicas).selectDistinct(...args); + const $with: Q['with'] = (...args: []) => getReplica(replicas).with(...args); + + const update: Q['update'] = (...args: [any]) => primary.update(...args); + const insert: Q['insert'] = (...args: [any]) => primary.insert(...args); + const $delete: Q['delete'] = (...args: [any]) => primary.delete(...args); + const execute: Q['execute'] = (...args: [any]) => primary.execute(...args); + const transaction: Q['transaction'] = (...args: [any, any]) => primary.transaction(...args); + + return { + ...primary, + update, + insert, + delete: $delete, + execute, + transaction, + $primary: primary, + select, + selectDistinct, + with: $with, + get query() { + return getReplica(replicas).query; }, - ); + }; }; diff --git a/drizzle-orm/src/mysql-core/query-builders/delete.ts b/drizzle-orm/src/mysql-core/query-builders/delete.ts index 33588fd16..ab615c603 100644 --- a/drizzle-orm/src/mysql-core/query-builders/delete.ts +++ b/drizzle-orm/src/mysql-core/query-builders/delete.ts @@ -97,6 +97,35 @@ export class MySqlDeleteBase< this.config = { table }; } + /** + * Adds a `where` clause to the query. + * + * Calling this method will delete only those rows that fulfill a specified condition. + * + * See docs: {@link https://orm.drizzle.team/docs/delete} + * + * @param where the `where` clause. + * + * @example + * You can use conditional operators and `sql function` to filter the rows to be deleted. + * + * ```ts + * // Delete all cars with green color + * db.delete(cars).where(eq(cars.color, 'green')); + * // or + * db.delete(cars).where(sql`${cars.color} = 'green'`) + * ``` + * + * You can logically combine conditional operators with `and()` and `or()` operators: + * + * ```ts + * // Delete all BMW cars with a green color + * db.delete(cars).where(and(eq(cars.color, 'green'), eq(cars.brand, 'BMW'))); + * + * // Delete all cars with the green or blue color + * db.delete(cars).where(or(eq(cars.color, 'green'), eq(cars.color, 'blue'))); + * ``` + */ where(where: SQL | undefined): MySqlDeleteWithout { this.config.where = where; return this as any; diff --git a/drizzle-orm/src/mysql-core/query-builders/insert.ts b/drizzle-orm/src/mysql-core/query-builders/insert.ts index 4a007d06d..7390d4219 100644 --- a/drizzle-orm/src/mysql-core/query-builders/insert.ts +++ b/drizzle-orm/src/mysql-core/query-builders/insert.ts @@ -158,6 +158,32 @@ export class MySqlInsertBase< this.config = { table, values, ignore }; } + /** + * Adds an `on duplicate key update` clause to the query. + * + * Calling this method will update update the row if any unique index conflicts. MySQL will automatically determine the conflict target based on the primary key and unique indexes. + * + * See docs: {@link https://orm.drizzle.team/docs/insert#on-duplicate-key-update} + * + * @param config The `set` clause + * + * @example + * ```ts + * await db.insert(cars) + * .values({ id: 1, brand: 'BMW'}) + * .onDuplicateKeyUpdate({ set: { brand: 'Porsche' }}); + * ``` + * + * While MySQL does not directly support doing nothing on conflict, you can perform a no-op by setting any column's value to itself and achieve the same effect: + * + * ```ts + * import { sql } from 'drizzle-orm'; + * + * await db.insert(cars) + * .values({ id: 1, brand: 'BMW' }) + * .onDuplicateKeyUpdate({ set: { id: sql`id` } }); + * ``` + */ onDuplicateKeyUpdate( config: MySqlInsertOnDuplicateKeyUpdateConfig, ): MySqlInsertWithout { diff --git a/drizzle-orm/src/mysql-core/query-builders/query.ts b/drizzle-orm/src/mysql-core/query-builders/query.ts index f14a5a74e..8efeb0692 100644 --- a/drizzle-orm/src/mysql-core/query-builders/query.ts +++ b/drizzle-orm/src/mysql-core/query-builders/query.ts @@ -105,7 +105,7 @@ export class MySqlRelationalQuery< ) as PreparedQueryKind; } - private _toSQL(): { query: BuildRelationalQueryResult; builtQuery: QueryWithTypings } { + private _getQuery() { const query = this.mode === 'planetscale' ? this.dialect.buildRelationalQueryWithoutLateralSubqueries({ fullSchema: this.fullSchema, @@ -125,12 +125,22 @@ export class MySqlRelationalQuery< queryConfig: this.config, tableAlias: this.tableConfig.tsName, }); + return query; + } + + private _toSQL(): { query: BuildRelationalQueryResult; builtQuery: QueryWithTypings } { + const query = this._getQuery(); const builtQuery = this.dialect.sqlToQuery(query.sql as SQL); return { builtQuery, query }; } + /** @internal */ + getSQL(): SQL { + return this._getQuery().sql as SQL; + } + toSQL(): Query { return this._toSQL().builtQuery; } diff --git a/drizzle-orm/src/mysql-core/query-builders/select.ts b/drizzle-orm/src/mysql-core/query-builders/select.ts index 23f31342e..588b0bec3 100644 --- a/drizzle-orm/src/mysql-core/query-builders/select.ts +++ b/drizzle-orm/src/mysql-core/query-builders/select.ts @@ -261,12 +261,120 @@ export abstract class MySqlSelectQueryBuilderBase< }; } + /** + * 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'); - + + /** + * 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'); + /** + * 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'); - + + /** + * 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'); private createSetOperator( @@ -301,16 +409,196 @@ export abstract class MySqlSelectQueryBuilderBase< }; } + /** + * 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/mysql-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/mysql-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/mysql-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/mysql-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/mysql-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/mysql-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 */ @@ -324,6 +612,35 @@ export abstract class MySqlSelectQueryBuilderBase< return this as any; } + /** + * 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: ((aliases: this['_']['selection']) => SQL | undefined) | SQL | undefined, ): MySqlSelectWithout { @@ -339,6 +656,28 @@ export abstract class MySqlSelectQueryBuilderBase< return this as any; } + /** + * 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`cast(count(${cars.id}) as int)`, + * }) + * .from(cars) + * .groupBy(cars.brand) + * .having(({ count }) => gt(count, 1)); + * ``` + */ having( having: ((aliases: this['_']['selection']) => SQL | undefined) | SQL | undefined, ): MySqlSelectWithout { @@ -354,6 +693,25 @@ export abstract class MySqlSelectQueryBuilderBase< return this as any; } + /** + * Adds a `group by` clause to the query. + * + * Calling this method will group rows that have the same values into summary rows, often used for aggregation purposes. + * + * See docs: {@link https://orm.drizzle.team/docs/select#aggregations} + * + * @example + * + * ```ts + * // Group and count people by their last names + * await db.select({ + * lastName: people.lastName, + * count: sql`cast(count(*) as int)` + * }) + * .from(people) + * .groupBy(people.lastName); + * ``` + */ groupBy( builder: (aliases: this['_']['selection']) => ValueOrArray, ): MySqlSelectWithout; @@ -377,6 +735,30 @@ export abstract class MySqlSelectQueryBuilderBase< return this as any; } + /** + * Adds an `order by` clause to the query. + * + * Calling this method will sort the result-set in ascending or descending order. By default, the sort order is ascending. + * + * See docs: {@link https://orm.drizzle.team/docs/select#order-by} + * + * @example + * + * ``` + * // Select cars ordered by year + * await db.select().from(cars).orderBy(cars.year); + * ``` + * + * You can specify whether results are in ascending or descending order with the `asc()` and `desc()` operators. + * + * ```ts + * // Select cars ordered by year in descending order + * await db.select().from(cars).orderBy(desc(cars.year)); + * + * // Select cars ordered by year and price + * await db.select().from(cars).orderBy(asc(cars.year), desc(cars.price)); + * ``` + */ orderBy( builder: (aliases: this['_']['selection']) => ValueOrArray, ): MySqlSelectWithout; @@ -413,6 +795,22 @@ export abstract class MySqlSelectQueryBuilderBase< return this as any; } + /** + * 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: number): MySqlSelectWithout { if (this.config.setOperators.length > 0) { this.config.setOperators.at(-1)!.limit = limit; @@ -422,6 +820,22 @@ export abstract class MySqlSelectQueryBuilderBase< return this as any; } + /** + * 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: number): MySqlSelectWithout { if (this.config.setOperators.length > 0) { this.config.setOperators.at(-1)!.offset = offset; @@ -431,6 +845,16 @@ export abstract class MySqlSelectQueryBuilderBase< return this as any; } + /** + * 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://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html} + * + * @param strength the lock strength. + * @param config the lock configuration. + */ for(strength: LockStrength, config: LockConfig = {}): MySqlSelectWithout { this.config.lockingClause = { strength, config }; return this as any; @@ -578,14 +1002,194 @@ const getMySqlSetOperators = () => ({ exceptAll, }); +/** + * 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 + * import { union } from 'drizzle-orm/mysql-core' + * + * await union( + * db.select({ name: users.name }).from(users), + * db.select({ name: customers.name }).from(customers) + * ); + * // or + * await db.select({ name: users.name }) + * .from(users) + * .union( + * db.select({ name: customers.name }).from(customers) + * ); + * ``` + */ export const union = 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 + * import { unionAll } from 'drizzle-orm/mysql-core' + * + * await unionAll( + * db.select({ transaction: onlineSales.transactionId }).from(onlineSales), + * db.select({ transaction: inStoreSales.transactionId }).from(inStoreSales) + * ); + * // or + * await db.select({ transaction: onlineSales.transactionId }) + * .from(onlineSales) + * .unionAll( + * db.select({ transaction: inStoreSales.transactionId }).from(inStoreSales) + * ); + * ``` + */ export const unionAll = 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 + * import { intersect } from 'drizzle-orm/mysql-core' + * + * await intersect( + * db.select({ courseName: depA.courseName }).from(depA), + * db.select({ courseName: depB.courseName }).from(depB) + * ); + * // or + * await db.select({ courseName: depA.courseName }) + * .from(depA) + * .intersect( + * db.select({ courseName: depB.courseName }).from(depB) + * ); + * ``` + */ export const intersect = 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 + * import { intersectAll } from 'drizzle-orm/mysql-core' + * + * await intersectAll( + * db.select({ + * productId: regularCustomerOrders.productId, + * quantityOrdered: regularCustomerOrders.quantityOrdered + * }) + * .from(regularCustomerOrders), + * db.select({ + * productId: vipCustomerOrders.productId, + * quantityOrdered: vipCustomerOrders.quantityOrdered + * }) + * .from(vipCustomerOrders) + * ); + * // or + * await db.select({ + * productId: regularCustomerOrders.productId, + * quantityOrdered: regularCustomerOrders.quantityOrdered + * }) + * .from(regularCustomerOrders) + * .intersectAll( + * db.select({ + * productId: vipCustomerOrders.productId, + * quantityOrdered: vipCustomerOrders.quantityOrdered + * }) + * .from(vipCustomerOrders) + * ); + * ``` + */ export const intersectAll = 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 + * import { except } from 'drizzle-orm/mysql-core' + * + * await except( + * db.select({ courseName: depA.courseName }).from(depA), + * db.select({ courseName: depB.courseName }).from(depB) + * ); + * // or + * await db.select({ courseName: depA.courseName }) + * .from(depA) + * .except( + * db.select({ courseName: depB.courseName }).from(depB) + * ); + * ``` + */ export const except = 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 + * import { exceptAll } from 'drizzle-orm/mysql-core' + * + * await exceptAll( + * db.select({ + * productId: regularCustomerOrders.productId, + * quantityOrdered: regularCustomerOrders.quantityOrdered + * }) + * .from(regularCustomerOrders), + * db.select({ + * productId: vipCustomerOrders.productId, + * quantityOrdered: vipCustomerOrders.quantityOrdered + * }) + * .from(vipCustomerOrders) + * ); + * // or + * await db.select({ + * productId: regularCustomerOrders.productId, + * quantityOrdered: regularCustomerOrders.quantityOrdered, + * }) + * .from(regularCustomerOrders) + * .exceptAll( + * db.select({ + * productId: vipCustomerOrders.productId, + * quantityOrdered: vipCustomerOrders.quantityOrdered, + * }) + * .from(vipCustomerOrders) + * ); + * ``` + */ export const exceptAll = createSetOperator('except', true); diff --git a/drizzle-orm/src/mysql-core/query-builders/update.ts b/drizzle-orm/src/mysql-core/query-builders/update.ts index 94b884058..ce83e9b4b 100644 --- a/drizzle-orm/src/mysql-core/query-builders/update.ts +++ b/drizzle-orm/src/mysql-core/query-builders/update.ts @@ -131,6 +131,39 @@ export class MySqlUpdateBase< this.config = { set, table }; } + /** + * Adds a 'where' clause to the query. + * + * Calling this method will update only those rows that fulfill a specified condition. + * + * See docs: {@link https://orm.drizzle.team/docs/update} + * + * @param where the 'where' clause. + * + * @example + * You can use conditional operators and `sql function` to filter the rows to be updated. + * + * ```ts + * // Update all cars with green color + * db.update(cars).set({ color: 'red' }) + * .where(eq(cars.color, 'green')); + * // or + * db.update(cars).set({ color: 'red' }) + * .where(sql`${cars.color} = 'green'`) + * ``` + * + * You can logically combine conditional operators with `and()` and `or()` operators: + * + * ```ts + * // Update all BMW cars with a green color + * db.update(cars).set({ color: 'red' }) + * .where(and(eq(cars.color, 'green'), eq(cars.brand, 'BMW'))); + * + * // Update all cars with the green or blue color + * db.update(cars).set({ color: 'red' }) + * .where(or(eq(cars.color, 'green'), eq(cars.color, 'blue'))); + * ``` + */ where(where: SQL | undefined): MySqlUpdateWithout { this.config.where = where; return this as any; diff --git a/drizzle-orm/src/mysql-core/view.ts b/drizzle-orm/src/mysql-core/view.ts index dd7d2f82d..4cc7d416c 100644 --- a/drizzle-orm/src/mysql-core/view.ts +++ b/drizzle-orm/src/mysql-core/view.ts @@ -2,15 +2,15 @@ import type { BuildColumns } from '~/column-builder.ts'; import { entityKind } from '~/entity.ts'; import type { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; import type { AddAliasToSelection } from '~/query-builders/select.types.ts'; +import { SelectionProxyHandler } from '~/selection-proxy.ts'; import type { ColumnsSelection, SQL } from '~/sql/sql.ts'; import { getTableColumns } from '~/utils.ts'; import type { MySqlColumn, MySqlColumnBuilderBase } from './columns/index.ts'; import { QueryBuilder } from './query-builders/query-builder.ts'; import type { SelectedFields } from './query-builders/select.types.ts'; import { mysqlTable } from './table.ts'; -import { MySqlViewConfig } from './view-common.ts'; import { MySqlViewBase } from './view-base.ts'; -import { SelectionProxyHandler } from '~/selection-proxy.ts'; +import { MySqlViewConfig } from './view-common.ts'; export interface ViewBuilderConfig { algorithm?: 'undefined' | 'merge' | 'temptable'; diff --git a/drizzle-orm/src/mysql-proxy/migrator.ts b/drizzle-orm/src/mysql-proxy/migrator.ts index b75184934..368efc18b 100644 --- a/drizzle-orm/src/mysql-proxy/migrator.ts +++ b/drizzle-orm/src/mysql-proxy/migrator.ts @@ -26,8 +26,8 @@ export async function migrate>( id: sql.raw('id'), hash: sql.raw('hash'), created_at: sql.raw('created_at'), - }).from(sql.raw(migrationsTable)).orderBy( - sql.raw('created_at desc'), + }).from(sql.identifier(migrationsTable).getSQL()).orderBy( + sql.raw('created_at desc') ).limit(1); const lastDbMigration = dbMigrations[0]; @@ -41,9 +41,7 @@ export async function migrate>( ) { queriesToRun.push( ...migration.sql, - `insert into ${ - sql.identifier(migrationsTable) - } (\`hash\`, \`created_at\`) values(${migration.hash}, ${migration.folderMillis})`, + `insert into ${sql.identifier(migrationsTable).value} (\`hash\`, \`created_at\`) values('${migration.hash}', '${migration.folderMillis}')`, ); } } diff --git a/drizzle-orm/src/pg-core/db.ts b/drizzle-orm/src/pg-core/db.ts index 8a1be58de..a5050ce53 100644 --- a/drizzle-orm/src/pg-core/db.ts +++ b/drizzle-orm/src/pg-core/db.ts @@ -17,6 +17,7 @@ import type { import type { PgTable } from '~/pg-core/table.ts'; import type { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; import type { ExtractTablesWithRelations, RelationalSchemaConfig, TablesRelationalConfig } from '~/relations.ts'; +import { SelectionProxyHandler } from '~/selection-proxy.ts'; import type { ColumnsSelection, SQLWrapper } from '~/sql/sql.ts'; import { WithSubquery } from '~/subquery.ts'; import type { DrizzleTypeError } from '~/utils.ts'; @@ -26,7 +27,6 @@ import { PgRefreshMaterializedView } from './query-builders/refresh-materialized import type { SelectedFields } from './query-builders/select.types.ts'; import type { WithSubqueryWithSelection } from './subquery.ts'; import type { PgMaterializedView } from './view.ts'; -import { SelectionProxyHandler } from '~/selection-proxy.ts'; export class PgDatabase< TQueryResult extends QueryResultHKT, @@ -72,6 +72,38 @@ export class PgDatabase< } } + /** + * Creates a subquery that defines a temporary named result set as a CTE. + * + * It is useful for breaking down complex queries into simpler parts and for reusing the result set in subsequent parts of the query. + * + * See docs: {@link https://orm.drizzle.team/docs/select#with-clause} + * + * @param alias The alias for the subquery. + * + * Failure to provide an alias will result in a DrizzleTypeError, preventing the subquery from being referenced in other queries. + * + * @example + * + * ```ts + * // Create a subquery with alias 'sq' and use it in the select query + * const sq = db.$with('sq').as(db.select().from(users).where(eq(users.id, 42))); + * + * const result = await db.with(sq).select().from(sq); + * ``` + * + * To select arbitrary SQL values as fields in a CTE and reference them in other CTEs or in the main query, you need to add aliases to them: + * + * ```ts + * // Select an arbitrary SQL value as a field in a CTE and reference it in the main query + * const sq = db.$with('sq').as(db.select({ + * name: sql`upper(${users.name})`.as('name'), + * }) + * .from(users)); + * + * const result = await db.with(sq).select({ name: sq.name }).from(sq); + * ``` + */ $with(alias: TAlias) { return { as( @@ -89,6 +121,25 @@ export class PgDatabase< }; } + /** + * Incorporates a previously defined CTE (using `$with`) into the main query. + * + * This method allows the main query to reference a temporary named result set. + * + * See docs: {@link https://orm.drizzle.team/docs/select#with-clause} + * + * @param queries The CTEs to incorporate into the main query. + * + * @example + * + * ```ts + * // Define a subquery 'sq' as a CTE using $with + * const sq = db.$with('sq').as(db.select().from(users).where(eq(users.id, 42))); + * + * // Incorporate the CTE 'sq' into the main query and select from it + * const result = await db.with(sq).select().from(sq); + * ``` + */ with(...queries: WithSubquery[]) { const self = this; @@ -106,6 +157,42 @@ export class PgDatabase< return { select }; } + /** + * Creates a select query. + * + * Calling this method with no arguments will select all columns from the table. Pass a selection object to specify the columns you want to select. + * + * Use `.from()` method to specify which table to select from. + * + * See docs: {@link https://orm.drizzle.team/docs/select} + * + * @param fields The selection object. + * + * @example + * + * ```ts + * // Select all columns and all rows from the 'cars' table + * const allCars: Car[] = await db.select().from(cars); + * + * // Select specific columns and all rows from the 'cars' table + * const carsIdsAndBrands: { id: number; brand: string }[] = await db.select({ + * id: cars.id, + * brand: cars.brand + * }) + * .from(cars); + * ``` + * + * Like in SQL, you can use arbitrary expressions as selection fields, not just table columns: + * + * ```ts + * // Select specific columns along with expression and all rows from the 'cars' table + * const carsIdsAndLowerNames: { id: number; lowerBrand: string }[] = await db.select({ + * id: cars.id, + * lowerBrand: sql`lower(${cars.brand})`, + * }) + * .from(cars); + * ``` + */ select(): PgSelectBuilder; select(fields: TSelection): PgSelectBuilder; select(fields?: SelectedFields): PgSelectBuilder { @@ -116,6 +203,30 @@ export class PgDatabase< }); } + /** + * Adds `distinct` expression to the select query. + * + * Calling this method will return only unique values. When multiple columns are selected, it returns rows with unique combinations of values in these columns. + * + * Use `.from()` method to specify which table to select from. + * + * See docs: {@link https://orm.drizzle.team/docs/select#distinct} + * + * @param fields The selection object. + * + * @example + * ```ts + * // Select all unique rows from the 'cars' table + * await db.selectDistinct() + * .from(cars) + * .orderBy(cars.id, cars.brand, cars.color); + * + * // Select all unique brands from the 'cars' table + * await db.selectDistinct({ brand: cars.brand }) + * .from(cars) + * .orderBy(cars.brand); + * ``` + */ selectDistinct(): PgSelectBuilder; selectDistinct(fields: TSelection): PgSelectBuilder; selectDistinct(fields?: SelectedFields): PgSelectBuilder { @@ -127,6 +238,31 @@ export class PgDatabase< }); } + /** + * Adds `distinct on` expression to the select query. + * + * Calling this method will specify how the unique rows are determined. + * + * Use `.from()` method to specify which table to select from. + * + * See docs: {@link https://orm.drizzle.team/docs/select#distinct} + * + * @param on The expression defining uniqueness. + * @param fields The selection object. + * + * @example + * ```ts + * // Select the first row for each unique brand from the 'cars' table + * await db.selectDistinctOn([cars.brand]) + * .from(cars) + * .orderBy(cars.brand); + * + * // Selects the first occurrence of each unique car brand along with its color from the 'cars' table + * await db.selectDistinctOn([cars.brand], { brand: cars.brand, color: cars.color }) + * .from(cars) + * .orderBy(cars.brand, cars.color); + * ``` + */ selectDistinctOn(on: (PgColumn | SQLWrapper)[]): PgSelectBuilder; selectDistinctOn( on: (PgColumn | SQLWrapper)[], @@ -144,14 +280,89 @@ export class PgDatabase< }); } + /** + * Creates an update query. + * + * Calling this method without `.where()` clause will update all rows in a table. The `.where()` clause specifies which rows should be updated. + * + * Use `.set()` method to specify which values to update. + * + * See docs: {@link https://orm.drizzle.team/docs/update} + * + * @param table The table to update. + * + * @example + * + * ```ts + * // Update all rows in the 'cars' table + * await db.update(cars).set({ color: 'red' }); + * + * // Update rows with filters and conditions + * await db.update(cars).set({ color: 'red' }).where(eq(cars.brand, 'BMW')); + * + * // Update with returning clause + * const updatedCar: Car[] = await db.update(cars) + * .set({ color: 'red' }) + * .where(eq(cars.id, 1)) + * .returning(); + * ``` + */ update(table: TTable): PgUpdateBuilder { return new PgUpdateBuilder(table, this.session, this.dialect); } + /** + * Creates an insert query. + * + * Calling this method will create new rows in a table. Use `.values()` method to specify which values to insert. + * + * See docs: {@link https://orm.drizzle.team/docs/insert} + * + * @param table The table to insert into. + * + * @example + * + * ```ts + * // Insert one row + * await db.insert(cars).values({ brand: 'BMW' }); + * + * // Insert multiple rows + * await db.insert(cars).values([{ brand: 'BMW' }, { brand: 'Porsche' }]); + * + * // Insert with returning clause + * const insertedCar: Car[] = await db.insert(cars) + * .values({ brand: 'BMW' }) + * .returning(); + * ``` + */ insert(table: TTable): PgInsertBuilder { return new PgInsertBuilder(table, this.session, this.dialect); } + /** + * Creates a delete query. + * + * Calling this method without `.where()` clause will delete all rows in a table. The `.where()` clause specifies which rows should be deleted. + * + * See docs: {@link https://orm.drizzle.team/docs/delete} + * + * @param table The table to delete from. + * + * @example + * + * ```ts + * // Delete all rows in the 'cars' table + * await db.delete(cars); + * + * // Delete rows with filters and conditions + * await db.delete(cars).where(eq(cars.color, 'green')); + * + * // Delete with returning clause + * const deletedCar: Car[] = await db.delete(cars) + * .where(eq(cars.id, 1)) + * .returning(); + * ``` + */ delete(table: TTable): PgDeleteBase { return new PgDeleteBase(table, this.session, this.dialect); } @@ -186,40 +397,34 @@ export const withReplicas = < replicas: [Q, ...Q[]], getReplica: (replicas: Q[]) => Q = () => replicas[Math.floor(Math.random() * replicas.length)]!, ): PgWithReplicas => { - const select: Q['select'] = (...args: any) => getReplica(replicas).select(args); - const selectDistinct: Q['selectDistinct'] = (...args: any) => getReplica(replicas).selectDistinct(args); - const selectDistinctOn: Q['selectDistinctOn'] = (...args: any) => getReplica(replicas).selectDistinctOn(args); - const $with: Q['with'] = (...args: any) => getReplica(replicas).with(args); - - const update: Q['update'] = (...args: any) => primary.update(args); - const insert: Q['insert'] = (...args: any) => primary.insert(args); - const $delete: Q['delete'] = (...args: any) => primary.delete(args); - const execute: Q['execute'] = (...args: any) => primary.execute(args); - const transaction: Q['transaction'] = (...args: any) => primary.transaction(args); - const refreshMaterializedView: Q['refreshMaterializedView'] = (...args: any) => primary.refreshMaterializedView(args); - - return new Proxy( - { - ...primary, - update, - insert, - delete: $delete, - execute, - transaction, - refreshMaterializedView, - $primary: primary, - select, - selectDistinct, - selectDistinctOn, - with: $with, - }, - { - get(target, prop, _receiver) { - if (prop === 'query') { - return getReplica(replicas).query; - } - return target[prop as keyof typeof target]; - }, + const select: Q['select'] = (...args: []) => getReplica(replicas).select(...args); + const selectDistinct: Q['selectDistinct'] = (...args: []) => getReplica(replicas).selectDistinct(...args); + const selectDistinctOn: Q['selectDistinctOn'] = (...args: [any]) => getReplica(replicas).selectDistinctOn(...args); + const $with: Q['with'] = (...args: any) => getReplica(replicas).with(...args); + + const update: Q['update'] = (...args: [any]) => primary.update(...args); + const insert: Q['insert'] = (...args: [any]) => primary.insert(...args); + const $delete: Q['delete'] = (...args: [any]) => primary.delete(...args); + const execute: Q['execute'] = (...args: [any]) => primary.execute(...args); + const transaction: Q['transaction'] = (...args: [any]) => primary.transaction(...args); + const refreshMaterializedView: Q['refreshMaterializedView'] = (...args: [any]) => + primary.refreshMaterializedView(...args); + + return { + ...primary, + update, + insert, + delete: $delete, + execute, + transaction, + refreshMaterializedView, + $primary: primary, + select, + selectDistinct, + selectDistinctOn, + with: $with, + get query() { + return getReplica(replicas).query; }, - ); + }; }; diff --git a/drizzle-orm/src/pg-core/dialect.ts b/drizzle-orm/src/pg-core/dialect.ts index 366436e29..cf47d1e11 100644 --- a/drizzle-orm/src/pg-core/dialect.ts +++ b/drizzle-orm/src/pg-core/dialect.ts @@ -253,7 +253,7 @@ export class PgDialect { let distinctSql: SQL | undefined; if (distinct) { - distinctSql = distinct === true ? sql` distinct` : sql` distinct on (${sql.join(distinct.on, ', ')})`; + distinctSql = distinct === true ? sql` distinct` : sql` distinct on (${sql.join(distinct.on, sql`, `)})`; } const selection = this.buildSelection(fieldsList, { isSingleTable }); diff --git a/drizzle-orm/src/pg-core/query-builders/delete.ts b/drizzle-orm/src/pg-core/query-builders/delete.ts index 5f446e03f..df22a110e 100644 --- a/drizzle-orm/src/pg-core/query-builders/delete.ts +++ b/drizzle-orm/src/pg-core/query-builders/delete.ts @@ -15,6 +15,7 @@ import { Table } from '~/table.ts'; import { tracer } from '~/tracing.ts'; import { orderSelectedFields } from '~/utils.ts'; import type { SelectedFieldsFlat, SelectedFieldsOrdered } from './select.types.ts'; +import type { PgColumn } from '../columns/common.ts'; export type PgDeleteWithout< T extends AnyPgDeleteBase, @@ -129,11 +130,60 @@ export class PgDeleteBase< this.config = { table }; } + /** + * Adds a `where` clause to the query. + * + * Calling this method will delete only those rows that fulfill a specified condition. + * + * See docs: {@link https://orm.drizzle.team/docs/delete} + * + * @param where the `where` clause. + * + * @example + * You can use conditional operators and `sql function` to filter the rows to be deleted. + * + * ```ts + * // Delete all cars with green color + * await db.delete(cars).where(eq(cars.color, 'green')); + * // or + * await db.delete(cars).where(sql`${cars.color} = 'green'`) + * ``` + * + * You can logically combine conditional operators with `and()` and `or()` operators: + * + * ```ts + * // Delete all BMW cars with a green color + * await db.delete(cars).where(and(eq(cars.color, 'green'), eq(cars.brand, 'BMW'))); + * + * // Delete all cars with the green or blue color + * await db.delete(cars).where(or(eq(cars.color, 'green'), eq(cars.color, 'blue'))); + * ``` + */ where(where: SQL | undefined): PgDeleteWithout { this.config.where = where; return this as any; } + /** + * Adds a `returning` clause to the query. + * + * Calling this method will return the specified fields of the deleted rows. If no fields are specified, all fields will be returned. + * + * See docs: {@link https://orm.drizzle.team/docs/delete#delete-with-return} + * + * @example + * ```ts + * // Delete all cars with the green color and return all fields + * const deletedCars: Car[] = await db.delete(cars) + * .where(eq(cars.color, 'green')) + * .returning(); + * + * // Delete all cars with the green color and return only their id and brand fields + * const deletedCarsIdsAndBrands: { id: number, brand: string }[] = await db.delete(cars) + * .where(eq(cars.color, 'green')) + * .returning({ id: cars.id, brand: cars.brand }); + * ``` + */ returning(): PgDeleteReturningAll; returning( fields: TSelectedFields, @@ -141,7 +191,7 @@ export class PgDeleteBase< returning( fields: SelectedFieldsFlat = this.config.table[Table.Symbol.Columns], ): PgDeleteReturning { - this.config.returning = orderSelectedFields(fields); + this.config.returning = orderSelectedFields(fields); return this as any; } diff --git a/drizzle-orm/src/pg-core/query-builders/insert.ts b/drizzle-orm/src/pg-core/query-builders/insert.ts index b886426be..b4ec31a93 100644 --- a/drizzle-orm/src/pg-core/query-builders/insert.ts +++ b/drizzle-orm/src/pg-core/query-builders/insert.ts @@ -18,6 +18,7 @@ import { tracer } from '~/tracing.ts'; import { mapUpdateSet, orderSelectedFields } from '~/utils.ts'; import type { SelectedFieldsFlat, SelectedFieldsOrdered } from './select.types.ts'; import type { PgUpdateSetSource } from './update.ts'; +import type { PgColumn } from '../columns/common.ts'; export interface PgInsertConfig { table: TTable; @@ -163,6 +164,26 @@ export class PgInsertBase< this.config = { table, values }; } + /** + * Adds a `returning` clause to the query. + * + * Calling this method will return the specified fields of the inserted rows. If no fields are specified, all fields will be returned. + * + * See docs: {@link https://orm.drizzle.team/docs/insert#insert-returning} + * + * @example + * ```ts + * // Insert one row and return all fields + * const insertedCar: Car[] = await db.insert(cars) + * .values({ brand: 'BMW' }) + * .returning(); + * + * // Insert one row and return only the id + * const insertedCarId: { id: number }[] = await db.insert(cars) + * .values({ brand: 'BMW' }) + * .returning({ id: cars.id }); + * ``` + */ returning(): PgInsertWithout, TDynamic, 'returning'>; returning( fields: TSelectedFields, @@ -170,10 +191,32 @@ export class PgInsertBase< returning( fields: SelectedFieldsFlat = this.config.table[Table.Symbol.Columns], ): PgInsertWithout { - this.config.returning = orderSelectedFields(fields); + this.config.returning = orderSelectedFields(fields); return this as any; } + /** + * Adds an `on conflict do nothing` clause to the query. + * + * Calling this method simply avoids inserting a row as its alternative action. + * + * See docs: {@link https://orm.drizzle.team/docs/insert#on-conflict-do-nothing} + * + * @param config The `target` and `where` clauses. + * + * @example + * ```ts + * // Insert one row and cancel the insert if there's a conflict + * await db.insert(cars) + * .values({ id: 1, brand: 'BMW' }) + * .onConflictDoNothing(); + * + * // Explicitly specify conflict target + * await db.insert(cars) + * .values({ id: 1, brand: 'BMW' }) + * .onConflictDoNothing({ target: cars.id }); + * ``` + */ onConflictDoNothing( config: { target?: IndexColumn | IndexColumn[]; where?: SQL } = {}, ): PgInsertWithout { @@ -191,6 +234,36 @@ export class PgInsertBase< return this as any; } + + /** + * Adds an `on conflict do update` clause to the query. + * + * Calling this method will update the existing row that conflicts with the row proposed for insertion as its alternative action. + * + * See docs: {@link https://orm.drizzle.team/docs/insert#upserts-and-conflicts} + * + * @param config The `target`, `set` and `where` clauses. + * + * @example + * ```ts + * // Update the row if there's a conflict + * await db.insert(cars) + * .values({ id: 1, brand: 'BMW' }) + * .onConflictDoUpdate({ + * target: cars.id, + * set: { brand: 'Porsche' } + * }); + * + * // Upsert with 'where' clause + * await db.insert(cars) + * .values({ id: 1, brand: 'BMW' }) + * .onConflictDoUpdate({ + * target: cars.id, + * set: { brand: 'newBMW' }, + * where: sql`${cars.createdAt} > '2023-01-01'::date`, + * }); + * ``` + */ onConflictDoUpdate( config: PgInsertOnConflictDoUpdateConfig, ): PgInsertWithout { diff --git a/drizzle-orm/src/pg-core/query-builders/query.ts b/drizzle-orm/src/pg-core/query-builders/query.ts index ab0a99839..ef352e734 100644 --- a/drizzle-orm/src/pg-core/query-builders/query.ts +++ b/drizzle-orm/src/pg-core/query-builders/query.ts @@ -105,8 +105,8 @@ export class PgRelationalQuery extends QueryPromise { return this._prepare(name); } - private _toSQL(): { query: BuildRelationalQueryResult; builtQuery: QueryWithTypings } { - const query = this.dialect.buildRelationalQueryWithoutPK({ + private _getQuery() { + return this.dialect.buildRelationalQueryWithoutPK({ fullSchema: this.fullSchema, schema: this.schema, tableNamesMap: this.tableNamesMap, @@ -115,6 +115,15 @@ export class PgRelationalQuery extends QueryPromise { queryConfig: this.config, tableAlias: this.tableConfig.tsName, }); + } + + /** @internal */ + getSQL(): SQL { + return this._getQuery().sql as SQL; + } + + private _toSQL(): { query: BuildRelationalQueryResult; builtQuery: QueryWithTypings } { + const query = this._getQuery(); const builtQuery = this.dialect.sqlToQuery(query.sql as SQL); diff --git a/drizzle-orm/src/pg-core/query-builders/select.ts b/drizzle-orm/src/pg-core/query-builders/select.ts index b76c9c778..a71bd53ab 100644 --- a/drizzle-orm/src/pg-core/query-builders/select.ts +++ b/drizzle-orm/src/pg-core/query-builders/select.ts @@ -268,35 +268,118 @@ export abstract class PgSelectQueryBuilderBase< } /** - * For each row of the table, include - * values from a matching row of the joined - * table, if there is a matching row. If not, - * all of the columns of the joined table - * will be set to null. + * 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'); /** - * Includes all of the rows of the joined table. - * If there is no matching row in the main table, - * all the columns of the main table will be - * set to null. + * 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'); /** - * This is the default type of join. - * - * For each row of the table, the joined table - * needs to have a matching row, or it will - * be excluded from results. + * 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'); /** - * Rows from both the main & joined are included, - * regardless of whether or not they have matching - * rows in the other table. + * 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'); @@ -332,16 +415,196 @@ export abstract class PgSelectQueryBuilderBase< }; } + /** + * 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/pg-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/pg-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/pg-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/pg-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/pg-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/pg-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 */ @@ -355,18 +618,35 @@ export abstract class PgSelectQueryBuilderBase< return this as any; } - /** - * Specify a condition to narrow the result set. Multiple - * conditions can be combined with the `and` and `or` - * functions. - * - * ## Examples - * + /** + * 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 - * // Find cars made in the year 2000 - * db.select().from(cars).where(eq(cars.year, 2000)); + * // 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: ((aliases: this['_']['selection']) => SQL | undefined) | SQL | undefined, ): PgSelectWithout { @@ -383,11 +663,26 @@ export abstract class PgSelectQueryBuilderBase< } /** - * Sets the HAVING clause of this query, which often - * used with GROUP BY and filters rows after they've been - * grouped together and combined. - * - * {@link https://www.postgresql.org/docs/current/sql-select.html#SQL-HAVING | Postgres having clause documentation} + * 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`cast(count(${cars.id}) as int)`, + * }) + * .from(cars) + * .groupBy(cars.brand) + * .having(({ count }) => gt(count, 1)); + * ``` */ having( having: ((aliases: this['_']['selection']) => SQL | undefined) | SQL | undefined, @@ -405,22 +700,23 @@ export abstract class PgSelectQueryBuilderBase< } /** - * Specify the GROUP BY of this query: given - * a list of columns or SQL expressions, Postgres will - * combine all rows with the same values in those columns - * into a single row. - * - * ## Examples + * Adds a `group by` clause to the query. + * + * Calling this method will group rows that have the same values into summary rows, often used for aggregation purposes. + * + * See docs: {@link https://orm.drizzle.team/docs/select#aggregations} * + * @example + * * ```ts * // Group and count people by their last names - * db.select({ + * await db.select({ * lastName: people.lastName, - * count: sql`count(*)::integer` - * }).from(people).groupBy(people.lastName); + * count: sql`cast(count(*) as int)` + * }) + * .from(people) + * .groupBy(people.lastName); * ``` - * - * {@link https://www.postgresql.org/docs/current/sql-select.html#SQL-GROUPBY | Postgres GROUP BY documentation} */ groupBy( builder: (aliases: this['_']['selection']) => ValueOrArray, @@ -446,19 +742,28 @@ export abstract class PgSelectQueryBuilderBase< } /** - * Specify the ORDER BY clause of this query: a number of - * columns or SQL expressions that will control sorting - * of results. You can specify whether results are in ascending - * or descending order with the `asc()` and `desc()` operators. + * Adds an `order by` clause to the query. + * + * Calling this method will sort the result-set in ascending or descending order. By default, the sort order is ascending. + * + * See docs: {@link https://orm.drizzle.team/docs/select#order-by} * - * ## Examples + * @example * * ``` - * // Select cars by year released - * db.select().from(cars).orderBy(cars.year); + * // Select cars ordered by year + * await db.select().from(cars).orderBy(cars.year); + * ``` + * + * You can specify whether results are in ascending or descending order with the `asc()` and `desc()` operators. + * + * ```ts + * // Select cars ordered by year in descending order + * await db.select().from(cars).orderBy(desc(cars.year)); + * + * // Select cars ordered by year and price + * await db.select().from(cars).orderBy(asc(cars.year), desc(cars.price)); * ``` - * - * {@link https://www.postgresql.org/docs/current/sql-select.html#SQL-ORDERBY | Postgres ORDER BY documentation} */ orderBy( builder: (aliases: this['_']['selection']) => ValueOrArray, @@ -497,17 +802,20 @@ export abstract class PgSelectQueryBuilderBase< } /** - * Set the maximum number of rows that will be - * returned by this query. + * Adds a `limit` clause to the query. + * + * Calling this method will set the maximum number of rows that will be returned by this query. * - * ## Examples + * 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. - * db.select().from(people).limit(10); + * await db.select().from(people).limit(10); * ``` - * - * {@link https://www.postgresql.org/docs/current/sql-select.html#SQL-LIMIT | Postgres LIMIT documentation} */ limit(limit: number | Placeholder): PgSelectWithout { if (this.config.setOperators.length > 0) { @@ -519,14 +827,19 @@ export abstract class PgSelectQueryBuilderBase< } /** - * Skip a number of rows when returning results - * from this query. - * - * ## Examples + * 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. - * db.select().from(people).offset(10).limit(10); + * await db.select().from(people).offset(10).limit(10); * ``` */ offset(offset: number | Placeholder): PgSelectWithout { @@ -539,11 +852,14 @@ export abstract class PgSelectQueryBuilderBase< } /** - * The FOR clause specifies a lock strength for this query - * that controls how strictly it acquires exclusive access to - * the rows being queried. - * - * {@link https://www.postgresql.org/docs/current/sql-select.html#SQL-FOR-UPDATE-SHARE | PostgreSQL locking clause documentation} + * 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: LockStrength, config: LockConfig = {}): PgSelectWithout { this.config.lockingClause = { strength, config }; @@ -694,14 +1010,194 @@ const getPgSetOperators = () => ({ exceptAll, }); +/** + * 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 + * import { union } from 'drizzle-orm/pg-core' + * + * await union( + * db.select({ name: users.name }).from(users), + * db.select({ name: customers.name }).from(customers) + * ); + * // or + * await db.select({ name: users.name }) + * .from(users) + * .union( + * db.select({ name: customers.name }).from(customers) + * ); + * ``` + */ export const union = 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 + * import { unionAll } from 'drizzle-orm/pg-core' + * + * await unionAll( + * db.select({ transaction: onlineSales.transactionId }).from(onlineSales), + * db.select({ transaction: inStoreSales.transactionId }).from(inStoreSales) + * ); + * // or + * await db.select({ transaction: onlineSales.transactionId }) + * .from(onlineSales) + * .unionAll( + * db.select({ transaction: inStoreSales.transactionId }).from(inStoreSales) + * ); + * ``` + */ export const unionAll = 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 + * import { intersect } from 'drizzle-orm/pg-core' + * + * await intersect( + * db.select({ courseName: depA.courseName }).from(depA), + * db.select({ courseName: depB.courseName }).from(depB) + * ); + * // or + * await db.select({ courseName: depA.courseName }) + * .from(depA) + * .intersect( + * db.select({ courseName: depB.courseName }).from(depB) + * ); + * ``` + */ export const intersect = 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 + * import { intersectAll } from 'drizzle-orm/pg-core' + * + * await intersectAll( + * db.select({ + * productId: regularCustomerOrders.productId, + * quantityOrdered: regularCustomerOrders.quantityOrdered + * }) + * .from(regularCustomerOrders), + * db.select({ + * productId: vipCustomerOrders.productId, + * quantityOrdered: vipCustomerOrders.quantityOrdered + * }) + * .from(vipCustomerOrders) + * ); + * // or + * await db.select({ + * productId: regularCustomerOrders.productId, + * quantityOrdered: regularCustomerOrders.quantityOrdered + * }) + * .from(regularCustomerOrders) + * .intersectAll( + * db.select({ + * productId: vipCustomerOrders.productId, + * quantityOrdered: vipCustomerOrders.quantityOrdered + * }) + * .from(vipCustomerOrders) + * ); + * ``` + */ export const intersectAll = 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 + * import { except } from 'drizzle-orm/pg-core' + * + * await except( + * db.select({ courseName: depA.courseName }).from(depA), + * db.select({ courseName: depB.courseName }).from(depB) + * ); + * // or + * await db.select({ courseName: depA.courseName }) + * .from(depA) + * .except( + * db.select({ courseName: depB.courseName }).from(depB) + * ); + * ``` + */ export const except = 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 + * import { exceptAll } from 'drizzle-orm/pg-core' + * + * await exceptAll( + * db.select({ + * productId: regularCustomerOrders.productId, + * quantityOrdered: regularCustomerOrders.quantityOrdered + * }) + * .from(regularCustomerOrders), + * db.select({ + * productId: vipCustomerOrders.productId, + * quantityOrdered: vipCustomerOrders.quantityOrdered + * }) + * .from(vipCustomerOrders) + * ); + * // or + * await db.select({ + * productId: regularCustomerOrders.productId, + * quantityOrdered: regularCustomerOrders.quantityOrdered, + * }) + * .from(regularCustomerOrders) + * .exceptAll( + * db.select({ + * productId: vipCustomerOrders.productId, + * quantityOrdered: vipCustomerOrders.quantityOrdered, + * }) + * .from(vipCustomerOrders) + * ); + * ``` + */ export const exceptAll = createSetOperator('except', true); diff --git a/drizzle-orm/src/pg-core/query-builders/update.ts b/drizzle-orm/src/pg-core/query-builders/update.ts index 449f99149..87238b6d1 100644 --- a/drizzle-orm/src/pg-core/query-builders/update.ts +++ b/drizzle-orm/src/pg-core/query-builders/update.ts @@ -15,6 +15,7 @@ import type { Query, SQL, SQLWrapper } from '~/sql/sql.ts'; import { Table } from '~/table.ts'; import { mapUpdateSet, orderSelectedFields, type UpdateSet } from '~/utils.ts'; import type { SelectedFields, SelectedFieldsOrdered } from './select.types.ts'; +import type { PgColumn } from '../columns/common.ts'; export interface PgUpdateConfig { where?: SQL | undefined; @@ -159,11 +160,66 @@ export class PgUpdateBase< this.config = { set, table }; } + /** + * Adds a 'where' clause to the query. + * + * Calling this method will update only those rows that fulfill a specified condition. + * + * See docs: {@link https://orm.drizzle.team/docs/update} + * + * @param where the 'where' clause. + * + * @example + * You can use conditional operators and `sql function` to filter the rows to be updated. + * + * ```ts + * // Update all cars with green color + * await db.update(cars).set({ color: 'red' }) + * .where(eq(cars.color, 'green')); + * // or + * await db.update(cars).set({ color: 'red' }) + * .where(sql`${cars.color} = 'green'`) + * ``` + * + * You can logically combine conditional operators with `and()` and `or()` operators: + * + * ```ts + * // Update all BMW cars with a green color + * await db.update(cars).set({ color: 'red' }) + * .where(and(eq(cars.color, 'green'), eq(cars.brand, 'BMW'))); + * + * // Update all cars with the green or blue color + * await db.update(cars).set({ color: 'red' }) + * .where(or(eq(cars.color, 'green'), eq(cars.color, 'blue'))); + * ``` + */ where(where: SQL | undefined): PgUpdateWithout { this.config.where = where; return this as any; } + /** + * Adds a `returning` clause to the query. + * + * Calling this method will return the specified fields of the updated rows. If no fields are specified, all fields will be returned. + * + * See docs: {@link https://orm.drizzle.team/docs/update#update-with-returning} + * + * @example + * ```ts + * // Update all cars with the green color and return all fields + * const updatedCars: Car[] = await db.update(cars) + * .set({ color: 'red' }) + * .where(eq(cars.color, 'green')) + * .returning(); + * + * // Update all cars with the green color and return only their id and brand fields + * const updatedCarsIdsAndBrands: { id: number, brand: string }[] = await db.update(cars) + * .set({ color: 'red' }) + * .where(eq(cars.color, 'green')) + * .returning({ id: cars.id, brand: cars.brand }); + * ``` + */ returning(): PgUpdateReturningAll; returning( fields: TSelectedFields, @@ -171,7 +227,7 @@ export class PgUpdateBase< returning( fields: SelectedFields = this.config.table[Table.Symbol.Columns], ): PgUpdateWithout { - this.config.returning = orderSelectedFields(fields); + this.config.returning = orderSelectedFields(fields); return this as any; } diff --git a/drizzle-orm/src/pg-proxy/migrator.ts b/drizzle-orm/src/pg-proxy/migrator.ts index f1b3fb107..3224afa8c 100644 --- a/drizzle-orm/src/pg-proxy/migrator.ts +++ b/drizzle-orm/src/pg-proxy/migrator.ts @@ -23,9 +23,13 @@ export async function migrate>( await db.execute(sql`CREATE SCHEMA IF NOT EXISTS "drizzle"`); await db.execute(migrationTableCreate); - const dbMigrations = await db.execute( - sql`SELECT id, hash, created_at FROM "drizzle"."__drizzle_migrations" ORDER BY created_at DESC LIMIT 1`, - ) as unknown as [number, string, string][]; + const dbMigrations = await db.execute<{ + id: number; + hash: string; + created_at: string; + }>( + sql`SELECT id, hash, created_at FROM "drizzle"."__drizzle_migrations" ORDER BY created_at DESC LIMIT 1` + ); const lastDbMigration = dbMigrations[0] ?? undefined; @@ -34,7 +38,7 @@ export async function migrate>( for (const migration of migrations) { if ( !lastDbMigration - || Number(lastDbMigration[2])! < migration.folderMillis + || Number(lastDbMigration.created_at)! < migration.folderMillis ) { queriesToRun.push( ...migration.sql, diff --git a/drizzle-orm/src/sql/functions/aggregate.ts b/drizzle-orm/src/sql/functions/aggregate.ts new file mode 100644 index 000000000..3ca6a78d2 --- /dev/null +++ b/drizzle-orm/src/sql/functions/aggregate.ts @@ -0,0 +1,129 @@ +import { is } from '~/entity.ts'; +import { type SQL, sql, type SQLWrapper } from '../sql.ts'; +import { type AnyColumn, Column } from '~/column.ts'; + +/** + * Returns the number of values in `expression`. + * + * ## Examples + * + * ```ts + * // Number employees with null values + * db.select({ value: count() }).from(employees) + * // Number of employees where `name` is not null + * db.select({ value: count(employees.name) }).from(employees) + * ``` + * + * @see countDistinct to get the number of non-duplicate values in `expression` + */ +export function count(expression?: SQLWrapper): SQL { + return sql`count(${expression || sql.raw('*')})`.mapWith(Number); +} + +/** + * Returns the number of non-duplicate values in `expression`. + * + * ## Examples + * + * ```ts + * // Number of employees where `name` is distinct + * db.select({ value: countDistinct(employees.name) }).from(employees) + * ``` + * + * @see count to get the number of values in `expression`, including duplicates + */ +export function countDistinct(expression: SQLWrapper): SQL { + return sql`count(distinct ${expression})`.mapWith(Number); +} + +/** + * Returns the average (arithmetic mean) of all non-null values in `expression`. + * + * ## Examples + * + * ```ts + * // Average salary of an employee + * db.select({ value: avg(employees.salary) }).from(employees) + * ``` + * + * @see avgDistinct to get the average of all non-null and non-duplicate values in `expression` + */ +export function avg(expression: SQLWrapper): SQL { + return sql`avg(${expression})`.mapWith(String); +} + +/** + * Returns the average (arithmetic mean) of all non-null and non-duplicate values in `expression`. + * + * ## Examples + * + * ```ts + * // Average salary of an employee where `salary` is distinct + * db.select({ value: avgDistinct(employees.salary) }).from(employees) + * ``` + * + * @see avg to get the average of all non-null values in `expression`, including duplicates + */ +export function avgDistinct(expression: SQLWrapper): SQL { + return sql`avg(distinct ${expression})`.mapWith(String); +} + +/** + * Returns the sum of all non-null values in `expression`. + * + * ## Examples + * + * ```ts + * // Sum of every employee's salary + * db.select({ value: sum(employees.salary) }).from(employees) + * ``` + * + * @see sumDistinct to get the sum of all non-null and non-duplicate values in `expression` + */ +export function sum(expression: SQLWrapper): SQL { + return sql`sum(${expression})`.mapWith(String); +} + +/** + * Returns the sum of all non-null and non-duplicate values in `expression`. + * + * ## Examples + * + * ```ts + * // Sum of every employee's salary where `salary` is distinct (no duplicates) + * db.select({ value: sumDistinct(employees.salary) }).from(employees) + * ``` + * + * @see sum to get the sum of all non-null values in `expression`, including duplicates + */ +export function sumDistinct(expression: SQLWrapper): SQL { + return sql`sum(distinct ${expression})`.mapWith(String); +} + +/** + * Returns the maximum value in `expression`. + * + * ## Examples + * + * ```ts + * // The employee with the highest salary + * db.select({ value: max(employees.salary) }).from(employees) + * ``` + */ +export function max(expression: T): SQL<(T extends AnyColumn ? T['_']['data'] : string) | null> { + return sql`max(${expression})`.mapWith(is(expression, Column) ? expression : String) as any; +} + +/** + * Returns the minimum value in `expression`. + * + * ## Examples + * + * ```ts + * // The employee with the lowest salary + * db.select({ value: min(employees.salary) }).from(employees) + * ``` + */ +export function min(expression: T): SQL<(T extends AnyColumn ? T['_']['data'] : string) | null> { + return sql`min(${expression})`.mapWith(is(expression, Column) ? expression : String) as any; +} diff --git a/drizzle-orm/src/sql/functions/index.ts b/drizzle-orm/src/sql/functions/index.ts new file mode 100644 index 000000000..64ed5f04f --- /dev/null +++ b/drizzle-orm/src/sql/functions/index.ts @@ -0,0 +1 @@ +export * from './aggregate.ts'; diff --git a/drizzle-orm/src/sql/index.ts b/drizzle-orm/src/sql/index.ts index ec7f9ed76..19d77cd9b 100644 --- a/drizzle-orm/src/sql/index.ts +++ b/drizzle-orm/src/sql/index.ts @@ -1,2 +1,3 @@ export * from './expressions/index.ts'; export * from './sql.ts'; +export * from './functions/index.ts'; diff --git a/drizzle-orm/src/sql/sql.ts b/drizzle-orm/src/sql/sql.ts index a40eb079d..cad140d38 100644 --- a/drizzle-orm/src/sql/sql.ts +++ b/drizzle-orm/src/sql/sql.ts @@ -643,4 +643,4 @@ Table.prototype.getSQL = function() { // Defined separately from the Column class to resolve circular dependency Subquery.prototype.getSQL = function() { return new SQL([this]); -}; \ No newline at end of file +}; diff --git a/drizzle-orm/src/sqlite-core/db.ts b/drizzle-orm/src/sqlite-core/db.ts index 18594a3ba..f98fea324 100644 --- a/drizzle-orm/src/sqlite-core/db.ts +++ b/drizzle-orm/src/sqlite-core/db.ts @@ -1,6 +1,7 @@ import { entityKind } from '~/entity.ts'; import type { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; import type { ExtractTablesWithRelations, RelationalSchemaConfig, TablesRelationalConfig } from '~/relations.ts'; +import { SelectionProxyHandler } from '~/selection-proxy.ts'; import type { ColumnsSelection, SQLWrapper } from '~/sql/sql.ts'; import type { SQLiteAsyncDialect, SQLiteSyncDialect } from '~/sqlite-core/dialect.ts'; import { @@ -24,7 +25,6 @@ import { RelationalQueryBuilder } from './query-builders/query.ts'; import { SQLiteRaw } from './query-builders/raw.ts'; import type { SelectedFields } from './query-builders/select.types.ts'; import type { WithSubqueryWithSelection } from './subquery.ts'; -import { SelectionProxyHandler } from '~/selection-proxy.ts'; export class BaseSQLiteDatabase< TResultKind extends 'sync' | 'async', @@ -73,6 +73,38 @@ export class BaseSQLiteDatabase< } } + /** + * Creates a subquery that defines a temporary named result set as a CTE. + * + * It is useful for breaking down complex queries into simpler parts and for reusing the result set in subsequent parts of the query. + * + * See docs: {@link https://orm.drizzle.team/docs/select#with-clause} + * + * @param alias The alias for the subquery. + * + * Failure to provide an alias will result in a DrizzleTypeError, preventing the subquery from being referenced in other queries. + * + * @example + * + * ```ts + * // Create a subquery with alias 'sq' and use it in the select query + * const sq = db.$with('sq').as(db.select().from(users).where(eq(users.id, 42))); + * + * const result = await db.with(sq).select().from(sq); + * ``` + * + * To select arbitrary SQL values as fields in a CTE and reference them in other CTEs or in the main query, you need to add aliases to them: + * + * ```ts + * // Select an arbitrary SQL value as a field in a CTE and reference it in the main query + * const sq = db.$with('sq').as(db.select({ + * name: sql`upper(${users.name})`.as('name'), + * }) + * .from(users)); + * + * const result = await db.with(sq).select({ name: sq.name }).from(sq); + * ``` + */ $with(alias: TAlias) { return { as( @@ -90,6 +122,25 @@ export class BaseSQLiteDatabase< }; } + /** + * Incorporates a previously defined CTE (using `$with`) into the main query. + * + * This method allows the main query to reference a temporary named result set. + * + * See docs: {@link https://orm.drizzle.team/docs/select#with-clause} + * + * @param queries The CTEs to incorporate into the main query. + * + * @example + * + * ```ts + * // Define a subquery 'sq' as a CTE using $with + * const sq = db.$with('sq').as(db.select().from(users).where(eq(users.id, 42))); + * + * // Incorporate the CTE 'sq' into the main query and select from it + * const result = await db.with(sq).select().from(sq); + * ``` + */ with(...queries: WithSubquery[]) { const self = this; @@ -127,6 +178,42 @@ export class BaseSQLiteDatabase< return { select, selectDistinct }; } + /** + * Creates a select query. + * + * Calling this method with no arguments will select all columns from the table. Pass a selection object to specify the columns you want to select. + * + * Use `.from()` method to specify which table to select from. + * + * See docs: {@link https://orm.drizzle.team/docs/select} + * + * @param fields The selection object. + * + * @example + * + * ```ts + * // Select all columns and all rows from the 'cars' table + * const allCars: Car[] = await db.select().from(cars); + * + * // Select specific columns and all rows from the 'cars' table + * const carsIdsAndBrands: { id: number; brand: string }[] = await db.select({ + * id: cars.id, + * brand: cars.brand + * }) + * .from(cars); + * ``` + * + * Like in SQL, you can use arbitrary expressions as selection fields, not just table columns: + * + * ```ts + * // Select specific columns along with expression and all rows from the 'cars' table + * const carsIdsAndLowerNames: { id: number; lowerBrand: string }[] = await db.select({ + * id: cars.id, + * lowerBrand: sql`lower(${cars.brand})`, + * }) + * .from(cars); + * ``` + */ select(): SQLiteSelectBuilder; select( fields: TSelection, @@ -135,6 +222,31 @@ export class BaseSQLiteDatabase< return new SQLiteSelectBuilder({ fields: fields ?? undefined, session: this.session, dialect: this.dialect }); } + /** + * Adds `distinct` expression to the select query. + * + * Calling this method will return only unique values. When multiple columns are selected, it returns rows with unique combinations of values in these columns. + * + * Use `.from()` method to specify which table to select from. + * + * See docs: {@link https://orm.drizzle.team/docs/select#distinct} + * + * @param fields The selection object. + * + * @example + * + * ```ts + * // Select all unique rows from the 'cars' table + * await db.selectDistinct() + * .from(cars) + * .orderBy(cars.id, cars.brand, cars.color); + * + * // Select all unique brands from the 'cars' table + * await db.selectDistinct({ brand: cars.brand }) + * .from(cars) + * .orderBy(cars.brand); + * ``` + */ selectDistinct(): SQLiteSelectBuilder; selectDistinct( fields: TSelection, @@ -150,14 +262,89 @@ export class BaseSQLiteDatabase< }); } + /** + * Creates an update query. + * + * Calling this method without `.where()` clause will update all rows in a table. The `.where()` clause specifies which rows should be updated. + * + * Use `.set()` method to specify which values to update. + * + * See docs: {@link https://orm.drizzle.team/docs/update} + * + * @param table The table to update. + * + * @example + * + * ```ts + * // Update all rows in the 'cars' table + * await db.update(cars).set({ color: 'red' }); + * + * // Update rows with filters and conditions + * await db.update(cars).set({ color: 'red' }).where(eq(cars.brand, 'BMW')); + * + * // Update with returning clause + * const updatedCar: Car[] = await db.update(cars) + * .set({ color: 'red' }) + * .where(eq(cars.id, 1)) + * .returning(); + * ``` + */ update(table: TTable): SQLiteUpdateBuilder { return new SQLiteUpdateBuilder(table, this.session, this.dialect); } + /** + * Creates an insert query. + * + * Calling this method will create new rows in a table. Use `.values()` method to specify which values to insert. + * + * See docs: {@link https://orm.drizzle.team/docs/insert} + * + * @param table The table to insert into. + * + * @example + * + * ```ts + * // Insert one row + * await db.insert(cars).values({ brand: 'BMW' }); + * + * // Insert multiple rows + * await db.insert(cars).values([{ brand: 'BMW' }, { brand: 'Porsche' }]); + * + * // Insert with returning clause + * const insertedCar: Car[] = await db.insert(cars) + * .values({ brand: 'BMW' }) + * .returning(); + * ``` + */ insert(into: TTable): SQLiteInsertBuilder { return new SQLiteInsertBuilder(into, this.session, this.dialect); } + /** + * Creates a delete query. + * + * Calling this method without `.where()` clause will delete all rows in a table. The `.where()` clause specifies which rows should be deleted. + * + * See docs: {@link https://orm.drizzle.team/docs/delete} + * + * @param table The table to delete from. + * + * @example + * + * ```ts + * // Delete all rows in the 'cars' table + * await db.delete(cars); + * + * // Delete rows with filters and conditions + * await db.delete(cars).where(eq(cars.color, 'green')); + * + * // Delete with returning clause + * const deletedCar: Car[] = await db.delete(cars) + * .where(eq(cars.id, 1)) + * .returning(); + * ``` + */ delete(from: TTable): SQLiteDeleteBase { return new SQLiteDeleteBase(from, this.session, this.dialect); } @@ -244,42 +431,35 @@ export const withReplicas = < replicas: [Q, ...Q[]], getReplica: (replicas: Q[]) => Q = () => replicas[Math.floor(Math.random() * replicas.length)]!, ): SQLiteWithReplicas => { - const select: Q['select'] = (...args: any) => getReplica(replicas).select(args); - const selectDistinct: Q['selectDistinct'] = (...args: any) => getReplica(replicas).selectDistinct(args); - const $with: Q['with'] = (...args: any) => getReplica(replicas).with(args); + const select: Q['select'] = (...args: []) => getReplica(replicas).select(...args); + const selectDistinct: Q['selectDistinct'] = (...args: []) => getReplica(replicas).selectDistinct(...args); + const $with: Q['with'] = (...args: []) => getReplica(replicas).with(...args); - const update: Q['update'] = (...args: any) => primary.update(args); - const insert: Q['insert'] = (...args: any) => primary.insert(args); - const $delete: Q['delete'] = (...args: any) => primary.delete(args); - const run: Q['run'] = (...args: any) => primary.run(args); - const all: Q['all'] = (...args: any) => primary.all(args); - const get: Q['get'] = (...args: any) => primary.get(args); - const values: Q['values'] = (...args: any) => primary.values(args); - const transaction: Q['transaction'] = (...args: any) => primary.transaction(args); + const update: Q['update'] = (...args: [any]) => primary.update(...args); + const insert: Q['insert'] = (...args: [any]) => primary.insert(...args); + const $delete: Q['delete'] = (...args: [any]) => primary.delete(...args); + const run: Q['run'] = (...args: [any]) => primary.run(...args); + const all: Q['all'] = (...args: [any]) => primary.all(...args); + const get: Q['get'] = (...args: [any]) => primary.get(...args); + const values: Q['values'] = (...args: [any]) => primary.values(...args); + const transaction: Q['transaction'] = (...args: [any]) => primary.transaction(...args); - return new Proxy( - { - ...primary, - update, - insert, - delete: $delete, - run, - all, - get, - values, - transaction, - $primary: primary, - select, - selectDistinct, - with: $with, - }, - { - get(target, prop, _receiver) { - if (prop === 'query') { - return getReplica(replicas).query; - } - return target[prop as keyof typeof target]; - }, + return { + ...primary, + update, + insert, + delete: $delete, + run, + all, + get, + values, + transaction, + $primary: primary, + select, + selectDistinct, + with: $with, + get query() { + return getReplica(replicas).query; }, - ); + }; }; diff --git a/drizzle-orm/src/sqlite-core/query-builders/delete.ts b/drizzle-orm/src/sqlite-core/query-builders/delete.ts index 2296310f5..2fa983c12 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/delete.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/delete.ts @@ -8,6 +8,7 @@ import type { SQLitePreparedQuery, SQLiteSession } from '~/sqlite-core/session.t import { SQLiteTable } from '~/sqlite-core/table.ts'; import { type DrizzleTypeError, orderSelectedFields } from '~/utils.ts'; import type { SelectedFieldsFlat, SelectedFieldsOrdered } from './select.types.ts'; +import type { SQLiteColumn } from '../columns/common.ts'; export type SQLiteDeleteWithout< T extends AnySQLiteDeleteBase, @@ -143,11 +144,60 @@ export class SQLiteDeleteBase< this.config = { table }; } + /** + * Adds a `where` clause to the query. + * + * Calling this method will delete only those rows that fulfill a specified condition. + * + * See docs: {@link https://orm.drizzle.team/docs/delete} + * + * @param where the `where` clause. + * + * @example + * You can use conditional operators and `sql function` to filter the rows to be deleted. + * + * ```ts + * // Delete all cars with green color + * db.delete(cars).where(eq(cars.color, 'green')); + * // or + * db.delete(cars).where(sql`${cars.color} = 'green'`) + * ``` + * + * You can logically combine conditional operators with `and()` and `or()` operators: + * + * ```ts + * // Delete all BMW cars with a green color + * db.delete(cars).where(and(eq(cars.color, 'green'), eq(cars.brand, 'BMW'))); + * + * // Delete all cars with the green or blue color + * db.delete(cars).where(or(eq(cars.color, 'green'), eq(cars.color, 'blue'))); + * ``` + */ where(where: SQL | undefined): SQLiteDeleteWithout { this.config.where = where; return this as any; } + /** + * Adds a `returning` clause to the query. + * + * Calling this method will return the specified fields of the deleted rows. If no fields are specified, all fields will be returned. + * + * See docs: {@link https://orm.drizzle.team/docs/delete#delete-with-return} + * + * @example + * ```ts + * // Delete all cars with the green color and return all fields + * const deletedCars: Car[] = await db.delete(cars) + * .where(eq(cars.color, 'green')) + * .returning(); + * + * // Delete all cars with the green color and return only their id and brand fields + * const deletedCarsIdsAndBrands: { id: number, brand: string }[] = await db.delete(cars) + * .where(eq(cars.color, 'green')) + * .returning({ id: cars.id, brand: cars.brand }); + * ``` + */ returning(): SQLiteDeleteReturningAll; returning( fields: TSelectedFields, @@ -155,7 +205,7 @@ export class SQLiteDeleteBase< returning( fields: SelectedFieldsFlat = this.table[SQLiteTable.Symbol.Columns], ): SQLiteDeleteReturning { - this.config.returning = orderSelectedFields(fields); + this.config.returning = orderSelectedFields(fields); return this as any; } diff --git a/drizzle-orm/src/sqlite-core/query-builders/insert.ts b/drizzle-orm/src/sqlite-core/query-builders/insert.ts index 1190fe79e..4f2e23320 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/insert.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/insert.ts @@ -12,6 +12,7 @@ import { Table } from '~/table.ts'; import { type DrizzleTypeError, mapUpdateSet, orderSelectedFields, type Simplify } from '~/utils.ts'; import type { SelectedFieldsFlat, SelectedFieldsOrdered } from './select.types.ts'; import type { SQLiteUpdateSetSource } from './update.ts'; +import type { SQLiteColumn } from '../columns/common.ts'; export interface SQLiteInsertConfig { table: TTable; @@ -206,6 +207,26 @@ export class SQLiteInsertBase< this.config = { table, values }; } + /** + * Adds a `returning` clause to the query. + * + * Calling this method will return the specified fields of the inserted rows. If no fields are specified, all fields will be returned. + * + * See docs: {@link https://orm.drizzle.team/docs/insert#insert-returning} + * + * @example + * ```ts + * // Insert one row and return all fields + * const insertedCar: Car[] = await db.insert(cars) + * .values({ brand: 'BMW' }) + * .returning(); + * + * // Insert one row and return only the id + * const insertedCarId: { id: number }[] = await db.insert(cars) + * .values({ brand: 'BMW' }) + * .returning({ id: cars.id }); + * ``` + */ returning(): SQLiteInsertReturningAll; returning( fields: TSelectedFields, @@ -213,10 +234,32 @@ export class SQLiteInsertBase< returning( fields: SelectedFieldsFlat = this.config.table[SQLiteTable.Symbol.Columns], ): SQLiteInsertWithout { - this.config.returning = orderSelectedFields(fields); + this.config.returning = orderSelectedFields(fields); return this as any; } + /** + * Adds an `on conflict do nothing` clause to the query. + * + * Calling this method simply avoids inserting a row as its alternative action. + * + * See docs: {@link https://orm.drizzle.team/docs/insert#on-conflict-do-nothing} + * + * @param config The `target` and `where` clauses. + * + * @example + * ```ts + * // Insert one row and cancel the insert if there's a conflict + * await db.insert(cars) + * .values({ id: 1, brand: 'BMW' }) + * .onConflictDoNothing(); + * + * // Explicitly specify conflict target + * await db.insert(cars) + * .values({ id: 1, brand: 'BMW' }) + * .onConflictDoNothing({ target: cars.id }); + * ``` + */ onConflictDoNothing(config: { target?: IndexColumn | IndexColumn[]; where?: SQL } = {}): this { if (config.target === undefined) { this.config.onConflict = sql`do nothing`; @@ -228,6 +271,35 @@ export class SQLiteInsertBase< return this; } + /** + * Adds an `on conflict do update` clause to the query. + * + * Calling this method will update the existing row that conflicts with the row proposed for insertion as its alternative action. + * + * See docs: {@link https://orm.drizzle.team/docs/insert#upserts-and-conflicts} + * + * @param config The `target`, `set` and `where` clauses. + * + * @example + * ```ts + * // Update the row if there's a conflict + * await db.insert(cars) + * .values({ id: 1, brand: 'BMW' }) + * .onConflictDoUpdate({ + * target: cars.id, + * set: { brand: 'Porsche' } + * }); + * + * // Upsert with 'where' clause + * await db.insert(cars) + * .values({ id: 1, brand: 'BMW' }) + * .onConflictDoUpdate({ + * target: cars.id, + * set: { brand: 'newBMW' }, + * where: sql`${cars.createdAt} > '2023-01-01'::date`, + * }); + * ``` + */ onConflictDoUpdate(config: SQLiteInsertOnConflictDoUpdateConfig): this { const targetSql = Array.isArray(config.target) ? sql`${config.target}` : sql`${[config.target]}`; const whereSql = config.where ? sql` where ${config.where}` : sql``; diff --git a/drizzle-orm/src/sqlite-core/query-builders/select.ts b/drizzle-orm/src/sqlite-core/query-builders/select.ts index 67cae227f..47447bf0d 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/select.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/select.ts @@ -267,12 +267,120 @@ export abstract class SQLiteSelectQueryBuilderBase< }; } + /** + * 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'); - + + /** + * 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'); + /** + * 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'); + /** + * 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'); private createSetOperator( @@ -307,12 +415,112 @@ export abstract class SQLiteSelectQueryBuilderBase< }; } + /** + * 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/sqlite-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/sqlite-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/sqlite-core' + * + * await intersect( + * db.select({ courseName: depA.courseName }).from(depA), + * db.select({ courseName: depB.courseName }).from(depB) + * ); + * ``` + */ intersect = this.createSetOperator('intersect', false); + /** + * 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/sqlite-core' + * + * await except( + * db.select({ courseName: depA.courseName }).from(depA), + * db.select({ courseName: depB.courseName }).from(depB) + * ); + * ``` + */ except = this.createSetOperator('except', false); /** @internal */ @@ -326,6 +534,35 @@ export abstract class SQLiteSelectQueryBuilderBase< return this as any; } + /** + * 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: ((aliases: TSelection) => SQL | undefined) | SQL | undefined, ): SQLiteSelectWithout { @@ -341,6 +578,28 @@ export abstract class SQLiteSelectQueryBuilderBase< return this as any; } + /** + * 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`cast(count(${cars.id}) as int)`, + * }) + * .from(cars) + * .groupBy(cars.brand) + * .having(({ count }) => gt(count, 1)); + * ``` + */ having( having: ((aliases: this['_']['selection']) => SQL | undefined) | SQL | undefined, ): SQLiteSelectWithout { @@ -356,6 +615,25 @@ export abstract class SQLiteSelectQueryBuilderBase< return this as any; } + /** + * Adds a `group by` clause to the query. + * + * Calling this method will group rows that have the same values into summary rows, often used for aggregation purposes. + * + * See docs: {@link https://orm.drizzle.team/docs/select#aggregations} + * + * @example + * + * ```ts + * // Group and count people by their last names + * await db.select({ + * lastName: people.lastName, + * count: sql`cast(count(*) as int)` + * }) + * .from(people) + * .groupBy(people.lastName); + * ``` + */ groupBy( builder: (aliases: this['_']['selection']) => ValueOrArray, ): SQLiteSelectWithout; @@ -379,6 +657,30 @@ export abstract class SQLiteSelectQueryBuilderBase< return this as any; } + /** + * Adds an `order by` clause to the query. + * + * Calling this method will sort the result-set in ascending or descending order. By default, the sort order is ascending. + * + * See docs: {@link https://orm.drizzle.team/docs/select#order-by} + * + * @example + * + * ``` + * // Select cars ordered by year + * await db.select().from(cars).orderBy(cars.year); + * ``` + * + * You can specify whether results are in ascending or descending order with the `asc()` and `desc()` operators. + * + * ```ts + * // Select cars ordered by year in descending order + * await db.select().from(cars).orderBy(desc(cars.year)); + * + * // Select cars ordered by year and price + * await db.select().from(cars).orderBy(asc(cars.year), desc(cars.price)); + * ``` + */ orderBy( builder: (aliases: this['_']['selection']) => ValueOrArray, ): SQLiteSelectWithout; @@ -415,6 +717,22 @@ export abstract class SQLiteSelectQueryBuilderBase< return this as any; } + /** + * 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: number | Placeholder): SQLiteSelectWithout { if (this.config.setOperators.length > 0) { this.config.setOperators.at(-1)!.limit = limit; @@ -424,6 +742,22 @@ export abstract class SQLiteSelectQueryBuilderBase< return this as any; } + /** + * 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: number | Placeholder): SQLiteSelectWithout { if (this.config.setOperators.length > 0) { this.config.setOperators.at(-1)!.offset = offset; @@ -586,10 +920,110 @@ const getSQLiteSetOperators = () => ({ except, }); +/** + * 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 + * import { union } from 'drizzle-orm/sqlite-core' + * + * await union( + * db.select({ name: users.name }).from(users), + * db.select({ name: customers.name }).from(customers) + * ); + * // or + * await db.select({ name: users.name }) + * .from(users) + * .union( + * db.select({ name: customers.name }).from(customers) + * ); + * ``` + */ export const union = 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 + * import { unionAll } from 'drizzle-orm/sqlite-core' + * + * await unionAll( + * db.select({ transaction: onlineSales.transactionId }).from(onlineSales), + * db.select({ transaction: inStoreSales.transactionId }).from(inStoreSales) + * ); + * // or + * await db.select({ transaction: onlineSales.transactionId }) + * .from(onlineSales) + * .unionAll( + * db.select({ transaction: inStoreSales.transactionId }).from(inStoreSales) + * ); + * ``` + */ export const unionAll = 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 + * import { intersect } from 'drizzle-orm/sqlite-core' + * + * await intersect( + * db.select({ courseName: depA.courseName }).from(depA), + * db.select({ courseName: depB.courseName }).from(depB) + * ); + * // or + * await db.select({ courseName: depA.courseName }) + * .from(depA) + * .intersect( + * db.select({ courseName: depB.courseName }).from(depB) + * ); + * ``` + */ export const intersect = createSetOperator('intersect', false); +/** + * 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 + * import { except } from 'drizzle-orm/sqlite-core' + * + * await except( + * db.select({ courseName: depA.courseName }).from(depA), + * db.select({ courseName: depB.courseName }).from(depB) + * ); + * // or + * await db.select({ courseName: depA.courseName }) + * .from(depA) + * .except( + * db.select({ courseName: depB.courseName }).from(depB) + * ); + * ``` + */ export const except = createSetOperator('except', false); diff --git a/drizzle-orm/src/sqlite-core/query-builders/select.types.ts b/drizzle-orm/src/sqlite-core/query-builders/select.types.ts index a66c8ca38..36100a2c2 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/select.types.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/select.types.ts @@ -1,6 +1,5 @@ import type { ColumnsSelection, Placeholder, SQL, View } from '~/sql/sql.ts'; import type { Assume, ValidateShape } from '~/utils.ts'; - import type { SQLiteColumn } from '~/sqlite-core/columns/index.ts'; import type { SQLiteTable, SQLiteTableWithColumns } from '~/sqlite-core/table.ts'; diff --git a/drizzle-orm/src/sqlite-core/query-builders/update.ts b/drizzle-orm/src/sqlite-core/query-builders/update.ts index 857a944d8..f07a1cac6 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/update.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/update.ts @@ -9,6 +9,7 @@ import type { SQLitePreparedQuery, SQLiteSession } from '~/sqlite-core/session.t import { SQLiteTable } from '~/sqlite-core/table.ts'; import { type DrizzleTypeError, mapUpdateSet, orderSelectedFields, type UpdateSet } from '~/utils.ts'; import type { SelectedFields, SelectedFieldsOrdered } from './select.types.ts'; +import type { SQLiteColumn } from '../columns/common.ts'; export interface SQLiteUpdateConfig { where?: SQL | undefined; @@ -174,11 +175,66 @@ export class SQLiteUpdateBase< this.config = { set, table }; } + /** + * Adds a 'where' clause to the query. + * + * Calling this method will update only those rows that fulfill a specified condition. + * + * See docs: {@link https://orm.drizzle.team/docs/update} + * + * @param where the 'where' clause. + * + * @example + * You can use conditional operators and `sql function` to filter the rows to be updated. + * + * ```ts + * // Update all cars with green color + * db.update(cars).set({ color: 'red' }) + * .where(eq(cars.color, 'green')); + * // or + * db.update(cars).set({ color: 'red' }) + * .where(sql`${cars.color} = 'green'`) + * ``` + * + * You can logically combine conditional operators with `and()` and `or()` operators: + * + * ```ts + * // Update all BMW cars with a green color + * db.update(cars).set({ color: 'red' }) + * .where(and(eq(cars.color, 'green'), eq(cars.brand, 'BMW'))); + * + * // Update all cars with the green or blue color + * db.update(cars).set({ color: 'red' }) + * .where(or(eq(cars.color, 'green'), eq(cars.color, 'blue'))); + * ``` + */ where(where: SQL | undefined): SQLiteUpdateWithout { this.config.where = where; return this as any; } + /** + * Adds a `returning` clause to the query. + * + * Calling this method will return the specified fields of the updated rows. If no fields are specified, all fields will be returned. + * + * See docs: {@link https://orm.drizzle.team/docs/update#update-with-returning} + * + * @example + * ```ts + * // Update all cars with the green color and return all fields + * const updatedCars: Car[] = await db.update(cars) + * .set({ color: 'red' }) + * .where(eq(cars.color, 'green')) + * .returning(); + * + * // Update all cars with the green color and return only their id and brand fields + * const updatedCarsIdsAndBrands: { id: number, brand: string }[] = await db.update(cars) + * .set({ color: 'red' }) + * .where(eq(cars.color, 'green')) + * .returning({ id: cars.id, brand: cars.brand }); + * ``` + */ returning(): SQLiteUpdateReturningAll; returning( fields: TSelectedFields, @@ -186,7 +242,7 @@ export class SQLiteUpdateBase< returning( fields: SelectedFields = this.config.table[SQLiteTable.Symbol.Columns], ): SQLiteUpdateWithout { - this.config.returning = orderSelectedFields(fields); + this.config.returning = orderSelectedFields(fields); return this as any; } diff --git a/eslint-plugin-drizzle/.gitignore b/eslint-plugin-drizzle/.gitignore new file mode 100644 index 000000000..11e45930a --- /dev/null +++ b/eslint-plugin-drizzle/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +tsconfig.tsbuildinfo diff --git a/eslint-plugin-drizzle/package.json b/eslint-plugin-drizzle/package.json new file mode 100644 index 000000000..f2a51feb9 --- /dev/null +++ b/eslint-plugin-drizzle/package.json @@ -0,0 +1,36 @@ +{ + "name": "eslint-plugin-drizzle", + "version": "0.2.1", + "description": "Eslint plugin for drizzle users to avoid common pitfalls", + "main": "src/index.js", + "scripts": { + "test": "vitest run", + "build": "tsc -b && pnpm cpy readme.md dist/", + "pack": "(cd dist && npm pack --pack-destination ..) && rm -f package.tgz && mv *.tgz package.tgz", + "publish": "npm publish package.tgz" + }, + "keywords": [ + "eslint", + "eslintplugin", + "eslint-plugin", + "drizzle" + ], + "author": "Drizzle Team", + "repository": { + "type": "git", + "url": "git+https://github.com/drizzle-team/drizzle-orm/tree/main/eslint-plugin-drizzle.git" + }, + "license": "Apache-2.0", + "devDependencies": { + "@typescript-eslint/parser": "^6.10.0", + "@typescript-eslint/rule-tester": "^6.10.0", + "@typescript-eslint/utils": "^6.10.0", + "cpy-cli": "^5.0.0", + "eslint": "^8.53.0", + "typescript": "^5.2.2", + "vitest": "^0.34.6" + }, + "peerDependencies": { + "eslint": ">=8.0.0" + } +} diff --git a/eslint-plugin-drizzle/readme.md b/eslint-plugin-drizzle/readme.md new file mode 100644 index 000000000..1d45b2345 --- /dev/null +++ b/eslint-plugin-drizzle/readme.md @@ -0,0 +1,163 @@ +# eslint-plugin-drizzle + +For cases where it's impossible to perform type checks for specific scenarios, or where it's possible but error messages would be challenging to understand, we've decided to create an ESLint package with recommended rules. This package aims to assist developers in handling crucial scenarios during development + +> Big thanks to @Angelelz for initiating the development of this package and transferring it to the Drizzle Team's npm + +## Install + +```sh +[ npm | yarn | pnpm | bun ] install eslint eslint-plugin-drizzle +``` +You can install those packages for typescript support in your IDE +```sh +[ npm | yarn | pnpm | bun ] install @typescript-eslint/eslint-plugin @typescript-eslint/parser +``` + +## Usage + +Create a `.eslintrc.yml` file, add `drizzle` to the `plugins`, and specify the rules you want to use. You can find a list of all existing rules below + +```yml +root: true +parser: '@typescript-eslint/parser' +parserOptions: + project: './tsconfig.json' +plugins: + - drizzle +rules: + 'drizzle/enforce-delete-with-where': "error" + 'drizzle/enforce-update-with-where': "error" +``` + +### All config + +This plugin exports an [`all` config](src/configs/all.js) that makes use of all rules (except for deprecated ones). + +```yml +root: true +extends: + - "plugin:drizzle/all" +parser: '@typescript-eslint/parser' +parserOptions: + project: './tsconfig.json' +plugins: + - drizzle +``` + +At the moment, `all` is equivalent to `recommended` + +```yml +root: true +extends: + - "plugin:drizzle/recommended" +parser: '@typescript-eslint/parser' +parserOptions: + project: './tsconfig.json' +plugins: + - drizzle +``` + +## Rules + +**enforce-delete-with-where**: Enforce using `delete` with the`.where()` clause in the `.delete()` statement. Most of the time, you don't need to delete all rows in the table and require some kind of `WHERE` statements. + +Optionally, you can define a `drizzleObjectName` in the plugin options that accept a `string` or `string[]`. This is useful when you have objects or classes with a delete method that's not from Drizzle. Such a `delete` method will trigger the ESLint rule. To avoid that, you can define the name of the Drizzle object that you use in your codebase (like db) so that the rule would only trigger if the delete method comes from this object: + +Example, config 1: +```json +"rules": { + "drizzle/enforce-delete-with-where": ["error"] +} +``` + +```ts +class MyClass { + public delete() { + return {} + } +} + +const myClassObj = new MyClass(); + +// ---> Will be triggered by ESLint Rule +myClassObj.delete() + +const db = drizzle(...) +// ---> Will be triggered by ESLint Rule +db.delete() +``` + +Example, config 2: +```json +"rules": { + "drizzle/enforce-delete-with-where": ["error", { "drizzleObjectName": ["db"] }], +} +``` +```ts +class MyClass { + public delete() { + return {} + } +} + +const myClassObj = new MyClass(); + +// ---> Will NOT be triggered by ESLint Rule +myClassObj.delete() + +const db = drizzle(...) +// ---> Will be triggered by ESLint Rule +db.delete() +``` + +**enforce-update-with-where**: Enforce using `update` with the`.where()` clause in the `.update()` statement. Most of the time, you don't need to update all rows in the table and require some kind of `WHERE` statements. + +Optionally, you can define a `drizzleObjectName` in the plugin options that accept a `string` or `string[]`. This is useful when you have objects or classes with a delete method that's not from Drizzle. Such as `update` method will trigger the ESLint rule. To avoid that, you can define the name of the Drizzle object that you use in your codebase (like db) so that the rule would only trigger if the delete method comes from this object: + +Example, config 1: +```json +"rules": { + "drizzle/enforce-update-with-where": ["error"] +} +``` + +```ts +class MyClass { + public update() { + return {} + } +} + +const myClassObj = new MyClass(); + +// ---> Will be triggered by ESLint Rule +myClassObj.update() + +const db = drizzle(...) +// ---> Will be triggered by ESLint Rule +db.update() +``` + +Example, config 2: +```json +"rules": { + "drizzle/enforce-update-with-where": ["error", { "drizzleObjectName": ["db"] }], +} +``` +```ts +class MyClass { + public update() { + return {} + } +} + +const myClassObj = new MyClass(); + +// ---> Will NOT be triggered by ESLint Rule +myClassObj.update() + +const db = drizzle(...) +// ---> Will be triggered by ESLint Rule +db.update() +``` diff --git a/eslint-plugin-drizzle/src/configs/all.ts b/eslint-plugin-drizzle/src/configs/all.ts new file mode 100644 index 000000000..18093b5bf --- /dev/null +++ b/eslint-plugin-drizzle/src/configs/all.ts @@ -0,0 +1,14 @@ +export default { + env: { + es2024: true, + }, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + plugins: ['drizzle'], + rules: { + 'drizzle/enforce-delete-with-where': 'error', + 'drizzle/enforce-update-with-where': 'error', + }, +}; diff --git a/eslint-plugin-drizzle/src/configs/recommended.ts b/eslint-plugin-drizzle/src/configs/recommended.ts new file mode 100644 index 000000000..18093b5bf --- /dev/null +++ b/eslint-plugin-drizzle/src/configs/recommended.ts @@ -0,0 +1,14 @@ +export default { + env: { + es2024: true, + }, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + plugins: ['drizzle'], + rules: { + 'drizzle/enforce-delete-with-where': 'error', + 'drizzle/enforce-update-with-where': 'error', + }, +}; diff --git a/eslint-plugin-drizzle/src/enforce-delete-with-where.ts b/eslint-plugin-drizzle/src/enforce-delete-with-where.ts new file mode 100644 index 000000000..8b1b2d569 --- /dev/null +++ b/eslint-plugin-drizzle/src/enforce-delete-with-where.ts @@ -0,0 +1,50 @@ +import { ESLintUtils } from '@typescript-eslint/utils'; +import { isDrizzleObj, type Options } from './utils/options'; + +const createRule = ESLintUtils.RuleCreator(() => 'https://github.com/drizzle-team/eslint-plugin-drizzle'); + +type MessageIds = 'enforceDeleteWithWhere'; + +let lastNodeName: string = ''; + +const deleteRule = createRule({ + defaultOptions: [{ drizzleObjectName: [] }], + name: 'enforce-delete-with-where', + meta: { + type: 'problem', + docs: { + description: 'Enforce that `delete` method is used with `where` to avoid deleting all the rows in a table.', + }, + fixable: 'code', + messages: { + enforceDeleteWithWhere: 'Without `.where(...)` you will delete all the rows in a table. If you didn\'t want to do it, please use `db.delete(...).where(...)` instead. Otherwise you can ignore this rule here', + }, + schema: [{ + type: 'object', + properties: { + drizzleObjectName: { + type: ['string', 'array'], + }, + }, + additionalProperties: false, + }], + }, + create(context, options) { + return { + MemberExpression: (node) => { + if (node.property.type === 'Identifier') { + if (isDrizzleObj(node, options) && node.property.name === 'delete' && lastNodeName !== 'where') { + context.report({ + node, + messageId: 'enforceDeleteWithWhere', + }); + } + lastNodeName = node.property.name; + } + return; + }, + }; + }, +}); + +export default deleteRule; diff --git a/eslint-plugin-drizzle/src/enforce-update-with-where.ts b/eslint-plugin-drizzle/src/enforce-update-with-where.ts new file mode 100644 index 000000000..bbe822d43 --- /dev/null +++ b/eslint-plugin-drizzle/src/enforce-update-with-where.ts @@ -0,0 +1,58 @@ +import { ESLintUtils } from '@typescript-eslint/utils'; +import { isDrizzleObj, type Options } from './utils/options'; + +const createRule = ESLintUtils.RuleCreator(() => 'https://github.com/drizzle-team/eslint-plugin-drizzle'); +type MessageIds = 'enforceUpdateWithWhere'; + +let lastNodeName: string = ''; + +const updateRule = createRule({ + defaultOptions: [{ drizzleObjectName: [] }], + name: 'enforce-update-with-where', + meta: { + type: 'problem', + docs: { + description: 'Enforce that `update` method is used with `where` to avoid deleting all the rows in a table.', + }, + fixable: 'code', + messages: { + enforceUpdateWithWhere: + 'Without `.where(...)` you will update all the rows in a table. If you didn\'t want to do it, please use `db.update(...).set(...).where(...)` instead. Otherwise you can ignore this rule here' + }, + schema: [{ + type: 'object', + properties: { + drizzleObjectName: { + type: ['string', 'array'], + }, + }, + additionalProperties: false, + }], + }, + create(context, options) { + return { + MemberExpression: (node) => { + if (node.property.type === 'Identifier') { + if ( + lastNodeName !== 'where' + && node.property.name === 'set' + && node.object.type === 'CallExpression' + && node.object.callee.type === 'MemberExpression' + && node.object.callee.property.type === 'Identifier' + && node.object.callee.property.name === 'update' + && isDrizzleObj(node.object.callee, options) + ) { + context.report({ + node, + messageId: 'enforceUpdateWithWhere', + }); + } + lastNodeName = node.property.name; + } + return; + }, + }; + }, +}); + +export default updateRule; diff --git a/eslint-plugin-drizzle/src/index.ts b/eslint-plugin-drizzle/src/index.ts new file mode 100644 index 000000000..15ded747f --- /dev/null +++ b/eslint-plugin-drizzle/src/index.ts @@ -0,0 +1,16 @@ +import type { TSESLint } from '@typescript-eslint/utils'; +import { name, version } from '../package.json'; +import all from './configs/all'; +import recommended from './configs/recommended'; +import deleteRule from './enforce-delete-with-where'; +import updateRule from './enforce-update-with-where'; +import type { Options } from './utils/options'; + +export const rules = { + 'enforce-delete-with-where': deleteRule, + 'enforce-update-with-where': updateRule, +} satisfies Record>; + +export const configs = { all, recommended }; + +export const meta = { name, version }; diff --git a/eslint-plugin-drizzle/src/utils/options.ts b/eslint-plugin-drizzle/src/utils/options.ts new file mode 100644 index 000000000..834f18142 --- /dev/null +++ b/eslint-plugin-drizzle/src/utils/options.ts @@ -0,0 +1,31 @@ +import type { TSESTree } from '@typescript-eslint/utils'; + +export type Options = readonly [{ + drizzleObjectName: string[] | string; +}]; + +export const isDrizzleObj = ( + node: TSESTree.MemberExpression, + options: Options, +) => { + const drizzleObjectName = options[0].drizzleObjectName; + + if ( + node.object.type === 'Identifier' && typeof drizzleObjectName === 'string' + && node.object.name === drizzleObjectName + ) { + return true; + } + + if (Array.isArray(drizzleObjectName)) { + if (drizzleObjectName.length === 0) { + return true; + } + + if (node.object.type === 'Identifier' && drizzleObjectName.includes(node.object.name)) { + return true; + } + } + + return false; +}; diff --git a/eslint-plugin-drizzle/tests/delete.test.ts b/eslint-plugin-drizzle/tests/delete.test.ts new file mode 100644 index 000000000..dfc1f2da3 --- /dev/null +++ b/eslint-plugin-drizzle/tests/delete.test.ts @@ -0,0 +1,115 @@ +// @ts-ignore +import { RuleTester } from '@typescript-eslint/rule-tester'; + +import myRule from '../src/enforce-delete-with-where'; + +const parserResolver = require.resolve('@typescript-eslint/parser'); + +const ruleTester = new RuleTester({ + parser: parserResolver, +}); + +ruleTester.run('enforce delete with where (default options)', myRule, { + valid: [ + 'const a = db.delete({}).where({});', + 'delete db.something', + `dataSource + .delete() + .where()`, + ], + invalid: [ + { + code: 'db.delete({})', + errors: [{ messageId: 'enforceDeleteWithWhere' }], + }, + { + code: 'const a = await db.delete({})', + errors: [{ messageId: 'enforceDeleteWithWhere' }], + }, + { + code: 'const a = db.delete({})', + errors: [{ messageId: 'enforceDeleteWithWhere' }], + }, + { + code: `const a = database + .delete({})`, + errors: [{ messageId: 'enforceDeleteWithWhere' }], + }, + ], +}); + +ruleTester.run('enforce delete with where (string option)', myRule, { + valid: [ + { code: 'const a = db.delete({}).where({});', options: [{ drizzleObjectName: 'db' }] }, + { code: 'const a = something.delete({})', options: [{ drizzleObjectName: 'db' }] }, + { code: 'delete db.something', options: [{ drizzleObjectName: 'db' }] }, + { + code: `dataSource + .delete() + .where()`, + options: [{ drizzleObjectName: 'db' }], + }, + { + code: `const a = database + .delete({})`, + options: [{ drizzleObjectName: 'db' }], + }, + ], + invalid: [ + { + code: 'db.delete({})', + errors: [{ messageId: 'enforceDeleteWithWhere' }], + options: [{ drizzleObjectName: 'db' }], + }, + { + code: 'const a = await db.delete({})', + errors: [{ messageId: 'enforceDeleteWithWhere' }], + options: [{ drizzleObjectName: 'db' }], + }, + { + code: 'const a = db.delete({})', + errors: [{ messageId: 'enforceDeleteWithWhere' }], + options: [{ drizzleObjectName: 'db' }], + }, + ], +}); + +ruleTester.run('enforce delete with where (array option)', myRule, { + valid: [ + { code: 'const a = db.delete({}).where({});', options: [{ drizzleObjectName: ['db'] }] }, + { code: 'delete db.something', options: [{ drizzleObjectName: ['db'] }] }, + { + code: `dataSource + .delete() + .where()`, + options: [{ drizzleObjectName: ['db', 'dataSource'] }], + }, + { + code: `const a = database + .delete({})`, + options: [{ drizzleObjectName: ['db'] }], + }, + ], + invalid: [ + { + code: 'db.delete({})', + errors: [{ messageId: 'enforceDeleteWithWhere' }], + options: [{ drizzleObjectName: ['db', 'anotherName'] }], + }, + { + code: 'dataSource.delete({})', + errors: [{ messageId: 'enforceDeleteWithWhere' }], + options: [{ drizzleObjectName: ['db', 'dataSource'] }], + }, + { + code: 'const a = await db.delete({})', + errors: [{ messageId: 'enforceDeleteWithWhere' }], + options: [{ drizzleObjectName: ['db'] }], + }, + { + code: 'const a = db.delete({})', + errors: [{ messageId: 'enforceDeleteWithWhere' }], + options: [{ drizzleObjectName: ['db'] }], + }, + ], +}); diff --git a/eslint-plugin-drizzle/tests/update.test.ts b/eslint-plugin-drizzle/tests/update.test.ts new file mode 100644 index 000000000..d148f6b4f --- /dev/null +++ b/eslint-plugin-drizzle/tests/update.test.ts @@ -0,0 +1,122 @@ +// @ts-ignore +import { RuleTester } from '@typescript-eslint/rule-tester'; + +import myRule from '../src/enforce-update-with-where'; + +const parserResolver = require.resolve('@typescript-eslint/parser'); + +const ruleTester = new RuleTester({ + parser: parserResolver, +}); + +ruleTester.run('enforce update with where (default options)', myRule, { + valid: [ + 'const a = db.update({}).set().where({});', + 'const a = db.update();', + 'update()', + `db + .update() + .set() + .where()`, + `dataSource + .update() + .set() + .where()`, + ], + invalid: [ + { + code: 'db.update({}).set()', + errors: [{ messageId: 'enforceUpdateWithWhere' }], + }, + { + code: 'const a = await db.update({}).set()', + errors: [{ messageId: 'enforceUpdateWithWhere' }], + }, + { + code: 'const a = db.update({}).set', + errors: [{ messageId: 'enforceUpdateWithWhere' }], + }, + { + code: `const a = database + .update({}) + .set()`, + errors: [{ messageId: 'enforceUpdateWithWhere' }], + }, + ], +}); + +ruleTester.run('enforce update with where (string option)', myRule, { + valid: [ + { code: 'const a = db.update({}).set().where({});', options: [{ drizzleObjectName: 'db' }] }, + { code: 'update.db.update()', options: [{ drizzleObjectName: 'db' }] }, + { + code: `dataSource + .update() + .set()`, + options: [{ drizzleObjectName: 'db' }], + }, + { + code: `const a = database + .update({})`, + options: [{ drizzleObjectName: 'db' }], + }, + ], + invalid: [ + { + code: 'db.update({}).set({})', + errors: [{ messageId: 'enforceUpdateWithWhere' }], + options: [{ drizzleObjectName: 'db' }], + }, + { + code: 'const a = await db.update({}).set()', + errors: [{ messageId: 'enforceUpdateWithWhere' }], + options: [{ drizzleObjectName: 'db' }], + }, + { + code: 'const a = db.update({}).set()', + errors: [{ messageId: 'enforceUpdateWithWhere' }], + options: [{ drizzleObjectName: 'db' }], + }, + ], +}); + +ruleTester.run('enforce delete with where (array option)', myRule, { + valid: [ + { code: 'const a = db.update({}).set().where({});', options: [{ drizzleObjectName: ['db'] }] }, + { code: 'update.db.something', options: [{ drizzleObjectName: ['db'] }] }, + { + code: `dataSource + .update() + .set() + .where()`, + options: [{ drizzleObjectName: ['db', 'dataSource'] }], + }, + { + code: `const a = database + .update({})`, + options: [{ drizzleObjectName: ['db'] }], + }, + ], + invalid: [ + { + code: 'db.update({}).set()', + errors: [{ messageId: 'enforceUpdateWithWhere' }], + options: [{ drizzleObjectName: ['db', 'anotherName'] }], + }, + { + code: 'dataSource.update({}).set({})', + errors: [{ messageId: 'enforceUpdateWithWhere' }], + options: [{ drizzleObjectName: ['db', 'dataSource'] }], + }, + { + code: 'const a = await db.update({}).set()', + errors: [{ messageId: 'enforceUpdateWithWhere' }], + options: [{ drizzleObjectName: ['db'] }], + }, + { + code: 'const a = db.update({}).set()', + errors: [{ messageId: 'enforceUpdateWithWhere' }], + options: [{ drizzleObjectName: ['db'] }], + }, + ], +}); diff --git a/eslint-plugin-drizzle/tsconfig.json b/eslint-plugin-drizzle/tsconfig.json new file mode 100644 index 000000000..e11453ec3 --- /dev/null +++ b/eslint-plugin-drizzle/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "moduleResolution": "nodenext", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "resolveJsonModule": true, + "lib": [ + "esnext" + ], + "composite": false, + "incremental": false, + "skipLibCheck": true, + "outDir": "dist", + "module": "nodenext", + "target": "es6", + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true + }, + "include": [ + "src/**/*.ts", + "package.json", + ] +} diff --git a/eslint-plugin-drizzle/vitest.config.ts b/eslint-plugin-drizzle/vitest.config.ts new file mode 100644 index 000000000..2fa31886f --- /dev/null +++ b/eslint-plugin-drizzle/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + exclude: ['**/dist/**'], + }, +}); diff --git a/examples/cloudflare-d1/drizzle.config.json b/examples/cloudflare-d1/drizzle.config.json new file mode 100644 index 000000000..d25ab99a5 --- /dev/null +++ b/examples/cloudflare-d1/drizzle.config.json @@ -0,0 +1,4 @@ +{ + "out": "drizzle", + "schema": "src/schema.ts" +} \ No newline at end of file diff --git a/examples/cloudflare-d1/package.json b/examples/cloudflare-d1/package.json index 2cef3dc1d..e26a9a934 100644 --- a/examples/cloudflare-d1/package.json +++ b/examples/cloudflare-d1/package.json @@ -4,8 +4,8 @@ "description": "", "scripts": { "test:types": "tsc --noEmit", - "generate": "drizzle-kit generate:sqlite --schema=src/schema.ts", - "up": "drizzle-kit up:sqlite --schema=src/schema.ts" + "generate": "drizzle-kit generate:sqlite", + "up": "drizzle-kit up:sqlite" }, "keywords": [], "author": "", diff --git a/integration-tests/drizzle2/mysql-proxy/first/0000_nostalgic_carnage.sql b/integration-tests/drizzle2/mysql-proxy/first/0000_nostalgic_carnage.sql new file mode 100644 index 000000000..4266589a6 --- /dev/null +++ b/integration-tests/drizzle2/mysql-proxy/first/0000_nostalgic_carnage.sql @@ -0,0 +1,7 @@ +CREATE TABLE `userstest` ( + `id` serial PRIMARY KEY, + `name` text NOT NULL, + `verified` boolean NOT NULL DEFAULT false, + `jsonb` json, + `created_at` timestamp NOT NULL DEFAULT now() +); \ No newline at end of file diff --git a/integration-tests/drizzle2/mysql-proxy/first/meta/0000_snapshot.json b/integration-tests/drizzle2/mysql-proxy/first/meta/0000_snapshot.json new file mode 100644 index 000000000..1d01f3bcf --- /dev/null +++ b/integration-tests/drizzle2/mysql-proxy/first/meta/0000_snapshot.json @@ -0,0 +1,60 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "8e8c8378-0496-40f6-88e3-98aab8282b1f", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "userstest": { + "name": "userstest", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": false, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false, + "autoincrement": false + }, + "jsonb": { + "name": "jsonb", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()", + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {} + } + }, + "schemas": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} diff --git a/integration-tests/drizzle2/mysql-proxy/first/meta/_journal.json b/integration-tests/drizzle2/mysql-proxy/first/meta/_journal.json new file mode 100644 index 000000000..708471cf5 --- /dev/null +++ b/integration-tests/drizzle2/mysql-proxy/first/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "5", + "dialect": "mysql", + "entries": [ + { + "idx": 0, + "version": "5", + "when": 1680270921944, + "tag": "0000_nostalgic_carnage", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/integration-tests/drizzle2/mysql-proxy/second/0000_nostalgic_carnage.sql b/integration-tests/drizzle2/mysql-proxy/second/0000_nostalgic_carnage.sql new file mode 100644 index 000000000..4266589a6 --- /dev/null +++ b/integration-tests/drizzle2/mysql-proxy/second/0000_nostalgic_carnage.sql @@ -0,0 +1,7 @@ +CREATE TABLE `userstest` ( + `id` serial PRIMARY KEY, + `name` text NOT NULL, + `verified` boolean NOT NULL DEFAULT false, + `jsonb` json, + `created_at` timestamp NOT NULL DEFAULT now() +); \ No newline at end of file diff --git a/integration-tests/drizzle2/mysql-proxy/second/0001_test.sql b/integration-tests/drizzle2/mysql-proxy/second/0001_test.sql new file mode 100644 index 000000000..1d2757a0b --- /dev/null +++ b/integration-tests/drizzle2/mysql-proxy/second/0001_test.sql @@ -0,0 +1,5 @@ +CREATE TABLE `users12` ( + `id` serial AUTO_INCREMENT PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `email` text NOT NULL +); \ No newline at end of file diff --git a/integration-tests/drizzle2/mysql-proxy/second/meta/0000_snapshot.json b/integration-tests/drizzle2/mysql-proxy/second/meta/0000_snapshot.json new file mode 100644 index 000000000..1d01f3bcf --- /dev/null +++ b/integration-tests/drizzle2/mysql-proxy/second/meta/0000_snapshot.json @@ -0,0 +1,60 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "8e8c8378-0496-40f6-88e3-98aab8282b1f", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "userstest": { + "name": "userstest", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": false, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false, + "autoincrement": false + }, + "jsonb": { + "name": "jsonb", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()", + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {} + } + }, + "schemas": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} diff --git a/integration-tests/drizzle2/mysql-proxy/second/meta/0001_snapshot.json b/integration-tests/drizzle2/mysql-proxy/second/meta/0001_snapshot.json new file mode 100644 index 000000000..bda94226a --- /dev/null +++ b/integration-tests/drizzle2/mysql-proxy/second/meta/0001_snapshot.json @@ -0,0 +1,96 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "47362df0-c353-4bd1-8107-fcc36f0e61bd", + "prevId": "8e8c8378-0496-40f6-88e3-98aab8282b1f", + "tables": { + "userstest": { + "name": "userstest", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": false, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false, + "autoincrement": false + }, + "jsonb": { + "name": "jsonb", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()", + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {} + }, + "users12": { + "name": "users12", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "my_unique_index": { + "name": "my_unique_index", + "columns": ["name"], + "isUnique": true, + "using": "btree" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {} + } + }, + "schemas": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} diff --git a/integration-tests/drizzle2/mysql-proxy/second/meta/_journal.json b/integration-tests/drizzle2/mysql-proxy/second/meta/_journal.json new file mode 100644 index 000000000..fdd1a2ee3 --- /dev/null +++ b/integration-tests/drizzle2/mysql-proxy/second/meta/_journal.json @@ -0,0 +1,20 @@ +{ + "version": "5", + "dialect": "mysql", + "entries": [ + { + "idx": 0, + "version": "5", + "when": 1680270921944, + "tag": "0000_nostalgic_carnage", + "breakpoints": true + }, + { + "idx": 1, + "version": "5", + "when": 1680270921945, + "tag": "0001_test", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/integration-tests/drizzle2/pg-proxy/first/0000_puzzling_flatman.sql b/integration-tests/drizzle2/pg-proxy/first/0000_puzzling_flatman.sql new file mode 100644 index 000000000..06ed6ebf7 --- /dev/null +++ b/integration-tests/drizzle2/pg-proxy/first/0000_puzzling_flatman.sql @@ -0,0 +1,7 @@ +CREATE TABLE "users" ( + id serial PRIMARY KEY, + name text NOT NULL, + verified boolean NOT NULL DEFAULT false, + jsonb jsonb, + created_at timestamptz NOT NULL DEFAULT now() +); \ No newline at end of file diff --git a/integration-tests/drizzle2/pg-proxy/first/meta/0000_snapshot.json b/integration-tests/drizzle2/pg-proxy/first/meta/0000_snapshot.json new file mode 100644 index 000000000..4bb618d40 --- /dev/null +++ b/integration-tests/drizzle2/pg-proxy/first/meta/0000_snapshot.json @@ -0,0 +1,56 @@ +{ + "version": "5", + "dialect": "pg", + "id": "cb1644bb-c5da-465a-8d70-f63d81e34514", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "jsonb": { + "name": "jsonb", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} diff --git a/integration-tests/drizzle2/pg-proxy/first/meta/_journal.json b/integration-tests/drizzle2/pg-proxy/first/meta/_journal.json new file mode 100644 index 000000000..6b2a35b4a --- /dev/null +++ b/integration-tests/drizzle2/pg-proxy/first/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "5", + "dialect": "pg", + "entries": [ + { + "idx": 0, + "version": "5", + "when": 1680271923328, + "tag": "0000_puzzling_flatman", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/integration-tests/drizzle2/pg-proxy/second/0000_puzzling_flatman.sql b/integration-tests/drizzle2/pg-proxy/second/0000_puzzling_flatman.sql new file mode 100644 index 000000000..06ed6ebf7 --- /dev/null +++ b/integration-tests/drizzle2/pg-proxy/second/0000_puzzling_flatman.sql @@ -0,0 +1,7 @@ +CREATE TABLE "users" ( + id serial PRIMARY KEY, + name text NOT NULL, + verified boolean NOT NULL DEFAULT false, + jsonb jsonb, + created_at timestamptz NOT NULL DEFAULT now() +); \ No newline at end of file diff --git a/integration-tests/drizzle2/pg-proxy/second/0001_test.sql b/integration-tests/drizzle2/pg-proxy/second/0001_test.sql new file mode 100644 index 000000000..bb8e5f34d --- /dev/null +++ b/integration-tests/drizzle2/pg-proxy/second/0001_test.sql @@ -0,0 +1,5 @@ +CREATE TABLE "users12" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "email" text NOT NULL +); \ No newline at end of file diff --git a/integration-tests/drizzle2/pg-proxy/second/meta/0000_snapshot.json b/integration-tests/drizzle2/pg-proxy/second/meta/0000_snapshot.json new file mode 100644 index 000000000..4bb618d40 --- /dev/null +++ b/integration-tests/drizzle2/pg-proxy/second/meta/0000_snapshot.json @@ -0,0 +1,56 @@ +{ + "version": "5", + "dialect": "pg", + "id": "cb1644bb-c5da-465a-8d70-f63d81e34514", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "jsonb": { + "name": "jsonb", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} diff --git a/integration-tests/drizzle2/pg-proxy/second/meta/0001_snapshot.json b/integration-tests/drizzle2/pg-proxy/second/meta/0001_snapshot.json new file mode 100644 index 000000000..c5d4b7cfe --- /dev/null +++ b/integration-tests/drizzle2/pg-proxy/second/meta/0001_snapshot.json @@ -0,0 +1,56 @@ +{ + "version": "5", + "dialect": "pg", + "id": "f2a88b25-f2da-4973-879e-60b57f24e7b9", + "prevId": "cb1644bb-c5da-465a-8d70-f63d81e34514", + "tables": { + "users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "jsonb": { + "name": "jsonb", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} diff --git a/integration-tests/drizzle2/pg-proxy/second/meta/_journal.json b/integration-tests/drizzle2/pg-proxy/second/meta/_journal.json new file mode 100644 index 000000000..de7a2ac56 --- /dev/null +++ b/integration-tests/drizzle2/pg-proxy/second/meta/_journal.json @@ -0,0 +1,20 @@ +{ + "version": "5", + "dialect": "pg", + "entries": [ + { + "idx": 0, + "version": "5", + "when": 1680271923328, + "tag": "0000_puzzling_flatman", + "breakpoints": true + }, + { + "idx": 1, + "version": "5", + "when": 1680271923329, + "tag": "0001_test", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/integration-tests/tests/libsql.test.ts b/integration-tests/tests/libsql.test.ts index b8e224e1b..762f9ed5b 100644 --- a/integration-tests/tests/libsql.test.ts +++ b/integration-tests/tests/libsql.test.ts @@ -15,6 +15,14 @@ import { placeholder, sql, TransactionRollbackError, + count, + countDistinct, + avg, + avgDistinct, + sum, + sumDistinct, + max, + min } from 'drizzle-orm'; import { drizzle, type LibSQLDatabase } from 'drizzle-orm/libsql'; import { migrate } from 'drizzle-orm/libsql/migrator'; @@ -111,6 +119,16 @@ const bigIntExample = sqliteTable('big_int_example', { bigInt: blob('big_int', { mode: 'bigint' }).notNull(), }); +// To test aggregate functions +const aggregateTable = sqliteTable('aggregate_table', { + id: integer('id').primaryKey({ autoIncrement: true }).notNull(), + name: text('name').notNull(), + a: integer('a'), + b: integer('b'), + c: integer('c'), + nullOnly: integer('null_only') +}); + test.before(async (t) => { const ctx = t.context; const url = process.env['LIBSQL_URL']; @@ -255,6 +273,31 @@ async function setupSetOperationTest(db: LibSQLDatabase>) ]); } +async function setupAggregateFunctionsTest(db: LibSQLDatabase>) { + await db.run(sql`drop table if exists "aggregate_table"`); + await db.run( + sql` + create table "aggregate_table" ( + "id" integer primary key autoincrement not null, + "name" text not null, + "a" integer, + "b" integer, + "c" integer, + "null_only" integer + ); + `, + ); + await db.insert(aggregateTable).values([ + { name: 'value 1', a: 5, b: 10, c: 20 }, + { name: 'value 1', a: 5, b: 20, c: 30 }, + { name: 'value 2', a: 10, b: 50, c: 60 }, + { name: 'value 3', a: 20, b: 20, c: null }, + { name: 'value 4', a: null, b: 90, c: 120 }, + { name: 'value 5', a: 80, b: 10, c: null }, + { name: 'value 6', a: null, b: null, c: 150 }, + ]); +} + test.serial('table config: foreign keys name', async (t) => { const table = sqliteTable('cities', { id: int('id').primaryKey(), @@ -2423,3 +2466,69 @@ test.serial('set operations (mixed all) as function with subquery', async (t) => ).orderBy(asc(sql`id`)); }); }); + +test.serial('aggregate function: count', async (t) => { + const { db } = t.context; + const table = aggregateTable; + await setupAggregateFunctionsTest(db); + + const result1 = await db.select({ value: count() }).from(table); + const result2 = await db.select({ value: count(table.a) }).from(table); + const result3 = await db.select({ value: countDistinct(table.name) }).from(table); + + t.deepEqual(result1[0]?.value, 7); + t.deepEqual(result2[0]?.value, 5); + t.deepEqual(result3[0]?.value, 6); +}); + +test.serial('aggregate function: avg', async (t) => { + const { db } = t.context; + const table = aggregateTable; + await setupAggregateFunctionsTest(db); + + const result1 = await db.select({ value: avg(table.a) }).from(table); + const result2 = await db.select({ value: avg(table.nullOnly) }).from(table); + const result3 = await db.select({ value: avgDistinct(table.b) }).from(table); + + t.deepEqual(result1[0]?.value, '24'); + t.deepEqual(result2[0]?.value, null); + t.deepEqual(result3[0]?.value, '42.5'); +}); + +test.serial('aggregate function: sum', async (t) => { + const { db } = t.context; + const table = aggregateTable; + await setupAggregateFunctionsTest(db); + + const result1 = await db.select({ value: sum(table.b) }).from(table); + const result2 = await db.select({ value: sum(table.nullOnly) }).from(table); + const result3 = await db.select({ value: sumDistinct(table.b) }).from(table); + + t.deepEqual(result1[0]?.value, '200'); + t.deepEqual(result2[0]?.value, null); + t.deepEqual(result3[0]?.value, '170'); +}); + +test.serial('aggregate function: max', async (t) => { + const { db } = t.context; + const table = aggregateTable; + await setupAggregateFunctionsTest(db); + + const result1 = await db.select({ value: max(table.b) }).from(table); + const result2 = await db.select({ value: max(table.nullOnly) }).from(table); + + t.deepEqual(result1[0]?.value, 90); + t.deepEqual(result2[0]?.value, null); +}); + +test.serial('aggregate function: min', async (t) => { + const { db } = t.context; + const table = aggregateTable; + await setupAggregateFunctionsTest(db); + + const result1 = await db.select({ value: min(table.b) }).from(table); + const result2 = await db.select({ value: min(table.nullOnly) }).from(table); + + t.deepEqual(result1[0]?.value, 10); + t.deepEqual(result2[0]?.value, null); +}); diff --git a/integration-tests/tests/mysql-proxy.test.ts b/integration-tests/tests/mysql-proxy.test.ts index 861d6811e..df2e5e346 100644 --- a/integration-tests/tests/mysql-proxy.test.ts +++ b/integration-tests/tests/mysql-proxy.test.ts @@ -97,7 +97,7 @@ const usersMigratorTable = mysqlTable('users12', { // eslint-disable-next-line drizzle/require-entity-kind class ServerSimulator { - constructor(private db: mysql.Connection) {} + constructor(private db: mysql.Connection) { } async query(sql: string, params: any[], method: 'all' | 'execute') { if (method === 'all') { @@ -106,7 +106,7 @@ class ServerSimulator { sql, values: params, rowsAsArray: true, - typeCast: function(field: any, next: any) { + typeCast: function (field: any, next: any) { if (field.type === 'TIMESTAMP' || field.type === 'DATETIME' || field.type === 'DATE') { return field.string(); } @@ -123,7 +123,7 @@ class ServerSimulator { const result = await this.db.query({ sql, values: params, - typeCast: function(field: any, next: any) { + typeCast: function (field: any, next: any) { if (field.type === 'TIMESTAMP' || field.type === 'DATETIME' || field.type === 'DATE') { return field.string(); } @@ -141,14 +141,15 @@ class ServerSimulator { } async migrations(queries: string[]) { - await this.db.query('BEGIN'); + await this.db.query('START TRANSACTION'); try { for (const query of queries) { await this.db.query(query); } await this.db.query('COMMIT'); - } catch { + } catch (e) { await this.db.query('ROLLBACK'); + throw e; } return {}; @@ -253,11 +254,11 @@ test.beforeEach(async (t) => { await ctx.db.execute( sql` create table \`userstest\` ( - \`id\` serial primary key, - \`name\` text not null, - \`verified\` boolean not null default false, - \`jsonb\` json, - \`created_at\` timestamp not null default now() + \`id\` serial primary key, + \`name\` text not null, + \`verified\` boolean not null default false, + \`jsonb\` json, + \`created_at\` timestamp not null default now() ) `, ); @@ -265,9 +266,9 @@ test.beforeEach(async (t) => { await ctx.db.execute( sql` create table \`users2\` ( - \`id\` serial primary key, - \`name\` text not null, - \`city_id\` int references \`cities\`(\`id\`) + \`id\` serial primary key, + \`name\` text not null, + \`city_id\` int references \`cities\`(\`id\`) ) `, ); @@ -275,8 +276,8 @@ test.beforeEach(async (t) => { await ctx.db.execute( sql` create table \`cities\` ( - \`id\` serial primary key, - \`name\` text not null + \`id\` serial primary key, + \`name\` text not null ) `, ); @@ -994,8 +995,7 @@ test.serial('prepared statement with placeholder in .where', async (t) => { test.serial('migrator', async (t) => { const { db, serverSimulator } = t.context; - await db.execute(sql`drop table if exists cities_migration`); - await db.execute(sql`drop table if exists users_migration`); + await db.execute(sql`drop table if exists userstest`); await db.execute(sql`drop table if exists users12`); await db.execute(sql`drop table if exists __drizzle_migrations`); @@ -1006,16 +1006,36 @@ test.serial('migrator', async (t) => { console.error(e); throw new Error('Proxy server cannot run migrations'); } - }, { migrationsFolder: './drizzle2/mysql' }); + }, { migrationsFolder: './drizzle2/mysql-proxy/first' }); - await db.insert(usersMigratorTable).values({ name: 'John', email: 'email' }); + await t.notThrowsAsync(async () => { + await db.insert(usersTable).values({ name: 'John' }); + }); - const result = await db.select().from(usersMigratorTable); + await t.throwsAsync(async () => { + await db.insert(usersMigratorTable).values({ name: 'John', email: 'email' }); + }, { + message: 'Table \'drizzle.users12\' doesn\'t exist' + }); - t.deepEqual(result, [{ id: 1, name: 'John', email: 'email' }]); + await migrate(db, async (queries) => { + try { + await serverSimulator.migrations(queries); + } catch (e) { + console.error(e); + throw new Error('Proxy server cannot run migrations'); + } + }, { migrationsFolder: './drizzle2/mysql-proxy/second' }); + + await t.notThrowsAsync(async () => { + await db.insert(usersTable).values({ name: 'John' }); + }); + + await t.notThrowsAsync(async () => { + await db.insert(usersMigratorTable).values({ name: 'John', email: 'email' }); + }); - await db.execute(sql`drop table cities_migration`); - await db.execute(sql`drop table users_migration`); + await db.execute(sql`drop table userstest`); await db.execute(sql`drop table users12`); await db.execute(sql`drop table __drizzle_migrations`); }); diff --git a/integration-tests/tests/mysql.test.ts b/integration-tests/tests/mysql.test.ts index 3b545fcd8..d916cc22d 100644 --- a/integration-tests/tests/mysql.test.ts +++ b/integration-tests/tests/mysql.test.ts @@ -15,6 +15,14 @@ import { placeholder, sql, TransactionRollbackError, + sum, + sumDistinct, + count, + countDistinct, + avg, + avgDistinct, + max, + min, } from 'drizzle-orm'; import { alias, @@ -119,6 +127,16 @@ const usersMigratorTable = mysqlTable('users12', { }; }); +// To test aggregate functions +const aggregateTable = mysqlTable('aggregate_table', { + id: serial('id').notNull(), + name: text('name').notNull(), + a: int('a'), + b: int('b'), + c: int('c'), + nullOnly: int('null_only') +}); + interface Context { docker: Docker; mysqlContainer: Docker.Container; @@ -268,6 +286,31 @@ async function setupSetOperationTest(db: MySql2Database) { ]); } +async function setupAggregateFunctionsTest(db: MySql2Database) { + await db.execute(sql`drop table if exists \`aggregate_table\``); + await db.execute( + sql` + create table \`aggregate_table\` ( + \`id\` integer primary key auto_increment not null, + \`name\` text not null, + \`a\` integer, + \`b\` integer, + \`c\` integer, + \`null_only\` integer + ); + `, + ); + await db.insert(aggregateTable).values([ + { name: 'value 1', a: 5, b: 10, c: 20 }, + { name: 'value 1', a: 5, b: 20, c: 30 }, + { name: 'value 2', a: 10, b: 50, c: 60 }, + { name: 'value 3', a: 20, b: 20, c: null }, + { name: 'value 4', a: null, b: 90, c: 120 }, + { name: 'value 5', a: 80, b: 10, c: null }, + { name: 'value 6', a: null, b: null, c: 150 }, + ]); +} + test.serial('table config: unsigned ints', async (t) => { const unsignedInts = mysqlTable('cities1', { bigint: bigint('bigint', { mode: 'number', unsigned: true }), @@ -2654,3 +2697,69 @@ test.serial('set operations (mixed all) as function with subquery', async (t) => ); }); }); + +test.serial('aggregate function: count', async (t) => { + const { db } = t.context; + const table = aggregateTable; + await setupAggregateFunctionsTest(db); + + const result1 = await db.select({ value: count() }).from(table); + const result2 = await db.select({ value: count(table.a) }).from(table); + const result3 = await db.select({ value: countDistinct(table.name) }).from(table); + + t.deepEqual(result1[0]?.value, 7); + t.deepEqual(result2[0]?.value, 5); + t.deepEqual(result3[0]?.value, 6); +}); + +test.serial('aggregate function: avg', async (t) => { + const { db } = t.context; + const table = aggregateTable; + await setupAggregateFunctionsTest(db); + + const result1 = await db.select({ value: avg(table.b) }).from(table); + const result2 = await db.select({ value: avg(table.nullOnly) }).from(table); + const result3 = await db.select({ value: avgDistinct(table.b) }).from(table); + + t.deepEqual(result1[0]?.value, '33.3333'); + t.deepEqual(result2[0]?.value, null); + t.deepEqual(result3[0]?.value, '42.5000'); +}); + +test.serial('aggregate function: sum', async (t) => { + const { db } = t.context; + const table = aggregateTable; + await setupAggregateFunctionsTest(db); + + const result1 = await db.select({ value: sum(table.b) }).from(table); + const result2 = await db.select({ value: sum(table.nullOnly) }).from(table); + const result3 = await db.select({ value: sumDistinct(table.b) }).from(table); + + t.deepEqual(result1[0]?.value, '200'); + t.deepEqual(result2[0]?.value, null); + t.deepEqual(result3[0]?.value, '170'); +}); + +test.serial('aggregate function: max', async (t) => { + const { db } = t.context; + const table = aggregateTable; + await setupAggregateFunctionsTest(db); + + const result1 = await db.select({ value: max(table.b) }).from(table); + const result2 = await db.select({ value: max(table.nullOnly) }).from(table); + + t.deepEqual(result1[0]?.value, 90); + t.deepEqual(result2[0]?.value, null); +}); + +test.serial('aggregate function: min', async (t) => { + const { db } = t.context; + const table = aggregateTable; + await setupAggregateFunctionsTest(db); + + const result1 = await db.select({ value: min(table.b) }).from(table); + const result2 = await db.select({ value: min(table.nullOnly) }).from(table); + + t.deepEqual(result1[0]?.value, 10); + t.deepEqual(result2[0]?.value, null); +}); diff --git a/integration-tests/tests/pg-proxy.test.ts b/integration-tests/tests/pg-proxy.test.ts index 02c48cffc..bc494c662 100644 --- a/integration-tests/tests/pg-proxy.test.ts +++ b/integration-tests/tests/pg-proxy.test.ts @@ -58,7 +58,7 @@ import { Expect } from './utils.ts'; // eslint-disable-next-line drizzle/require-entity-kind class ServerSimulator { - constructor(private db: pg.Client) {} + constructor(private db: pg.Client) { } async query(sql: string, params: any[], method: 'all' | 'execute') { if (method === 'all') { @@ -96,8 +96,9 @@ class ServerSimulator { await this.db.query(query); } await this.db.query('COMMIT'); - } catch { + } catch (e) { await this.db.query('ROLLBACK'); + throw e; } return {}; @@ -1045,7 +1046,7 @@ test.serial('prepared statement with placeholder in .offset', async (t) => { test.serial('migrator', async (t) => { const { db, serverSimulator } = t.context; - await db.execute(sql`drop table if exists all_columns`); + await db.execute(sql`drop table if exists users`); await db.execute(sql`drop table if exists users12`); await db.execute(sql`drop table if exists "drizzle"."__drizzle_migrations"`); @@ -1056,15 +1057,36 @@ test.serial('migrator', async (t) => { console.error(e); throw new Error('Proxy server cannot run migrations'); } - }, { migrationsFolder: './drizzle2/pg' }); + }, { migrationsFolder: './drizzle2/pg-proxy/first' }); + + await t.notThrowsAsync(async () => { + await db.insert(usersTable).values({ name: 'John' }); + }); - await db.insert(usersMigratorTable).values({ name: 'John', email: 'email' }); + await t.throwsAsync(async () => { + await db.insert(usersMigratorTable).values({ name: 'John', email: 'email' }); + }, { + message: 'relation "users12" does not exist' + }); + + await migrate(db, async (queries) => { + try { + await serverSimulator.migrations(queries); + } catch (e) { + console.error(e); + throw new Error('Proxy server cannot run migrations'); + } + }, { migrationsFolder: './drizzle2/pg-proxy/second' }); - const result = await db.select().from(usersMigratorTable); + await t.notThrowsAsync(async () => { + await db.insert(usersTable).values({ name: 'John' }); + }); - t.deepEqual(result, [{ id: 1, name: 'John', email: 'email' }]); + await t.notThrowsAsync(async () => { + await db.insert(usersMigratorTable).values({ name: 'John', email: 'email' }); + }); - await db.execute(sql`drop table all_columns`); + await db.execute(sql`drop table users`); await db.execute(sql`drop table users12`); await db.execute(sql`drop table "drizzle"."__drizzle_migrations"`); }); @@ -1087,11 +1109,10 @@ test.serial('insert via db.execute + returning', async (t) => { const { db } = t.context; const inserted = await db.execute<{ id: number; name: string }>( - sql`insert into ${usersTable} (${ - name( - usersTable.name.name, - ) - }) values (${'John'}) returning ${usersTable.id}, ${usersTable.name}`, + sql`insert into ${usersTable} (${name( + usersTable.name.name, + ) + }) values (${'John'}) returning ${usersTable.id}, ${usersTable.name}`, ); t.deepEqual(inserted, [{ id: 1, name: 'John' }]); }); @@ -2064,19 +2085,11 @@ test.serial('select from enum', async (t) => { await db.execute(sql`drop type if exists ${name(equipmentEnum.enumName)}`); await db.execute(sql`drop type if exists ${name(categoryEnum.enumName)}`); - await db.execute( - sql`create type ${ - name(muscleEnum.enumName) - } as enum ('abdominals', 'hamstrings', 'adductors', 'quadriceps', 'biceps', 'shoulders', 'chest', 'middle_back', 'calves', 'glutes', 'lower_back', 'lats', 'triceps', 'traps', 'forearms', 'neck', 'abductors')`, - ); + await db.execute(sql`create type ${name(muscleEnum.enumName)} as enum ('abdominals', 'hamstrings', 'adductors', 'quadriceps', 'biceps', 'shoulders', 'chest', 'middle_back', 'calves', 'glutes', 'lower_back', 'lats', 'triceps', 'traps', 'forearms', 'neck', 'abductors')`,); await db.execute(sql`create type ${name(forceEnum.enumName)} as enum ('isometric', 'isotonic', 'isokinetic')`); await db.execute(sql`create type ${name(levelEnum.enumName)} as enum ('beginner', 'intermediate', 'advanced')`); await db.execute(sql`create type ${name(mechanicEnum.enumName)} as enum ('compound', 'isolation')`); - await db.execute( - sql`create type ${ - name(equipmentEnum.enumName) - } as enum ('barbell', 'dumbbell', 'bodyweight', 'machine', 'cable', 'kettlebell')`, - ); + await db.execute(sql`create type ${name(equipmentEnum.enumName)} as enum ('barbell', 'dumbbell', 'bodyweight', 'machine', 'cable', 'kettlebell')`,); await db.execute(sql`create type ${name(categoryEnum.enumName)} as enum ('upper_body', 'lower_body', 'full_body')`); await db.execute(sql` create table ${exercises} ( diff --git a/integration-tests/tests/pg.test.ts b/integration-tests/tests/pg.test.ts index 38fd1a8a3..3c1b1c9d7 100644 --- a/integration-tests/tests/pg.test.ts +++ b/integration-tests/tests/pg.test.ts @@ -20,6 +20,14 @@ import { sql, type SQLWrapper, TransactionRollbackError, + count, + countDistinct, + avg, + avgDistinct, + sum, + sumDistinct, + max, + min, } from 'drizzle-orm'; import { drizzle, type NodePgDatabase } from 'drizzle-orm/node-postgres'; import { migrate } from 'drizzle-orm/node-postgres/migrator'; @@ -134,6 +142,16 @@ const usersMigratorTable = pgTable('users12', { email: text('email').notNull(), }); +// To test aggregate functions +const aggregateTable = pgTable('aggregate_table', { + id: serial('id').notNull(), + name: text('name').notNull(), + a: integer('a'), + b: integer('b'), + c: integer('c'), + nullOnly: integer('null_only') +}); + interface Context { docker: Docker; pgContainer: Docker.Container; @@ -333,6 +351,31 @@ async function setupSetOperationTest(db: NodePgDatabase) { ]); } +async function setupAggregateFunctionsTest(db: NodePgDatabase) { + await db.execute(sql`drop table if exists "aggregate_table"`); + await db.execute( + sql` + create table "aggregate_table" ( + "id" serial not null, + "name" text not null, + "a" integer, + "b" integer, + "c" integer, + "null_only" integer + ); + `, + ); + await db.insert(aggregateTable).values([ + { name: 'value 1', a: 5, b: 10, c: 20 }, + { name: 'value 1', a: 5, b: 20, c: 30 }, + { name: 'value 2', a: 10, b: 50, c: 60 }, + { name: 'value 3', a: 20, b: 20, c: null }, + { name: 'value 4', a: null, b: 90, c: 120 }, + { name: 'value 5', a: 80, b: 10, c: null }, + { name: 'value 6', a: null, b: null, c: 150 }, + ]); +} + test.serial('table configs: unique third param', async (t) => { const cities1Table = pgTable('cities1', { id: serial('id').primaryKey(), @@ -480,16 +523,18 @@ test.serial('select distinct', async (t) => { const usersDistinctTable = pgTable('users_distinct', { id: integer('id').notNull(), name: text('name').notNull(), + age: integer('age').notNull() }); await db.execute(sql`drop table if exists ${usersDistinctTable}`); - await db.execute(sql`create table ${usersDistinctTable} (id integer, name text)`); + await db.execute(sql`create table ${usersDistinctTable} (id integer, name text, age integer)`); await db.insert(usersDistinctTable).values([ - { id: 1, name: 'John' }, - { id: 1, name: 'John' }, - { id: 2, name: 'John' }, - { id: 1, name: 'Jane' }, + { id: 1, name: 'John', age: 24 }, + { id: 1, name: 'John', age: 24 }, + { id: 2, name: 'John', age: 25 }, + { id: 1, name: 'Jane', age: 24 }, + { id: 1, name: 'Jane', age: 26 } ]); const users1 = await db.selectDistinct().from(usersDistinctTable).orderBy( usersDistinctTable.id, @@ -501,10 +546,18 @@ test.serial('select distinct', async (t) => { const users3 = await db.selectDistinctOn([usersDistinctTable.name], { name: usersDistinctTable.name }).from( usersDistinctTable, ).orderBy(usersDistinctTable.name); + const users4 = await db.selectDistinctOn([usersDistinctTable.id, usersDistinctTable.age]).from( + usersDistinctTable + ).orderBy(usersDistinctTable.id, usersDistinctTable.age) await db.execute(sql`drop table ${usersDistinctTable}`); - t.deepEqual(users1, [{ id: 1, name: 'Jane' }, { id: 1, name: 'John' }, { id: 2, name: 'John' }]); + t.deepEqual(users1, [ + { id: 1, name: 'Jane', age: 24 }, + { id: 1, name: 'Jane', age: 26 }, + { id: 1, name: 'John', age: 24 }, + { id: 2, name: 'John', age: 25 } + ]); t.deepEqual(users2.length, 2); t.deepEqual(users2[0]?.id, 1); @@ -513,6 +566,12 @@ test.serial('select distinct', async (t) => { t.deepEqual(users3.length, 2); t.deepEqual(users3[0]?.name, 'Jane'); t.deepEqual(users3[1]?.name, 'John'); + + t.deepEqual(users4, [ + { id: 1, name: 'John', age: 24 }, + { id: 1, name: 'Jane', age: 26 }, + { id: 2, name: 'John', age: 25 } + ]); }); test.serial('insert returning sql', async (t) => { @@ -3151,3 +3210,69 @@ test.serial('set operations (mixed all) as function', async (t) => { ).orderBy(asc(sql`id`)); }); }); + +test.serial('aggregate function: count', async (t) => { + const { db } = t.context; + const table = aggregateTable; + await setupAggregateFunctionsTest(db); + + const result1 = await db.select({ value: count() }).from(table); + const result2 = await db.select({ value: count(table.a) }).from(table); + const result3 = await db.select({ value: countDistinct(table.name) }).from(table); + + t.deepEqual(result1[0]?.value, 7); + t.deepEqual(result2[0]?.value, 5); + t.deepEqual(result3[0]?.value, 6); +}); + +test.serial('aggregate function: avg', async (t) => { + const { db } = t.context; + const table = aggregateTable; + await setupAggregateFunctionsTest(db); + + const result1 = await db.select({ value: avg(table.b) }).from(table); + const result2 = await db.select({ value: avg(table.nullOnly) }).from(table); + const result3 = await db.select({ value: avgDistinct(table.b) }).from(table); + + t.deepEqual(result1[0]?.value, '33.3333333333333333'); + t.deepEqual(result2[0]?.value, null); + t.deepEqual(result3[0]?.value, '42.5000000000000000'); +}); + +test.serial('aggregate function: sum', async (t) => { + const { db } = t.context; + const table = aggregateTable; + await setupAggregateFunctionsTest(db); + + const result1 = await db.select({ value: sum(table.b) }).from(table); + const result2 = await db.select({ value: sum(table.nullOnly) }).from(table); + const result3 = await db.select({ value: sumDistinct(table.b) }).from(table); + + t.deepEqual(result1[0]?.value, '200'); + t.deepEqual(result2[0]?.value, null); + t.deepEqual(result3[0]?.value, '170'); +}); + +test.serial('aggregate function: max', async (t) => { + const { db } = t.context; + const table = aggregateTable; + await setupAggregateFunctionsTest(db); + + const result1 = await db.select({ value: max(table.b) }).from(table); + const result2 = await db.select({ value: max(table.nullOnly) }).from(table); + + t.deepEqual(result1[0]?.value, 90); + t.deepEqual(result2[0]?.value, null); +}); + +test.serial('aggregate function: min', async (t) => { + const { db } = t.context; + const table = aggregateTable; + await setupAggregateFunctionsTest(db); + + const result1 = await db.select({ value: min(table.b) }).from(table); + const result2 = await db.select({ value: min(table.nullOnly) }).from(table); + + t.deepEqual(result1[0]?.value, 10); + t.deepEqual(result2[0]?.value, null); +}); diff --git a/integration-tests/tests/replicas/mysql.test.ts b/integration-tests/tests/replicas/mysql.test.ts index 62fa6af41..a7de02411 100644 --- a/integration-tests/tests/replicas/mysql.test.ts +++ b/integration-tests/tests/replicas/mysql.test.ts @@ -9,7 +9,11 @@ const usersTable = mysqlTable('users', { verified: boolean('verified').notNull().default(false), }); -describe('[select] read replicas postgres', () => { +const users = mysqlTable('users', { + id: serial('id' as string).primaryKey(), +}); + +describe('[select] read replicas mysql', () => { it('primary select', () => { const primaryDb = drizzle({} as any); const read1 = drizzle({} as any); @@ -21,9 +25,11 @@ describe('[select] read replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'select'); const spyRead2 = vi.spyOn(read2, 'select'); - db.$primary.select().from({} as any); + const query = db.$primary.select().from(users); expect(spyPrimary).toHaveBeenCalledTimes(1); + expect(query.toSQL().sql).toEqual('select `id` from `users`'); + expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); }); @@ -43,15 +49,18 @@ describe('[select] read replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'select'); const spyRead2 = vi.spyOn(read2, 'select'); - db.select().from({} as any); + const query1 = db.select({ count: sql`count(*)`.as('count') }).from(users).limit(1); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(0); - db.select().from({} as any); + expect(query1.toSQL().sql).toEqual('select count(*) as `count` from `users` limit ?'); + + const query2 = db.select().from(users); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(1); + expect(query2.toSQL().sql).toEqual('select `id` from `users`'); }); it('single read replica select', () => { @@ -63,13 +72,15 @@ describe('[select] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb, 'select'); const spyRead1 = vi.spyOn(read1, 'select'); - db.select().from({} as any); + const query1 = db.select().from(users); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); + expect(query1.toSQL().sql).toEqual('select `id` from `users`'); - db.select().from({} as any); + const query2 = db.select().from(users); expect(spyRead1).toHaveBeenCalledTimes(2); + expect(query2.toSQL().sql).toEqual('select `id` from `users`'); }); it('single read replica select + primary select', () => { @@ -81,14 +92,16 @@ describe('[select] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb, 'select'); const spyRead1 = vi.spyOn(read1, 'select'); - db.select().from({} as any); + const query1 = db.select({ id: users.id }).from(users); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); + expect(query1.toSQL().sql).toEqual('select `id` from `users`'); - db.$primary.select().from({} as any); + const query2 = db.$primary.select().from(users); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(1); + expect(query2.toSQL().sql).toEqual('select `id` from `users`'); }); it('always first read select', () => { @@ -104,19 +117,22 @@ describe('[select] read replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'select'); const spyRead2 = vi.spyOn(read2, 'select'); - db.select().from({} as any); + const query1 = db.select().from(users); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query1.toSQL().sql).toEqual('select `id` from `users`'); + + const query2 = db.select().from(users); - db.select().from({} as any); expect(spyRead1).toHaveBeenCalledTimes(2); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query2.toSQL().sql).toEqual('select `id` from `users`'); }); }); -describe('[selectDistinct] read replicas postgres', () => { +describe('[selectDistinct] read replicas mysql', () => { it('primary selectDistinct', () => { const primaryDb = drizzle({} as any); const read1 = drizzle({} as any); @@ -128,11 +144,12 @@ describe('[selectDistinct] read replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'selectDistinct'); const spyRead2 = vi.spyOn(read2, 'selectDistinct'); - db.$primary.selectDistinct().from({} as any); + const query = db.$primary.selectDistinct().from(users); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query.toSQL().sql).toEqual('select distinct `id` from `users`'); }); it('random replica selectDistinct', () => { @@ -150,15 +167,17 @@ describe('[selectDistinct] read replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'selectDistinct'); const spyRead2 = vi.spyOn(read2, 'selectDistinct'); - db.selectDistinct().from({} as any); + const query1 = db.selectDistinct().from(users); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query1.toSQL().sql).toEqual('select distinct `id` from `users`'); - db.selectDistinct().from({} as any); + const query2 = db.selectDistinct().from(users); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(1); + expect(query2.toSQL().sql).toEqual('select distinct `id` from `users`'); }); it('single read replica selectDistinct', () => { @@ -170,13 +189,15 @@ describe('[selectDistinct] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb, 'selectDistinct'); const spyRead1 = vi.spyOn(read1, 'selectDistinct'); - db.selectDistinct().from({} as any); + const query1 = db.selectDistinct().from(users); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); + expect(query1.toSQL().sql).toEqual('select distinct `id` from `users`'); - db.selectDistinct().from({} as any); + const query2 = db.selectDistinct().from(users); expect(spyRead1).toHaveBeenCalledTimes(2); + expect(query2.toSQL().sql).toEqual('select distinct `id` from `users`'); }); it('single read replica selectDistinct + primary selectDistinct', () => { @@ -188,14 +209,16 @@ describe('[selectDistinct] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb, 'selectDistinct'); const spyRead1 = vi.spyOn(read1, 'selectDistinct'); - db.selectDistinct().from({} as any); + const query1 = db.selectDistinct().from(users); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); + expect(query1.toSQL().sql).toEqual('select distinct `id` from `users`'); - db.$primary.selectDistinct().from({} as any); + const query2 = db.$primary.selectDistinct().from(users); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(1); + expect(query2.toSQL().sql).toEqual('select distinct `id` from `users`'); }); it('always first read selectDistinct', () => { @@ -211,19 +234,21 @@ describe('[selectDistinct] read replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'selectDistinct'); const spyRead2 = vi.spyOn(read2, 'selectDistinct'); - db.selectDistinct().from({} as any); + const query1 = db.selectDistinct().from(users); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query1.toSQL().sql).toEqual('select distinct `id` from `users`'); - db.selectDistinct().from({} as any); + const query2 = db.selectDistinct().from(users); expect(spyRead1).toHaveBeenCalledTimes(2); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query2.toSQL().sql).toEqual('select distinct `id` from `users`'); }); }); -describe('[with] read replicas postgres', () => { +describe('[with] read replicas mysql', () => { it('primary with', () => { const primaryDb = drizzle({} as any); const read1 = drizzle({} as any); @@ -234,12 +259,17 @@ describe('[with] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb, 'with'); const spyRead1 = vi.spyOn(read1, 'with'); const spyRead2 = vi.spyOn(read2, 'with'); + const obj1 = {} as any; + const obj2 = {} as any; + const obj3 = {} as any; + const obj4 = {} as any; - db.$primary.with(); + db.$primary.with(obj1, obj2, obj3, obj4); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenCalledWith(obj1, obj2, obj3, obj4); }); it('random replica with', () => { @@ -317,20 +347,25 @@ describe('[with] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb, 'with'); const spyRead1 = vi.spyOn(read1, 'with'); const spyRead2 = vi.spyOn(read2, 'with'); + const obj1 = {} as any; + const obj2 = {} as any; + const obj3 = {} as any; - db.with(); + db.with(obj1); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledWith(obj1); - db.with(); + db.with(obj2, obj3); expect(spyRead1).toHaveBeenCalledTimes(2); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledWith(obj2, obj3); }); }); -describe('[update] replicas postgres', () => { +describe('[update] replicas mysql', () => { it('primary update', () => { const primaryDb = drizzle({} as any); const read1 = drizzle({} as any); @@ -342,27 +377,30 @@ describe('[update] replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'update'); const spyRead2 = vi.spyOn(read2, 'update'); - db.update({} as any); + const query1 = db.update(users).set({ id: 1 }); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query1.toSQL().sql).toEqual('update `users` set `id` = ?'); - db.update({} as any); + const query2 = db.update(users).set({ id: 1 }); expect(spyPrimary).toHaveBeenCalledTimes(2); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query2.toSQL().sql).toEqual('update `users` set `id` = ?'); - db.$primary.update({} as any); + const query3 = db.$primary.update(users).set({ id: 1 }); expect(spyPrimary).toHaveBeenCalledTimes(3); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query3.toSQL().sql).toEqual('update `users` set `id` = ?'); }); }); -describe('[delete] replicas postgres', () => { +describe('[delete] replicas mysql', () => { it('primary delete', () => { const primaryDb = drizzle({} as any); const read1 = drizzle({} as any); @@ -374,17 +412,21 @@ describe('[delete] replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'delete'); const spyRead2 = vi.spyOn(read2, 'delete'); - db.delete({} as any); + const query1 = db.delete(users); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenCalledWith(users); + expect(query1.toSQL().sql).toEqual('delete from `users`'); - db.delete({} as any); + const query2 = db.delete(users); expect(spyPrimary).toHaveBeenCalledTimes(2); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenNthCalledWith(2, users); + expect(query2.toSQL().sql).toEqual('delete from `users`'); db.$primary.delete({} as any); @@ -394,7 +436,7 @@ describe('[delete] replicas postgres', () => { }); }); -describe('[insert] replicas postgres', () => { +describe('[insert] replicas mysql', () => { it('primary insert', () => { const primaryDb = drizzle({} as any); const read1 = drizzle({} as any); @@ -406,17 +448,20 @@ describe('[insert] replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'insert'); const spyRead2 = vi.spyOn(read2, 'insert'); - db.insert({} as any); + const query = db.insert(users).values({ id: 1 }); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenCalledWith(users); + expect(query.toSQL().sql).toEqual('insert into `users` (`id`) values (?)'); - db.insert({} as any); + db.insert(users); expect(spyPrimary).toHaveBeenCalledTimes(2); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenNthCalledWith(2, users); db.$primary.insert({} as any); @@ -426,7 +471,7 @@ describe('[insert] replicas postgres', () => { }); }); -describe('[execute] replicas postgres', () => { +describe('[execute] replicas mysql', () => { it('primary execute', async () => { const primaryDb = drizzle({} as any); const read1 = drizzle({} as any); @@ -438,27 +483,29 @@ describe('[execute] replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'execute'); const spyRead2 = vi.spyOn(read2, 'execute'); - // expect(db.execute(sql``)).rejects.toThrow(); + expect(db.execute(sql``)).rejects.toThrow(); - try { - db.execute(sql``); - } catch { /* empty */ } + // try { + // db.execute(sql``); + // } catch { /* empty */ } expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); - try { - db.execute(sql``); - } catch { /* empty */ } + expect(db.execute(sql``)).rejects.toThrow(); + // try { + // db.execute(sql``); + // } catch { /* empty */ } expect(spyPrimary).toHaveBeenCalledTimes(2); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); - try { - db.execute(sql``); - } catch { /* empty */ } + expect(db.execute(sql``)).rejects.toThrow(); + // try { + // db.execute(sql``); + // } catch { /* empty */ } expect(spyPrimary).toHaveBeenCalledTimes(3); expect(spyRead1).toHaveBeenCalledTimes(0); @@ -466,7 +513,7 @@ describe('[execute] replicas postgres', () => { }); }); -describe('[transaction] replicas postgres', () => { +describe('[transaction] replicas mysql', () => { it('primary transaction', async () => { const primaryDb = drizzle({} as any); const read1 = drizzle({} as any); @@ -477,22 +524,27 @@ describe('[transaction] replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb, 'transaction'); const spyRead1 = vi.spyOn(read1, 'transaction'); const spyRead2 = vi.spyOn(read2, 'transaction'); - - expect(db.transaction(async (tx) => { + const txFn1 = async (tx: any) => { tx.select().from({} as any); - })).rejects.toThrow(); + }; + + expect(db.transaction(txFn1)).rejects.toThrow(); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenCalledWith(txFn1); - expect(db.transaction(async (tx) => { + const txFn2 = async (tx: any) => { tx.select().from({} as any); - })).rejects.toThrow(); + }; + + expect(db.transaction(txFn2)).rejects.toThrow(); expect(spyPrimary).toHaveBeenCalledTimes(2); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenNthCalledWith(2, txFn2); expect(db.transaction(async (tx) => { tx.select().from({} as any); @@ -504,7 +556,7 @@ describe('[transaction] replicas postgres', () => { }); }); -describe('[findFirst] read replicas postgres', () => { +describe('[findFirst] read replicas mysql', () => { it('primary findFirst', () => { const primaryDb = drizzle({} as any, { schema: { usersTable }, mode: 'default' }); const read1 = drizzle({} as any, { schema: { usersTable }, mode: 'default' }); @@ -515,12 +567,14 @@ describe('[findFirst] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findFirst'); const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findFirst'); const spyRead2 = vi.spyOn(read2['query']['usersTable'], 'findFirst'); + const obj = {} as any; - db.$primary.query.usersTable.findFirst(); + db.$primary.query.usersTable.findFirst(obj); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenCalledWith(obj); }); it('random replica findFirst', () => { @@ -537,16 +591,19 @@ describe('[findFirst] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findFirst'); const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findFirst'); const spyRead2 = vi.spyOn(read2['query']['usersTable'], 'findFirst'); + const par1 = {} as any; - db.query.usersTable.findFirst(); + db.query.usersTable.findFirst(par1); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledWith(par1); - db.query.usersTable.findFirst(); + const query = db.query.usersTable.findFirst(); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(1); + expect(query.toSQL().sql).toEqual('select `id`, `name`, `verified` from `users` `usersTable` limit ?'); }); it('single read replica findFirst', () => { @@ -611,7 +668,7 @@ describe('[findFirst] read replicas postgres', () => { }); }); -describe('[findMany] read replicas postgres', () => { +describe('[findMany] read replicas mysql', () => { it('primary findMany', () => { const primaryDb = drizzle({} as any, { schema: { usersTable }, mode: 'default' }); const read1 = drizzle({} as any, { schema: { usersTable }, mode: 'default' }); @@ -622,12 +679,15 @@ describe('[findMany] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findMany'); const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findMany'); const spyRead2 = vi.spyOn(read2['query']['usersTable'], 'findMany'); + const obj = {} as any; - db.$primary.query.usersTable.findMany(); + const query = db.$primary.query.usersTable.findMany(obj); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenCalledWith(obj); + expect(query.toSQL().sql).toEqual('select `id`, `name`, `verified` from `users` `usersTable`'); }); it('random replica findMany', () => { @@ -644,16 +704,23 @@ describe('[findMany] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findMany'); const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findMany'); const spyRead2 = vi.spyOn(read2['query']['usersTable'], 'findMany'); + const obj1 = {} as any; + const obj2 = {} as any; - db.query.usersTable.findMany(); + const query1 = db.query.usersTable.findMany(obj1); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query1.toSQL().sql).toEqual('select `id`, `name`, `verified` from `users` `usersTable`'); + expect(spyRead1).toHaveBeenCalledWith(obj1); + + const query2 = db.query.usersTable.findMany(obj2); - db.query.usersTable.findMany(); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(1); + expect(query2.toSQL().sql).toEqual('select `id`, `name`, `verified` from `users` `usersTable`'); + expect(spyRead2).toHaveBeenCalledWith(obj2); }); it('single read replica findMany', () => { @@ -664,14 +731,20 @@ describe('[findMany] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findMany'); const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findMany'); + const obj1 = {} as any; + const obj2 = {} as any; - db.query.usersTable.findMany(); + const query1 = db.query.usersTable.findMany(obj1); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); + expect(spyRead1).toHaveBeenCalledWith(obj1); + expect(query1.toSQL().sql).toEqual('select `id`, `name`, `verified` from `users` `usersTable`'); - db.query.usersTable.findMany(); + const query2 = db.query.usersTable.findMany(obj2); expect(spyRead1).toHaveBeenCalledTimes(2); + expect(spyRead1).toHaveBeenNthCalledWith(2, obj2); + expect(query2.toSQL().sql).toEqual('select `id`, `name`, `verified` from `users` `usersTable`'); }); it('single read replica findMany + primary findMany', () => { @@ -682,15 +755,22 @@ describe('[findMany] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findMany'); const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findMany'); + const obj1 = {} as any; + const obj2 = {} as any; - db.query.usersTable.findMany(); + const query1 = db.query.usersTable.findMany(obj1); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); + expect(spyRead1).toHaveBeenCalledWith(obj1); + expect(query1.toSQL().sql).toEqual('select `id`, `name`, `verified` from `users` `usersTable`'); + + const query2 = db.$primary.query.usersTable.findMany(obj2); - db.$primary.query.usersTable.findMany(); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(1); + expect(spyPrimary).toHaveBeenNthCalledWith(1, obj2); + expect(query2.toSQL().sql).toEqual('select `id`, `name`, `verified` from `users` `usersTable`'); }); it('always first read findMany', () => { @@ -705,15 +785,21 @@ describe('[findMany] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findMany'); const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findMany'); const spyRead2 = vi.spyOn(read2['query']['usersTable'], 'findMany'); + const obj1 = {} as any; + const obj2 = {} as any; - db.query.usersTable.findMany(); + const query1 = db.query.usersTable.findMany(obj1); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledWith(obj1); + expect(query1.toSQL().sql).toEqual('select `id`, `name`, `verified` from `users` `usersTable`'); - db.query.usersTable.findMany(); + const query2 = db.query.usersTable.findMany(obj2); expect(spyRead1).toHaveBeenCalledTimes(2); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenNthCalledWith(2, obj2); + expect(query2.toSQL().sql).toEqual('select `id`, `name`, `verified` from `users` `usersTable`'); }); -}); \ No newline at end of file +}); diff --git a/integration-tests/tests/replicas/postgres.test.ts b/integration-tests/tests/replicas/postgres.test.ts index a782045c4..6165ae413 100644 --- a/integration-tests/tests/replicas/postgres.test.ts +++ b/integration-tests/tests/replicas/postgres.test.ts @@ -11,6 +11,10 @@ const usersTable = pgTable('users', { createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), }); +const users = pgTable('users', { + id: serial('id' as string).primaryKey(), +}); + describe('[select] read replicas postgres', () => { it('primary select', () => { const primaryDb = drizzle({} as any); @@ -23,9 +27,11 @@ describe('[select] read replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'select'); const spyRead2 = vi.spyOn(read2, 'select'); - db.$primary.select().from({} as any); + const query = db.$primary.select().from(users); expect(spyPrimary).toHaveBeenCalledTimes(1); + expect(query.toSQL().sql).toEqual('select "id" from "users"'); + expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); }); @@ -45,15 +51,18 @@ describe('[select] read replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'select'); const spyRead2 = vi.spyOn(read2, 'select'); - db.select().from({} as any); + const query1 = db.select({ count: sql`count(*)`.as('count') }).from(users).limit(1); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(0); - db.select().from({} as any); + expect(query1.toSQL().sql).toEqual('select count(*) as "count" from "users" limit $1'); + + const query2 = db.select().from(users); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(1); + expect(query2.toSQL().sql).toEqual('select "id" from "users"'); }); it('single read replica select', () => { @@ -65,13 +74,15 @@ describe('[select] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb, 'select'); const spyRead1 = vi.spyOn(read1, 'select'); - db.select().from({} as any); + const query1 = db.select().from(users); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); + expect(query1.toSQL().sql).toEqual('select "id" from "users"'); - db.select().from({} as any); + const query2 = db.select().from(users); expect(spyRead1).toHaveBeenCalledTimes(2); + expect(query2.toSQL().sql).toEqual('select "id" from "users"'); }); it('single read replica select + primary select', () => { @@ -83,14 +94,16 @@ describe('[select] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb, 'select'); const spyRead1 = vi.spyOn(read1, 'select'); - db.select().from({} as any); + const query1 = db.select({ id: users.id }).from(users); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); + expect(query1.toSQL().sql).toEqual('select "id" from "users"'); - db.$primary.select().from({} as any); + const query2 = db.$primary.select().from(users); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(1); + expect(query2.toSQL().sql).toEqual('select "id" from "users"'); }); it('always first read select', () => { @@ -106,15 +119,18 @@ describe('[select] read replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'select'); const spyRead2 = vi.spyOn(read2, 'select'); - db.select().from({} as any); + const query1 = db.select().from(users); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query1.toSQL().sql).toEqual('select "id" from "users"'); + + const query2 = db.select().from(users); - db.select().from({} as any); expect(spyRead1).toHaveBeenCalledTimes(2); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query2.toSQL().sql).toEqual('select "id" from "users"'); }); }); @@ -130,11 +146,12 @@ describe('[selectDistinct] read replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'selectDistinct'); const spyRead2 = vi.spyOn(read2, 'selectDistinct'); - db.$primary.selectDistinct().from({} as any); + const query = db.$primary.selectDistinct().from(users); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query.toSQL().sql).toEqual('select distinct "id" from "users"'); }); it('random replica selectDistinct', () => { @@ -152,15 +169,17 @@ describe('[selectDistinct] read replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'selectDistinct'); const spyRead2 = vi.spyOn(read2, 'selectDistinct'); - db.selectDistinct().from({} as any); + const query1 = db.selectDistinct().from(users); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query1.toSQL().sql).toEqual('select distinct "id" from "users"'); - db.selectDistinct().from({} as any); + const query2 = db.selectDistinct().from(users); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(1); + expect(query2.toSQL().sql).toEqual('select distinct "id" from "users"'); }); it('single read replica selectDistinct', () => { @@ -172,13 +191,15 @@ describe('[selectDistinct] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb, 'selectDistinct'); const spyRead1 = vi.spyOn(read1, 'selectDistinct'); - db.selectDistinct().from({} as any); + const query1 = db.selectDistinct().from(users); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); + expect(query1.toSQL().sql).toEqual('select distinct "id" from "users"'); - db.selectDistinct().from({} as any); + const query2 = db.selectDistinct().from(users); expect(spyRead1).toHaveBeenCalledTimes(2); + expect(query2.toSQL().sql).toEqual('select distinct "id" from "users"'); }); it('single read replica selectDistinct + primary selectDistinct', () => { @@ -190,14 +211,16 @@ describe('[selectDistinct] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb, 'selectDistinct'); const spyRead1 = vi.spyOn(read1, 'selectDistinct'); - db.selectDistinct().from({} as any); + const query1 = db.selectDistinct().from(users); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); + expect(query1.toSQL().sql).toEqual('select distinct "id" from "users"'); - db.$primary.selectDistinct().from({} as any); + const query2 = db.$primary.selectDistinct().from(users); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(1); + expect(query2.toSQL().sql).toEqual('select distinct "id" from "users"'); }); it('always first read selectDistinct', () => { @@ -213,122 +236,17 @@ describe('[selectDistinct] read replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'selectDistinct'); const spyRead2 = vi.spyOn(read2, 'selectDistinct'); - db.selectDistinct().from({} as any); - - expect(spyPrimary).toHaveBeenCalledTimes(0); - expect(spyRead1).toHaveBeenCalledTimes(1); - expect(spyRead2).toHaveBeenCalledTimes(0); - - db.selectDistinct().from({} as any); - expect(spyRead1).toHaveBeenCalledTimes(2); - expect(spyRead2).toHaveBeenCalledTimes(0); - }); -}); - -describe('[selectDistinctOn] read replicas postgres', () => { - it('primary selectDistinctOn', () => { - const primaryDb = drizzle({} as any); - const read1 = drizzle({} as any); - const read2 = drizzle({} as any); - - const db = withReplicas(primaryDb, [read1, read2]); - - const spyPrimary = vi.spyOn(primaryDb, 'selectDistinctOn'); - const spyRead1 = vi.spyOn(read1, 'selectDistinctOn'); - const spyRead2 = vi.spyOn(read2, 'selectDistinctOn'); - - db.$primary.selectDistinctOn({} as any).from({} as any); - - expect(spyPrimary).toHaveBeenCalledTimes(1); - expect(spyRead1).toHaveBeenCalledTimes(0); - expect(spyRead2).toHaveBeenCalledTimes(0); - }); - - it('random replica selectDistinctOn', () => { - const primaryDb = drizzle({} as any); - const read1 = drizzle({} as any); - const read2 = drizzle({} as any); - - const randomMockReplica = vi.fn().mockReturnValueOnce(read1).mockReturnValueOnce(read2); - - const db = withReplicas(primaryDb, [read1, read2], () => { - return randomMockReplica(); - }); - - const spyPrimary = vi.spyOn(primaryDb, 'selectDistinctOn'); - const spyRead1 = vi.spyOn(read1, 'selectDistinctOn'); - const spyRead2 = vi.spyOn(read2, 'selectDistinctOn'); - - db.selectDistinctOn({} as any).from({} as any); - - expect(spyPrimary).toHaveBeenCalledTimes(0); - expect(spyRead1).toHaveBeenCalledTimes(1); - expect(spyRead2).toHaveBeenCalledTimes(0); - - db.selectDistinctOn({} as any).from({} as any); - expect(spyRead1).toHaveBeenCalledTimes(1); - expect(spyRead2).toHaveBeenCalledTimes(1); - }); - - it('single read replica selectDistinctOn', () => { - const primaryDb = drizzle({} as any); - const read1 = drizzle({} as any); - - const db = withReplicas(primaryDb, [read1]); - - const spyPrimary = vi.spyOn(primaryDb, 'selectDistinctOn'); - const spyRead1 = vi.spyOn(read1, 'selectDistinctOn'); - - db.selectDistinctOn({} as any).from({} as any); - - expect(spyPrimary).toHaveBeenCalledTimes(0); - expect(spyRead1).toHaveBeenCalledTimes(1); - - db.selectDistinctOn({} as any).from({} as any); - expect(spyRead1).toHaveBeenCalledTimes(2); - }); - - it('single read replica selectDistinctOn + primary selectDistinctOn', () => { - const primaryDb = drizzle({} as any); - const read1 = drizzle({} as any); - - const db = withReplicas(primaryDb, [read1]); - - const spyPrimary = vi.spyOn(primaryDb, 'selectDistinctOn'); - const spyRead1 = vi.spyOn(read1, 'selectDistinctOn'); - - db.selectDistinctOn({} as any).from({} as any); - - expect(spyPrimary).toHaveBeenCalledTimes(0); - expect(spyRead1).toHaveBeenCalledTimes(1); - - db.$primary.selectDistinctOn({} as any).from({} as any); - expect(spyPrimary).toHaveBeenCalledTimes(1); - expect(spyRead1).toHaveBeenCalledTimes(1); - }); - - it('always first read selectDistinctOn', () => { - const primaryDb = drizzle({} as any); - const read1 = drizzle({} as any); - const read2 = drizzle({} as any); - - const db = withReplicas(primaryDb, [read1, read2], (replicas) => { - return replicas[0]!; - }); - - const spyPrimary = vi.spyOn(primaryDb, 'selectDistinctOn'); - const spyRead1 = vi.spyOn(read1, 'selectDistinctOn'); - const spyRead2 = vi.spyOn(read2, 'selectDistinctOn'); - - db.selectDistinctOn({} as any).from({} as any); + const query1 = db.selectDistinct().from(users); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query1.toSQL().sql).toEqual('select distinct "id" from "users"'); - db.selectDistinctOn({} as any).from({} as any); + const query2 = db.selectDistinct().from(users); expect(spyRead1).toHaveBeenCalledTimes(2); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query2.toSQL().sql).toEqual('select distinct "id" from "users"'); }); }); @@ -343,12 +261,17 @@ describe('[with] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb, 'with'); const spyRead1 = vi.spyOn(read1, 'with'); const spyRead2 = vi.spyOn(read2, 'with'); + const obj1 = {} as any; + const obj2 = {} as any; + const obj3 = {} as any; + const obj4 = {} as any; - db.$primary.with(); + db.$primary.with(obj1, obj2, obj3, obj4); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenCalledWith(obj1, obj2, obj3, obj4); }); it('random replica with', () => { @@ -426,16 +349,21 @@ describe('[with] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb, 'with'); const spyRead1 = vi.spyOn(read1, 'with'); const spyRead2 = vi.spyOn(read2, 'with'); + const obj1 = {} as any; + const obj2 = {} as any; + const obj3 = {} as any; - db.with(); + db.with(obj1); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledWith(obj1); - db.with(); + db.with(obj2, obj3); expect(spyRead1).toHaveBeenCalledTimes(2); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledWith(obj2, obj3); }); }); @@ -451,23 +379,26 @@ describe('[update] replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'update'); const spyRead2 = vi.spyOn(read2, 'update'); - db.update({} as any); + const query1 = db.update(users).set({ id: 1 }); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query1.toSQL().sql).toEqual('update "users" set "id" = $1'); - db.update({} as any); + const query2 = db.update(users).set({ id: 1 }); expect(spyPrimary).toHaveBeenCalledTimes(2); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query2.toSQL().sql).toEqual('update "users" set "id" = $1'); - db.$primary.update({} as any); + const query3 = db.$primary.update(users).set({ id: 1 }); expect(spyPrimary).toHaveBeenCalledTimes(3); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query3.toSQL().sql).toEqual('update "users" set "id" = $1'); }); }); @@ -483,17 +414,21 @@ describe('[delete] replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'delete'); const spyRead2 = vi.spyOn(read2, 'delete'); - db.delete({} as any); + const query1 = db.delete(users); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenCalledWith(users); + expect(query1.toSQL().sql).toEqual('delete from "users"'); - db.delete({} as any); + const query2 = db.delete(users); expect(spyPrimary).toHaveBeenCalledTimes(2); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenNthCalledWith(2, users); + expect(query2.toSQL().sql).toEqual('delete from "users"'); db.$primary.delete({} as any); @@ -515,17 +450,20 @@ describe('[insert] replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'insert'); const spyRead2 = vi.spyOn(read2, 'insert'); - db.insert({} as any); + const query = db.insert(users).values({ id: 1 }); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenCalledWith(users); + expect(query.toSQL().sql).toEqual('insert into "users" ("id") values ($1)'); - db.insert({} as any); + db.insert(users); expect(spyPrimary).toHaveBeenCalledTimes(2); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenNthCalledWith(2, users); db.$primary.insert({} as any); @@ -547,27 +485,29 @@ describe('[execute] replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'execute'); const spyRead2 = vi.spyOn(read2, 'execute'); - // expect(db.execute(sql``)).rejects.toThrow(); + expect(db.execute(sql``)).rejects.toThrow(); - try { - db.execute(sql``); - } catch { /* empty */ } + // try { + // db.execute(sql``); + // } catch { /* empty */ } expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); - try { - db.execute(sql``); - } catch { /* empty */ } + expect(db.execute(sql``)).rejects.toThrow(); + // try { + // db.execute(sql``); + // } catch { /* empty */ } expect(spyPrimary).toHaveBeenCalledTimes(2); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); - try { - db.execute(sql``); - } catch { /* empty */ } + expect(db.execute(sql``)).rejects.toThrow(); + // try { + // db.execute(sql``); + // } catch { /* empty */ } expect(spyPrimary).toHaveBeenCalledTimes(3); expect(spyRead1).toHaveBeenCalledTimes(0); @@ -586,22 +526,27 @@ describe('[transaction] replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb, 'transaction'); const spyRead1 = vi.spyOn(read1, 'transaction'); const spyRead2 = vi.spyOn(read2, 'transaction'); - - expect(db.transaction(async (tx) => { + const txFn1 = async (tx: any) => { tx.select().from({} as any); - })).rejects.toThrow(); + }; + + expect(db.transaction(txFn1)).rejects.toThrow(); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenCalledWith(txFn1); - expect(db.transaction(async (tx) => { + const txFn2 = async (tx: any) => { tx.select().from({} as any); - })).rejects.toThrow(); + }; + + expect(db.transaction(txFn2)).rejects.toThrow(); expect(spyPrimary).toHaveBeenCalledTimes(2); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenNthCalledWith(2, txFn2); expect(db.transaction(async (tx) => { tx.select().from({} as any); @@ -613,38 +558,6 @@ describe('[transaction] replicas postgres', () => { }); }); -describe('[refreshView] replicas postgres', () => { - it('primary refreshView', () => { - const primaryDb = drizzle({} as any); - const read1 = drizzle({} as any); - const read2 = drizzle({} as any); - - const db = withReplicas(primaryDb, [read1, read2]); - - const spyPrimary = vi.spyOn(primaryDb, 'refreshMaterializedView'); - const spyRead1 = vi.spyOn(read1, 'refreshMaterializedView'); - const spyRead2 = vi.spyOn(read2, 'refreshMaterializedView'); - - db.refreshMaterializedView({} as any); - - expect(spyPrimary).toHaveBeenCalledTimes(1); - expect(spyRead1).toHaveBeenCalledTimes(0); - expect(spyRead2).toHaveBeenCalledTimes(0); - - db.refreshMaterializedView({} as any); - - expect(spyPrimary).toHaveBeenCalledTimes(2); - expect(spyRead1).toHaveBeenCalledTimes(0); - expect(spyRead2).toHaveBeenCalledTimes(0); - - db.$primary.refreshMaterializedView({} as any); - - expect(spyPrimary).toHaveBeenCalledTimes(3); - expect(spyRead1).toHaveBeenCalledTimes(0); - expect(spyRead2).toHaveBeenCalledTimes(0); - }); -}); - describe('[findFirst] read replicas postgres', () => { it('primary findFirst', () => { const primaryDb = drizzle({} as any, { schema: { usersTable } }); @@ -656,19 +569,21 @@ describe('[findFirst] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findFirst'); const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findFirst'); const spyRead2 = vi.spyOn(read2['query']['usersTable'], 'findFirst'); + const obj = {} as any; - db.$primary.query.usersTable.findFirst(); + db.$primary.query.usersTable.findFirst(obj); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenCalledWith(obj); }); it('random replica findFirst', () => { const primaryDb = drizzle({} as any, { schema: { usersTable } }); const read1 = drizzle({} as any, { schema: { usersTable } }); const read2 = drizzle({} as any, { schema: { usersTable } }); - + const randomMockReplica = vi.fn().mockReturnValueOnce(read1).mockReturnValueOnce(read2); const db = withReplicas(primaryDb, [read1, read2], () => { @@ -678,16 +593,21 @@ describe('[findFirst] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findFirst'); const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findFirst'); const spyRead2 = vi.spyOn(read2['query']['usersTable'], 'findFirst'); + const par1 = {} as any; - db.query.usersTable.findFirst(); + db.query.usersTable.findFirst(par1); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledWith(par1); - db.query.usersTable.findFirst(); + const query = db.query.usersTable.findFirst(); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(1); + expect(query.toSQL().sql).toEqual( + 'select "id", "name", "verified", "jsonb", "created_at" from "users" "usersTable" limit $1', + ); }); it('single read replica findFirst', () => { @@ -711,7 +631,7 @@ describe('[findFirst] read replicas postgres', () => { it('single read replica findFirst + primary findFirst', () => { const primaryDb = drizzle({} as any, { schema: { usersTable } }); const read1 = drizzle({} as any, { schema: { usersTable } }); - + const db = withReplicas(primaryDb, [read1]); const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findFirst'); @@ -763,19 +683,24 @@ describe('[findMany] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findMany'); const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findMany'); const spyRead2 = vi.spyOn(read2['query']['usersTable'], 'findMany'); + const obj = {} as any; - db.$primary.query.usersTable.findMany(); + const query = db.$primary.query.usersTable.findMany(obj); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenCalledWith(obj); + expect(query.toSQL().sql).toEqual( + 'select "id", "name", "verified", "jsonb", "created_at" from "users" "usersTable"', + ); }); it('random replica findMany', () => { const primaryDb = drizzle({} as any, { schema: { usersTable } }); const read1 = drizzle({} as any, { schema: { usersTable } }); const read2 = drizzle({} as any, { schema: { usersTable } }); - + const randomMockReplica = vi.fn().mockReturnValueOnce(read1).mockReturnValueOnce(read2); const db = withReplicas(primaryDb, [read1, read2], () => { @@ -785,16 +710,27 @@ describe('[findMany] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findMany'); const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findMany'); const spyRead2 = vi.spyOn(read2['query']['usersTable'], 'findMany'); + const obj1 = {} as any; + const obj2 = {} as any; - db.query.usersTable.findMany(); + const query1 = db.query.usersTable.findMany(obj1); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query1.toSQL().sql).toEqual( + 'select "id", "name", "verified", "jsonb", "created_at" from "users" "usersTable"', + ); + expect(spyRead1).toHaveBeenCalledWith(obj1); + + const query2 = db.query.usersTable.findMany(obj2); - db.query.usersTable.findMany(); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(1); + expect(query2.toSQL().sql).toEqual( + 'select "id", "name", "verified", "jsonb", "created_at" from "users" "usersTable"', + ); + expect(spyRead2).toHaveBeenCalledWith(obj2); }); it('single read replica findMany', () => { @@ -805,33 +741,54 @@ describe('[findMany] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findMany'); const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findMany'); + const obj1 = {} as any; + const obj2 = {} as any; - db.query.usersTable.findMany(); + const query1 = db.query.usersTable.findMany(obj1); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); + expect(spyRead1).toHaveBeenCalledWith(obj1); + expect(query1.toSQL().sql).toEqual( + 'select "id", "name", "verified", "jsonb", "created_at" from "users" "usersTable"', + ); - db.query.usersTable.findMany(); + const query2 = db.query.usersTable.findMany(obj2); expect(spyRead1).toHaveBeenCalledTimes(2); + expect(spyRead1).toHaveBeenNthCalledWith(2, obj2); + expect(query2.toSQL().sql).toEqual( + 'select "id", "name", "verified", "jsonb", "created_at" from "users" "usersTable"', + ); }); it('single read replica findMany + primary findMany', () => { const primaryDb = drizzle({} as any, { schema: { usersTable } }); const read1 = drizzle({} as any, { schema: { usersTable } }); - + const db = withReplicas(primaryDb, [read1]); const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findMany'); const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findMany'); + const obj1 = {} as any; + const obj2 = {} as any; - db.query.usersTable.findMany(); + const query1 = db.query.usersTable.findMany(obj1); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); + expect(spyRead1).toHaveBeenCalledWith(obj1); + expect(query1.toSQL().sql).toEqual( + 'select "id", "name", "verified", "jsonb", "created_at" from "users" "usersTable"', + ); + + const query2 = db.$primary.query.usersTable.findMany(obj2); - db.$primary.query.usersTable.findMany(); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(1); + expect(spyPrimary).toHaveBeenNthCalledWith(1, obj2); + expect(query2.toSQL().sql).toEqual( + 'select "id", "name", "verified", "jsonb", "created_at" from "users" "usersTable"', + ); }); it('always first read findMany', () => { @@ -846,15 +803,25 @@ describe('[findMany] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findMany'); const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findMany'); const spyRead2 = vi.spyOn(read2['query']['usersTable'], 'findMany'); + const obj1 = {} as any; + const obj2 = {} as any; - db.query.usersTable.findMany(); + const query1 = db.query.usersTable.findMany(obj1); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledWith(obj1); + expect(query1.toSQL().sql).toEqual( + 'select "id", "name", "verified", "jsonb", "created_at" from "users" "usersTable"', + ); - db.query.usersTable.findMany(); + const query2 = db.query.usersTable.findMany(obj2); expect(spyRead1).toHaveBeenCalledTimes(2); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenNthCalledWith(2, obj2); + expect(query2.toSQL().sql).toEqual( + 'select "id", "name", "verified", "jsonb", "created_at" from "users" "usersTable"', + ); }); }); diff --git a/integration-tests/tests/replicas/sqlite.test.ts b/integration-tests/tests/replicas/sqlite.test.ts index b607ce1a0..7f00dbd1a 100644 --- a/integration-tests/tests/replicas/sqlite.test.ts +++ b/integration-tests/tests/replicas/sqlite.test.ts @@ -1,14 +1,19 @@ import { sql } from 'drizzle-orm'; -import { sqliteTable, int, text, withReplicas } from 'drizzle-orm/sqlite-core'; import { drizzle } from 'drizzle-orm/libsql'; +import { int, sqliteTable, text, withReplicas } from 'drizzle-orm/sqlite-core'; import { describe, expect, it, vi } from 'vitest'; const usersTable = sqliteTable('users', { id: int('id' as string).primaryKey(), name: text('name').notNull(), + verified: text('verified').notNull().default('true'), }); -describe('[select] read replicas postgres', () => { +const users = sqliteTable('users', { + id: int('id' as string).primaryKey(), +}); + +describe('[select] read replicas sqlite', () => { it('primary select', () => { const primaryDb = drizzle({} as any); const read1 = drizzle({} as any); @@ -20,9 +25,11 @@ describe('[select] read replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'select'); const spyRead2 = vi.spyOn(read2, 'select'); - db.$primary.select().from({} as any); + const query = db.$primary.select().from(users); expect(spyPrimary).toHaveBeenCalledTimes(1); + expect(query.toSQL().sql).toEqual('select "id" from "users"'); + expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); }); @@ -42,15 +49,18 @@ describe('[select] read replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'select'); const spyRead2 = vi.spyOn(read2, 'select'); - db.select().from({} as any); + const query1 = db.select({ count: sql`count(*)`.as('count') }).from(users).limit(1); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(0); - db.select().from({} as any); + expect(query1.toSQL().sql).toEqual('select count(*) as "count" from "users" limit ?'); + + const query2 = db.select().from(users); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(1); + expect(query2.toSQL().sql).toEqual('select "id" from "users"'); }); it('single read replica select', () => { @@ -62,13 +72,15 @@ describe('[select] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb, 'select'); const spyRead1 = vi.spyOn(read1, 'select'); - db.select().from({} as any); + const query1 = db.select().from(users); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); + expect(query1.toSQL().sql).toEqual('select "id" from "users"'); - db.select().from({} as any); + const query2 = db.select().from(users); expect(spyRead1).toHaveBeenCalledTimes(2); + expect(query2.toSQL().sql).toEqual('select "id" from "users"'); }); it('single read replica select + primary select', () => { @@ -80,14 +92,16 @@ describe('[select] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb, 'select'); const spyRead1 = vi.spyOn(read1, 'select'); - db.select().from({} as any); + const query1 = db.select({ id: users.id }).from(users); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); + expect(query1.toSQL().sql).toEqual('select "id" from "users"'); - db.$primary.select().from({} as any); + const query2 = db.$primary.select().from(users); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(1); + expect(query2.toSQL().sql).toEqual('select "id" from "users"'); }); it('always first read select', () => { @@ -103,19 +117,22 @@ describe('[select] read replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'select'); const spyRead2 = vi.spyOn(read2, 'select'); - db.select().from({} as any); + const query1 = db.select().from(users); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query1.toSQL().sql).toEqual('select "id" from "users"'); + + const query2 = db.select().from(users); - db.select().from({} as any); expect(spyRead1).toHaveBeenCalledTimes(2); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query2.toSQL().sql).toEqual('select "id" from "users"'); }); }); -describe('[selectDistinct] read replicas postgres', () => { +describe('[selectDistinct] read replicas sqlite', () => { it('primary selectDistinct', () => { const primaryDb = drizzle({} as any); const read1 = drizzle({} as any); @@ -127,11 +144,12 @@ describe('[selectDistinct] read replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'selectDistinct'); const spyRead2 = vi.spyOn(read2, 'selectDistinct'); - db.$primary.selectDistinct().from({} as any); + const query = db.$primary.selectDistinct().from(users); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query.toSQL().sql).toEqual('select distinct "id" from "users"'); }); it('random replica selectDistinct', () => { @@ -149,15 +167,17 @@ describe('[selectDistinct] read replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'selectDistinct'); const spyRead2 = vi.spyOn(read2, 'selectDistinct'); - db.selectDistinct().from({} as any); + const query1 = db.selectDistinct().from(users); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query1.toSQL().sql).toEqual('select distinct "id" from "users"'); - db.selectDistinct().from({} as any); + const query2 = db.selectDistinct().from(users); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(1); + expect(query2.toSQL().sql).toEqual('select distinct "id" from "users"'); }); it('single read replica selectDistinct', () => { @@ -169,13 +189,15 @@ describe('[selectDistinct] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb, 'selectDistinct'); const spyRead1 = vi.spyOn(read1, 'selectDistinct'); - db.selectDistinct().from({} as any); + const query1 = db.selectDistinct().from(users); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); + expect(query1.toSQL().sql).toEqual('select distinct "id" from "users"'); - db.selectDistinct().from({} as any); + const query2 = db.selectDistinct().from(users); expect(spyRead1).toHaveBeenCalledTimes(2); + expect(query2.toSQL().sql).toEqual('select distinct "id" from "users"'); }); it('single read replica selectDistinct + primary selectDistinct', () => { @@ -187,14 +209,16 @@ describe('[selectDistinct] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb, 'selectDistinct'); const spyRead1 = vi.spyOn(read1, 'selectDistinct'); - db.selectDistinct().from({} as any); + const query1 = db.selectDistinct().from(users); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); + expect(query1.toSQL().sql).toEqual('select distinct "id" from "users"'); - db.$primary.selectDistinct().from({} as any); + const query2 = db.$primary.selectDistinct().from(users); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(1); + expect(query2.toSQL().sql).toEqual('select distinct "id" from "users"'); }); it('always first read selectDistinct', () => { @@ -210,19 +234,21 @@ describe('[selectDistinct] read replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'selectDistinct'); const spyRead2 = vi.spyOn(read2, 'selectDistinct'); - db.selectDistinct().from({} as any); + const query1 = db.selectDistinct().from(users); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query1.toSQL().sql).toEqual('select distinct "id" from "users"'); - db.selectDistinct().from({} as any); + const query2 = db.selectDistinct().from(users); expect(spyRead1).toHaveBeenCalledTimes(2); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query2.toSQL().sql).toEqual('select distinct "id" from "users"'); }); }); -describe('[with] read replicas postgres', () => { +describe('[with] read replicas sqlite', () => { it('primary with', () => { const primaryDb = drizzle({} as any); const read1 = drizzle({} as any); @@ -233,12 +259,17 @@ describe('[with] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb, 'with'); const spyRead1 = vi.spyOn(read1, 'with'); const spyRead2 = vi.spyOn(read2, 'with'); + const obj1 = {} as any; + const obj2 = {} as any; + const obj3 = {} as any; + const obj4 = {} as any; - db.$primary.with(); + db.$primary.with(obj1, obj2, obj3, obj4); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenCalledWith(obj1, obj2, obj3, obj4); }); it('random replica with', () => { @@ -316,20 +347,25 @@ describe('[with] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb, 'with'); const spyRead1 = vi.spyOn(read1, 'with'); const spyRead2 = vi.spyOn(read2, 'with'); + const obj1 = {} as any; + const obj2 = {} as any; + const obj3 = {} as any; - db.with(); + db.with(obj1); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledWith(obj1); - db.with(); + db.with(obj2, obj3); expect(spyRead1).toHaveBeenCalledTimes(2); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledWith(obj2, obj3); }); }); -describe('[update] replicas postgres', () => { +describe('[update] replicas sqlite', () => { it('primary update', () => { const primaryDb = drizzle({} as any); const read1 = drizzle({} as any); @@ -341,27 +377,30 @@ describe('[update] replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'update'); const spyRead2 = vi.spyOn(read2, 'update'); - db.update({} as any); + const query1 = db.update(users).set({ id: 1 }); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query1.toSQL().sql).toEqual('update "users" set "id" = ?'); - db.update({} as any); + const query2 = db.update(users).set({ id: 1 }); expect(spyPrimary).toHaveBeenCalledTimes(2); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query2.toSQL().sql).toEqual('update "users" set "id" = ?'); - db.$primary.update({} as any); + const query3 = db.$primary.update(users).set({ id: 1 }); expect(spyPrimary).toHaveBeenCalledTimes(3); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query3.toSQL().sql).toEqual('update "users" set "id" = ?'); }); }); -describe('[delete] replicas postgres', () => { +describe('[delete] replicas sqlite', () => { it('primary delete', () => { const primaryDb = drizzle({} as any); const read1 = drizzle({} as any); @@ -373,17 +412,21 @@ describe('[delete] replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'delete'); const spyRead2 = vi.spyOn(read2, 'delete'); - db.delete({} as any); + const query1 = db.delete(users); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenCalledWith(users); + expect(query1.toSQL().sql).toEqual('delete from "users"'); - db.delete({} as any); + const query2 = db.delete(users); expect(spyPrimary).toHaveBeenCalledTimes(2); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenNthCalledWith(2, users); + expect(query2.toSQL().sql).toEqual('delete from "users"'); db.$primary.delete({} as any); @@ -393,7 +436,7 @@ describe('[delete] replicas postgres', () => { }); }); -describe('[insert] replicas postgres', () => { +describe('[insert] replicas sqlite', () => { it('primary insert', () => { const primaryDb = drizzle({} as any); const read1 = drizzle({} as any); @@ -405,17 +448,20 @@ describe('[insert] replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'insert'); const spyRead2 = vi.spyOn(read2, 'insert'); - db.insert({} as any); + const query = db.insert(users).values({ id: 1 }); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenCalledWith(users); + expect(query.toSQL().sql).toEqual('insert into "users" ("id") values (?)'); - db.insert({} as any); + db.insert(users); expect(spyPrimary).toHaveBeenCalledTimes(2); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenNthCalledWith(2, users); db.$primary.insert({} as any); @@ -425,7 +471,7 @@ describe('[insert] replicas postgres', () => { }); }); -describe('[execute] replicas postgres', () => { +describe('[execute] replicas sqlite', () => { it('primary execute', async () => { const primaryDb = drizzle({} as any); const read1 = drizzle({} as any); @@ -437,27 +483,25 @@ describe('[execute] replicas postgres', () => { const spyRead1 = vi.spyOn(read1, 'all'); const spyRead2 = vi.spyOn(read2, 'all'); - // expect(db.execute(sql``)).rejects.toThrow(); - - try { - db.all(sql``); - } catch { /* empty */ } + expect(db.all(sql``)).rejects.toThrow(); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); - try { - db.all(sql``); - } catch { /* empty */ } + expect(db.all(sql``)).rejects.toThrow(); + // try { + // db.execute(sql``); + // } catch { /* empty */ } expect(spyPrimary).toHaveBeenCalledTimes(2); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); - try { - db.all(sql``); - } catch { /* empty */ } + expect(db.all(sql``)).rejects.toThrow(); + // try { + // db.execute(sql``); + // } catch { /* empty */ } expect(spyPrimary).toHaveBeenCalledTimes(3); expect(spyRead1).toHaveBeenCalledTimes(0); @@ -465,7 +509,7 @@ describe('[execute] replicas postgres', () => { }); }); -describe('[transaction] replicas postgres', () => { +describe('[transaction] replicas sqlite', () => { it('primary transaction', async () => { const primaryDb = drizzle({} as any); const read1 = drizzle({} as any); @@ -476,22 +520,27 @@ describe('[transaction] replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb, 'transaction'); const spyRead1 = vi.spyOn(read1, 'transaction'); const spyRead2 = vi.spyOn(read2, 'transaction'); - - expect(db.transaction(async (tx) => { + const txFn1 = async (tx: any) => { tx.select().from({} as any); - })).rejects.toThrow(); + }; + + expect(db.transaction(txFn1)).rejects.toThrow(); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenCalledWith(txFn1); - expect(db.transaction(async (tx) => { + const txFn2 = async (tx: any) => { tx.select().from({} as any); - })).rejects.toThrow(); + }; + + expect(db.transaction(txFn2)).rejects.toThrow(); expect(spyPrimary).toHaveBeenCalledTimes(2); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenNthCalledWith(2, txFn2); expect(db.transaction(async (tx) => { tx.select().from({} as any); @@ -503,7 +552,7 @@ describe('[transaction] replicas postgres', () => { }); }); -describe('[findFirst] read replicas postgres', () => { +describe('[findFirst] read replicas sqlite', () => { it('primary findFirst', () => { const primaryDb = drizzle({} as any, { schema: { usersTable } }); const read1 = drizzle({} as any, { schema: { usersTable } }); @@ -514,19 +563,21 @@ describe('[findFirst] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findFirst'); const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findFirst'); const spyRead2 = vi.spyOn(read2['query']['usersTable'], 'findFirst'); + const obj = {} as any; - db.$primary.query.usersTable.findFirst(); + db.$primary.query.usersTable.findFirst(obj); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenCalledWith(obj); }); it('random replica findFirst', () => { const primaryDb = drizzle({} as any, { schema: { usersTable } }); const read1 = drizzle({} as any, { schema: { usersTable } }); const read2 = drizzle({} as any, { schema: { usersTable } }); - + const randomMockReplica = vi.fn().mockReturnValueOnce(read1).mockReturnValueOnce(read2); const db = withReplicas(primaryDb, [read1, read2], () => { @@ -536,16 +587,19 @@ describe('[findFirst] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findFirst'); const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findFirst'); const spyRead2 = vi.spyOn(read2['query']['usersTable'], 'findFirst'); + const par1 = {} as any; - db.query.usersTable.findFirst(); + db.query.usersTable.findFirst(par1); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledWith(par1); - db.query.usersTable.findFirst(); + const query = db.query.usersTable.findFirst(); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(1); + expect(query.toSQL().sql).toEqual('select "id", "name", "verified" from "users" "usersTable" limit ?'); }); it('single read replica findFirst', () => { @@ -569,7 +623,7 @@ describe('[findFirst] read replicas postgres', () => { it('single read replica findFirst + primary findFirst', () => { const primaryDb = drizzle({} as any, { schema: { usersTable } }); const read1 = drizzle({} as any, { schema: { usersTable } }); - + const db = withReplicas(primaryDb, [read1]); const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findFirst'); @@ -610,7 +664,7 @@ describe('[findFirst] read replicas postgres', () => { }); }); -describe('[findMany] read replicas postgres', () => { +describe('[findMany] read replicas sqlite', () => { it('primary findMany', () => { const primaryDb = drizzle({} as any, { schema: { usersTable } }); const read1 = drizzle({} as any, { schema: { usersTable } }); @@ -621,19 +675,22 @@ describe('[findMany] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findMany'); const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findMany'); const spyRead2 = vi.spyOn(read2['query']['usersTable'], 'findMany'); + const obj = {} as any; - db.$primary.query.usersTable.findMany(); + const query = db.$primary.query.usersTable.findMany(obj); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(0); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyPrimary).toHaveBeenCalledWith(obj); + expect(query.toSQL().sql).toEqual('select "id", "name", "verified" from "users" "usersTable"'); }); it('random replica findMany', () => { const primaryDb = drizzle({} as any, { schema: { usersTable } }); const read1 = drizzle({} as any, { schema: { usersTable } }); const read2 = drizzle({} as any, { schema: { usersTable } }); - + const randomMockReplica = vi.fn().mockReturnValueOnce(read1).mockReturnValueOnce(read2); const db = withReplicas(primaryDb, [read1, read2], () => { @@ -643,16 +700,23 @@ describe('[findMany] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findMany'); const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findMany'); const spyRead2 = vi.spyOn(read2['query']['usersTable'], 'findMany'); + const obj1 = {} as any; + const obj2 = {} as any; - db.query.usersTable.findMany(); + const query1 = db.query.usersTable.findMany(obj1); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(query1.toSQL().sql).toEqual('select "id", "name", "verified" from "users" "usersTable"'); + expect(spyRead1).toHaveBeenCalledWith(obj1); + + const query2 = db.query.usersTable.findMany(obj2); - db.query.usersTable.findMany(); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(1); + expect(query2.toSQL().sql).toEqual('select "id", "name", "verified" from "users" "usersTable"'); + expect(spyRead2).toHaveBeenCalledWith(obj2); }); it('single read replica findMany', () => { @@ -663,33 +727,46 @@ describe('[findMany] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findMany'); const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findMany'); + const obj1 = {} as any; + const obj2 = {} as any; - db.query.usersTable.findMany(); + const query1 = db.query.usersTable.findMany(obj1); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); + expect(spyRead1).toHaveBeenCalledWith(obj1); + expect(query1.toSQL().sql).toEqual('select "id", "name", "verified" from "users" "usersTable"'); - db.query.usersTable.findMany(); + const query2 = db.query.usersTable.findMany(obj2); expect(spyRead1).toHaveBeenCalledTimes(2); + expect(spyRead1).toHaveBeenNthCalledWith(2, obj2); + expect(query2.toSQL().sql).toEqual('select "id", "name", "verified" from "users" "usersTable"'); }); it('single read replica findMany + primary findMany', () => { const primaryDb = drizzle({} as any, { schema: { usersTable } }); const read1 = drizzle({} as any, { schema: { usersTable } }); - + const db = withReplicas(primaryDb, [read1]); const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findMany'); const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findMany'); + const obj1 = {} as any; + const obj2 = {} as any; - db.query.usersTable.findMany(); + const query1 = db.query.usersTable.findMany(obj1); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); + expect(spyRead1).toHaveBeenCalledWith(obj1); + expect(query1.toSQL().sql).toEqual('select "id", "name", "verified" from "users" "usersTable"'); + + const query2 = db.$primary.query.usersTable.findMany(obj2); - db.$primary.query.usersTable.findMany(); expect(spyPrimary).toHaveBeenCalledTimes(1); expect(spyRead1).toHaveBeenCalledTimes(1); + expect(spyPrimary).toHaveBeenNthCalledWith(1, obj2); + expect(query2.toSQL().sql).toEqual('select "id", "name", "verified" from "users" "usersTable"'); }); it('always first read findMany', () => { @@ -704,15 +781,22 @@ describe('[findMany] read replicas postgres', () => { const spyPrimary = vi.spyOn(primaryDb['query']['usersTable'], 'findMany'); const spyRead1 = vi.spyOn(read1['query']['usersTable'], 'findMany'); const spyRead2 = vi.spyOn(read2['query']['usersTable'], 'findMany'); + const obj1 = {} as any; + const obj2 = {} as any; - db.query.usersTable.findMany(); + const query1 = db.query.usersTable.findMany(obj1); expect(spyPrimary).toHaveBeenCalledTimes(0); expect(spyRead1).toHaveBeenCalledTimes(1); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenCalledWith(obj1); + expect(query1.toSQL().sql).toEqual('select "id", "name", "verified" from "users" "usersTable"'); - db.query.usersTable.findMany(); + const query2 = db.query.usersTable.findMany(obj2); expect(spyRead1).toHaveBeenCalledTimes(2); expect(spyRead2).toHaveBeenCalledTimes(0); + expect(spyRead1).toHaveBeenNthCalledWith(2, obj2); + expect(query2.toSQL().sql).toEqual('select "id", "name", "verified" from "users" "usersTable"'); }); -}); \ No newline at end of file +}); + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 15af08a5e..0c6406ebb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -284,6 +284,30 @@ importers: specifier: ^7.2.2 version: 7.2.2 + eslint-plugin-drizzle: + devDependencies: + '@typescript-eslint/parser': + specifier: ^6.10.0 + version: 6.10.0(eslint@8.53.0)(typescript@5.2.2) + '@typescript-eslint/rule-tester': + specifier: ^6.10.0 + version: 6.10.0(@eslint/eslintrc@2.1.3)(eslint@8.53.0)(typescript@5.2.2) + '@typescript-eslint/utils': + specifier: ^6.10.0 + version: 6.10.0(eslint@8.53.0)(typescript@5.2.2) + cpy-cli: + specifier: ^5.0.0 + version: 5.0.0 + eslint: + specifier: ^8.53.0 + version: 8.53.0 + typescript: + specifier: ^5.2.2 + version: 5.2.2(patch_hash=wmhs4olj6eveeldp6si4l46ssq) + vitest: + specifier: ^0.34.6 + version: 0.34.6 + integration-tests: dependencies: '@aws-sdk/client-rds-data': @@ -2106,6 +2130,16 @@ packages: eslint-visitor-keys: 3.4.3 dev: true + /@eslint-community/eslint-utils@4.4.0(eslint@8.53.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.53.0 + eslint-visitor-keys: 3.4.3 + dev: true + /@eslint-community/regexpp@4.9.0: resolution: {integrity: sha512-zJmuCWj2VLBt4c25CfBIbMZLGLyhkvs7LznyVX5HfpzeocThgIj5XQK4L+g3U36mMcx8bPMhGyPpwCATamC4jQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -2128,11 +2162,33 @@ packages: - supports-color dev: true + /@eslint/eslintrc@2.1.3: + resolution: {integrity: sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4 + espree: 9.6.1 + globals: 13.22.0 + ignore: 5.2.4 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + /@eslint/js@8.50.0: resolution: {integrity: sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@eslint/js@8.53.0: + resolution: {integrity: sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + /@gar/promisify@1.1.3: resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} requiresBuild: true @@ -2149,6 +2205,17 @@ packages: - supports-color dev: true + /@humanwhocodes/config-array@0.11.13: + resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 2.0.1 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + /@humanwhocodes/module-importer@1.0.1: resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} @@ -2158,6 +2225,10 @@ packages: resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} dev: true + /@humanwhocodes/object-schema@2.0.1: + resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} + dev: true + /@iarna/toml@2.2.5: resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} dev: false @@ -2174,6 +2245,13 @@ packages: wrap-ansi-cjs: /wrap-ansi@7.0.0 dev: true + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + dev: true + /@jridgewell/gen-mapping@0.3.3: resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} engines: {node: '>=6.0.0'} @@ -2487,6 +2565,10 @@ packages: rollup: 3.27.2 dev: true + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: true + /@sinclair/typebox@0.29.6: resolution: {integrity: sha512-aX5IFYWlMa7tQ8xZr3b2gtVReCvg7f3LEhjir/JAjX2bJCMVJA5tIPv30wTD4KDfcwMd7DDYY3hFDeGmOgtrZQ==} dev: true @@ -2611,10 +2693,6 @@ packages: '@types/node': 20.8.7 dev: true - /@types/json-schema@7.0.12: - resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} - dev: true - /@types/json-schema@7.0.13: resolution: {integrity: sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==} dev: true @@ -2690,10 +2768,6 @@ packages: resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} dev: true - /@types/semver@7.5.0: - resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==} - dev: true - /@types/semver@7.5.3: resolution: {integrity: sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==} dev: true @@ -2773,6 +2847,27 @@ packages: - typescript dev: true + /@typescript-eslint/parser@6.10.0(eslint@8.53.0)(typescript@5.2.2): + resolution: {integrity: sha512-+sZwIj+s+io9ozSxIWbNB5873OSdfeBEH/FR0re14WLI6BaKuSOnnwCJ2foUiu8uXf4dRp1UqHP0vrZ1zXGrog==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 6.10.0 + '@typescript-eslint/types': 6.10.0 + '@typescript-eslint/typescript-estree': 6.10.0(typescript@5.2.2) + '@typescript-eslint/visitor-keys': 6.10.0 + debug: 4.3.4 + eslint: 8.53.0 + typescript: 5.2.2(patch_hash=wmhs4olj6eveeldp6si4l46ssq) + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/parser@6.7.3(eslint@8.50.0)(typescript@5.2.2): resolution: {integrity: sha512-TlutE+iep2o7R8Lf+yoer3zU6/0EAUc8QIBB3GYBc1KGz4c4TRm83xwXUZVPlZ6YCLss4r77jbu6j3sendJoiQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -2794,6 +2889,25 @@ packages: - supports-color dev: true + /@typescript-eslint/rule-tester@6.10.0(@eslint/eslintrc@2.1.3)(eslint@8.53.0)(typescript@5.2.2): + resolution: {integrity: sha512-I0ZY+9ei73dlOuXwIYWsn/r/ue26Ygf4yEJPxeJRPI06YWDawmR1FI1dXL6ChAWVrmBQRvWep/1PxnV41zfcMA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@eslint/eslintrc': '>=2' + eslint: '>=8' + dependencies: + '@eslint/eslintrc': 2.1.3 + '@typescript-eslint/typescript-estree': 6.10.0(typescript@5.2.2) + '@typescript-eslint/utils': 6.10.0(eslint@8.53.0)(typescript@5.2.2) + ajv: 6.12.6 + eslint: 8.53.0 + lodash.merge: 4.6.2 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /@typescript-eslint/scope-manager@5.62.0: resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2802,6 +2916,14 @@ packages: '@typescript-eslint/visitor-keys': 5.62.0 dev: true + /@typescript-eslint/scope-manager@6.10.0: + resolution: {integrity: sha512-TN/plV7dzqqC2iPNf1KrxozDgZs53Gfgg5ZHyw8erd6jd5Ta/JIEcdCheXFt9b1NYb93a1wmIIVW/2gLkombDg==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.10.0 + '@typescript-eslint/visitor-keys': 6.10.0 + dev: true + /@typescript-eslint/scope-manager@6.7.3: resolution: {integrity: sha512-wOlo0QnEou9cHO2TdkJmzF7DFGvAKEnB82PuPNHpT8ZKKaZu6Bm63ugOTn9fXNJtvuDPanBc78lGUGGytJoVzQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -2835,6 +2957,11 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@typescript-eslint/types@6.10.0: + resolution: {integrity: sha512-36Fq1PWh9dusgo3vH7qmQAj5/AZqARky1Wi6WpINxB6SkQdY5vQoT2/7rW7uBIsPDcvvGCLi4r10p0OJ7ITAeg==} + engines: {node: ^16.0.0 || >=18.0.0} + dev: true + /@typescript-eslint/types@6.7.3: resolution: {integrity: sha512-4g+de6roB2NFcfkZb439tigpAMnvEIg3rIjWQ+EM7IBaYt/CdJt6em9BJ4h4UpdgaBWdmx2iWsafHTrqmgIPNw==} engines: {node: ^16.0.0 || >=18.0.0} @@ -2861,6 +2988,27 @@ packages: - supports-color dev: true + /@typescript-eslint/typescript-estree@6.10.0(typescript@5.2.2): + resolution: {integrity: sha512-ek0Eyuy6P15LJVeghbWhSrBCj/vJpPXXR+EpaRZqou7achUWL8IdYnMSC5WHAeTWswYQuP2hAZgij/bC9fanBg==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.10.0 + '@typescript-eslint/visitor-keys': 6.10.0 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@5.2.2) + typescript: 5.2.2(patch_hash=wmhs4olj6eveeldp6si4l46ssq) + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/typescript-estree@6.7.3(typescript@5.2.2): resolution: {integrity: sha512-YLQ3tJoS4VxLFYHTw21oe1/vIZPRqAO91z6Uv0Ss2BKm/Ag7/RVQBcXTGcXhgJMdA4U+HrKuY5gWlJlvoaKZ5g==} engines: {node: ^16.0.0 || >=18.0.0} @@ -2889,8 +3037,8 @@ packages: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.50.0) - '@types/json-schema': 7.0.12 - '@types/semver': 7.5.0 + '@types/json-schema': 7.0.13 + '@types/semver': 7.5.3 '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.2.2) @@ -2902,6 +3050,25 @@ packages: - typescript dev: true + /@typescript-eslint/utils@6.10.0(eslint@8.53.0)(typescript@5.2.2): + resolution: {integrity: sha512-v+pJ1/RcVyRc0o4wAGux9x42RHmAjIGzPRo538Z8M1tVx6HOnoQBCX/NoadHQlZeC+QO2yr4nNSFWOoraZCAyg==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) + '@types/json-schema': 7.0.13 + '@types/semver': 7.5.3 + '@typescript-eslint/scope-manager': 6.10.0 + '@typescript-eslint/types': 6.10.0 + '@typescript-eslint/typescript-estree': 6.10.0(typescript@5.2.2) + eslint: 8.53.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /@typescript-eslint/utils@6.7.3(eslint@8.50.0)(typescript@5.2.2): resolution: {integrity: sha512-vzLkVder21GpWRrmSR9JxGZ5+ibIUSudXlW52qeKpzUEQhRSmyZiVDDj3crAth7+5tmN1ulvgKaCU2f/bPRCzg==} engines: {node: ^16.0.0 || >=18.0.0} @@ -2929,6 +3096,14 @@ packages: eslint-visitor-keys: 3.4.3 dev: true + /@typescript-eslint/visitor-keys@6.10.0: + resolution: {integrity: sha512-xMGluxQIEtOM7bqFCo+rCMh5fqI+ZxV5RUUOa29iVPz1OgCZrtc7rFnz5cLUazlkPKYqX+75iuDq7m0HQ48nCg==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.10.0 + eslint-visitor-keys: 3.4.3 + dev: true + /@typescript-eslint/visitor-keys@6.7.3: resolution: {integrity: sha512-HEVXkU9IB+nk9o63CeICMHxFWbHWr3E1mpilIQBe9+7L/lH97rleFLVtYsfnWB+JVMaiFnEaxvknvmIzX+CqVg==} engines: {node: ^16.0.0 || >=18.0.0} @@ -2951,6 +3126,10 @@ packages: yargs: 16.2.0 dev: false + /@ungap/structured-clone@1.2.0: + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + dev: true + /@vercel/postgres@0.3.0: resolution: {integrity: sha512-cOC+x6qMnN54B4y0Fh0DV5LJQp2M7puIKbehQBMutY/8/zpzh+oKaQmnZb2QHn489MGOQKyRLJLgHa2P8M085Q==} engines: {node: '>=14.6'} @@ -2967,6 +3146,14 @@ packages: '@vitest/utils': 0.31.4 chai: 4.3.7 + /@vitest/expect@0.34.6: + resolution: {integrity: sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==} + dependencies: + '@vitest/spy': 0.34.6 + '@vitest/utils': 0.34.6 + chai: 4.3.10 + dev: true + /@vitest/runner@0.31.4: resolution: {integrity: sha512-Wgm6UER+gwq6zkyrm5/wbpXGF+g+UBB78asJlFkIOwyse0pz8lZoiC6SW5i4gPnls/zUcPLWS7Zog0LVepXnpg==} dependencies: @@ -2975,6 +3162,14 @@ packages: p-limit: 4.0.0 pathe: 1.1.1 + /@vitest/runner@0.34.6: + resolution: {integrity: sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==} + dependencies: + '@vitest/utils': 0.34.6 + p-limit: 4.0.0 + pathe: 1.1.1 + dev: true + /@vitest/snapshot@0.31.4: resolution: {integrity: sha512-LemvNumL3NdWSmfVAMpXILGyaXPkZbG5tyl6+RQSdcHnTj6hvA49UAI8jzez9oQyE/FWLKRSNqTGzsHuk89LRA==} dependencies: @@ -2982,11 +3177,25 @@ packages: pathe: 1.1.1 pretty-format: 27.5.1 + /@vitest/snapshot@0.34.6: + resolution: {integrity: sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==} + dependencies: + magic-string: 0.30.5 + pathe: 1.1.1 + pretty-format: 29.7.0 + dev: true + /@vitest/spy@0.31.4: resolution: {integrity: sha512-3ei5ZH1s3aqbEyftPAzSuunGICRuhE+IXOmpURFdkm5ybUADk+viyQfejNk6q8M5QGX8/EVKw+QWMEP3DTJDag==} dependencies: tinyspy: 2.1.1 + /@vitest/spy@0.34.6: + resolution: {integrity: sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==} + dependencies: + tinyspy: 2.1.1 + dev: true + /@vitest/ui@0.31.4(vitest@0.31.4): resolution: {integrity: sha512-sKM16ITX6HrNFF+lNZ2AQAen4/6Bx2i6KlBfIvkUjcTgc5YII/j2ltcX14oCUv4EA0OTWGQuGhO3zDoAsTENGA==} peerDependencies: @@ -3008,6 +3217,14 @@ packages: loupe: 2.3.6 pretty-format: 27.5.1 + /@vitest/utils@0.34.6: + resolution: {integrity: sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==} + dependencies: + diff-sequences: 29.6.3 + loupe: 2.3.6 + pretty-format: 29.7.0 + dev: true + /abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} requiresBuild: true @@ -3639,6 +3856,19 @@ packages: nofilter: 3.1.0 dev: true + /chai@4.3.10: + resolution: {integrity: sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==} + engines: {node: '>=4'} + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.3 + deep-eql: 4.1.3 + get-func-name: 2.0.2 + loupe: 2.3.6 + pathval: 1.1.1 + type-detect: 4.0.8 + dev: true + /chai@4.3.7: resolution: {integrity: sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==} engines: {node: '>=4'} @@ -3681,6 +3911,12 @@ packages: /check-error@1.0.2: resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} + /check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + dependencies: + get-func-name: 2.0.2 + dev: true + /chokidar@3.5.3: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} engines: {node: '>= 8.10.0'} @@ -3936,6 +4172,15 @@ packages: dev: false optional: true + /cpy-cli@5.0.0: + resolution: {integrity: sha512-fb+DZYbL9KHc0BC4NYqGRrDIJZPXUmjjtqdw4XRRg8iV8dIfghUX/WiL+q4/B/KFTy3sK6jsbUhBaz0/Hxg7IQ==} + engines: {node: '>=16'} + hasBin: true + dependencies: + cpy: 10.1.0 + meow: 12.1.1 + dev: true + /cpy@10.1.0: resolution: {integrity: sha512-VC2Gs20JcTyeQob6UViBLnyP0bYHkBh6EiKzot9vi2DmeGlFT9Wd7VG3NBrkNx/jYvFBeyDOMMHdHQhbtKLgHQ==} engines: {node: '>=16'} @@ -4055,7 +4300,7 @@ packages: resolution: {integrity: sha512-tQbV/4u5WVB8HMJr08pgw0b6nG4RGt/tj+7Numvq+zqcvUFeMaIWWOUFltiU+6go8BSO2/ogsB4EasDaj0y68Q==} engines: {node: '>=14.16'} dependencies: - globby: 13.1.3 + globby: 13.1.4 graceful-fs: 4.2.11 is-glob: 4.0.3 is-path-cwd: 3.0.0 @@ -4095,6 +4340,11 @@ packages: resolution: {integrity: sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==} engines: {node: '>=8'} + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + /diff@5.1.0: resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==} engines: {node: '>=0.3.1'} @@ -4924,6 +5174,53 @@ packages: - supports-color dev: true + /eslint@8.53.0: + resolution: {integrity: sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) + '@eslint-community/regexpp': 4.9.0 + '@eslint/eslintrc': 2.1.3 + '@eslint/js': 8.53.0 + '@humanwhocodes/config-array': 0.11.13 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.2.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.22.0 + graphemer: 1.4.0 + ignore: 5.2.4 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.3 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + /esm@3.2.25: resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} engines: {node: '>=6'} @@ -5383,6 +5680,10 @@ packages: /get-func-name@2.0.0: resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} + /get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + dev: true + /get-intrinsic@1.2.0: resolution: {integrity: sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==} dependencies: @@ -5538,7 +5839,7 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: dir-glob: 3.0.1 - fast-glob: 3.2.12 + fast-glob: 3.3.1 ignore: 5.2.4 merge2: 1.4.1 slash: 4.0.0 @@ -6320,6 +6621,13 @@ packages: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 + /magic-string@0.30.5: + resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + /make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} @@ -6423,6 +6731,11 @@ packages: timers-ext: 0.1.7 dev: true + /meow@12.1.1: + resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} + engines: {node: '>=16.10'} + dev: true + /merge-descriptors@1.0.1: resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} dev: false @@ -6592,6 +6905,15 @@ packages: pkg-types: 1.0.3 ufo: 1.1.2 + /mlly@1.4.2: + resolution: {integrity: sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==} + dependencies: + acorn: 8.10.0 + pathe: 1.1.1 + pkg-types: 1.0.3 + ufo: 1.3.1 + dev: true + /mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -7318,6 +7640,15 @@ packages: ansi-styles: 5.2.0 react-is: 17.0.2 + /pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.2.0 + dev: true + /pretty-ms@8.0.0: resolution: {integrity: sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q==} engines: {node: '>=14.16'} @@ -7418,6 +7749,10 @@ packages: /react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + /react-is@18.2.0: + resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + dev: true + /read-pkg-up@7.0.1: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} engines: {node: '>=8'} @@ -8250,6 +8585,11 @@ packages: resolution: {integrity: sha512-paHQtnrlS1QZYKF/GnLoOM/DN9fqaGOFbCbxzAhwniySnzl9Ebk8w73/dd34DAhe/obUbPAOldTyYXQZxnPBPQ==} engines: {node: '>=14.0.0'} + /tinypool@0.7.0: + resolution: {integrity: sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==} + engines: {node: '>=14.0.0'} + dev: true + /tinyspy@2.1.1: resolution: {integrity: sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==} engines: {node: '>=14.0.0'} @@ -8578,6 +8918,10 @@ packages: /ufo@1.1.2: resolution: {integrity: sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==} + /ufo@1.3.1: + resolution: {integrity: sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==} + dev: true + /unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: @@ -8726,6 +9070,27 @@ packages: - supports-color - terser + /vite-node@0.34.6(@types/node@20.8.7): + resolution: {integrity: sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==} + engines: {node: '>=v14.18.0'} + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.3.4 + mlly: 1.4.2 + pathe: 1.1.1 + picocolors: 1.0.0 + vite: 4.3.9(@types/node@20.8.7) + transitivePeerDependencies: + - '@types/node' + - less + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /vite-tsconfig-paths@4.2.0(typescript@5.2.2)(vite@4.3.9): resolution: {integrity: sha512-jGpus0eUy5qbbMVGiTxCL1iB9ZGN6Bd37VGLJU39kTDD6ZfULTTb1bcc5IeTWqWJKiWV5YihCaibeASPiGi8kw==} peerDependencies: @@ -8775,6 +9140,39 @@ packages: optionalDependencies: fsevents: 2.3.3 + /vite@4.3.9(@types/node@20.8.7): + resolution: {integrity: sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 20.8.7 + esbuild: 0.17.19 + postcss: 8.4.24 + rollup: 3.27.2 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /vitest@0.31.4(@vitest/ui@0.31.4): resolution: {integrity: sha512-GoV0VQPmWrUFOZSg3RpQAPN+LPmHg2/gxlMNJlyxJihkz6qReHDV6b0pPDcqFLNEPya4tWJ1pgwUNP9MLmUfvQ==} engines: {node: '>=v14.18.0'} @@ -8840,6 +9238,70 @@ packages: - supports-color - terser + /vitest@0.34.6: + resolution: {integrity: sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==} + engines: {node: '>=v14.18.0'} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@vitest/browser': '*' + '@vitest/ui': '*' + happy-dom: '*' + jsdom: '*' + playwright: '*' + safaridriver: '*' + webdriverio: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + playwright: + optional: true + safaridriver: + optional: true + webdriverio: + optional: true + dependencies: + '@types/chai': 4.3.5 + '@types/chai-subset': 1.3.3 + '@types/node': 20.8.7 + '@vitest/expect': 0.34.6 + '@vitest/runner': 0.34.6 + '@vitest/snapshot': 0.34.6 + '@vitest/spy': 0.34.6 + '@vitest/utils': 0.34.6 + acorn: 8.10.0 + acorn-walk: 8.2.0 + cac: 6.7.14 + chai: 4.3.10 + debug: 4.3.4 + local-pkg: 0.4.3 + magic-string: 0.30.5 + pathe: 1.1.1 + picocolors: 1.0.0 + std-env: 3.3.3 + strip-literal: 1.0.1 + tinybench: 2.5.0 + tinypool: 0.7.0 + vite: 4.3.9(@types/node@20.8.7) + vite-node: 0.34.6(@types/node@20.8.7) + why-is-node-running: 2.2.2 + transitivePeerDependencies: + - less + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /web-streams-polyfill@3.2.1: resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==} engines: {node: '>= 8'} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4e2fa3436..1c10dc1ec 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,3 +4,4 @@ packages: - drizzle-typebox - drizzle-valibot - integration-tests + - eslint-plugin-drizzle