diff --git a/changelogs/drizzle-orm/0.30.5.md b/changelogs/drizzle-orm/0.30.5.md index 0adcc0a6e..ccadcee9a 100644 --- a/changelogs/drizzle-orm/0.30.5.md +++ b/changelogs/drizzle-orm/0.30.5.md @@ -22,4 +22,4 @@ const usersOnUpdate = pgTable('users_on_update', { - [BUG]: insertions on columns with the smallserial datatype are not optional - #1848 -Thanks @Angelelz and @gabrielDonnantuoni! \ No newline at end of file +Thanks @Angelelz and @gabrielDonnantuoni! diff --git a/changelogs/drizzle-orm/0.30.7-preview.md b/changelogs/drizzle-orm/0.30.7-preview.md new file mode 100644 index 000000000..fb3b575a7 --- /dev/null +++ b/changelogs/drizzle-orm/0.30.7-preview.md @@ -0,0 +1,10 @@ +- 🎉 Added custom schema support to enums in Postgres: + ```ts + import { pgSchema } from 'drizzle-orm/pg-core'; + + const mySchema = pgSchema('mySchema'); + const colors = mySchema.enum('colors', ['red', 'green', 'blue']); + ``` + +- 🐛 Split `where` clause in Postgres `.onConflictDoUpdate` method into `setWhere` and `targetWhere` clauses, to support both `where` cases in `on conflict ...` clause (#1628, #1302) +- 🐛 Fix query generation for `where` clause in Postgres `.onConflictDoNothing` method, as it was placed in a wrong spot (#1628) \ No newline at end of file diff --git a/changelogs/drizzle-orm/0.32.0-beta.md b/changelogs/drizzle-orm/0.32.0-beta.md new file mode 100644 index 000000000..bbf7859e7 --- /dev/null +++ b/changelogs/drizzle-orm/0.32.0-beta.md @@ -0,0 +1,184 @@ +# Preview release for `drizzle-orm@0.32.0` and `drizzle-kit@0.23.0` + +> It's not mandatory to upgrade both packages, but if you want to use the new features in both queries and migrations, you will need to upgrade both packages + +## New Features + +### 🎉 PostgreSQL Sequences + +You can now specify sequences in Postgres within any schema you need and define all the available properties + +##### **Example** + +```ts +import { pgSchema, pgSequence } from "drizzle-orm/pg-core"; + +// No params specified +export const customSequence = pgSequence("name"); + +// Sequence with params +export const customSequence = pgSequence("name", { + startWith: 100, + maxValue: 10000, + minValue: 100, + cycle: true, + cache: 10, + increment: 2 +}); + +// Sequence in custom schema +export const customSchema = pgSchema('custom_schema'); + +export const customSequence = customSchema.sequence("name"); +``` + +### 🎉 PostgreSQL Identity Columns + +[Source](https://wiki.postgresql.org/wiki/Don%27t_Do_This#Don.27t_use_serial): As mentioned, the `serial` type in Postgres is outdated and should be deprecated. Ideally, you should not use it. `Identity columns` are the recommended way to specify sequences in your schema, which is why we are introducing the `identity columns` feature + +##### **Example** + +```ts +import { pgTable, integer, text } from 'drizzle-orm/pg-core' + +export const ingredients = pgTable("ingredients", { + id: integer("id").primaryKey().generatedAlwaysAsIdentity({ startWith: 1000 }), + name: text("name").notNull(), + description: text("description"), +}); +``` + +You can specify all properties available for sequences in the `.generatedAlwaysAsIdentity()` function. Additionally, you can specify custom names for these sequences + +PostgreSQL docs [reference](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-PARMS-GENERATED-IDENTITY). + +### 🎉 PostgreSQL Generated Columns + +You can now specify generated columns on any column supported by PostgreSQL to use with generated columns + +##### **Example** with generated column for `tsvector` + +> Note: we will add `tsVector` column type before latest release + +```ts +import { SQL, sql } from "drizzle-orm"; +import { customType, index, integer, pgTable, text } from "drizzle-orm/pg-core"; + +const tsVector = customType<{ data: string }>({ + dataType() { + return "tsvector"; + }, +}); + +export const test = pgTable( + "test", + { + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + content: text("content"), + contentSearch: tsVector("content_search", { + dimensions: 3, + }).generatedAlwaysAs( + (): SQL => sql`to_tsvector('english', ${test.content})` + ), + }, + (t) => ({ + idx: index("idx_content_search").using("gin", t.contentSearch), + }) +); +``` + +In case you don't need to reference any columns from your table, you can use just `sql` template or a `string` + +```ts +export const users = pgTable("users", { + id: integer("id"), + name: text("name"), + generatedName: text("gen_name").generatedAlwaysAs(sql`hello world!`), + generatedName1: text("gen_name1").generatedAlwaysAs("hello world!"), +}), +``` + +### 🎉 MySQL Generated Columns + +You can now specify generated columns on any column supported by MySQL to use with generated columns + +You can specify both `stored` and `virtual` options, for more info you can check [MySQL docs](https://dev.mysql.com/doc/refman/8.4/en/create-table-generated-columns.html) + +Also MySQL has a few limitation for such columns usage, which is described [here](https://dev.mysql.com/doc/refman/8.4/en/alter-table-generated-columns.html) + +Drizzle Kit will also have limitations for `push` command: + +1. You can't change the generated constraint expression and type using `push`. Drizzle-kit will ignore this change. To make it work, you would need to `drop the column`, `push`, and then `add a column with a new expression`. This was done due to the complex mapping from the database side, where the schema expression will be modified on the database side and, on introspection, we will get a different string. We can't be sure if you changed this expression or if it was changed and formatted by the database. As long as these are generated columns and `push` is mostly used for prototyping on a local database, it should be fast to `drop` and `create` generated columns. Since these columns are `generated`, all the data will be restored + +2. `generate` should have no limitations + +##### **Example** + +```ts +export const users = mysqlTable("users", { + id: int("id"), + id2: int("id2"), + name: text("name"), + generatedName: text("gen_name").generatedAlwaysAs( + (): SQL => sql`${schema2.users.name} || 'hello'`, + { mode: "stored" } + ), + generatedName1: text("gen_name1").generatedAlwaysAs( + (): SQL => sql`${schema2.users.name} || 'hello'`, + { mode: "virtual" } + ), +}), +``` + +In case you don't need to reference any columns from your table, you can use just `sql` template or a `string` in `.generatedAlwaysAs()` + +### 🎉 SQLite Generated Columns + +You can now specify generated columns on any column supported by SQLite to use with generated columns + +You can specify both `stored` and `virtual` options, for more info you can check [SQLite docs](https://www.sqlite.org/gencol.html) + +Also SQLite has a few limitation for such columns usage, which is described [here](https://www.sqlite.org/gencol.html) + +Drizzle Kit will also have limitations for `push` and `generate` command: + +1. You can't change the generated constraint expression with the stored type in an existing table. You would need to delete this table and create it again. This is due to SQLite limitations for such actions. We will handle this case in future releases (it will involve the creation of a new table with data migration). + +2. You can't add a `stored` generated expression to an existing column for the same reason as above. However, you can add a `virtual` expression to an existing column. + +3. You can't change a `stored` generated expression in an existing column for the same reason as above. However, you can change a `virtual` expression. + +4. You can't change the generated constraint type from `virtual` to `stored` for the same reason as above. However, you can change from `stored` to `virtual`. + +## New Drizzle Kit features + +### 🎉 Migrations support for all the new orm features + +PostgreSQL sequences, identity columns and generated columns for all dialects + +### 🎉 New flag `--force` for `drizzle-kit push` + +You can auto-accept all data-loss statements using the push command. It's only available in CLI parameters. Make sure you always use it if you are fine with running data-loss statements on your database + +### 🎉 New `migrations` flag `prefix` + +You can now customize migration file prefixes to make the format suitable for your migration tools: + +- `index` is the default type and will result in `0001_name.sql` file names; +- `supabase` and `timestamp` are equal and will result in `20240627123900_name.sql` file names; +- `unix` will result in unix seconds prefixes `1719481298_name.sql` file names; +- `none` will omit the prefix completely; + + +##### **Example**: Supabase migrations format +```ts +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + dialect: "postgresql", + migrations: { + prefix: 'supabase' + } +}); + +``` diff --git a/changelogs/drizzle-orm/0.32.0.md b/changelogs/drizzle-orm/0.32.0.md new file mode 100644 index 000000000..a8a2c17e7 --- /dev/null +++ b/changelogs/drizzle-orm/0.32.0.md @@ -0,0 +1,220 @@ +# Release notes for `drizzle-orm@0.32.0` and `drizzle-kit@0.23.0` + +> It's not mandatory to upgrade both packages, but if you want to use the new features in both queries and migrations, you will need to upgrade both packages + +## New Features + +### 🎉 MySQL `$returningId()` function + +MySQL itself doesn't have native support for `RETURNING` after using `INSERT`. There is only one way to do it for `primary keys` with `autoincrement` (or `serial`) types, where you can access `insertId` and `affectedRows` fields. We've prepared an automatic way for you to handle such cases with Drizzle and automatically receive all inserted IDs as separate objects + +```ts +import { boolean, int, text, mysqlTable } from 'drizzle-orm/mysql-core'; + +const usersTable = mysqlTable('users', { + id: int('id').primaryKey(), + name: text('name').notNull(), + verified: boolean('verified').notNull().default(false), +}); + + +const result = await db.insert(usersTable).values([{ name: 'John' }, { name: 'John1' }]).$returningId(); +// ^? { id: number }[] +``` + +Also with Drizzle, you can specify a `primary key` with `$default` function that will generate custom primary keys at runtime. We will also return those generated keys for you in the `$returningId()` call + +```ts +import { varchar, text, mysqlTable } from 'drizzle-orm/mysql-core'; +import { createId } from '@paralleldrive/cuid2'; + +const usersTableDefFn = mysqlTable('users_default_fn', { + customId: varchar('id', { length: 256 }).primaryKey().$defaultFn(createId), + name: text('name').notNull(), +}); + + +const result = await db.insert(usersTableDefFn).values([{ name: 'John' }, { name: 'John1' }]).$returningId(); +// ^? { customId: string }[] +``` + +> If there is no primary keys -> type will be `{}[]` for such queries + +### 🎉 PostgreSQL Sequences + +You can now specify sequences in Postgres within any schema you need and define all the available properties + +##### **Example** + +```ts +import { pgSchema, pgSequence } from "drizzle-orm/pg-core"; + +// No params specified +export const customSequence = pgSequence("name"); + +// Sequence with params +export const customSequence = pgSequence("name", { + startWith: 100, + maxValue: 10000, + minValue: 100, + cycle: true, + cache: 10, + increment: 2 +}); + +// Sequence in custom schema +export const customSchema = pgSchema('custom_schema'); + +export const customSequence = customSchema.sequence("name"); +``` + +### 🎉 PostgreSQL Identity Columns + +[Source](https://wiki.postgresql.org/wiki/Don%27t_Do_This#Don.27t_use_serial): As mentioned, the `serial` type in Postgres is outdated and should be deprecated. Ideally, you should not use it. `Identity columns` are the recommended way to specify sequences in your schema, which is why we are introducing the `identity columns` feature + +##### **Example** + +```ts +import { pgTable, integer, text } from 'drizzle-orm/pg-core' + +export const ingredients = pgTable("ingredients", { + id: integer("id").primaryKey().generatedAlwaysAsIdentity({ startWith: 1000 }), + name: text("name").notNull(), + description: text("description"), +}); +``` + +You can specify all properties available for sequences in the `.generatedAlwaysAsIdentity()` function. Additionally, you can specify custom names for these sequences + +PostgreSQL docs [reference](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-PARMS-GENERATED-IDENTITY). + +### 🎉 PostgreSQL Generated Columns + +You can now specify generated columns on any column supported by PostgreSQL to use with generated columns + +##### **Example** with generated column for `tsvector` + +> Note: we will add `tsVector` column type before latest release + +```ts +import { SQL, sql } from "drizzle-orm"; +import { customType, index, integer, pgTable, text } from "drizzle-orm/pg-core"; + +const tsVector = customType<{ data: string }>({ + dataType() { + return "tsvector"; + }, +}); + +export const test = pgTable( + "test", + { + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + content: text("content"), + contentSearch: tsVector("content_search", { + dimensions: 3, + }).generatedAlwaysAs( + (): SQL => sql`to_tsvector('english', ${test.content})` + ), + }, + (t) => ({ + idx: index("idx_content_search").using("gin", t.contentSearch), + }) +); +``` + +In case you don't need to reference any columns from your table, you can use just `sql` template or a `string` + +```ts +export const users = pgTable("users", { + id: integer("id"), + name: text("name"), + generatedName: text("gen_name").generatedAlwaysAs(sql`hello world!`), + generatedName1: text("gen_name1").generatedAlwaysAs("hello world!"), +}), +``` + +### 🎉 MySQL Generated Columns + +You can now specify generated columns on any column supported by MySQL to use with generated columns + +You can specify both `stored` and `virtual` options, for more info you can check [MySQL docs](https://dev.mysql.com/doc/refman/8.4/en/create-table-generated-columns.html) + +Also MySQL has a few limitation for such columns usage, which is described [here](https://dev.mysql.com/doc/refman/8.4/en/alter-table-generated-columns.html) + +Drizzle Kit will also have limitations for `push` command: + +1. You can't change the generated constraint expression and type using `push`. Drizzle-kit will ignore this change. To make it work, you would need to `drop the column`, `push`, and then `add a column with a new expression`. This was done due to the complex mapping from the database side, where the schema expression will be modified on the database side and, on introspection, we will get a different string. We can't be sure if you changed this expression or if it was changed and formatted by the database. As long as these are generated columns and `push` is mostly used for prototyping on a local database, it should be fast to `drop` and `create` generated columns. Since these columns are `generated`, all the data will be restored + +2. `generate` should have no limitations + +##### **Example** + +```ts +export const users = mysqlTable("users", { + id: int("id"), + id2: int("id2"), + name: text("name"), + generatedName: text("gen_name").generatedAlwaysAs( + (): SQL => sql`${schema2.users.name} || 'hello'`, + { mode: "stored" } + ), + generatedName1: text("gen_name1").generatedAlwaysAs( + (): SQL => sql`${schema2.users.name} || 'hello'`, + { mode: "virtual" } + ), +}), +``` + +In case you don't need to reference any columns from your table, you can use just `sql` template or a `string` in `.generatedAlwaysAs()` + +### 🎉 SQLite Generated Columns + +You can now specify generated columns on any column supported by SQLite to use with generated columns + +You can specify both `stored` and `virtual` options, for more info you can check [SQLite docs](https://www.sqlite.org/gencol.html) + +Also SQLite has a few limitation for such columns usage, which is described [here](https://www.sqlite.org/gencol.html) + +Drizzle Kit will also have limitations for `push` and `generate` command: + +1. You can't change the generated constraint expression with the stored type in an existing table. You would need to delete this table and create it again. This is due to SQLite limitations for such actions. We will handle this case in future releases (it will involve the creation of a new table with data migration). + +2. You can't add a `stored` generated expression to an existing column for the same reason as above. However, you can add a `virtual` expression to an existing column. + +3. You can't change a `stored` generated expression in an existing column for the same reason as above. However, you can change a `virtual` expression. + +4. You can't change the generated constraint type from `virtual` to `stored` for the same reason as above. However, you can change from `stored` to `virtual`. + +## New Drizzle Kit features + +### 🎉 Migrations support for all the new orm features + +PostgreSQL sequences, identity columns and generated columns for all dialects + +### 🎉 New flag `--force` for `drizzle-kit push` + +You can auto-accept all data-loss statements using the push command. It's only available in CLI parameters. Make sure you always use it if you are fine with running data-loss statements on your database + +### 🎉 New `migrations` flag `prefix` + +You can now customize migration file prefixes to make the format suitable for your migration tools: + +- `index` is the default type and will result in `0001_name.sql` file names; +- `supabase` and `timestamp` are equal and will result in `20240627123900_name.sql` file names; +- `unix` will result in unix seconds prefixes `1719481298_name.sql` file names; +- `none` will omit the prefix completely; + + +##### **Example**: Supabase migrations format +```ts +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + dialect: "postgresql", + migrations: { + prefix: 'supabase' + } +}); + +``` diff --git a/drizzle-orm/package.json b/drizzle-orm/package.json index acffd5927..fd57ac469 100644 --- a/drizzle-orm/package.json +++ b/drizzle-orm/package.json @@ -1,6 +1,6 @@ { "name": "drizzle-orm", - "version": "0.31.4", + "version": "0.32.0", "description": "Drizzle ORM package for SQL databases", "type": "module", "scripts": { diff --git a/drizzle-orm/src/column-builder.ts b/drizzle-orm/src/column-builder.ts index 02c257b4f..4a19a79a9 100644 --- a/drizzle-orm/src/column-builder.ts +++ b/drizzle-orm/src/column-builder.ts @@ -1,7 +1,7 @@ import { entityKind } from '~/entity.ts'; import type { Column } from './column.ts'; import type { MySqlColumn } from './mysql-core/index.ts'; -import type { ExtraConfigColumn, PgColumn } from './pg-core/index.ts'; +import type { ExtraConfigColumn, PgColumn, PgSequenceOptions } from './pg-core/index.ts'; import type { SQL } from './sql/sql.ts'; import type { SQLiteColumn } from './sqlite-core/index.ts'; import type { Simplify } from './utils.ts'; @@ -19,6 +19,22 @@ export type ColumnDataType = export type Dialect = 'pg' | 'mysql' | 'sqlite' | 'common'; +export type GeneratedStorageMode = 'virtual' | 'stored'; + +export type GeneratedType = 'always' | 'byDefault'; + +export type GeneratedColumnConfig = { + as: TDataType | SQL | (() => SQL); + type?: GeneratedType; + mode?: GeneratedStorageMode; +}; + +export type GeneratedIdentityConfig = { + sequenceName?: string; + sequenceOptions?: PgSequenceOptions; + type: 'always' | 'byDefault'; +}; + export interface ColumnBuilderBaseConfig { name: string; dataType: TDataType; @@ -26,23 +42,29 @@ export interface ColumnBuilderBaseConfig | undefined; } export type MakeColumnConfig< T extends ColumnBuilderBaseConfig, TTableName extends string, + TData = T extends { $type: infer U } ? U : T['data'], > = { name: T['name']; tableName: TTableName; dataType: T['dataType']; columnType: T['columnType']; - data: T extends { $type: infer U } ? U : T['data']; + data: TData; driverParam: T['driverParam']; notNull: T extends { notNull: true } ? true : false; hasDefault: T extends { hasDefault: true } ? true : false; + isPrimaryKey: T extends { isPrimaryKey: true } ? true : false; + isAutoincrement: T extends { isAutoincrement: true } ? true : false; + hasRuntimeDefault: T extends { hasRuntimeDefault: true } ? true : false; enumValues: T['enumValues']; baseColumn: T extends { baseBuilder: infer U extends ColumnBuilderBase } ? BuildColumn : never; + generated: T['generated'] extends object ? T['generated'] : undefined; } & {}; export type ColumnBuilderTypeConfig< @@ -60,6 +82,7 @@ export type ColumnBuilderTypeConfig< notNull: T extends { notNull: infer U } ? U : boolean; hasDefault: T extends { hasDefault: infer U } ? U : boolean; enumValues: T['enumValues']; + generated: GeneratedColumnConfig | undefined; } & TTypeConfig >; @@ -77,6 +100,8 @@ export type ColumnBuilderRuntimeConfig | undefined; + generatedIdentity: GeneratedIdentityConfig | undefined; } & TRuntimeConfig; export interface ColumnBuilderExtraConfig { @@ -95,12 +120,48 @@ export type HasDefault = T & { }; }; +export type IsPrimaryKey = T & { + _: { + isPrimaryKey: true; + }; +}; + +export type IsAutoincrement = T & { + _: { + isAutoincrement: true; + }; +}; + +export type HasRuntimeDefault = T & { + _: { + hasRuntimeDefault: true; + }; +}; + export type $Type = T & { _: { $type: TType; }; }; +export type HasGenerated = T & { + _: { + hasDefault: true; + generated: TGenerated; + }; +}; + +export type IsIdentityByDefault< + T extends ColumnBuilderBase, + TType extends 'always' | 'byDefault', +> = T & { + _: { + notNull: true; + hasDefault: true; + generated: { as: any; type: TType }; + }; +}; + export interface ColumnBuilderBase< T extends ColumnBuilderBaseConfig = ColumnBuilderBaseConfig, TTypeConfig extends object = object, @@ -133,6 +194,7 @@ export abstract class ColumnBuilder< uniqueType: undefined, dataType, columnType, + generated: undefined, } as ColumnBuilderRuntimeConfig; } @@ -182,10 +244,10 @@ export abstract class ColumnBuilder< */ $defaultFn( fn: () => (this['_'] extends { $type: infer U } ? U : this['_']['data']) | SQL, - ): HasDefault { + ): HasRuntimeDefault> { this.config.defaultFn = fn; this.config.hasDefault = true; - return this as HasDefault; + return this as HasRuntimeDefault>; } /** @@ -218,11 +280,19 @@ export abstract class ColumnBuilder< * * In SQLite, `integer primary key` implicitly makes the column auto-incrementing. */ - primaryKey(): TExtraConfig['primaryKeyHasDefault'] extends true ? HasDefault> : NotNull { + primaryKey(): TExtraConfig['primaryKeyHasDefault'] extends true ? IsPrimaryKey>> + : IsPrimaryKey> + { this.config.primaryKey = true; this.config.notNull = true; - return this as TExtraConfig['primaryKeyHasDefault'] extends true ? HasDefault> : NotNull; + return this as TExtraConfig['primaryKeyHasDefault'] extends true ? IsPrimaryKey>> + : IsPrimaryKey>; } + + abstract generatedAlwaysAs( + as: SQL | T['data'] | (() => SQL), + config?: Partial>, + ): HasGenerated; } export type BuildColumn< diff --git a/drizzle-orm/src/column.ts b/drizzle-orm/src/column.ts index 450f65e04..e740acaa0 100644 --- a/drizzle-orm/src/column.ts +++ b/drizzle-orm/src/column.ts @@ -1,4 +1,10 @@ -import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, ColumnDataType } from './column-builder.ts'; +import type { + ColumnBuilderBaseConfig, + ColumnBuilderRuntimeConfig, + ColumnDataType, + GeneratedColumnConfig, + GeneratedIdentityConfig, +} from './column-builder.ts'; import { entityKind } from './entity.ts'; import type { DriverValueMapper, SQL, SQLWrapper } from './sql/sql.ts'; import type { Table } from './table.ts'; @@ -11,6 +17,9 @@ export interface ColumnBaseConfig< tableName: string; notNull: boolean; hasDefault: boolean; + isPrimaryKey: boolean; + isAutoincrement: boolean; + hasRuntimeDefault: boolean; } export type ColumnTypeConfig, TTypeConfig extends object> = T & { @@ -23,8 +32,12 @@ export type ColumnTypeConfig, driverParam: T['driverParam']; notNull: T['notNull']; hasDefault: T['hasDefault']; + isPrimaryKey: T['isPrimaryKey']; + isAutoincrement: T['isAutoincrement']; + hasRuntimeDefault: T['hasRuntimeDefault']; enumValues: T['enumValues']; baseColumn: T extends { baseColumn: infer U } ? U : unknown; + generated: GeneratedColumnConfig | undefined; } & TTypeConfig; export type ColumnRuntimeConfig = ColumnBuilderRuntimeConfig< @@ -68,6 +81,8 @@ export abstract class Column< readonly dataType: T['dataType']; readonly columnType: T['columnType']; readonly enumValues: T['enumValues'] = undefined; + readonly generated: GeneratedColumnConfig | undefined = undefined; + readonly generatedIdentity: GeneratedIdentityConfig | undefined = undefined; protected config: ColumnRuntimeConfig; @@ -88,6 +103,8 @@ export abstract class Column< this.uniqueType = config.uniqueType; this.dataType = config.dataType as T['dataType']; this.columnType = config.columnType; + this.generated = config.generated; + this.generatedIdentity = config.generatedIdentity; } abstract getSQLType(): string; @@ -99,6 +116,11 @@ export abstract class Column< mapToDriverValue(value: unknown): unknown { return value; } + + // ** @internal */ + shouldDisableInsert(): boolean { + return this.config.generated !== undefined && this.config.generated.type !== 'byDefault'; + } } export type UpdateColConfig< diff --git a/drizzle-orm/src/mysql-core/columns/bigint.ts b/drizzle-orm/src/mysql-core/columns/bigint.ts index c80770d22..ca1eedb3f 100644 --- a/drizzle-orm/src/mysql-core/columns/bigint.ts +++ b/drizzle-orm/src/mysql-core/columns/bigint.ts @@ -11,6 +11,7 @@ export type MySqlBigInt53BuilderInitial = MySqlBigInt53Bui data: number; driverParam: number | string; enumValues: undefined; + generated: undefined; }>; export class MySqlBigInt53Builder> @@ -58,6 +59,7 @@ export type MySqlBigInt64BuilderInitial = MySqlBigInt64Bui data: bigint; driverParam: string; enumValues: undefined; + generated: undefined; }>; export class MySqlBigInt64Builder> diff --git a/drizzle-orm/src/mysql-core/columns/binary.ts b/drizzle-orm/src/mysql-core/columns/binary.ts index 6deb385d8..87a8e0f8c 100644 --- a/drizzle-orm/src/mysql-core/columns/binary.ts +++ b/drizzle-orm/src/mysql-core/columns/binary.ts @@ -11,6 +11,7 @@ export type MySqlBinaryBuilderInitial = MySqlBinaryBuilder data: string; driverParam: string; enumValues: undefined; + generated: undefined; }>; export class MySqlBinaryBuilder> extends MySqlColumnBuilder< diff --git a/drizzle-orm/src/mysql-core/columns/boolean.ts b/drizzle-orm/src/mysql-core/columns/boolean.ts index a75131469..3a915e673 100644 --- a/drizzle-orm/src/mysql-core/columns/boolean.ts +++ b/drizzle-orm/src/mysql-core/columns/boolean.ts @@ -11,6 +11,7 @@ export type MySqlBooleanBuilderInitial = MySqlBooleanBuild data: boolean; driverParam: number | boolean; enumValues: undefined; + generated: undefined; }>; export class MySqlBooleanBuilder> diff --git a/drizzle-orm/src/mysql-core/columns/char.ts b/drizzle-orm/src/mysql-core/columns/char.ts index 5466ec046..f871796a5 100644 --- a/drizzle-orm/src/mysql-core/columns/char.ts +++ b/drizzle-orm/src/mysql-core/columns/char.ts @@ -12,6 +12,7 @@ export type MySqlCharBuilderInitial; export class MySqlCharBuilder> extends MySqlColumnBuilder< diff --git a/drizzle-orm/src/mysql-core/columns/common.ts b/drizzle-orm/src/mysql-core/columns/common.ts index fe518cebb..a0a192477 100644 --- a/drizzle-orm/src/mysql-core/columns/common.ts +++ b/drizzle-orm/src/mysql-core/columns/common.ts @@ -6,6 +6,8 @@ import type { ColumnBuilderRuntimeConfig, ColumnDataType, HasDefault, + HasGenerated, + IsAutoincrement, MakeColumnConfig, } from '~/column-builder.ts'; import type { ColumnBaseConfig } from '~/column.ts'; @@ -14,6 +16,7 @@ import { entityKind } from '~/entity.ts'; import type { ForeignKey, UpdateDeleteAction } from '~/mysql-core/foreign-keys.ts'; import { ForeignKeyBuilder } from '~/mysql-core/foreign-keys.ts'; import type { AnyMySqlTable, MySqlTable } from '~/mysql-core/table.ts'; +import type { SQL } from '~/sql/sql.ts'; import type { Update } from '~/utils.ts'; import { uniqueKeyName } from '../unique-constraint.ts'; @@ -30,6 +33,10 @@ export interface MySqlColumnBuilderBase< TTypeConfig extends object = object, > extends ColumnBuilderBase {} +export interface MySqlGeneratedColumnConfig { + mode?: 'virtual' | 'stored'; +} + export abstract class MySqlColumnBuilder< T extends ColumnBuilderBaseConfig = ColumnBuilderBaseConfig & { data: any; @@ -55,6 +62,15 @@ export abstract class MySqlColumnBuilder< return this; } + generatedAlwaysAs(as: SQL | T['data'] | (() => SQL), config?: MySqlGeneratedColumnConfig): HasGenerated { + this.config.generated = { + as, + type: 'always', + mode: config?.mode ?? 'virtual', + }; + return this as any; + } + /** @internal */ buildForeignKeys(column: MySqlColumn, table: MySqlTable): ForeignKey[] { return this.foreignKeyConfigs.map(({ ref, actions }) => { @@ -118,10 +134,10 @@ export abstract class MySqlColumnBuilderWithAutoIncrement< this.config.autoIncrement = false; } - autoincrement(): HasDefault { + autoincrement(): IsAutoincrement> { this.config.autoIncrement = true; this.config.hasDefault = true; - return this as HasDefault; + return this as IsAutoincrement>; } } diff --git a/drizzle-orm/src/mysql-core/columns/custom.ts b/drizzle-orm/src/mysql-core/columns/custom.ts index 135bc8c09..1c5e2603f 100644 --- a/drizzle-orm/src/mysql-core/columns/custom.ts +++ b/drizzle-orm/src/mysql-core/columns/custom.ts @@ -14,6 +14,7 @@ export type ConvertCustomConfig = MySqlDateBuilder<{ data: Date; driverParam: string | number; enumValues: undefined; + generated: undefined; }>; export class MySqlDateBuilder> extends MySqlColumnBuilder { @@ -55,6 +56,7 @@ export type MySqlDateStringBuilderInitial = MySqlDateStrin data: string; driverParam: string | number; enumValues: undefined; + generated: undefined; }>; export class MySqlDateStringBuilder> diff --git a/drizzle-orm/src/mysql-core/columns/datetime.ts b/drizzle-orm/src/mysql-core/columns/datetime.ts index cfe9ce0b7..040c57130 100644 --- a/drizzle-orm/src/mysql-core/columns/datetime.ts +++ b/drizzle-orm/src/mysql-core/columns/datetime.ts @@ -12,6 +12,7 @@ export type MySqlDateTimeBuilderInitial = MySqlDateTimeBui data: Date; driverParam: string | number; enumValues: undefined; + generated: undefined; }>; export class MySqlDateTimeBuilder> @@ -68,8 +69,8 @@ export type MySqlDateTimeStringBuilderInitial = MySqlDateT columnType: 'MySqlDateTimeString'; data: string; driverParam: string | number; - enumValues: undefined; + generated: undefined; }>; export class MySqlDateTimeStringBuilder> diff --git a/drizzle-orm/src/mysql-core/columns/decimal.ts b/drizzle-orm/src/mysql-core/columns/decimal.ts index db2bd78ac..fa25d9cdb 100644 --- a/drizzle-orm/src/mysql-core/columns/decimal.ts +++ b/drizzle-orm/src/mysql-core/columns/decimal.ts @@ -11,6 +11,7 @@ export type MySqlDecimalBuilderInitial = MySqlDecimalBuild data: string; driverParam: string; enumValues: undefined; + generated: undefined; }>; export class MySqlDecimalBuilder< diff --git a/drizzle-orm/src/mysql-core/columns/double.ts b/drizzle-orm/src/mysql-core/columns/double.ts index 52dc66f72..dd349cf27 100644 --- a/drizzle-orm/src/mysql-core/columns/double.ts +++ b/drizzle-orm/src/mysql-core/columns/double.ts @@ -11,6 +11,7 @@ export type MySqlDoubleBuilderInitial = MySqlDoubleBuilder data: number; driverParam: number | string; enumValues: undefined; + generated: undefined; }>; export class MySqlDoubleBuilder> diff --git a/drizzle-orm/src/mysql-core/columns/enum.ts b/drizzle-orm/src/mysql-core/columns/enum.ts index a7d5399ed..1d8b4c1f5 100644 --- a/drizzle-orm/src/mysql-core/columns/enum.ts +++ b/drizzle-orm/src/mysql-core/columns/enum.ts @@ -13,6 +13,7 @@ export type MySqlEnumColumnBuilderInitial; export class MySqlEnumColumnBuilder> diff --git a/drizzle-orm/src/mysql-core/columns/float.ts b/drizzle-orm/src/mysql-core/columns/float.ts index 71b0291f3..b66f1e05a 100644 --- a/drizzle-orm/src/mysql-core/columns/float.ts +++ b/drizzle-orm/src/mysql-core/columns/float.ts @@ -11,6 +11,7 @@ export type MySqlFloatBuilderInitial = MySqlFloatBuilder<{ data: number; driverParam: number | string; enumValues: undefined; + generated: undefined; }>; export class MySqlFloatBuilder> diff --git a/drizzle-orm/src/mysql-core/columns/int.ts b/drizzle-orm/src/mysql-core/columns/int.ts index 4fa1bb936..dbfb85760 100644 --- a/drizzle-orm/src/mysql-core/columns/int.ts +++ b/drizzle-orm/src/mysql-core/columns/int.ts @@ -11,6 +11,7 @@ export type MySqlIntBuilderInitial = MySqlIntBuilder<{ data: number; driverParam: number | string; enumValues: undefined; + generated: undefined; }>; export class MySqlIntBuilder> diff --git a/drizzle-orm/src/mysql-core/columns/json.ts b/drizzle-orm/src/mysql-core/columns/json.ts index 9e52d7bf8..f30ea1534 100644 --- a/drizzle-orm/src/mysql-core/columns/json.ts +++ b/drizzle-orm/src/mysql-core/columns/json.ts @@ -11,6 +11,7 @@ export type MySqlJsonBuilderInitial = MySqlJsonBuilder<{ data: unknown; driverParam: string; enumValues: undefined; + generated: undefined; }>; export class MySqlJsonBuilder> extends MySqlColumnBuilder { diff --git a/drizzle-orm/src/mysql-core/columns/mediumint.ts b/drizzle-orm/src/mysql-core/columns/mediumint.ts index 9a9277fe0..268028b44 100644 --- a/drizzle-orm/src/mysql-core/columns/mediumint.ts +++ b/drizzle-orm/src/mysql-core/columns/mediumint.ts @@ -12,6 +12,7 @@ export type MySqlMediumIntBuilderInitial = MySqlMediumIntB data: number; driverParam: number | string; enumValues: undefined; + generated: undefined; }>; export class MySqlMediumIntBuilder> diff --git a/drizzle-orm/src/mysql-core/columns/real.ts b/drizzle-orm/src/mysql-core/columns/real.ts index 37607d9c5..7dd41dda0 100644 --- a/drizzle-orm/src/mysql-core/columns/real.ts +++ b/drizzle-orm/src/mysql-core/columns/real.ts @@ -11,6 +11,7 @@ export type MySqlRealBuilderInitial = MySqlRealBuilder<{ data: number; driverParam: number | string; enumValues: undefined; + generated: undefined; }>; export class MySqlRealBuilder> diff --git a/drizzle-orm/src/mysql-core/columns/serial.ts b/drizzle-orm/src/mysql-core/columns/serial.ts index 5a555c52a..0f87f0bf5 100644 --- a/drizzle-orm/src/mysql-core/columns/serial.ts +++ b/drizzle-orm/src/mysql-core/columns/serial.ts @@ -2,6 +2,8 @@ import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, HasDefault, + IsAutoincrement, + IsPrimaryKey, MakeColumnConfig, NotNull, } from '~/column-builder.ts'; @@ -10,16 +12,21 @@ import { entityKind } from '~/entity.ts'; import type { AnyMySqlTable } from '~/mysql-core/table.ts'; import { MySqlColumnBuilderWithAutoIncrement, MySqlColumnWithAutoIncrement } from './common.ts'; -export type MySqlSerialBuilderInitial = NotNull< - HasDefault< - MySqlSerialBuilder<{ - name: TName; - dataType: 'number'; - columnType: 'MySqlSerial'; - data: number; - driverParam: number; - enumValues: undefined; - }> +export type MySqlSerialBuilderInitial = IsAutoincrement< + IsPrimaryKey< + NotNull< + HasDefault< + MySqlSerialBuilder<{ + name: TName; + dataType: 'number'; + columnType: 'MySqlSerial'; + data: number; + driverParam: number; + enumValues: undefined; + generated: undefined; + }> + > + > > >; diff --git a/drizzle-orm/src/mysql-core/columns/smallint.ts b/drizzle-orm/src/mysql-core/columns/smallint.ts index e4653f5dd..fc1dd0d55 100644 --- a/drizzle-orm/src/mysql-core/columns/smallint.ts +++ b/drizzle-orm/src/mysql-core/columns/smallint.ts @@ -12,6 +12,7 @@ export type MySqlSmallIntBuilderInitial = MySqlSmallIntBui data: number; driverParam: number | string; enumValues: undefined; + generated: undefined; }>; export class MySqlSmallIntBuilder> diff --git a/drizzle-orm/src/mysql-core/columns/text.ts b/drizzle-orm/src/mysql-core/columns/text.ts index 8a4a30822..72c232e16 100644 --- a/drizzle-orm/src/mysql-core/columns/text.ts +++ b/drizzle-orm/src/mysql-core/columns/text.ts @@ -14,6 +14,7 @@ export type MySqlTextBuilderInitial; export class MySqlTextBuilder> extends MySqlColumnBuilder< diff --git a/drizzle-orm/src/mysql-core/columns/time.ts b/drizzle-orm/src/mysql-core/columns/time.ts index d3a86dcc4..ae2251bda 100644 --- a/drizzle-orm/src/mysql-core/columns/time.ts +++ b/drizzle-orm/src/mysql-core/columns/time.ts @@ -11,6 +11,7 @@ export type MySqlTimeBuilderInitial = MySqlTimeBuilder<{ data: string; driverParam: string | number; enumValues: undefined; + generated: undefined; }>; export class MySqlTimeBuilder> extends MySqlColumnBuilder< diff --git a/drizzle-orm/src/mysql-core/columns/timestamp.ts b/drizzle-orm/src/mysql-core/columns/timestamp.ts index 3b6df80d3..24e3b2650 100644 --- a/drizzle-orm/src/mysql-core/columns/timestamp.ts +++ b/drizzle-orm/src/mysql-core/columns/timestamp.ts @@ -12,6 +12,7 @@ export type MySqlTimestampBuilderInitial = MySqlTimestampB data: Date; driverParam: string | number; enumValues: undefined; + generated: undefined; }>; export class MySqlTimestampBuilder> @@ -63,6 +64,7 @@ export type MySqlTimestampStringBuilderInitial = MySqlTime data: string; driverParam: string | number; enumValues: undefined; + generated: undefined; }>; export class MySqlTimestampStringBuilder> diff --git a/drizzle-orm/src/mysql-core/columns/tinyint.ts b/drizzle-orm/src/mysql-core/columns/tinyint.ts index 35a68cbd2..c749e6da8 100644 --- a/drizzle-orm/src/mysql-core/columns/tinyint.ts +++ b/drizzle-orm/src/mysql-core/columns/tinyint.ts @@ -12,6 +12,7 @@ export type MySqlTinyIntBuilderInitial = MySqlTinyIntBuild data: number; driverParam: number | string; enumValues: undefined; + generated: undefined; }>; export class MySqlTinyIntBuilder> diff --git a/drizzle-orm/src/mysql-core/columns/varbinary.ts b/drizzle-orm/src/mysql-core/columns/varbinary.ts index a4a856509..be0a89cf6 100644 --- a/drizzle-orm/src/mysql-core/columns/varbinary.ts +++ b/drizzle-orm/src/mysql-core/columns/varbinary.ts @@ -11,6 +11,7 @@ export type MySqlVarBinaryBuilderInitial = MySqlVarBinaryB data: string; driverParam: string; enumValues: undefined; + generated: undefined; }>; export class MySqlVarBinaryBuilder> diff --git a/drizzle-orm/src/mysql-core/columns/varchar.ts b/drizzle-orm/src/mysql-core/columns/varchar.ts index 7db55563f..b692bf789 100644 --- a/drizzle-orm/src/mysql-core/columns/varchar.ts +++ b/drizzle-orm/src/mysql-core/columns/varchar.ts @@ -13,6 +13,7 @@ export type MySqlVarCharBuilderInitial; diff --git a/drizzle-orm/src/mysql-core/columns/year.ts b/drizzle-orm/src/mysql-core/columns/year.ts index 0e1a64d36..224de12e9 100644 --- a/drizzle-orm/src/mysql-core/columns/year.ts +++ b/drizzle-orm/src/mysql-core/columns/year.ts @@ -11,6 +11,7 @@ export type MySqlYearBuilderInitial = MySqlYearBuilder<{ data: number; driverParam: number; enumValues: undefined; + generated: undefined; }>; export class MySqlYearBuilder> extends MySqlColumnBuilder { diff --git a/drizzle-orm/src/mysql-core/dialect.ts b/drizzle-orm/src/mysql-core/dialect.ts index 215021898..0e2f7380a 100644 --- a/drizzle-orm/src/mysql-core/dialect.ts +++ b/drizzle-orm/src/mysql-core/dialect.ts @@ -412,15 +412,22 @@ export class MySqlDialect { return sql`${leftChunk}${operatorChunk}${rightChunk}${orderBySql}${limitSql}${offsetSql}`; } - buildInsertQuery({ table, values, ignore, onConflict }: MySqlInsertConfig): SQL { + buildInsertQuery( + { table, values, ignore, onConflict }: MySqlInsertConfig, + ): { sql: SQL; generatedIds: Record[] } { // const isSingleValue = values.length === 1; const valuesSqlList: ((SQLChunk | SQL)[] | SQL)[] = []; const columns: Record = table[Table.Symbol.Columns]; - const colEntries: [string, MySqlColumn][] = Object.entries(columns); + const colEntries: [string, MySqlColumn][] = Object.entries(columns).filter(([_, col]) => + !col.shouldDisableInsert() + ); const insertOrder = colEntries.map(([, column]) => sql.identifier(column.name)); + const generatedIdsResponse: Record[] = []; for (const [valueIndex, value] of values.entries()) { + const generatedIds: Record = {}; + const valueList: (SQLChunk | SQL)[] = []; for (const [fieldName, col] of colEntries) { const colValue = value[fieldName]; @@ -428,6 +435,7 @@ export class MySqlDialect { // eslint-disable-next-line unicorn/no-negated-condition if (col.defaultFn !== undefined) { const defaultFnResult = col.defaultFn(); + generatedIds[fieldName] = defaultFnResult; const defaultValue = is(defaultFnResult, SQL) ? defaultFnResult : sql.param(defaultFnResult, col); valueList.push(defaultValue); // eslint-disable-next-line unicorn/no-negated-condition @@ -439,9 +447,14 @@ export class MySqlDialect { valueList.push(sql`default`); } } else { + if (col.defaultFn && is(colValue, Param)) { + generatedIds[fieldName] = colValue.value; + } valueList.push(colValue); } } + + generatedIdsResponse.push(generatedIds); valuesSqlList.push(valueList); if (valueIndex < values.length - 1) { valuesSqlList.push(sql`, `); @@ -454,7 +467,10 @@ export class MySqlDialect { const onConflictSql = onConflict ? sql` on duplicate key ${onConflict}` : undefined; - return sql`insert${ignoreSql} into ${table} ${insertOrder} values ${valuesSql}${onConflictSql}`; + return { + sql: sql`insert${ignoreSql} into ${table} ${insertOrder} values ${valuesSql}${onConflictSql}`, + generatedIds: generatedIdsResponse, + }; } sqlToQuery(sql: SQL, invokeSource?: 'indexes' | undefined): QueryWithTypings { diff --git a/drizzle-orm/src/mysql-core/query-builders/insert.ts b/drizzle-orm/src/mysql-core/query-builders/insert.ts index 9b1b5c94e..97e61de74 100644 --- a/drizzle-orm/src/mysql-core/query-builders/insert.ts +++ b/drizzle-orm/src/mysql-core/query-builders/insert.ts @@ -11,10 +11,14 @@ import type { } from '~/mysql-core/session.ts'; import type { MySqlTable } from '~/mysql-core/table.ts'; import { QueryPromise } from '~/query-promise.ts'; +import type { RunnableQuery } from '~/runnable-query.ts'; import type { Placeholder, Query, SQLWrapper } from '~/sql/sql.ts'; import { Param, SQL, sql } from '~/sql/sql.ts'; +import type { InferModelFromColumns } from '~/table.ts'; import { Table } from '~/table.ts'; -import { mapUpdateSet } from '~/utils.ts'; +import { mapUpdateSet, orderSelectedFields } from '~/utils.ts'; +import type { AnyMySqlColumn, MySqlColumn } from '../columns/common.ts'; +import type { SelectedFieldsOrdered } from './select.types.ts'; import type { MySqlUpdateSetSource } from './update.ts'; export interface MySqlInsertConfig { @@ -22,6 +26,7 @@ export interface MySqlInsertConfig { values: Record[]; ignore: boolean; onConflict?: SQL; + returning?: SelectedFieldsOrdered; } export type AnyMySqlInsertConfig = MySqlInsertConfig; @@ -82,8 +87,9 @@ export type MySqlInsertWithout, T['_']['excludedMethods'] | K >; @@ -91,13 +97,17 @@ export type MySqlInsertWithout = MySqlInsert< T['_']['table'], T['_']['queryResult'], - T['_']['preparedQueryHKT'] + T['_']['preparedQueryHKT'], + T['_']['returning'] >; -export type MySqlInsertPrepare = PreparedQueryKind< +export type MySqlInsertPrepare< + T extends AnyMySqlInsert, + TReturning extends Record | undefined = undefined, +> = PreparedQueryKind< T['_']['preparedQueryHKT'], MySqlPreparedQueryConfig & { - execute: MySqlQueryResultKind; + execute: TReturning extends undefined ? MySqlQueryResultKind : TReturning[]; iterator: never; }, true @@ -111,36 +121,75 @@ export type MySqlInsert< TTable extends MySqlTable = MySqlTable, TQueryResult extends MySqlQueryResultHKT = AnyMySqlQueryResultHKT, TPreparedQueryHKT extends PreparedQueryHKTBase = PreparedQueryHKTBase, -> = MySqlInsertBase; + TReturning extends Record | undefined = Record | undefined, +> = MySqlInsertBase; + +export type MySqlInsertReturning< + T extends AnyMySqlInsert, + TDynamic extends boolean, +> = MySqlInsertBase< + T['_']['table'], + T['_']['queryResult'], + T['_']['preparedQueryHKT'], + InferModelFromColumns>, + TDynamic, + T['_']['excludedMethods'] | '$returning' +>; -export type AnyMySqlInsert = MySqlInsertBase; +export type AnyMySqlInsert = MySqlInsertBase; export interface MySqlInsertBase< TTable extends MySqlTable, TQueryResult extends MySqlQueryResultHKT, TPreparedQueryHKT extends PreparedQueryHKTBase, + TReturning extends Record | undefined = undefined, TDynamic extends boolean = false, TExcludedMethods extends string = never, -> extends QueryPromise>, SQLWrapper { +> extends + QueryPromise : TReturning[]>, + RunnableQuery : TReturning[], 'mysql'>, + SQLWrapper +{ readonly _: { + readonly dialect: 'mysql'; readonly table: TTable; readonly queryResult: TQueryResult; readonly preparedQueryHKT: TPreparedQueryHKT; readonly dynamic: TDynamic; readonly excludedMethods: TExcludedMethods; + readonly returning: TReturning; + readonly result: TReturning extends undefined ? MySqlQueryResultKind : TReturning[]; }; } +export type PrimaryKeyKeys> = { + [K in keyof T]: T[K]['_']['isPrimaryKey'] extends true ? T[K]['_']['isAutoincrement'] extends true ? K + : T[K]['_']['hasRuntimeDefault'] extends true ? T[K]['_']['isPrimaryKey'] extends true ? K : never + : never + : T[K]['_']['hasRuntimeDefault'] extends true ? T[K]['_']['isPrimaryKey'] extends true ? K : never + : never; +}[keyof T]; + +export type GetPrimarySerialOrDefaultKeys> = { + [K in PrimaryKeyKeys]: T[K]; +}; + export class MySqlInsertBase< TTable extends MySqlTable, TQueryResult extends MySqlQueryResultHKT, // eslint-disable-next-line @typescript-eslint/no-unused-vars TPreparedQueryHKT extends PreparedQueryHKTBase, // eslint-disable-next-line @typescript-eslint/no-unused-vars + TReturning extends Record | undefined = undefined, + // eslint-disable-next-line @typescript-eslint/no-unused-vars TDynamic extends boolean = false, // eslint-disable-next-line @typescript-eslint/no-unused-vars TExcludedMethods extends string = never, -> extends QueryPromise> implements SQLWrapper { +> extends QueryPromise : TReturning[]> + implements + RunnableQuery : TReturning[], 'mysql'>, + SQLWrapper +{ static readonly [entityKind]: string = 'MySqlInsert'; declare protected $table: TTable; @@ -192,9 +241,24 @@ export class MySqlInsertBase< return this as any; } + $returningId(): MySqlInsertWithout< + MySqlInsertReturning, + TDynamic, + '$returningId' + > { + const returning: SelectedFieldsOrdered = []; + for (const [key, value] of Object.entries(this.config.table[Table.Symbol.Columns])) { + if (value.primary) { + returning.push({ field: value, path: [key] }); + } + } + this.config.returning = orderSelectedFields(this.config.table[Table.Symbol.Columns]); + return this as any; + } + /** @internal */ getSQL(): SQL { - return this.dialect.buildInsertQuery(this.config); + return this.dialect.buildInsertQuery(this.config).sql; } toSQL(): Query { @@ -202,11 +266,15 @@ export class MySqlInsertBase< return rest; } - prepare(): MySqlInsertPrepare { + prepare(): MySqlInsertPrepare { + const { sql, generatedIds } = this.dialect.buildInsertQuery(this.config); return this.session.prepareQuery( - this.dialect.sqlToQuery(this.getSQL()), + this.dialect.sqlToQuery(sql), + undefined, undefined, - ) as MySqlInsertPrepare; + generatedIds, + this.config.returning, + ) as MySqlInsertPrepare; } override execute: ReturnType['execute'] = (placeholderValues) => { diff --git a/drizzle-orm/src/mysql-core/query-builders/update.ts b/drizzle-orm/src/mysql-core/query-builders/update.ts index 08dbf53a9..7884599cf 100644 --- a/drizzle-orm/src/mysql-core/query-builders/update.ts +++ b/drizzle-orm/src/mysql-core/query-builders/update.ts @@ -27,7 +27,7 @@ export interface MySqlUpdateConfig { export type MySqlUpdateSetSource = & { - [Key in keyof TTable['_']['columns']]?: + [Key in keyof TTable['$inferInsert']]?: | GetColumnData | SQL; } diff --git a/drizzle-orm/src/mysql-core/session.ts b/drizzle-orm/src/mysql-core/session.ts index 2dd1e6dcc..08ad1ebb6 100644 --- a/drizzle-orm/src/mysql-core/session.ts +++ b/drizzle-orm/src/mysql-core/session.ts @@ -73,6 +73,8 @@ export abstract class MySqlSession< query: Query, fields: SelectedFieldsOrdered | undefined, customResultMapper?: (rows: unknown[][]) => T['execute'], + generatedIds?: Record[], + returningIds?: SelectedFieldsOrdered, ): PreparedQueryKind; execute(query: SQL): Promise { diff --git a/drizzle-orm/src/mysql-proxy/driver.ts b/drizzle-orm/src/mysql-proxy/driver.ts index e9008f816..574db42c1 100644 --- a/drizzle-orm/src/mysql-proxy/driver.ts +++ b/drizzle-orm/src/mysql-proxy/driver.ts @@ -18,7 +18,7 @@ export type RemoteCallback = ( sql: string, params: any[], method: 'all' | 'execute', -) => Promise<{ rows: any[] }>; +) => Promise<{ rows: any[]; insertId?: number; affectedRows?: number }>; export function drizzle = Record>( callback: RemoteCallback, diff --git a/drizzle-orm/src/mysql-proxy/session.ts b/drizzle-orm/src/mysql-proxy/session.ts index c5ab0295d..03039cfb2 100644 --- a/drizzle-orm/src/mysql-proxy/session.ts +++ b/drizzle-orm/src/mysql-proxy/session.ts @@ -1,5 +1,6 @@ import type { FieldPacket, ResultSetHeader } from 'mysql2/promise'; -import { entityKind } from '~/entity.ts'; +import { Column } from '~/column.ts'; +import { entityKind, is } from '~/entity.ts'; import type { Logger } from '~/logger.ts'; import { NoopLogger } from '~/logger.ts'; import type { MySqlDialect } from '~/mysql-core/dialect.ts'; @@ -47,6 +48,8 @@ export class MySqlRemoteSession< query: Query, fields: SelectedFieldsOrdered | undefined, customResultMapper?: (rows: unknown[][]) => T['execute'], + generatedIds?: Record[], + returningIds?: SelectedFieldsOrdered, ): PreparedQueryKind { return new PreparedQuery( this.client, @@ -55,6 +58,8 @@ export class MySqlRemoteSession< this.logger, fields, customResultMapper, + generatedIds, + returningIds, ) as PreparedQueryKind; } @@ -95,6 +100,10 @@ export class PreparedQuery extends PreparedQ private logger: Logger, private fields: SelectedFieldsOrdered | undefined, private customResultMapper?: (rows: unknown[][]) => T['execute'], + // Keys that were used in $default and the value that was generated for them + private generatedIds?: Record[], + // Keys that should be returned, it has the column with all properries + key from object + private returningIds?: SelectedFieldsOrdered, ) { super(); } @@ -102,14 +111,41 @@ export class PreparedQuery extends PreparedQ async execute(placeholderValues: Record | undefined = {}): Promise { const params = fillPlaceholders(this.params, placeholderValues); - const { fields, client, queryString, logger, joinsNotNullableMap, customResultMapper } = this; + const { fields, client, queryString, logger, joinsNotNullableMap, customResultMapper, returningIds, generatedIds } = + this; logger.logQuery(queryString, params); if (!fields && !customResultMapper) { - const { rows } = await client(queryString, params, 'execute'); - - return rows; + const { rows: data } = await client(queryString, params, 'execute'); + + const insertId = data[0].insertId as number; + const affectedRows = data[0].affectedRows; + + if (returningIds) { + const returningResponse = []; + let j = 0; + for (let i = insertId; i < insertId + affectedRows; i++) { + for (const column of returningIds) { + const key = returningIds[0]!.path[0]!; + if (is(column.field, Column)) { + // @ts-ignore + if (column.field.primary && column.field.autoIncrement) { + returningResponse.push({ [key]: i }); + } + if (column.field.defaultFn && generatedIds) { + // generatedIds[rowIdx][key] + returningResponse.push({ [key]: generatedIds[j]![key] }); + } + } + } + j++; + } + + return returningResponse; + } + + return data; } const { rows } = await client(queryString, params, 'all'); diff --git a/drizzle-orm/src/mysql2/session.ts b/drizzle-orm/src/mysql2/session.ts index be7005c9c..ab11d1f17 100644 --- a/drizzle-orm/src/mysql2/session.ts +++ b/drizzle-orm/src/mysql2/session.ts @@ -10,7 +10,8 @@ import type { RowDataPacket, } from 'mysql2/promise'; import { once } from 'node:events'; -import { entityKind } from '~/entity.ts'; +import { Column } from '~/column.ts'; +import { entityKind, is } from '~/entity.ts'; import type { Logger } from '~/logger.ts'; import { NoopLogger } from '~/logger.ts'; import type { MySqlDialect } from '~/mysql-core/dialect.ts'; @@ -27,7 +28,8 @@ import { type PreparedQueryKind, } from '~/mysql-core/session.ts'; import type { RelationalSchemaConfig, TablesRelationalConfig } from '~/relations.ts'; -import { fillPlaceholders, type Query, type SQL, sql } from '~/sql/sql.ts'; +import { fillPlaceholders, sql } from '~/sql/sql.ts'; +import type { Query, SQL } from '~/sql/sql.ts'; import { type Assume, mapResultRow } from '~/utils.ts'; export type MySql2Client = Pool | Connection; @@ -51,6 +53,10 @@ export class MySql2PreparedQuery extends MyS private logger: Logger, private fields: SelectedFieldsOrdered | undefined, private customResultMapper?: (rows: unknown[][]) => T['execute'], + // Keys that were used in $default and the value that was generated for them + private generatedIds?: Record[], + // Keys that should be returned, it has the column with all properries + key from object + private returningIds?: SelectedFieldsOrdered, ) { super(); this.rawQuery = { @@ -80,9 +86,36 @@ export class MySql2PreparedQuery extends MyS this.logger.logQuery(this.rawQuery.sql, params); - const { fields, client, rawQuery, query, joinsNotNullableMap, customResultMapper } = this; + const { fields, client, rawQuery, query, joinsNotNullableMap, customResultMapper, returningIds, generatedIds } = + this; if (!fields && !customResultMapper) { - return client.query(rawQuery, params); + const res = await client.query(rawQuery, params); + const insertId = res[0].insertId; + const affectedRows = res[0].affectedRows; + // for each row, I need to check keys from + if (returningIds) { + const returningResponse = []; + let j = 0; + for (let i = insertId; i < insertId + affectedRows; i++) { + for (const column of returningIds) { + const key = returningIds[0]!.path[0]!; + if (is(column.field, Column)) { + // @ts-ignore + if (column.field.primary && column.field.autoIncrement) { + returningResponse.push({ [key]: i }); + } + if (column.field.defaultFn && generatedIds) { + // generatedIds[rowIdx][key] + returningResponse.push({ [key]: generatedIds[j]![key] }); + } + } + } + j++; + } + + return returningResponse; + } + return res; } const result = await client.query(query, params); @@ -177,7 +210,11 @@ export class MySql2Session< query: Query, fields: SelectedFieldsOrdered | undefined, customResultMapper?: (rows: unknown[][]) => T['execute'], + generatedIds?: Record[], + returningIds?: SelectedFieldsOrdered, ): PreparedQueryKind { + // Add returningId fields + // Each driver gets them from response from database return new MySql2PreparedQuery( this.client, query.sql, @@ -185,6 +222,8 @@ export class MySql2Session< this.logger, fields, customResultMapper, + generatedIds, + returningIds, ) as PreparedQueryKind; } diff --git a/drizzle-orm/src/operations.ts b/drizzle-orm/src/operations.ts index 09cf41b8a..492bb3f2a 100644 --- a/drizzle-orm/src/operations.ts +++ b/drizzle-orm/src/operations.ts @@ -8,10 +8,18 @@ export type RequiredKeyOnly = T extends A }> ? TKey : never; +export type NotGenerated = T extends AnyColumn<{ + generated: undefined; +}> ? TKey + : never; + export type OptionalKeyOnly< TKey extends string, T extends Column, -> = TKey extends RequiredKeyOnly ? never : TKey; +> = TKey extends RequiredKeyOnly ? never + : TKey extends NotGenerated ? TKey + : T['_']['generated'] extends object ? T['_']['generated']['type'] extends 'byDefault' ? TKey : never + : never; export type SelectedFieldsFlat = Record< string, diff --git a/drizzle-orm/src/pg-core/columns/bigint.ts b/drizzle-orm/src/pg-core/columns/bigint.ts index af2d8b036..81f40d4e9 100644 --- a/drizzle-orm/src/pg-core/columns/bigint.ts +++ b/drizzle-orm/src/pg-core/columns/bigint.ts @@ -3,7 +3,8 @@ import type { ColumnBaseConfig } from '~/column.ts'; import { entityKind } from '~/entity.ts'; import type { AnyPgTable } from '~/pg-core/table.ts'; -import { PgColumn, PgColumnBuilder } from './common.ts'; +import { PgColumn } from './common.ts'; +import { PgIntColumnBaseBuilder } from './int.common.ts'; export type PgBigInt53BuilderInitial = PgBigInt53Builder<{ name: TName; @@ -12,9 +13,12 @@ export type PgBigInt53BuilderInitial = PgBigInt53Builder<{ data: number; driverParam: number | string; enumValues: undefined; + generated: undefined; }>; -export class PgBigInt53Builder> extends PgColumnBuilder { +export class PgBigInt53Builder> + extends PgIntColumnBaseBuilder +{ static readonly [entityKind]: string = 'PgBigInt53Builder'; constructor(name: T['name']) { @@ -51,9 +55,12 @@ export type PgBigInt64BuilderInitial = PgBigInt64Builder<{ data: bigint; driverParam: string; enumValues: undefined; + generated: undefined; }>; -export class PgBigInt64Builder> extends PgColumnBuilder { +export class PgBigInt64Builder> + extends PgIntColumnBaseBuilder +{ static readonly [entityKind]: string = 'PgBigInt64Builder'; constructor(name: T['name']) { diff --git a/drizzle-orm/src/pg-core/columns/bigserial.ts b/drizzle-orm/src/pg-core/columns/bigserial.ts index 69917678f..0cca21577 100644 --- a/drizzle-orm/src/pg-core/columns/bigserial.ts +++ b/drizzle-orm/src/pg-core/columns/bigserial.ts @@ -19,6 +19,7 @@ export type PgBigSerial53BuilderInitial = NotNull< data: number; driverParam: number; enumValues: undefined; + generated: undefined; }> > >; @@ -69,6 +70,7 @@ export type PgBigSerial64BuilderInitial = NotNull< data: bigint; driverParam: string; enumValues: undefined; + generated: undefined; }> > >; diff --git a/drizzle-orm/src/pg-core/columns/boolean.ts b/drizzle-orm/src/pg-core/columns/boolean.ts index 83135e3de..f4670f1a6 100644 --- a/drizzle-orm/src/pg-core/columns/boolean.ts +++ b/drizzle-orm/src/pg-core/columns/boolean.ts @@ -11,6 +11,7 @@ export type PgBooleanBuilderInitial = PgBooleanBuilder<{ data: boolean; driverParam: boolean; enumValues: undefined; + generated: undefined; }>; export class PgBooleanBuilder> extends PgColumnBuilder { diff --git a/drizzle-orm/src/pg-core/columns/char.ts b/drizzle-orm/src/pg-core/columns/char.ts index 85eb65954..9f33de4ae 100644 --- a/drizzle-orm/src/pg-core/columns/char.ts +++ b/drizzle-orm/src/pg-core/columns/char.ts @@ -12,6 +12,7 @@ export type PgCharBuilderInitial; export class PgCharBuilder> extends PgColumnBuilder< diff --git a/drizzle-orm/src/pg-core/columns/cidr.ts b/drizzle-orm/src/pg-core/columns/cidr.ts index 2f37d0348..9c2e9e19f 100644 --- a/drizzle-orm/src/pg-core/columns/cidr.ts +++ b/drizzle-orm/src/pg-core/columns/cidr.ts @@ -11,6 +11,7 @@ export type PgCidrBuilderInitial = PgCidrBuilder<{ data: string; driverParam: string; enumValues: undefined; + generated: undefined; }>; export class PgCidrBuilder> extends PgColumnBuilder { diff --git a/drizzle-orm/src/pg-core/columns/common.ts b/drizzle-orm/src/pg-core/columns/common.ts index 346ea6466..a7440e24c 100644 --- a/drizzle-orm/src/pg-core/columns/common.ts +++ b/drizzle-orm/src/pg-core/columns/common.ts @@ -4,6 +4,8 @@ import type { ColumnBuilderExtraConfig, ColumnBuilderRuntimeConfig, ColumnDataType, + GeneratedColumnConfig, + HasGenerated, MakeColumnConfig, } from '~/column-builder.ts'; import { ColumnBuilder } from '~/column-builder.ts'; @@ -12,6 +14,7 @@ import { Column } from '~/column.ts'; import { entityKind, is } from '~/entity.ts'; import type { Update } from '~/utils.ts'; +import type { SQL } from '~/index.ts'; import type { ForeignKey, UpdateDeleteAction } from '~/pg-core/foreign-keys.ts'; import { ForeignKeyBuilder } from '~/pg-core/foreign-keys.ts'; import type { AnyPgTable, PgTable } from '~/pg-core/table.ts'; @@ -53,6 +56,7 @@ export abstract class PgColumnBuilder< data: T['data'][]; driverParam: T['driverParam'][] | string; enumValues: T['enumValues']; + generated: GeneratedColumnConfig; } & (T extends { notNull: true } ? { notNull: true } : {}) & (T extends { hasDefault: true } ? { hasDefault: true } : {}), @@ -79,6 +83,15 @@ export abstract class PgColumnBuilder< return this; } + generatedAlwaysAs(as: SQL | T['data'] | (() => SQL)): HasGenerated { + this.config.generated = { + as, + type: 'always', + mode: 'stored', + }; + return this as any; + } + /** @internal */ buildForeignKeys(column: PgColumn, table: PgTable): ForeignKey[] { return this.foreignKeyConfigs.map(({ ref, actions }) => { diff --git a/drizzle-orm/src/pg-core/columns/custom.ts b/drizzle-orm/src/pg-core/columns/custom.ts index 7af6c73d1..4249e326c 100644 --- a/drizzle-orm/src/pg-core/columns/custom.ts +++ b/drizzle-orm/src/pg-core/columns/custom.ts @@ -14,6 +14,7 @@ export type ConvertCustomConfig = PgDateBuilder<{ data: Date; driverParam: string; enumValues: undefined; + generated: undefined; }>; export class PgDateBuilder> extends PgDateColumnBaseBuilder { @@ -52,6 +53,7 @@ export type PgDateStringBuilderInitial = PgDateStringBuild data: string; driverParam: string; enumValues: undefined; + generated: undefined; }>; export class PgDateStringBuilder> diff --git a/drizzle-orm/src/pg-core/columns/double-precision.ts b/drizzle-orm/src/pg-core/columns/double-precision.ts index a6bbdc6ff..879c25ccb 100644 --- a/drizzle-orm/src/pg-core/columns/double-precision.ts +++ b/drizzle-orm/src/pg-core/columns/double-precision.ts @@ -11,6 +11,7 @@ export type PgDoublePrecisionBuilderInitial = PgDoublePrec data: number; driverParam: string | number; enumValues: undefined; + generated: undefined; }>; export class PgDoublePrecisionBuilder> diff --git a/drizzle-orm/src/pg-core/columns/enum.ts b/drizzle-orm/src/pg-core/columns/enum.ts index 912e9057e..9fbdc15c1 100644 --- a/drizzle-orm/src/pg-core/columns/enum.ts +++ b/drizzle-orm/src/pg-core/columns/enum.ts @@ -13,6 +13,7 @@ export type PgEnumColumnBuilderInitial; const isPgEnumSym = Symbol.for('drizzle:isPgEnum'); diff --git a/drizzle-orm/src/pg-core/columns/inet.ts b/drizzle-orm/src/pg-core/columns/inet.ts index cdca1797d..f8e473de7 100644 --- a/drizzle-orm/src/pg-core/columns/inet.ts +++ b/drizzle-orm/src/pg-core/columns/inet.ts @@ -11,6 +11,7 @@ export type PgInetBuilderInitial = PgInetBuilder<{ data: string; driverParam: string; enumValues: undefined; + generated: undefined; }>; export class PgInetBuilder> extends PgColumnBuilder { diff --git a/drizzle-orm/src/pg-core/columns/int.common.ts b/drizzle-orm/src/pg-core/columns/int.common.ts new file mode 100644 index 000000000..07c26ba9e --- /dev/null +++ b/drizzle-orm/src/pg-core/columns/int.common.ts @@ -0,0 +1,62 @@ +import type { + ColumnBuilderBaseConfig, + ColumnDataType, + GeneratedIdentityConfig, + IsIdentityByDefault, +} from '~/column-builder.ts'; +import { entityKind } from '~/entity.ts'; +import type { PgSequenceOptions } from '../sequence.ts'; +import { PgColumnBuilder } from './common.ts'; + +export abstract class PgIntColumnBaseBuilder< + T extends ColumnBuilderBaseConfig, +> extends PgColumnBuilder< + T, + { generatedIdentity: GeneratedIdentityConfig } +> { + static readonly [entityKind]: string = 'PgIntColumnBaseBuilder'; + + generatedAlwaysAsIdentity( + sequence?: PgSequenceOptions & { name?: string }, + ): IsIdentityByDefault { + if (sequence) { + const { name, ...options } = sequence; + this.config.generatedIdentity = { + type: 'always', + sequenceName: name, + sequenceOptions: options, + }; + } else { + this.config.generatedIdentity = { + type: 'always', + }; + } + + this.config.hasDefault = true; + this.config.notNull = true; + + return this as any; + } + + generatedByDefaultAsIdentity( + sequence?: PgSequenceOptions & { name?: string }, + ): IsIdentityByDefault { + if (sequence) { + const { name, ...options } = sequence; + this.config.generatedIdentity = { + type: 'byDefault', + sequenceName: name, + sequenceOptions: options, + }; + } else { + this.config.generatedIdentity = { + type: 'byDefault', + }; + } + + this.config.hasDefault = true; + this.config.notNull = true; + + return this as any; + } +} diff --git a/drizzle-orm/src/pg-core/columns/integer.ts b/drizzle-orm/src/pg-core/columns/integer.ts index 3ef9e248c..2c35c1e29 100644 --- a/drizzle-orm/src/pg-core/columns/integer.ts +++ b/drizzle-orm/src/pg-core/columns/integer.ts @@ -2,7 +2,8 @@ import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnCon import type { ColumnBaseConfig } from '~/column.ts'; import { entityKind } from '~/entity.ts'; import type { AnyPgTable } from '../table.ts'; -import { PgColumn, PgColumnBuilder } from './common.ts'; +import { PgColumn } from './common.ts'; +import { PgIntColumnBaseBuilder } from './int.common.ts'; type PgIntegerBuilderInitial = PgIntegerBuilder<{ name: TName; @@ -11,9 +12,12 @@ type PgIntegerBuilderInitial = PgIntegerBuilder<{ data: number; driverParam: number | string; enumValues: undefined; + generated: undefined; }>; -export class PgIntegerBuilder> extends PgColumnBuilder { +export class PgIntegerBuilder> + extends PgIntColumnBaseBuilder +{ static readonly [entityKind]: string = 'PgIntegerBuilder'; constructor(name: T['name']) { diff --git a/drizzle-orm/src/pg-core/columns/interval.ts b/drizzle-orm/src/pg-core/columns/interval.ts index c8b77d9a2..c70dd0c04 100644 --- a/drizzle-orm/src/pg-core/columns/interval.ts +++ b/drizzle-orm/src/pg-core/columns/interval.ts @@ -12,6 +12,7 @@ export type PgIntervalBuilderInitial = PgIntervalBuilder<{ data: string; driverParam: string; enumValues: undefined; + generated: undefined; }>; export class PgIntervalBuilder> diff --git a/drizzle-orm/src/pg-core/columns/json.ts b/drizzle-orm/src/pg-core/columns/json.ts index c6c869eb7..ddb97b67e 100644 --- a/drizzle-orm/src/pg-core/columns/json.ts +++ b/drizzle-orm/src/pg-core/columns/json.ts @@ -11,6 +11,7 @@ export type PgJsonBuilderInitial = PgJsonBuilder<{ data: unknown; driverParam: unknown; enumValues: undefined; + generated: undefined; }>; export class PgJsonBuilder> extends PgColumnBuilder< diff --git a/drizzle-orm/src/pg-core/columns/jsonb.ts b/drizzle-orm/src/pg-core/columns/jsonb.ts index 38d346b17..a44b26fd0 100644 --- a/drizzle-orm/src/pg-core/columns/jsonb.ts +++ b/drizzle-orm/src/pg-core/columns/jsonb.ts @@ -11,6 +11,7 @@ export type PgJsonbBuilderInitial = PgJsonbBuilder<{ data: unknown; driverParam: unknown; enumValues: undefined; + generated: undefined; }>; export class PgJsonbBuilder> extends PgColumnBuilder { diff --git a/drizzle-orm/src/pg-core/columns/line.ts b/drizzle-orm/src/pg-core/columns/line.ts index 8ff705481..bf4e653ad 100644 --- a/drizzle-orm/src/pg-core/columns/line.ts +++ b/drizzle-orm/src/pg-core/columns/line.ts @@ -13,6 +13,7 @@ export type PgLineBuilderInitial = PgLineBuilder<{ data: [number, number, number]; driverParam: number | string; enumValues: undefined; + generated: undefined; }>; export class PgLineBuilder> extends PgColumnBuilder { @@ -57,6 +58,7 @@ export type PgLineABCBuilderInitial = PgLineABCBuilder<{ data: { a: number; b: number; c: number }; driverParam: string; enumValues: undefined; + generated: undefined; }>; export class PgLineABCBuilder> extends PgColumnBuilder { diff --git a/drizzle-orm/src/pg-core/columns/macaddr.ts b/drizzle-orm/src/pg-core/columns/macaddr.ts index 189a56187..edc27f7c0 100644 --- a/drizzle-orm/src/pg-core/columns/macaddr.ts +++ b/drizzle-orm/src/pg-core/columns/macaddr.ts @@ -11,6 +11,7 @@ export type PgMacaddrBuilderInitial = PgMacaddrBuilder<{ data: string; driverParam: string; enumValues: undefined; + generated: undefined; }>; export class PgMacaddrBuilder> extends PgColumnBuilder { diff --git a/drizzle-orm/src/pg-core/columns/macaddr8.ts b/drizzle-orm/src/pg-core/columns/macaddr8.ts index cb78fc0b4..fc611d063 100644 --- a/drizzle-orm/src/pg-core/columns/macaddr8.ts +++ b/drizzle-orm/src/pg-core/columns/macaddr8.ts @@ -11,6 +11,7 @@ export type PgMacaddr8BuilderInitial = PgMacaddr8Builder<{ data: string; driverParam: string; enumValues: undefined; + generated: undefined; }>; export class PgMacaddr8Builder> extends PgColumnBuilder { diff --git a/drizzle-orm/src/pg-core/columns/numeric.ts b/drizzle-orm/src/pg-core/columns/numeric.ts index e3ea778e4..d740c1a4a 100644 --- a/drizzle-orm/src/pg-core/columns/numeric.ts +++ b/drizzle-orm/src/pg-core/columns/numeric.ts @@ -11,6 +11,7 @@ export type PgNumericBuilderInitial = PgNumericBuilder<{ data: string; driverParam: string; enumValues: undefined; + generated: undefined; }>; export class PgNumericBuilder> extends PgColumnBuilder< diff --git a/drizzle-orm/src/pg-core/columns/point.ts b/drizzle-orm/src/pg-core/columns/point.ts index 1e3ae1098..7bff25e55 100644 --- a/drizzle-orm/src/pg-core/columns/point.ts +++ b/drizzle-orm/src/pg-core/columns/point.ts @@ -13,6 +13,7 @@ export type PgPointTupleBuilderInitial = PgPointTupleBuild data: [number, number]; driverParam: number | string; enumValues: undefined; + generated: undefined; }>; export class PgPointTupleBuilder> @@ -62,6 +63,7 @@ export type PgPointObjectBuilderInitial = PgPointObjectBui data: { x: number; y: number }; driverParam: string; enumValues: undefined; + generated: undefined; }>; export class PgPointObjectBuilder> diff --git a/drizzle-orm/src/pg-core/columns/postgis_extension/geometry.ts b/drizzle-orm/src/pg-core/columns/postgis_extension/geometry.ts index ef84e20b5..5dc2b8955 100644 --- a/drizzle-orm/src/pg-core/columns/postgis_extension/geometry.ts +++ b/drizzle-orm/src/pg-core/columns/postgis_extension/geometry.ts @@ -14,6 +14,7 @@ export type PgGeometryBuilderInitial = PgGeometryBuilder<{ data: [number, number]; driverParam: string; enumValues: undefined; + generated: undefined; }>; export class PgGeometryBuilder> extends PgColumnBuilder { @@ -57,6 +58,7 @@ export type PgGeometryObjectBuilderInitial = PgGeometryObj data: { x: number; y: number }; driverParam: string; enumValues: undefined; + generated: undefined; }>; export class PgGeometryObjectBuilder> diff --git a/drizzle-orm/src/pg-core/columns/real.ts b/drizzle-orm/src/pg-core/columns/real.ts index 9059384db..6abe81441 100644 --- a/drizzle-orm/src/pg-core/columns/real.ts +++ b/drizzle-orm/src/pg-core/columns/real.ts @@ -11,6 +11,7 @@ export type PgRealBuilderInitial = PgRealBuilder<{ data: number; driverParam: string | number; enumValues: undefined; + generated: undefined; }>; export class PgRealBuilder> extends PgColumnBuilder< diff --git a/drizzle-orm/src/pg-core/columns/serial.ts b/drizzle-orm/src/pg-core/columns/serial.ts index b4ac9ed6b..a15619a87 100644 --- a/drizzle-orm/src/pg-core/columns/serial.ts +++ b/drizzle-orm/src/pg-core/columns/serial.ts @@ -19,6 +19,7 @@ export type PgSerialBuilderInitial = NotNull< data: number; driverParam: number; enumValues: undefined; + generated: undefined; }> > >; diff --git a/drizzle-orm/src/pg-core/columns/smallint.ts b/drizzle-orm/src/pg-core/columns/smallint.ts index 23c5d47f6..4a500bf5c 100644 --- a/drizzle-orm/src/pg-core/columns/smallint.ts +++ b/drizzle-orm/src/pg-core/columns/smallint.ts @@ -2,7 +2,8 @@ import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnCon import type { ColumnBaseConfig } from '~/column.ts'; import { entityKind } from '~/entity.ts'; import type { AnyPgTable } from '~/pg-core/table.ts'; -import { PgColumn, PgColumnBuilder } from './common.ts'; +import { PgColumn } from './common.ts'; +import { PgIntColumnBaseBuilder } from './int.common.ts'; export type PgSmallIntBuilderInitial = PgSmallIntBuilder<{ name: TName; @@ -11,9 +12,12 @@ export type PgSmallIntBuilderInitial = PgSmallIntBuilder<{ data: number; driverParam: number | string; enumValues: undefined; + generated: undefined; }>; -export class PgSmallIntBuilder> extends PgColumnBuilder { +export class PgSmallIntBuilder> + extends PgIntColumnBaseBuilder +{ static readonly [entityKind]: string = 'PgSmallIntBuilder'; constructor(name: T['name']) { diff --git a/drizzle-orm/src/pg-core/columns/smallserial.ts b/drizzle-orm/src/pg-core/columns/smallserial.ts index b7bf86757..02acbb87f 100644 --- a/drizzle-orm/src/pg-core/columns/smallserial.ts +++ b/drizzle-orm/src/pg-core/columns/smallserial.ts @@ -19,6 +19,7 @@ export type PgSmallSerialBuilderInitial = NotNull< data: number; driverParam: number; enumValues: undefined; + generated: undefined; }> > >; diff --git a/drizzle-orm/src/pg-core/columns/text.ts b/drizzle-orm/src/pg-core/columns/text.ts index 844e9182c..47c3c9045 100644 --- a/drizzle-orm/src/pg-core/columns/text.ts +++ b/drizzle-orm/src/pg-core/columns/text.ts @@ -12,6 +12,7 @@ type PgTextBuilderInitial; export class PgTextBuilder< diff --git a/drizzle-orm/src/pg-core/columns/time.ts b/drizzle-orm/src/pg-core/columns/time.ts index ff7772bb0..fe82c9142 100644 --- a/drizzle-orm/src/pg-core/columns/time.ts +++ b/drizzle-orm/src/pg-core/columns/time.ts @@ -13,6 +13,7 @@ export type PgTimeBuilderInitial = PgTimeBuilder<{ data: string; driverParam: string; enumValues: undefined; + generated: undefined; }>; export class PgTimeBuilder> extends PgDateColumnBaseBuilder< diff --git a/drizzle-orm/src/pg-core/columns/timestamp.ts b/drizzle-orm/src/pg-core/columns/timestamp.ts index 6df0c3be6..8293f9472 100644 --- a/drizzle-orm/src/pg-core/columns/timestamp.ts +++ b/drizzle-orm/src/pg-core/columns/timestamp.ts @@ -13,6 +13,7 @@ export type PgTimestampBuilderInitial = PgTimestampBuilder data: Date; driverParam: string; enumValues: undefined; + generated: undefined; }>; export class PgTimestampBuilder> @@ -74,6 +75,7 @@ export type PgTimestampStringBuilderInitial = PgTimestampS data: string; driverParam: string; enumValues: undefined; + generated: undefined; }>; export class PgTimestampStringBuilder> diff --git a/drizzle-orm/src/pg-core/columns/uuid.ts b/drizzle-orm/src/pg-core/columns/uuid.ts index 4c9ba04ed..24907ce99 100644 --- a/drizzle-orm/src/pg-core/columns/uuid.ts +++ b/drizzle-orm/src/pg-core/columns/uuid.ts @@ -12,6 +12,7 @@ export type PgUUIDBuilderInitial = PgUUIDBuilder<{ data: string; driverParam: string; enumValues: undefined; + generated: undefined; }>; export class PgUUIDBuilder> extends PgColumnBuilder { diff --git a/drizzle-orm/src/pg-core/columns/varchar.ts b/drizzle-orm/src/pg-core/columns/varchar.ts index 31d66aade..84283d40e 100644 --- a/drizzle-orm/src/pg-core/columns/varchar.ts +++ b/drizzle-orm/src/pg-core/columns/varchar.ts @@ -12,6 +12,7 @@ export type PgVarcharBuilderInitial; export class PgVarcharBuilder> extends PgColumnBuilder< diff --git a/drizzle-orm/src/pg-core/columns/vector_extension/bit.ts b/drizzle-orm/src/pg-core/columns/vector_extension/bit.ts index a0e23188e..95d60e7d8 100644 --- a/drizzle-orm/src/pg-core/columns/vector_extension/bit.ts +++ b/drizzle-orm/src/pg-core/columns/vector_extension/bit.ts @@ -11,6 +11,7 @@ export type PgBinaryVectorBuilderInitial = PgBinaryVectorB data: string; driverParam: string; enumValues: undefined; + generated: undefined; }>; export class PgBinaryVectorBuilder> diff --git a/drizzle-orm/src/pg-core/columns/vector_extension/halfvec.ts b/drizzle-orm/src/pg-core/columns/vector_extension/halfvec.ts index 8278f2b69..182beda7e 100644 --- a/drizzle-orm/src/pg-core/columns/vector_extension/halfvec.ts +++ b/drizzle-orm/src/pg-core/columns/vector_extension/halfvec.ts @@ -11,6 +11,7 @@ export type PgHalfVectorBuilderInitial = PgHalfVectorBuild data: number[]; driverParam: string; enumValues: undefined; + generated: undefined; }>; export class PgHalfVectorBuilder> extends PgColumnBuilder< diff --git a/drizzle-orm/src/pg-core/columns/vector_extension/sparsevec.ts b/drizzle-orm/src/pg-core/columns/vector_extension/sparsevec.ts index af98517c1..060003bc6 100644 --- a/drizzle-orm/src/pg-core/columns/vector_extension/sparsevec.ts +++ b/drizzle-orm/src/pg-core/columns/vector_extension/sparsevec.ts @@ -11,6 +11,7 @@ export type PgSparseVectorBuilderInitial = PgSparseVectorB data: string; driverParam: string; enumValues: undefined; + generated: undefined; }>; export class PgSparseVectorBuilder> diff --git a/drizzle-orm/src/pg-core/columns/vector_extension/vector.ts b/drizzle-orm/src/pg-core/columns/vector_extension/vector.ts index a551d36e6..c7099b5dc 100644 --- a/drizzle-orm/src/pg-core/columns/vector_extension/vector.ts +++ b/drizzle-orm/src/pg-core/columns/vector_extension/vector.ts @@ -11,6 +11,7 @@ export type PgVectorBuilderInitial = PgVectorBuilder<{ data: number[]; driverParam: string; enumValues: undefined; + generated: undefined; }>; export class PgVectorBuilder> extends PgColumnBuilder< diff --git a/drizzle-orm/src/pg-core/dialect.ts b/drizzle-orm/src/pg-core/dialect.ts index 8538ec3f4..065c49ce8 100644 --- a/drizzle-orm/src/pg-core/dialect.ts +++ b/drizzle-orm/src/pg-core/dialect.ts @@ -456,7 +456,7 @@ export class PgDialect { const valuesSqlList: ((SQLChunk | SQL)[] | SQL)[] = []; const columns: Record = table[Table.Symbol.Columns]; - const colEntries: [string, PgColumn][] = Object.entries(columns); + const colEntries: [string, PgColumn][] = Object.entries(columns).filter(([_, col]) => !col.shouldDisableInsert()); const insertOrder = colEntries.map(([, column]) => sql.identifier(column.name)); diff --git a/drizzle-orm/src/pg-core/index.ts b/drizzle-orm/src/pg-core/index.ts index 1a80ff7ad..084633c4a 100644 --- a/drizzle-orm/src/pg-core/index.ts +++ b/drizzle-orm/src/pg-core/index.ts @@ -8,6 +8,7 @@ export * from './indexes.ts'; export * from './primary-keys.ts'; export * from './query-builders/index.ts'; export * from './schema.ts'; +export * from './sequence.ts'; export * from './session.ts'; export * from './subquery.ts'; export * from './table.ts'; diff --git a/drizzle-orm/src/pg-core/query-builders/update.ts b/drizzle-orm/src/pg-core/query-builders/update.ts index ab579621f..ec404ac22 100644 --- a/drizzle-orm/src/pg-core/query-builders/update.ts +++ b/drizzle-orm/src/pg-core/query-builders/update.ts @@ -29,7 +29,7 @@ export interface PgUpdateConfig { export type PgUpdateSetSource = & { - [Key in keyof TTable['_']['columns']]?: + [Key in keyof TTable['$inferInsert']]?: | GetColumnData | SQL; } diff --git a/drizzle-orm/src/pg-core/schema.ts b/drizzle-orm/src/pg-core/schema.ts index 210ae7407..9d8985020 100644 --- a/drizzle-orm/src/pg-core/schema.ts +++ b/drizzle-orm/src/pg-core/schema.ts @@ -2,6 +2,7 @@ import { entityKind, is } from '~/entity.ts'; import { SQL, sql, type SQLWrapper } from '~/sql/sql.ts'; import type { pgEnum } from './columns/enum.ts'; import { pgEnumWithSchema } from './columns/enum.ts'; +import { type pgSequence, pgSequenceWithSchema } from './sequence.ts'; import { type PgTableFn, pgTableWithSchema } from './table.ts'; import { type pgMaterializedView, pgMaterializedViewWithSchema, type pgView, pgViewWithSchema } from './view.ts'; @@ -27,6 +28,10 @@ export class PgSchema implements SQLWrapper { return pgEnumWithSchema(name, values, this.schemaName); }); + sequence: typeof pgSequence = ((name, options) => { + return pgSequenceWithSchema(name, options, this.schemaName); + }); + getSQL(): SQL { return new SQL([sql.identifier(this.schemaName)]); } diff --git a/drizzle-orm/src/pg-core/sequence.ts b/drizzle-orm/src/pg-core/sequence.ts new file mode 100644 index 000000000..a437ba5d3 --- /dev/null +++ b/drizzle-orm/src/pg-core/sequence.ts @@ -0,0 +1,41 @@ +import { entityKind, is } from '~/entity.ts'; + +export type PgSequenceOptions = { + increment?: number | string; + minValue?: number | string; + maxValue?: number | string; + startWith?: number | string; + cache?: number | string; + cycle?: boolean; +}; + +export class PgSequence { + static readonly [entityKind]: string = 'PgSequence'; + + constructor( + public readonly seqName: string | undefined, + public readonly seqOptions: PgSequenceOptions | undefined, + public readonly schema: string | undefined, + ) { + } +} + +export function pgSequence( + name: string, + options?: PgSequenceOptions, +): PgSequence { + return pgSequenceWithSchema(name, options, undefined); +} + +/** @internal */ +export function pgSequenceWithSchema( + name: string, + options?: PgSequenceOptions, + schema?: string, +): PgSequence { + return new PgSequence(name, options, schema); +} + +export function isPgSequence(obj: unknown): obj is PgSequence { + return is(obj, PgSequence); +} diff --git a/drizzle-orm/src/planetscale-serverless/session.ts b/drizzle-orm/src/planetscale-serverless/session.ts index 60b7d83d8..f2275b7f2 100644 --- a/drizzle-orm/src/planetscale-serverless/session.ts +++ b/drizzle-orm/src/planetscale-serverless/session.ts @@ -1,5 +1,6 @@ import type { Client, Connection, ExecutedQuery, Transaction } from '@planetscale/database'; -import { entityKind } from '~/entity.ts'; +import { Column } from '~/column.ts'; +import { entityKind, is } from '~/entity.ts'; import type { Logger } from '~/logger.ts'; import { NoopLogger } from '~/logger.ts'; import type { MySqlDialect } from '~/mysql-core/dialect.ts'; @@ -29,6 +30,10 @@ export class PlanetScalePreparedQuery extend private logger: Logger, private fields: SelectedFieldsOrdered | undefined, private customResultMapper?: (rows: unknown[][]) => T['execute'], + // Keys that were used in $default and the value that was generated for them + private generatedIds?: Record[], + // Keys that should be returned, it has the column with all properries + key from object + private returningIds?: SelectedFieldsOrdered, ) { super(); } @@ -38,11 +43,47 @@ export class PlanetScalePreparedQuery extend this.logger.logQuery(this.queryString, params); - const { fields, client, queryString, rawQuery, query, joinsNotNullableMap, customResultMapper } = this; + const { + fields, + client, + queryString, + rawQuery, + query, + joinsNotNullableMap, + customResultMapper, + returningIds, + generatedIds, + } = this; if (!fields && !customResultMapper) { - return client.execute(queryString, params, rawQuery); + const res = await client.execute(queryString, params, rawQuery); + + const insertId = Number.parseFloat(res.insertId); + const affectedRows = res.rowsAffected; + + // for each row, I need to check keys from + if (returningIds) { + const returningResponse = []; + let j = 0; + for (let i = insertId; i < insertId + affectedRows; i++) { + for (const column of returningIds) { + const key = returningIds[0]!.path[0]!; + if (is(column.field, Column)) { + // @ts-ignore + if (column.field.primary && column.field.autoIncrement) { + returningResponse.push({ [key]: i }); + } + if (column.field.defaultFn && generatedIds) { + // generatedIds[rowIdx][key] + returningResponse.push({ [key]: generatedIds[j]![key] }); + } + } + } + j++; + } + return returningResponse; + } + return res; } - const { rows } = await client.execute(queryString, params, query); if (customResultMapper) { @@ -86,8 +127,19 @@ export class PlanetscaleSession< query: Query, fields: SelectedFieldsOrdered | undefined, customResultMapper?: (rows: unknown[][]) => T['execute'], + generatedIds?: Record[], + returningIds?: SelectedFieldsOrdered, ): MySqlPreparedQuery { - return new PlanetScalePreparedQuery(this.client, query.sql, query.params, this.logger, fields, customResultMapper); + return new PlanetScalePreparedQuery( + this.client, + query.sql, + query.params, + this.logger, + fields, + customResultMapper, + generatedIds, + returningIds, + ); } async query(query: string, params: unknown[]): Promise { @@ -106,7 +158,10 @@ export class PlanetscaleSession< override all(query: SQL): Promise { const querySql = this.dialect.sqlToQuery(query); this.logger.logQuery(querySql.sql, querySql.params); - return this.client.execute(querySql.sql, querySql.params, { as: 'object' }).then((eQuery) => eQuery.rows as T[]); + + return this.client.execute(querySql.sql, querySql.params, { as: 'object' }).then(( + eQuery, + ) => eQuery.rows as T[]); } override transaction( diff --git a/drizzle-orm/src/sqlite-core/columns/blob.ts b/drizzle-orm/src/sqlite-core/columns/blob.ts index 50a94c068..7371eb299 100644 --- a/drizzle-orm/src/sqlite-core/columns/blob.ts +++ b/drizzle-orm/src/sqlite-core/columns/blob.ts @@ -14,6 +14,7 @@ export type SQLiteBigIntBuilderInitial = SQLiteBigIntBuild data: bigint; driverParam: Buffer; enumValues: undefined; + generated: undefined; }>; export class SQLiteBigIntBuilder> @@ -56,6 +57,7 @@ export type SQLiteBlobJsonBuilderInitial = SQLiteBlobJsonB data: unknown; driverParam: Buffer; enumValues: undefined; + generated: undefined; }>; export class SQLiteBlobJsonBuilder> @@ -101,6 +103,7 @@ export type SQLiteBlobBufferBuilderInitial = SQLiteBlobBuf data: Buffer; driverParam: Buffer; enumValues: undefined; + generated: undefined; }>; export class SQLiteBlobBufferBuilder> diff --git a/drizzle-orm/src/sqlite-core/columns/common.ts b/drizzle-orm/src/sqlite-core/columns/common.ts index 83a5e175d..a0cdd755d 100644 --- a/drizzle-orm/src/sqlite-core/columns/common.ts +++ b/drizzle-orm/src/sqlite-core/columns/common.ts @@ -4,12 +4,14 @@ import type { ColumnBuilderExtraConfig, ColumnBuilderRuntimeConfig, ColumnDataType, + HasGenerated, MakeColumnConfig, } from '~/column-builder.ts'; import { ColumnBuilder } from '~/column-builder.ts'; import { Column } from '~/column.ts'; import type { ColumnBaseConfig } from '~/column.ts'; import { entityKind } from '~/entity.ts'; +import type { SQL } from '~/sql/sql.ts'; import type { ForeignKey, UpdateDeleteAction } from '~/sqlite-core/foreign-keys.ts'; import { ForeignKeyBuilder } from '~/sqlite-core/foreign-keys.ts'; import type { AnySQLiteTable, SQLiteTable } from '~/sqlite-core/table.ts'; @@ -29,6 +31,10 @@ export interface SQLiteColumnBuilderBase< TTypeConfig extends object = object, > extends ColumnBuilderBase {} +export interface SQLiteGeneratedColumnConfig { + mode?: 'virtual' | 'stored'; +} + export abstract class SQLiteColumnBuilder< T extends ColumnBuilderBaseConfig = ColumnBuilderBaseConfig, TRuntimeConfig extends object = object, @@ -57,6 +63,15 @@ export abstract class SQLiteColumnBuilder< return this; } + generatedAlwaysAs(as: SQL | T['data'] | (() => SQL), config?: SQLiteGeneratedColumnConfig): HasGenerated { + this.config.generated = { + as, + type: 'always', + mode: config?.mode ?? 'virtual', + }; + return this as any; + } + /** @internal */ buildForeignKeys(column: SQLiteColumn, table: SQLiteTable): ForeignKey[] { return this.foreignKeyConfigs.map(({ ref, actions }) => { diff --git a/drizzle-orm/src/sqlite-core/columns/custom.ts b/drizzle-orm/src/sqlite-core/columns/custom.ts index e8b765ffc..513f380e0 100644 --- a/drizzle-orm/src/sqlite-core/columns/custom.ts +++ b/drizzle-orm/src/sqlite-core/columns/custom.ts @@ -14,6 +14,7 @@ export type ConvertCustomConfig> { + override primaryKey(config?: PrimaryKeyConfig): IsPrimaryKey>> { if (config?.autoIncrement) { this.config.autoIncrement = true; } this.config.hasDefault = true; - return super.primaryKey() as HasDefault>; + return super.primaryKey() as IsPrimaryKey>>; } /** @internal */ @@ -69,6 +70,7 @@ export type SQLiteIntegerBuilderInitial = SQLiteIntegerBui data: number; driverParam: number; enumValues: undefined; + generated: undefined; }>; export class SQLiteIntegerBuilder> @@ -101,6 +103,7 @@ export type SQLiteTimestampBuilderInitial = SQLiteTimestam data: Date; driverParam: number; enumValues: undefined; + generated: undefined; }>; export class SQLiteTimestampBuilder> @@ -162,6 +165,7 @@ export type SQLiteBooleanBuilderInitial = SQLiteBooleanBui data: boolean; driverParam: number; enumValues: undefined; + generated: undefined; }>; export class SQLiteBooleanBuilder> diff --git a/drizzle-orm/src/sqlite-core/columns/numeric.ts b/drizzle-orm/src/sqlite-core/columns/numeric.ts index 041e1390c..5cd29f78f 100644 --- a/drizzle-orm/src/sqlite-core/columns/numeric.ts +++ b/drizzle-orm/src/sqlite-core/columns/numeric.ts @@ -11,6 +11,7 @@ export type SQLiteNumericBuilderInitial = SQLiteNumericBui data: string; driverParam: string; enumValues: undefined; + generated: undefined; }>; export class SQLiteNumericBuilder> diff --git a/drizzle-orm/src/sqlite-core/columns/real.ts b/drizzle-orm/src/sqlite-core/columns/real.ts index 3186d4b8f..c04cb1be3 100644 --- a/drizzle-orm/src/sqlite-core/columns/real.ts +++ b/drizzle-orm/src/sqlite-core/columns/real.ts @@ -11,6 +11,7 @@ export type SQLiteRealBuilderInitial = SQLiteRealBuilder<{ data: number; driverParam: number; enumValues: undefined; + generated: undefined; }>; export class SQLiteRealBuilder> diff --git a/drizzle-orm/src/sqlite-core/columns/text.ts b/drizzle-orm/src/sqlite-core/columns/text.ts index 4b1285259..7eecf1d1f 100644 --- a/drizzle-orm/src/sqlite-core/columns/text.ts +++ b/drizzle-orm/src/sqlite-core/columns/text.ts @@ -12,6 +12,7 @@ export type SQLiteTextBuilderInitial; export class SQLiteTextBuilder> extends SQLiteColumnBuilder< @@ -62,6 +63,7 @@ export type SQLiteTextJsonBuilderInitial = SQLiteTextJsonB data: unknown; driverParam: string; enumValues: undefined; + generated: undefined; }>; export class SQLiteTextJsonBuilder> diff --git a/drizzle-orm/src/sqlite-core/dialect.ts b/drizzle-orm/src/sqlite-core/dialect.ts index 7b7cf0ab1..50aa30e33 100644 --- a/drizzle-orm/src/sqlite-core/dialect.ts +++ b/drizzle-orm/src/sqlite-core/dialect.ts @@ -376,7 +376,9 @@ export abstract class SQLiteDialect { const valuesSqlList: ((SQLChunk | SQL)[] | SQL)[] = []; const columns: Record = table[Table.Symbol.Columns]; - const colEntries: [string, SQLiteColumn][] = Object.entries(columns); + const colEntries: [string, SQLiteColumn][] = Object.entries(columns).filter(([_, col]) => + !col.shouldDisableInsert() + ); const insertOrder = colEntries.map(([, column]) => sql.identifier(column.name)); for (const [valueIndex, value] of values.entries()) { diff --git a/drizzle-orm/src/sqlite-core/query-builders/update.ts b/drizzle-orm/src/sqlite-core/query-builders/update.ts index 6ea33aea0..7b25c090a 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/update.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/update.ts @@ -22,7 +22,7 @@ export interface SQLiteUpdateConfig { export type SQLiteUpdateSetSource = & { - [Key in keyof TTable['_']['columns']]?: + [Key in keyof TTable['$inferInsert']]?: | GetColumnData | SQL; } diff --git a/drizzle-orm/src/tidb-serverless/session.ts b/drizzle-orm/src/tidb-serverless/session.ts index e87c7a7e2..64a8d61d7 100644 --- a/drizzle-orm/src/tidb-serverless/session.ts +++ b/drizzle-orm/src/tidb-serverless/session.ts @@ -1,6 +1,7 @@ import type { Connection, ExecuteOptions, FullResult, Tx } from '@tidbcloud/serverless'; +import { Column } from '~/column.ts'; -import { entityKind } from '~/entity.ts'; +import { entityKind, is } from '~/entity.ts'; import type { Logger } from '~/logger.ts'; import { NoopLogger } from '~/logger.ts'; import type { MySqlDialect } from '~/mysql-core/dialect.ts'; @@ -30,6 +31,10 @@ export class TiDBServerlessPreparedQuery ext private logger: Logger, private fields: SelectedFieldsOrdered | undefined, private customResultMapper?: (rows: unknown[][]) => T['execute'], + // Keys that were used in $default and the value that was generated for them + private generatedIds?: Record[], + // Keys that should be returned, it has the column with all properries + key from object + private returningIds?: SelectedFieldsOrdered, ) { super(); } @@ -39,9 +44,35 @@ export class TiDBServerlessPreparedQuery ext this.logger.logQuery(this.queryString, params); - const { fields, client, queryString, joinsNotNullableMap, customResultMapper } = this; + const { fields, client, queryString, joinsNotNullableMap, customResultMapper, returningIds, generatedIds } = this; if (!fields && !customResultMapper) { - return client.execute(queryString, params, executeRawConfig); + const res = await client.execute(queryString, params, executeRawConfig) as FullResult; + const insertId = res.lastInsertId ?? 0; + const affectedRows = res.rowsAffected ?? 0; + // for each row, I need to check keys from + if (returningIds) { + const returningResponse = []; + let j = 0; + for (let i = insertId; i < insertId + affectedRows; i++) { + for (const column of returningIds) { + const key = returningIds[0]!.path[0]!; + if (is(column.field, Column)) { + // @ts-ignore + if (column.field.primary && column.field.autoIncrement) { + returningResponse.push({ [key]: i }); + } + if (column.field.defaultFn && generatedIds) { + // generatedIds[rowIdx][key] + returningResponse.push({ [key]: generatedIds[j]![key] }); + } + } + } + j++; + } + + return returningResponse; + } + return res; } const rows = await client.execute(queryString, params, queryConfig) as unknown[][]; @@ -87,6 +118,8 @@ export class TiDBServerlessSession< query: Query, fields: SelectedFieldsOrdered | undefined, customResultMapper?: (rows: unknown[][]) => T['execute'], + generatedIds?: Record[], + returningIds?: SelectedFieldsOrdered, ): MySqlPreparedQuery { return new TiDBServerlessPreparedQuery( this.client, @@ -95,6 +128,8 @@ export class TiDBServerlessSession< this.logger, fields, customResultMapper, + generatedIds, + returningIds, ); } diff --git a/drizzle-orm/type-tests/mysql/generated-columns.ts b/drizzle-orm/type-tests/mysql/generated-columns.ts new file mode 100644 index 000000000..d045fe1b3 --- /dev/null +++ b/drizzle-orm/type-tests/mysql/generated-columns.ts @@ -0,0 +1,158 @@ +import { type Equal, Expect } from 'type-tests/utils'; +import { type InferInsertModel, type InferSelectModel, sql } from '~/index'; +import { mysqlTable, serial, text, varchar } from '~/mysql-core'; +import { drizzle } from '~/mysql2'; +import { db } from './db'; + +const users = mysqlTable( + 'users', + { + id: serial('id').primaryKey(), + firstName: varchar('first_name', { length: 255 }), + lastName: varchar('last_name', { length: 255 }), + email: text('email').notNull(), + fullName: text('full_name').generatedAlwaysAs(sql`concat_ws(first_name, ' ', last_name)`), + upperName: text('upper_name').generatedAlwaysAs( + sql` case when first_name is null then null else upper(first_name) end `, + ).$type(), // There is no way for drizzle to detect nullability in these cases. This is how the user can work around it + }, +); +{ + type User = typeof users.$inferSelect; + type NewUser = typeof users.$inferInsert; + + Expect< + Equal< + { + id: number; + firstName: string | null; + lastName: string | null; + email: string; + fullName: string | null; + upperName: string | null; + }, + User + > + >(); + + Expect< + Equal< + { + email: string; + id?: number | undefined; + firstName?: string | null | undefined; + lastName?: string | null | undefined; + }, + NewUser + > + >(); +} + +{ + type User = InferSelectModel; + type NewUser = InferInsertModel; + + Expect< + Equal< + { + id: number; + firstName: string | null; + lastName: string | null; + email: string; + fullName: string | null; + upperName: string | null; + }, + User + > + >(); + + Expect< + Equal< + { + email: string; + id?: number | undefined; + firstName?: string | null | undefined; + lastName?: string | null | undefined; + }, + NewUser + > + >(); +} + +{ + const dbUsers = await db.select().from(users); + + Expect< + Equal< + { + id: number; + firstName: string | null; + lastName: string | null; + email: string; + fullName: string | null; + upperName: string | null; + }[], + typeof dbUsers + > + >(); +} + +{ + const db = drizzle({} as any, { schema: { users }, mode: 'default' }); + + const dbUser = await db.query.users.findFirst(); + + Expect< + Equal< + { + id: number; + firstName: string | null; + lastName: string | null; + email: string; + fullName: string | null; + upperName: string | null; + } | undefined, + typeof dbUser + > + >(); +} + +{ + const db = drizzle({} as any, { schema: { users }, mode: 'default' }); + + const dbUser = await db.query.users.findMany(); + + Expect< + Equal< + { + id: number; + firstName: string | null; + lastName: string | null; + email: string; + fullName: string | null; + upperName: string | null; + }[], + typeof dbUser + > + >(); +} + +{ + // @ts-expect-error - Can't use the fullName because it's a generated column + await db.insert(users).values({ + firstName: 'test', + lastName: 'test', + email: 'test', + fullName: 'test', + }); +} + +{ + await db.update(users).set({ + firstName: 'test', + lastName: 'test', + email: 'test', + // @ts-expect-error - Can't use the fullName because it's a generated column + fullName: 'test', + }); +} diff --git a/drizzle-orm/type-tests/mysql/insert.ts b/drizzle-orm/type-tests/mysql/insert.ts index edc48d5a2..b354410bd 100644 --- a/drizzle-orm/type-tests/mysql/insert.ts +++ b/drizzle-orm/type-tests/mysql/insert.ts @@ -1,11 +1,22 @@ import type { Equal } from 'type-tests/utils.ts'; import { Expect } from 'type-tests/utils.ts'; -import { int, type MySqlInsert, mysqlTable, text } from '~/mysql-core/index.ts'; +import { int, mysqlTable, text } from '~/mysql-core/index.ts'; +import type { MySqlInsert } from '~/mysql-core/index.ts'; import type { MySqlRawQueryResult } from '~/mysql2/index.ts'; import { sql } from '~/sql/sql.ts'; import { db } from './db.ts'; import { users } from './tables.ts'; +const mysqlInsertReturning = await db.insert(users).values({ + // ^? + homeCity: 1, + class: 'A', + age1: 1, + enumCol: 'a', +}).$returningId(); + +Expect>; + const insert = await db.insert(users).values({ homeCity: 1, class: 'A', @@ -110,6 +121,7 @@ Expect>; const qbBase = db.insert(users).values({ age1: 0, class: 'A', enumCol: 'a', homeCity: 0 }).$dynamic(); const qb = dynamic(qbBase); const result = await qb; + Expect>; } diff --git a/drizzle-orm/type-tests/mysql/tables.ts b/drizzle-orm/type-tests/mysql/tables.ts index 6eac879da..eac796e6f 100644 --- a/drizzle-orm/type-tests/mysql/tables.ts +++ b/drizzle-orm/type-tests/mysql/tables.ts @@ -12,6 +12,7 @@ import { foreignKey, index, int, + json, longtext, mediumtext, type MySqlColumn, @@ -22,6 +23,7 @@ import { text, timestamp, tinytext, + unique, uniqueIndex, varchar, } from '~/mysql-core/index.ts'; @@ -73,6 +75,62 @@ export const cities = mysqlTable('cities_table', { citiesNameIdx: index('citiesNameIdx').on(cities.id), })); +Expect< + Equal< + { + id: MySqlColumn<{ + name: 'id'; + tableName: 'cities_table'; + dataType: 'number'; + columnType: 'MySqlSerial'; + data: number; + driverParam: number; + notNull: true; + hasDefault: true; + isPrimaryKey: true; + enumValues: undefined; + baseColumn: never; + generated: undefined; + isAutoincrement: true; + hasRuntimeDefault: false; + }, object>; + name: MySqlColumn<{ + name: 'name_db'; + tableName: 'cities_table'; + dataType: 'string'; + columnType: 'MySqlText'; + data: string; + driverParam: string; + notNull: true; + hasDefault: false; + isPrimaryKey: false; + enumValues: [string, ...string[]]; + baseColumn: never; + generated: undefined; + isAutoincrement: false; + hasRuntimeDefault: false; + }, object>; + population: MySqlColumn<{ + name: 'population'; + tableName: 'cities_table'; + dataType: 'number'; + columnType: 'MySqlInt'; + data: number; + driverParam: string | number; + notNull: false; + hasDefault: true; + isPrimaryKey: false; + enumValues: undefined; + baseColumn: never; + generated: undefined; + isAutoincrement: false; + hasRuntimeDefault: false; + }, object>; + }, + typeof cities._.columns + > +>; + Expect< Equal<{ id: number; @@ -81,6 +139,14 @@ Expect< }, InferSelectModel> >; +Expect< + Equal<{ + id?: number; + name: string; + population?: number | null; + }, typeof cities.$inferInsert> +>; + export const customSchema = mysqlSchema('custom_schema'); export const citiesCustom = customSchema.table('cities_table', { @@ -135,6 +201,10 @@ Expect< tableName: 'new_yorkers'; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: true; + isAutoincrement: true; + hasRuntimeDefault: false; }>; cityId: MySqlColumn<{ name: 'id'; @@ -147,6 +217,10 @@ Expect< tableName: 'new_yorkers'; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: true; + isAutoincrement: true; + hasRuntimeDefault: false; }>; }>, typeof newYorkers @@ -184,6 +258,10 @@ Expect< tableName: 'new_yorkers'; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: true; + isAutoincrement: true; + hasRuntimeDefault: false; }>; cityId: MySqlColumn<{ name: 'id'; @@ -196,6 +274,10 @@ Expect< tableName: 'new_yorkers'; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: true; + isAutoincrement: true; + hasRuntimeDefault: false; }>; }>, typeof newYorkers @@ -231,6 +313,10 @@ Expect< tableName: 'new_yorkers'; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; }>; cityId: MySqlColumn<{ name: 'city_id'; @@ -243,6 +329,10 @@ Expect< tableName: 'new_yorkers'; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; }>; }>, typeof newYorkers @@ -278,6 +368,10 @@ Expect< tableName: 'new_yorkers'; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; }>; cityId: MySqlColumn<{ name: 'city_id'; @@ -290,6 +384,10 @@ Expect< tableName: 'new_yorkers'; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; }>; }>, typeof newYorkers @@ -317,6 +415,10 @@ Expect< tableName: 'new_yorkers'; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; }>; cityId: MySqlColumn<{ name: 'city_id'; @@ -329,6 +431,10 @@ Expect< tableName: 'new_yorkers'; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; }>; }>, typeof newYorkers @@ -356,6 +462,10 @@ Expect< tableName: 'new_yorkers'; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; }>; cityId: MySqlColumn<{ name: 'city_id'; @@ -368,6 +478,10 @@ Expect< tableName: 'new_yorkers'; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; }>; }>, typeof newYorkers @@ -398,6 +512,10 @@ Expect< enumValues: undefined; baseColumn: never; dialect: 'mysql'; + generated: undefined; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; }, Simplify['_']> > @@ -464,6 +582,41 @@ Expect< Expect>; } +{ // All types with generated columns + const test = mysqlTable('test', { + test1: mysqlEnum('test', ['a', 'b', 'c'] as const).generatedAlwaysAs(sql``), + test2: mysqlEnum('test', ['a', 'b', 'c']).generatedAlwaysAs(sql``), + test3: varchar('test', { length: 255, enum: ['a', 'b', 'c'] as const }).generatedAlwaysAs(sql``), + test4: varchar('test', { length: 255, enum: ['a', 'b', 'c'] }).generatedAlwaysAs(sql``), + test5: text('test', { enum: ['a', 'b', 'c'] as const }).generatedAlwaysAs(sql``), + test6: text('test', { enum: ['a', 'b', 'c'] }).generatedAlwaysAs(sql``), + test7: tinytext('test', { enum: ['a', 'b', 'c'] as const }).generatedAlwaysAs(sql``), + test8: tinytext('test', { enum: ['a', 'b', 'c'] }).generatedAlwaysAs(sql``), + test9: mediumtext('test', { enum: ['a', 'b', 'c'] as const }).generatedAlwaysAs(sql``), + test10: mediumtext('test', { enum: ['a', 'b', 'c'] }).generatedAlwaysAs(sql``), + test11: longtext('test', { enum: ['a', 'b', 'c'] as const }).generatedAlwaysAs(sql``), + test12: longtext('test', { enum: ['a', 'b', 'c'] }).generatedAlwaysAs(sql``), + test13: char('test', { enum: ['a', 'b', 'c'] as const }).generatedAlwaysAs(sql``), + test14: char('test', { enum: ['a', 'b', 'c'] }).generatedAlwaysAs(sql``), + test15: text('test').generatedAlwaysAs(sql``), + }); + Expect>; + Expect>; + Expect>; + Expect>; + Expect>; + Expect>; + Expect>; + Expect>; + Expect>; + Expect>; + Expect>; + Expect>; + Expect>; + Expect>; + Expect>; +} + { const getUsersTable = (schemaName: TSchema) => { return mysqlSchema(schemaName).table('users', { @@ -561,3 +714,49 @@ Expect< id4: int('id').$defaultFn(() => '1'), }); } +{ + const emailLog = mysqlTable( + 'email_log', + { + id: int('id', { unsigned: true }).autoincrement().notNull(), + clientId: int('id_client', { unsigned: true }).references((): MySqlColumn => emailLog.id, { + onDelete: 'set null', + onUpdate: 'cascade', + }), + receiverEmail: varchar('receiver_email', { length: 255 }).notNull(), + messageId: varchar('message_id', { length: 255 }), + contextId: int('context_id', { unsigned: true }), + contextType: mysqlEnum('context_type', ['test']).$type<['test']>(), + action: varchar('action', { length: 80 }).$type<['test']>(), + events: json('events').$type<{ t: 'test' }[]>(), + createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().onUpdateNow(), + }, + (table) => { + return { + emailLogId: primaryKey({ columns: [table.id], name: 'email_log_id' }), + emailLogMessageIdUnique: unique('email_log_message_id_unique').on(table.messageId), + }; + }, + ); + + Expect< + Equal<{ + receiverEmail: string; + id?: number | undefined; + createdAt?: string | undefined; + clientId?: number | null | undefined; + messageId?: string | null | undefined; + contextId?: number | null | undefined; + contextType?: ['test'] | null | undefined; + action?: ['test'] | null | undefined; + events?: + | { + t: 'test'; + }[] + | null + | undefined; + updatedAt?: string | null | undefined; + }, typeof emailLog.$inferInsert> + >; +} diff --git a/drizzle-orm/type-tests/mysql/with.ts b/drizzle-orm/type-tests/mysql/with.ts index 338f9c43b..b4e528191 100644 --- a/drizzle-orm/type-tests/mysql/with.ts +++ b/drizzle-orm/type-tests/mysql/with.ts @@ -11,6 +11,7 @@ const orders = mysqlTable('orders', { product: text('product').notNull(), amount: int('amount').notNull(), quantity: int('quantity').notNull(), + generated: text('generatedText').generatedAlwaysAs(sql``), }); { @@ -62,4 +63,18 @@ const orders = mysqlTable('orders', { productSales: number; }[], typeof result> >; + + const allOrdersWith = db.$with('all_orders_with').as(db.select().from(orders)); + const allFromWith = await db.with(allOrdersWith).select().from(allOrdersWith); + + Expect< + Equal<{ + id: number; + region: string; + product: string; + amount: number; + quantity: number; + generated: string | null; + }[], typeof allFromWith> + >; } diff --git a/drizzle-orm/type-tests/pg/array.ts b/drizzle-orm/type-tests/pg/array.ts index 03ea190b4..3961e92d0 100644 --- a/drizzle-orm/type-tests/pg/array.ts +++ b/drizzle-orm/type-tests/pg/array.ts @@ -20,6 +20,10 @@ import { integer, pgTable } from '~/pg-core/index.ts'; hasDefault: false; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; } >, typeof table['a']['_']['baseColumn'] diff --git a/drizzle-orm/type-tests/pg/generated-columns.ts b/drizzle-orm/type-tests/pg/generated-columns.ts new file mode 100644 index 000000000..afe84def6 --- /dev/null +++ b/drizzle-orm/type-tests/pg/generated-columns.ts @@ -0,0 +1,222 @@ +import { type Equal, Expect } from 'type-tests/utils'; +import { type InferInsertModel, type InferSelectModel, sql } from '~/index'; +import { drizzle } from '~/node-postgres'; +import { integer, pgTable, serial, text, varchar } from '~/pg-core'; +import { db } from './db'; + +const users = pgTable( + 'users', + { + id: serial('id').primaryKey(), + firstName: varchar('first_name', { length: 255 }), + lastName: varchar('last_name', { length: 255 }), + email: text('email').notNull(), + fullName: text('full_name').generatedAlwaysAs(sql`concat_ws(first_name, ' ', last_name)`).notNull(), + upperName: text('upper_name').generatedAlwaysAs( + sql` case when first_name is null then null else upper(first_name) end `, + ), + }, +); +{ + type User = typeof users.$inferSelect; + type NewUser = typeof users.$inferInsert; + + Expect< + Equal< + { + id: number; + firstName: string | null; + lastName: string | null; + email: string; + fullName: string; + upperName: string | null; + }, + User + > + >(); + + Expect< + Equal< + { + email: string; + id?: number | undefined; + firstName?: string | null | undefined; + lastName?: string | null | undefined; + }, + NewUser + > + >(); +} + +{ + type User = InferSelectModel; + type NewUser = InferInsertModel; + + Expect< + Equal< + { + id: number; + firstName: string | null; + lastName: string | null; + email: string; + fullName: string; + upperName: string | null; + }, + User + > + >(); + + Expect< + Equal< + { + email: string; + id?: number | undefined; + firstName?: string | null | undefined; + lastName?: string | null | undefined; + }, + NewUser + > + >(); +} + +{ + const dbUsers = await db.select().from(users); + + Expect< + Equal< + { + id: number; + firstName: string | null; + lastName: string | null; + email: string; + fullName: string; + upperName: string | null; + }[], + typeof dbUsers + > + >(); +} + +{ + const db = drizzle({} as any, { schema: { users } }); + + const dbUser = await db.query.users.findFirst(); + + Expect< + Equal< + { + id: number; + firstName: string | null; + lastName: string | null; + email: string; + fullName: string; + upperName: string | null; + } | undefined, + typeof dbUser + > + >(); +} + +{ + const db = drizzle({} as any, { schema: { users } }); + + const dbUser = await db.query.users.findMany(); + + Expect< + Equal< + { + id: number; + firstName: string | null; + lastName: string | null; + email: string; + fullName: string; + upperName: string | null; + }[], + typeof dbUser + > + >(); +} + +{ + // @ts-expect-error - Can't use the fullName because it's a generated column + await db.insert(users).values({ + firstName: 'test', + lastName: 'test', + email: 'test', + fullName: 'test', + }); +} + +{ + await db.update(users).set({ + firstName: 'test', + lastName: 'test', + email: 'test', + // @ts-expect-error - Can't use the fullName because it's a generated column + fullName: 'test', + }); +} + +const users2 = pgTable( + 'users', + { + id: integer('id').generatedByDefaultAsIdentity(), + id2: integer('id').generatedAlwaysAsIdentity(), + }, +); + +{ + type User = typeof users2.$inferSelect; + type NewUser = typeof users2.$inferInsert; + + Expect< + Equal< + { + id: number; + id2: number; + }, + User + > + >(); + + Expect< + Equal< + { + id?: number | undefined; + }, + NewUser + > + >(); +} + +const usersSeq = pgTable( + 'users', + { + id: integer('id').generatedByDefaultAsIdentity(), + id2: integer('id').generatedAlwaysAsIdentity(), + }, +); + +{ + type User = typeof usersSeq.$inferSelect; + type NewUser = typeof usersSeq.$inferInsert; + + Expect< + Equal< + { + id: number; + id2: number; + }, + User + > + >(); + + Expect< + Equal< + { + id?: number | undefined; + }, + NewUser + > + >(); +} diff --git a/drizzle-orm/type-tests/pg/tables.ts b/drizzle-orm/type-tests/pg/tables.ts index 1e69df79e..5faca5d7b 100644 --- a/drizzle-orm/type-tests/pg/tables.ts +++ b/drizzle-orm/type-tests/pg/tables.ts @@ -125,6 +125,14 @@ export const classes = pgTable('classes_table', { subClass: text('sub_class', { enum: ['B', 'D'] }).notNull(), }); +Expect< + Equal<{ + id?: number; + class?: 'A' | 'C' | null; + subClass: 'B' | 'D'; + }, typeof classes.$inferInsert> +>; + export const network = pgTable('network_table', { inet: inet('inet').notNull(), cidr: cidr('cidr').notNull(), @@ -193,6 +201,10 @@ Expect< hasDefault: true; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: true; + isAutoincrement: false; + hasRuntimeDefault: false; }>; cityId: PgColumn<{ tableName: 'new_yorkers'; @@ -205,6 +217,10 @@ Expect< hasDefault: true; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: true; + isAutoincrement: false; + hasRuntimeDefault: false; }>; }>, typeof newYorkers @@ -244,6 +260,10 @@ Expect< hasDefault: true; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: true; + isAutoincrement: false; + hasRuntimeDefault: false; }>; cityId: PgColumn<{ tableName: 'new_yorkers'; @@ -256,6 +276,10 @@ Expect< hasDefault: true; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: true; + isAutoincrement: false; + hasRuntimeDefault: false; }>; }>, typeof newYorkers @@ -293,6 +317,10 @@ Expect< notNull: true; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; }>; cityId: PgColumn<{ tableName: 'new_yorkers'; @@ -305,6 +333,10 @@ Expect< driverParam: string | number; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; }>; }>, typeof newYorkers @@ -342,6 +374,10 @@ Expect< notNull: true; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; }>; cityId: PgColumn<{ tableName: 'new_yorkers'; @@ -354,6 +390,10 @@ Expect< driverParam: string | number; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; }>; }>, typeof newYorkers @@ -381,6 +421,10 @@ Expect< notNull: true; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; }>; cityId: PgColumn<{ tableName: 'new_yorkers'; @@ -393,6 +437,10 @@ Expect< driverParam: string | number; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; }>; }>, typeof newYorkers @@ -420,6 +468,10 @@ Expect< notNull: true; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; }>; cityId: PgColumn<{ tableName: 'new_yorkers'; @@ -432,6 +484,10 @@ Expect< driverParam: string | number; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; }>; }>, typeof newYorkers @@ -474,6 +530,10 @@ Expect< hasDefault: true; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: true; + isAutoincrement: false; + hasRuntimeDefault: false; }>; cityId: PgColumn<{ tableName: 'new_yorkers'; @@ -486,6 +546,10 @@ Expect< hasDefault: true; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: true; + isAutoincrement: false; + hasRuntimeDefault: false; }>; }>, typeof newYorkers2 @@ -528,6 +592,10 @@ Expect< hasDefault: true; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: true; + isAutoincrement: false; + hasRuntimeDefault: false; }>; cityId: PgColumn<{ tableName: 'new_yorkers'; @@ -540,6 +608,10 @@ Expect< hasDefault: true; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: true; + isAutoincrement: false; + hasRuntimeDefault: false; }>; }>, typeof newYorkers2 @@ -580,6 +652,10 @@ Expect< notNull: true; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; }>; cityId: PgColumn<{ tableName: 'new_yorkers'; @@ -592,6 +668,10 @@ Expect< driverParam: string | number; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; }>; }>, typeof newYorkers2 @@ -632,6 +712,10 @@ Expect< notNull: true; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; }>; cityId: PgColumn<{ tableName: 'new_yorkers'; @@ -644,6 +728,10 @@ Expect< driverParam: string | number; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; }>; }>, typeof newYorkers2 @@ -671,6 +759,10 @@ Expect< notNull: true; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; }>; cityId: PgColumn<{ tableName: 'new_yorkers'; @@ -683,6 +775,10 @@ Expect< driverParam: string | number; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; }>; }>, typeof newYorkers2 @@ -710,6 +806,10 @@ Expect< notNull: true; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; }>; cityId: PgColumn<{ tableName: 'new_yorkers'; @@ -722,6 +822,10 @@ Expect< driverParam: string | number; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; }>; }>, typeof newYorkers2 @@ -824,6 +928,10 @@ await db.refreshMaterializedView(newYorkers2).withNoData().concurrently(); notNull: true; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: true; + isAutoincrement: false; + hasRuntimeDefault: false; }>; name: PgColumn<{ tableName: 'cities_table'; @@ -836,6 +944,10 @@ await db.refreshMaterializedView(newYorkers2).withNoData().concurrently(); enumValues: [string, ...string[]]; notNull: true; baseColumn: never; + generated: undefined; + isPrimaryKey: true; + isAutoincrement: false; + hasRuntimeDefault: false; }>; role: PgColumn<{ tableName: 'cities_table'; @@ -848,6 +960,10 @@ await db.refreshMaterializedView(newYorkers2).withNoData().concurrently(); enumValues: ['admin', 'user']; notNull: true; baseColumn: never; + generated: undefined; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; }>; population: PgColumn<{ tableName: 'cities_table'; @@ -860,6 +976,10 @@ await db.refreshMaterializedView(newYorkers2).withNoData().concurrently(); hasDefault: true; enumValues: undefined; baseColumn: never; + generated: undefined; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; }>; }; }>, @@ -1005,6 +1125,37 @@ await db.refreshMaterializedView(newYorkers2).withNoData().concurrently(); Expect>; } +{ + const e1 = pgEnum('test', ['a', 'b', 'c']); + const e2 = pgEnum('test', ['a', 'b', 'c'] as const); + + const test = pgTable('test', { + col1: char('col1', { enum: ['a', 'b', 'c'] as const }).generatedAlwaysAs(sql``), + col2: char('col2', { enum: ['a', 'b', 'c'] }).generatedAlwaysAs(sql``), + col3: char('col3').generatedAlwaysAs(sql``), + col4: e1('col4').generatedAlwaysAs(sql``), + col5: e2('col5').generatedAlwaysAs(sql``), + col6: text('col6', { enum: ['a', 'b', 'c'] as const }).generatedAlwaysAs(sql``), + col7: text('col7', { enum: ['a', 'b', 'c'] }).generatedAlwaysAs(sql``), + col8: text('col8').generatedAlwaysAs(sql``), + col9: varchar('col9', { enum: ['a', 'b', 'c'] as const }).generatedAlwaysAs(sql``), + col10: varchar('col10', { enum: ['a', 'b', 'c'] }).generatedAlwaysAs(sql``), + col11: varchar('col11').generatedAlwaysAs(sql``), + }); + + Expect>; + Expect>; + Expect>; + Expect>; + Expect>; + Expect>; + Expect>; + Expect>; + Expect>; + Expect>; + Expect>; +} + { const test = pgTable('test', { id: text('id').$defaultFn(() => crypto.randomUUID()).primaryKey(), diff --git a/drizzle-orm/type-tests/pg/with.ts b/drizzle-orm/type-tests/pg/with.ts index 1e4dfda99..d5fcc96ed 100644 --- a/drizzle-orm/type-tests/pg/with.ts +++ b/drizzle-orm/type-tests/pg/with.ts @@ -11,6 +11,7 @@ const orders = pgTable('orders', { product: text('product').notNull(), amount: integer('amount').notNull(), quantity: integer('quantity').notNull(), + generated: text('generatedText').generatedAlwaysAs(sql``), }); { @@ -62,4 +63,18 @@ const orders = pgTable('orders', { productSales: number; }[], typeof result> >; + + const allOrdersWith = db.$with('all_orders_with').as(db.select().from(orders)); + const allFromWith = await db.with(allOrdersWith).select().from(allOrdersWith); + + Expect< + Equal<{ + id: number; + region: string; + product: string; + amount: number; + quantity: number; + generated: string | null; + }[], typeof allFromWith> + >; } diff --git a/drizzle-orm/type-tests/sqlite/generated-columns.ts b/drizzle-orm/type-tests/sqlite/generated-columns.ts new file mode 100644 index 000000000..57ffea989 --- /dev/null +++ b/drizzle-orm/type-tests/sqlite/generated-columns.ts @@ -0,0 +1,158 @@ +import { type Equal, Expect } from 'type-tests/utils'; +import { type InferInsertModel, type InferSelectModel, sql } from '~/index'; +import { drizzle } from '~/libsql'; +import { int, sqliteTable, text } from '~/sqlite-core'; +import { db } from './db'; + +const users = sqliteTable( + 'users', + { + id: int('id').primaryKey(), + firstName: text('first_name', { length: 255 }), + lastName: text('last_name', { length: 255 }), + email: text('email').notNull(), + fullName: text('full_name').generatedAlwaysAs(sql`concat_ws(first_name, ' ', last_name)`), + upperName: text('upper_name').generatedAlwaysAs( + sql` case when first_name is null then null else upper(first_name) end `, + ).$type(), // There is no way for drizzle to detect nullability in these cases. This is how the user can work around it + }, +); +{ + type User = typeof users.$inferSelect; + type NewUser = typeof users.$inferInsert; + + Expect< + Equal< + { + id: number; + firstName: string | null; + lastName: string | null; + email: string; + fullName: string | null; + upperName: string | null; + }, + User + > + >(); + + Expect< + Equal< + { + email: string; + id?: number | undefined; + firstName?: string | null | undefined; + lastName?: string | null | undefined; + }, + NewUser + > + >(); +} + +{ + type User = InferSelectModel; + type NewUser = InferInsertModel; + + Expect< + Equal< + { + id: number; + firstName: string | null; + lastName: string | null; + email: string; + fullName: string | null; + upperName: string | null; + }, + User + > + >(); + + Expect< + Equal< + { + email: string; + id?: number | undefined; + firstName?: string | null | undefined; + lastName?: string | null | undefined; + }, + NewUser + > + >(); +} + +{ + const dbUsers = await db.select().from(users); + + Expect< + Equal< + { + id: number; + firstName: string | null; + lastName: string | null; + email: string; + fullName: string | null; + upperName: string | null; + }[], + typeof dbUsers + > + >(); +} + +{ + const db = drizzle({} as any, { schema: { users } }); + + const dbUser = await db.query.users.findFirst(); + + Expect< + Equal< + { + id: number; + firstName: string | null; + lastName: string | null; + email: string; + fullName: string | null; + upperName: string | null; + } | undefined, + typeof dbUser + > + >(); +} + +{ + const db = drizzle({} as any, { schema: { users } }); + + const dbUser = await db.query.users.findMany(); + + Expect< + Equal< + { + id: number; + firstName: string | null; + lastName: string | null; + email: string; + fullName: string | null; + upperName: string | null; + }[], + typeof dbUser + > + >(); +} + +{ + // @ts-expect-error - Can't use the fullName because it's a generated column + await db.insert(users).values({ + firstName: 'test', + lastName: 'test', + email: 'test', + fullName: 'test', + }); +} + +{ + await db.update(users).set({ + firstName: 'test', + lastName: 'test', + email: 'test', + // @ts-expect-error - Can't use the fullName because it's a generated column + fullName: 'test', + }); +} diff --git a/drizzle-orm/type-tests/sqlite/tables.ts b/drizzle-orm/type-tests/sqlite/tables.ts index d56b5fe09..01419b27a 100644 --- a/drizzle-orm/type-tests/sqlite/tables.ts +++ b/drizzle-orm/type-tests/sqlite/tables.ts @@ -166,6 +166,10 @@ Expect< tableName: 'new_yorkers'; enumValues: undefined; baseColumn: never; + generated: undefined; + isAutoincrement: false; + hasRuntimeDefault: false; + isPrimaryKey: true; }>; cityId: SQLiteColumn<{ name: 'id'; @@ -178,6 +182,10 @@ Expect< tableName: 'new_yorkers'; enumValues: undefined; baseColumn: never; + generated: undefined; + isAutoincrement: false; + hasRuntimeDefault: false; + isPrimaryKey: true; }>; }>, typeof newYorkers @@ -209,6 +217,10 @@ Expect< tableName: 'new_yorkers'; enumValues: undefined; baseColumn: never; + generated: undefined; + isAutoincrement: false; + hasRuntimeDefault: false; + isPrimaryKey: false; }>; cityId: SQLiteColumn<{ name: 'city_id'; @@ -221,6 +233,10 @@ Expect< tableName: 'new_yorkers'; enumValues: undefined; baseColumn: never; + generated: undefined; + isAutoincrement: false; + hasRuntimeDefault: false; + isPrimaryKey: false; }>; }>, typeof newYorkers @@ -248,6 +264,10 @@ Expect< tableName: 'new_yorkers'; enumValues: undefined; baseColumn: never; + generated: undefined; + isAutoincrement: false; + hasRuntimeDefault: false; + isPrimaryKey: false; }>; cityId: SQLiteColumn<{ name: 'city_id'; @@ -260,6 +280,10 @@ Expect< tableName: 'new_yorkers'; enumValues: undefined; baseColumn: never; + generated: undefined; + isAutoincrement: false; + hasRuntimeDefault: false; + isPrimaryKey: false; }>; }>, typeof newYorkers @@ -410,3 +434,29 @@ Expect< }, typeof table.$inferSelect> >; } + +{ + const test = sqliteTable('test', { + test1: text('test', { length: 255, enum: ['a', 'b', 'c'] as const }).notNull(), + test2: text('test', { length: 255, enum: ['a', 'b', 'c'] }).notNull(), + test3: text('test', { enum: ['a', 'b', 'c'] as const }).notNull(), + test4: text('test', { enum: ['a', 'b', 'c'] }).notNull(), + }); + Expect>; + Expect>; + Expect>; + Expect>; +} + +{ // All types with generated columns + const test = sqliteTable('test', { + test1: text('test', { length: 255, enum: ['a', 'b', 'c'] as const }).generatedAlwaysAs(sql``), + test2: text('test', { length: 255, enum: ['a', 'b', 'c'] }).generatedAlwaysAs(sql``), + test3: text('test', { enum: ['a', 'b', 'c'] as const }).generatedAlwaysAs(sql``), + test4: text('test', { enum: ['a', 'b', 'c'] }).generatedAlwaysAs(sql``), + }); + Expect>; + Expect>; + Expect>; + Expect>; +} diff --git a/drizzle-orm/type-tests/sqlite/with.ts b/drizzle-orm/type-tests/sqlite/with.ts index 94a969dac..8b5963eb6 100644 --- a/drizzle-orm/type-tests/sqlite/with.ts +++ b/drizzle-orm/type-tests/sqlite/with.ts @@ -11,6 +11,7 @@ const orders = sqliteTable('orders', { product: text('product').notNull(), amount: integer('amount').notNull(), quantity: integer('quantity').notNull(), + generated: text('generatedText').generatedAlwaysAs(sql``), }); { @@ -63,4 +64,18 @@ const orders = sqliteTable('orders', { productSales: number; }[], typeof result> >; + + const allOrdersWith = db.$with('all_orders_with').as(db.select().from(orders)); + const allFromWith = await db.with(allOrdersWith).select().from(allOrdersWith); + + Expect< + Equal<{ + id: number; + region: string; + product: string; + amount: number; + quantity: number; + generated: string | null; + }[], typeof allFromWith> + >; } diff --git a/integration-tests/package.json b/integration-tests/package.json index f50e5b08b..20c2d1fc3 100644 --- a/integration-tests/package.json +++ b/integration-tests/package.json @@ -16,6 +16,8 @@ "private": true, "devDependencies": { "@neondatabase/serverless": "0.9.0", + "@originjs/vite-plugin-commonjs": "^1.0.3", + "@paralleldrive/cuid2": "^2.2.2", "@types/async-retry": "^1.4.8", "@types/axios": "^0.14.0", "@types/better-sqlite3": "^7.6.4", diff --git a/integration-tests/tests/mysql-returning.test.ts b/integration-tests/tests/mysql-returning.test.ts new file mode 100644 index 000000000..557d8a756 --- /dev/null +++ b/integration-tests/tests/mysql-returning.test.ts @@ -0,0 +1,205 @@ +import 'dotenv/config'; + +import type { TestFn } from 'ava'; +import anyTest from 'ava'; +import Docker from 'dockerode'; +import { DefaultLogger, sql } from 'drizzle-orm'; +import { boolean, json, mysqlTable, serial, text, timestamp, varchar } from 'drizzle-orm/mysql-core'; +import type { MySql2Database } from 'drizzle-orm/mysql2'; +import { drizzle } from 'drizzle-orm/mysql2'; +import getPort from 'get-port'; +import * as mysql from 'mysql2/promise'; +import { v4 as uuid } from 'uuid'; + +const ENABLE_LOGGING = false; + +const usersTable = mysqlTable('userstest', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + verified: boolean('verified').notNull().default(false), + jsonb: json('jsonb').$type(), + createdAt: timestamp('created_at', { fsp: 2 }).notNull().defaultNow(), +}); + +interface Context { + docker: Docker; + mysqlContainer: Docker.Container; + db: MySql2Database; + client: mysql.Connection; +} + +const test = anyTest as TestFn; + +async function createDockerDB(ctx: Context): Promise { + const docker = (ctx.docker = new Docker()); + const port = await getPort({ port: 3306 }); + const image = 'mysql:8'; + + const pullStream = await docker.pull(image); + await new Promise((resolve, reject) => + docker.modem.followProgress(pullStream, (err) => (err ? reject(err) : resolve(err))) + ); + + ctx.mysqlContainer = await docker.createContainer({ + Image: image, + Env: ['MYSQL_ROOT_PASSWORD=mysql', 'MYSQL_DATABASE=drizzle'], + name: `drizzle-integration-tests-${uuid()}`, + HostConfig: { + AutoRemove: true, + PortBindings: { + '3306/tcp': [{ HostPort: `${port}` }], + }, + }, + }); + + await ctx.mysqlContainer.start(); + + return `mysql://root:mysql@127.0.0.1:${port}/drizzle`; +} + +test.before(async (t) => { + const ctx = t.context; + const connectionString = process.env['MYSQL_CONNECTION_STRING'] ?? await createDockerDB(ctx); + + const sleep = 1000; + let timeLeft = 20000; + let connected = false; + let lastError: unknown | undefined; + do { + try { + ctx.client = await mysql.createConnection(connectionString); + await ctx.client.connect(); + connected = true; + break; + } catch (e) { + lastError = e; + await new Promise((resolve) => setTimeout(resolve, sleep)); + timeLeft -= sleep; + } + } while (timeLeft > 0); + if (!connected) { + console.error('Cannot connect to MySQL'); + await ctx.client?.end().catch(console.error); + await ctx.mysqlContainer?.stop().catch(console.error); + throw lastError; + } + ctx.db = drizzle(ctx.client, { logger: ENABLE_LOGGING ? new DefaultLogger() : undefined }); +}); + +test.after.always(async (t) => { + const ctx = t.context; + await ctx.client?.end().catch(console.error); + await ctx.mysqlContainer?.stop().catch(console.error); +}); + +test.beforeEach(async (t) => { + const ctx = t.context; + await ctx.db.execute(sql`drop table if exists \`userstest\``); + await ctx.db.execute(sql`drop table if exists \`users2\``); + await ctx.db.execute(sql`drop table if exists \`cities\``); + + 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() + ) + `, + ); + + await ctx.db.execute( + sql` + create table \`users2\` ( + \`id\` serial primary key, + \`name\` text not null, + \`city_id\` int references \`cities\`(\`id\`) + ) + `, + ); + + await ctx.db.execute( + sql` + create table \`cities\` ( + \`id\` serial primary key, + \`name\` text not null + ) + `, + ); +}); + +async function setupReturningFunctionsTest(db: MySql2Database) { + await db.execute(sql`drop table if exists \`users_default_fn\``); + await db.execute( + sql` + create table \`users_default_fn\` ( + \`id\` varchar(256) primary key, + \`name\` text not null + ); + `, + ); +} + +test.serial('insert $returningId: serail as id', async (t) => { + const { db } = t.context; + + const result = await db.insert(usersTable).values({ name: 'John' }).$returningId(); + // ^? + t.deepEqual(result, [{ id: 1 }]); +}); + +test.serial('insert $returningId: serail as id, batch insert', async (t) => { + const { db } = t.context; + + const result = await db.insert(usersTable).values([{ name: 'John' }, { name: 'John1' }]).$returningId(); + // ^? + t.deepEqual(result, [{ id: 1 }, { id: 2 }]); +}); + +test.serial('insert $returningId: $default as primary key', async (t) => { + const { db } = t.context; + + const uniqueKeys = ['ao865jf3mcmkfkk8o5ri495z', 'dyqs529eom0iczo2efxzbcut']; + let iterator = 0; + + const usersTableDefFn = mysqlTable('users_default_fn', { + customId: varchar('id', { length: 256 }).primaryKey().$defaultFn(() => { + const value = uniqueKeys[iterator]!; + iterator++; + return value; + }), + name: text('name').notNull(), + }); + + await setupReturningFunctionsTest(db); + + const result = await db.insert(usersTableDefFn).values([{ name: 'John' }, { name: 'John1' }]) + // ^? + .$returningId(); + t.deepEqual(result, [{ customId: 'ao865jf3mcmkfkk8o5ri495z' }, { customId: 'dyqs529eom0iczo2efxzbcut' }]); +}); + +test.serial('insert $returningId: $default as primary key with value', async (t) => { + const { db } = t.context; + + const uniqueKeys = ['ao865jf3mcmkfkk8o5ri495z', 'dyqs529eom0iczo2efxzbcut']; + let iterator = 0; + + const usersTableDefFn = mysqlTable('users_default_fn', { + customId: varchar('id', { length: 256 }).primaryKey().$defaultFn(() => { + const value = uniqueKeys[iterator]!; + iterator++; + return value; + }), + name: text('name').notNull(), + }); + + await setupReturningFunctionsTest(db); + + const result = await db.insert(usersTableDefFn).values([{ name: 'John', customId: 'test' }, { name: 'John1' }]) + // ^? + .$returningId(); + t.deepEqual(result, [{ customId: 'test' }, { customId: 'ao865jf3mcmkfkk8o5ri495z' }]); +}); diff --git a/integration-tests/tests/mysql/mysql-common.ts b/integration-tests/tests/mysql/mysql-common.ts index e67e706fb..db1486270 100644 --- a/integration-tests/tests/mysql/mysql-common.ts +++ b/integration-tests/tests/mysql/mysql-common.ts @@ -59,13 +59,14 @@ import { unique, uniqueIndex, uniqueKeyName, + varchar, year, } from 'drizzle-orm/mysql-core'; import type { MySqlRemoteDatabase } from 'drizzle-orm/mysql-proxy'; import { migrate } from 'drizzle-orm/mysql2/migrator'; import getPort from 'get-port'; import { v4 as uuid } from 'uuid'; -import { afterAll, beforeEach, describe, expect, test } from 'vitest'; +import { afterAll, beforeEach, describe, expect, expectTypeOf, test } from 'vitest'; import { Expect, toLocalDate } from '~/utils.ts'; import type { Equal } from '~/utils.ts'; @@ -213,12 +214,16 @@ export async function createDockerDB(): Promise { return `mysql://root:mysql@127.0.0.1:${port}/drizzle`; } -afterAll(async () => { - await mysqlContainer?.stop().catch(console.error); -}); +// afterAll(async () => { +// await mysqlContainer?.stop().catch(console.error); +// }); export function tests(driver?: string) { describe('common', () => { + afterAll(async () => { + await mysqlContainer?.stop().catch(console.error); + }); + beforeEach(async (ctx) => { const { db } = ctx.mysql; await db.execute(sql`drop table if exists userstest`); @@ -296,6 +301,18 @@ export function tests(driver?: string) { } }); + async function setupReturningFunctionsTest(db: MySqlDatabase) { + await db.execute(sql`drop table if exists \`users_default_fn\``); + await db.execute( + sql` + create table \`users_default_fn\` ( + \`id\` varchar(256) primary key, + \`name\` text not null + ); + `, + ); + } + async function setupSetOperationTest(db: TestMySQLDB) { await db.execute(sql`drop table if exists \`users2\``); await db.execute(sql`drop table if exists \`cities\``); @@ -3324,6 +3341,88 @@ export function tests(driver?: string) { }]); }); + test('insert $returningId: serial as id', async (ctx) => { + const { db } = ctx.mysql; + + const result = await db.insert(usersTable).values({ name: 'John' }).$returningId(); + + expectTypeOf(result).toEqualTypeOf<{ + id: number; + }[]>(); + + expect(result).toStrictEqual([{ id: 1 }]); + }); + + test('insert $returningId: serial as id, batch insert', async (ctx) => { + const { db } = ctx.mysql; + + const result = await db.insert(usersTable).values([{ name: 'John' }, { name: 'John1' }]).$returningId(); + + expectTypeOf(result).toEqualTypeOf<{ + id: number; + }[]>(); + + expect(result).toStrictEqual([{ id: 1 }, { id: 2 }]); + }); + + test('insert $returningId: $default as primary key', async (ctx) => { + const { db } = ctx.mysql; + + const uniqueKeys = ['ao865jf3mcmkfkk8o5ri495z', 'dyqs529eom0iczo2efxzbcut']; + let iterator = 0; + + const usersTableDefFn = mysqlTable('users_default_fn', { + customId: varchar('id', { length: 256 }).primaryKey().$defaultFn(() => { + const value = uniqueKeys[iterator]!; + iterator++; + return value; + }), + name: text('name').notNull(), + }); + + await setupReturningFunctionsTest(db); + + const result = await db.insert(usersTableDefFn).values([{ name: 'John' }, { name: 'John1' }]) + // ^? + .$returningId(); + + expectTypeOf(result).toEqualTypeOf<{ + customId: string; + }[]>(); + + expect(result).toStrictEqual([{ customId: 'ao865jf3mcmkfkk8o5ri495z' }, { + customId: 'dyqs529eom0iczo2efxzbcut', + }]); + }); + + test('insert $returningId: $default as primary key with value', async (ctx) => { + const { db } = ctx.mysql; + + const uniqueKeys = ['ao865jf3mcmkfkk8o5ri495z', 'dyqs529eom0iczo2efxzbcut']; + let iterator = 0; + + const usersTableDefFn = mysqlTable('users_default_fn', { + customId: varchar('id', { length: 256 }).primaryKey().$defaultFn(() => { + const value = uniqueKeys[iterator]!; + iterator++; + return value; + }), + name: text('name').notNull(), + }); + + await setupReturningFunctionsTest(db); + + const result = await db.insert(usersTableDefFn).values([{ name: 'John', customId: 'test' }, { name: 'John1' }]) + // ^? + .$returningId(); + + expectTypeOf(result).toEqualTypeOf<{ + customId: string; + }[]>(); + + expect(result).toStrictEqual([{ customId: 'test' }, { customId: 'ao865jf3mcmkfkk8o5ri495z' }]); + }); + test('mySchema :: view', async (ctx) => { const { db } = ctx.mysql; diff --git a/integration-tests/tests/mysql/tidb-serverless.test.ts b/integration-tests/tests/mysql/tidb-serverless.test.ts index 05199e836..8187882af 100644 --- a/integration-tests/tests/mysql/tidb-serverless.test.ts +++ b/integration-tests/tests/mysql/tidb-serverless.test.ts @@ -1,152 +1,16 @@ import 'dotenv/config'; import { connect } from '@tidbcloud/serverless'; -import { - and, - asc, - avg, - avgDistinct, - count, - countDistinct, - eq, - exists, - getTableColumns, - gt, - gte, - inArray, - lt, - max, - min, - Name, - sql, - sum, - sumDistinct, - TransactionRollbackError, -} from 'drizzle-orm'; -import { - alias, - bigint, - boolean, - date, - datetime, - decimal, - except, - exceptAll, - foreignKey, - getTableConfig, - getViewConfig, - int, - intersect, - intersectAll, - json, - mediumint, - mysqlEnum, - mysqlTable, - mysqlTableCreator, - mysqlView, - primaryKey, - serial, - smallint, - text, - time, - timestamp, - tinyint, - union, - unionAll, - unique, - uniqueIndex, - uniqueKeyName, - varchar, - year, -} from 'drizzle-orm/mysql-core'; import type { TiDBServerlessDatabase } from 'drizzle-orm/tidb-serverless'; import { drizzle } from 'drizzle-orm/tidb-serverless'; -import { migrate } from 'drizzle-orm/tidb-serverless/migrator'; -import { beforeAll, beforeEach, expect, test } from 'vitest'; -import { type Equal, Expect, toLocalDate } from '../utils.ts'; +import { beforeAll, beforeEach } from 'vitest'; +import { skipTests } from '~/common.ts'; +import { tests } from './mysql-common.ts'; const ENABLE_LOGGING = false; let db: TiDBServerlessDatabase; -const usersTable = mysqlTable('userstest', { - id: serial('id').primaryKey(), - name: text('name').notNull(), - verified: boolean('verified').notNull().default(false), - jsonb: json('jsonb').$type(), - createdAt: timestamp('created_at', { fsp: 2 }).notNull().defaultNow(), -}); - -const users2Table = mysqlTable('users2', { - id: serial('id').primaryKey(), - name: text('name').notNull(), - cityId: int('city_id').references(() => citiesTable.id), -}); - -const citiesTable = mysqlTable('cities', { - id: serial('id').primaryKey(), - name: text('name').notNull(), -}); - -const usersOnUpdate = mysqlTable('users_on_update', { - id: serial('id').primaryKey(), - name: text('name').notNull(), - updateCounter: int('update_counter').default(sql`1`).$onUpdateFn(() => sql`update_counter + 1`), - updatedAt: datetime('updated_at', { mode: 'date', fsp: 3 }).$onUpdate(() => new Date()), - uppercaseName: text('uppercase_name').$onUpdateFn(() => sql`upper(name)`), - alwaysNull: text('always_null').$type().$onUpdateFn(() => null), // need to add $type because $onUpdate add a default value -}); - -const datesTable = mysqlTable('datestable', { - date: date('date'), - dateAsString: date('date_as_string', { mode: 'string' }), - time: time('time', { fsp: 1 }), - datetime: datetime('datetime', { fsp: 2 }), - datetimeAsString: datetime('datetime_as_string', { fsp: 2, mode: 'string' }), - timestamp: timestamp('timestamp', { fsp: 3 }), - timestampAsString: timestamp('timestamp_as_string', { fsp: 3, mode: 'string' }), - year: year('year'), -}); - -const coursesTable = mysqlTable('courses', { - id: serial('id').primaryKey(), - name: text('name').notNull(), - categoryId: int('category_id').references(() => courseCategoriesTable.id), -}); - -const courseCategoriesTable = mysqlTable('course_categories', { - id: serial('id').primaryKey(), - name: text('name').notNull(), -}); - -const orders = mysqlTable('orders', { - id: serial('id').primaryKey(), - region: text('region').notNull(), - product: text('product').notNull().$default(() => 'random_string'), - amount: int('amount').notNull(), - quantity: int('quantity').notNull(), -}); - -const usersMigratorTable = mysqlTable('users12', { - id: serial('id').primaryKey(), - name: text('name').notNull(), - email: text('email').notNull(), -}, (table) => { - return { - name: uniqueIndex('').on(table.name).using('btree'), - }; -}); - -// 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'), -}); - beforeAll(async () => { const connectionString = process.env['TIDB_CONNECTION_STRING']; if (!connectionString) { @@ -157,2627 +21,55 @@ beforeAll(async () => { db = drizzle(client!, { logger: ENABLE_LOGGING }); }); -beforeEach(async () => { - await db.execute(sql`drop table if exists \`userstest\``); - await db.execute(sql`drop table if exists \`users2\``); - await db.execute(sql`drop table if exists \`cities\``); - - await 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() - ) - `, - ); - - await db.execute( - sql` - create table \`users2\` ( - \`id\` serial primary key, - \`name\` text not null, - \`city_id\` int references \`cities\`(\`id\`) - ) - `, - ); - - await db.execute( - sql` - create table \`cities\` ( - \`id\` serial primary key, - \`name\` text not null - ) - `, - ); -}); - -async function setupSetOperationTest(db: TiDBServerlessDatabase) { - await db.execute(sql`drop table if exists \`users2\``); - await db.execute(sql`drop table if exists \`cities\``); - await db.execute( - sql` - create table \`users2\` ( - \`id\` serial primary key, - \`name\` text not null, - \`city_id\` int references \`cities\`(\`id\`) - ) - `, - ); - - await db.execute( - sql` - create table \`cities\` ( - \`id\` serial primary key, - \`name\` text not null - ) - `, - ); - - await db.insert(citiesTable).values([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - await db.insert(users2Table).values([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 2 }, - { id: 3, name: 'Jack', cityId: 3 }, - { id: 4, name: 'Peter', cityId: 3 }, - { id: 5, name: 'Ben', cityId: 2 }, - { id: 6, name: 'Jill', cityId: 1 }, - { id: 7, name: 'Mary', cityId: 2 }, - { id: 8, name: 'Sally', cityId: 1 }, - ]); -} - -async function setupAggregateFunctionsTest(db: TiDBServerlessDatabase) { - 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('table config: unsigned ints', async () => { - const unsignedInts = mysqlTable('cities1', { - bigint: bigint('bigint', { mode: 'number', unsigned: true }), - int: int('int', { unsigned: true }), - smallint: smallint('smallint', { unsigned: true }), - mediumint: mediumint('mediumint', { unsigned: true }), - tinyint: tinyint('tinyint', { unsigned: true }), - }); - - const tableConfig = getTableConfig(unsignedInts); - - const bigintColumn = tableConfig.columns.find((c) => c.name === 'bigint')!; - const intColumn = tableConfig.columns.find((c) => c.name === 'int')!; - const smallintColumn = tableConfig.columns.find((c) => c.name === 'smallint')!; - const mediumintColumn = tableConfig.columns.find((c) => c.name === 'mediumint')!; - const tinyintColumn = tableConfig.columns.find((c) => c.name === 'tinyint')!; - - expect(bigintColumn.getSQLType()).toEqual('bigint unsigned'); - expect(intColumn.getSQLType()).toEqual('int unsigned'); - expect(smallintColumn.getSQLType()).toEqual('smallint unsigned'); - expect(mediumintColumn.getSQLType()).toEqual('mediumint unsigned'); - expect(tinyintColumn.getSQLType()).toEqual('tinyint unsigned'); -}); - -test('table config: signed ints', async () => { - const unsignedInts = mysqlTable('cities1', { - bigint: bigint('bigint', { mode: 'number' }), - int: int('int'), - smallint: smallint('smallint'), - mediumint: mediumint('mediumint'), - tinyint: tinyint('tinyint'), - }); - - const tableConfig = getTableConfig(unsignedInts); - - const bigintColumn = tableConfig.columns.find((c) => c.name === 'bigint')!; - const intColumn = tableConfig.columns.find((c) => c.name === 'int')!; - const smallintColumn = tableConfig.columns.find((c) => c.name === 'smallint')!; - const mediumintColumn = tableConfig.columns.find((c) => c.name === 'mediumint')!; - const tinyintColumn = tableConfig.columns.find((c) => c.name === 'tinyint')!; - - expect(bigintColumn.getSQLType()).toEqual('bigint'); - expect(intColumn.getSQLType()).toEqual('int'); - expect(smallintColumn.getSQLType()).toEqual('smallint'); - expect(mediumintColumn.getSQLType()).toEqual('mediumint'); - expect(tinyintColumn.getSQLType()).toEqual('tinyint'); -}); - -test('table config: foreign keys name', async () => { - const table = mysqlTable('cities', { - id: serial('id').primaryKey(), - name: text('name').notNull(), - state: text('state'), - }, (t) => ({ - f: foreignKey({ foreignColumns: [t.id], columns: [t.id], name: 'custom_fk' }), - })); - - const tableConfig = getTableConfig(table); - - expect(tableConfig.foreignKeys.length).toEqual(1); - expect(tableConfig.foreignKeys[0]!.getName()).toEqual('custom_fk'); -}); - -test('table config: primary keys name', async () => { - const table = mysqlTable('cities', { - id: serial('id').primaryKey(), - name: text('name').notNull(), - state: text('state'), - }, (t) => ({ - f: primaryKey({ columns: [t.id, t.name], name: 'custom_pk' }), - })); - - const tableConfig = getTableConfig(table); - - expect(tableConfig.primaryKeys.length).toEqual(1); - expect(tableConfig.primaryKeys[0]!.getName()).toEqual('custom_pk'); -}); - -test('table configs: unique third param', async () => { - const cities1Table = mysqlTable('cities1', { - id: serial('id').primaryKey(), - name: text('name').notNull(), - state: text('state'), - }, (t) => ({ - f: unique('custom_name').on(t.name, t.state), - f1: unique('custom_name1').on(t.name, t.state), - })); - - const tableConfig = getTableConfig(cities1Table); - - expect(tableConfig.uniqueConstraints).toHaveLength(2); - - expect(tableConfig.uniqueConstraints[0]?.name).toEqual('custom_name'); - expect(tableConfig.uniqueConstraints[0]?.columns.map((t) => t.name)).toEqual(['name', 'state']); - - expect(tableConfig.uniqueConstraints[1]?.name).toEqual('custom_name1'); - expect(tableConfig.uniqueConstraints[1]?.columns.map((t) => t.name)).toEqual(['name', 'state']); -}); - -test('table configs: unique in column', async () => { - const cities1Table = mysqlTable('cities1', { - id: serial('id').primaryKey(), - name: text('name').notNull().unique(), - state: text('state').unique('custom'), - field: text('field').unique('custom_field'), - }); - - const tableConfig = getTableConfig(cities1Table); - - const columnName = tableConfig.columns.find((it) => it.name === 'name'); - expect(columnName?.uniqueName).toEqual(uniqueKeyName(cities1Table, [columnName!.name])); - expect(columnName?.isUnique).toEqual(true); - - const columnState = tableConfig.columns.find((it) => it.name === 'state'); - expect(columnState?.uniqueName === 'custom').toEqual(true); - expect(columnState?.isUnique).toEqual(true); - - const columnField = tableConfig.columns.find((it) => it.name === 'field'); - expect(columnField?.uniqueName === 'custom_field').toEqual(true); - expect(columnField?.isUnique).toEqual(true); -}); - -test('select all fields', async () => { - await db.insert(usersTable).values({ name: 'John' }); - const result = await db.select().from(usersTable); - - expect(result[0]!.createdAt instanceof Date).toEqual(true); // eslint-disable-line no-instanceof/no-instanceof - // not timezone based timestamp, thats why it should not work here - // expect(Math.abs(result[0]!.createdAt.getTime() - now) < 2000).toEqual(true); - expect(result).toEqual([{ id: 1, name: 'John', verified: false, jsonb: null, createdAt: result[0]!.createdAt }]); -}); - -test('select sql', async () => { - await db.insert(usersTable).values({ name: 'John' }); - const users = await db.select({ - name: sql`upper(${usersTable.name})`, - }).from(usersTable); - - expect(users).toEqual([{ name: 'JOHN' }]); -}); - -test('select typed sql', async () => { - await db.insert(usersTable).values({ name: 'John' }); - const users = await db.select({ - name: sql`upper(${usersTable.name})`, - }).from(usersTable); - - expect(users).toEqual([{ name: 'JOHN' }]); -}); - -test('select distinct', async () => { - const usersDistinctTable = mysqlTable('users_distinct', { - id: int('id').notNull(), - name: text('name').notNull(), - }); - - await db.execute(sql`drop table if exists ${usersDistinctTable}`); - await db.execute(sql`create table ${usersDistinctTable} (id int, name text)`); - - await db.insert(usersDistinctTable).values([ - { id: 1, name: 'John' }, - { id: 1, name: 'John' }, - { id: 2, name: 'John' }, - { id: 1, name: 'Jane' }, - ]); - const users = await db.selectDistinct().from(usersDistinctTable).orderBy( - usersDistinctTable.id, - usersDistinctTable.name, - ); - - await db.execute(sql`drop table ${usersDistinctTable}`); - - expect(users).toEqual([{ id: 1, name: 'Jane' }, { id: 1, name: 'John' }, { id: 2, name: 'John' }]); -}); - -test('insert returning sql', async () => { - const result = await db.insert(usersTable).values({ name: 'John' }); - - expect(result.lastInsertId).toEqual(1); -}); - -test('delete returning sql', async () => { - await db.insert(usersTable).values({ name: 'John' }); - const users = await db.delete(usersTable).where(eq(usersTable.name, 'John')); - - expect(users.rowsAffected).toEqual(1); -}); - -test('update returning sql', async () => { - await db.insert(usersTable).values({ name: 'John' }); - const users = await db.update(usersTable).set({ name: 'Jane' }).where(eq(usersTable.name, 'John')); - - expect(users.rowsAffected).toEqual(1); -}); - -test('update with returning all fields', async () => { - await db.insert(usersTable).values({ name: 'John' }); - const updatedUsers = await db.update(usersTable).set({ name: 'Jane' }).where(eq(usersTable.name, 'John')); - - const users = await db.select().from(usersTable).where(eq(usersTable.id, 1)); - - expect(updatedUsers.rowsAffected).toEqual(1); - - expect(users[0]!.createdAt instanceof Date).toEqual(true); // eslint-disable-line no-instanceof/no-instanceof - // not timezone based timestamp, thats why it should not work here - // expect(Math.abs(users[0]!.createdAt.getTime() - now) < 2000).toEqual(true); - expect(users).toEqual([{ id: 1, name: 'Jane', verified: false, jsonb: null, createdAt: users[0]!.createdAt }]); -}); - -test('update with returning partial', async () => { - await db.insert(usersTable).values({ name: 'John' }); - const updatedUsers = await db.update(usersTable).set({ name: 'Jane' }).where(eq(usersTable.name, 'John')); - - const users = await db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable).where( - eq(usersTable.id, 1), - ); - - expect(updatedUsers.rowsAffected).toEqual(1); - - expect(users).toEqual([{ id: 1, name: 'Jane' }]); -}); - -test('delete with returning all fields', async () => { - await db.insert(usersTable).values({ name: 'John' }); - const deletedUser = await db.delete(usersTable).where(eq(usersTable.name, 'John')); - - expect(deletedUser.rowsAffected).toEqual(1); -}); - -test('delete with returning partial', async () => { - await db.insert(usersTable).values({ name: 'John' }); - const deletedUser = await db.delete(usersTable).where(eq(usersTable.name, 'John')); - - expect(deletedUser.rowsAffected).toEqual(1); -}); - -test('insert + select', async () => { - await db.insert(usersTable).values({ name: 'John' }); - const result = await db.select().from(usersTable); - expect(result).toEqual([{ id: 1, name: 'John', verified: false, jsonb: null, createdAt: result[0]!.createdAt }]); - - await db.insert(usersTable).values({ name: 'Jane' }); - const result2 = await db.select().from(usersTable); - expect(result2).toEqual([ - { id: 1, name: 'John', verified: false, jsonb: null, createdAt: result2[0]!.createdAt }, - { id: 2, name: 'Jane', verified: false, jsonb: null, createdAt: result2[1]!.createdAt }, - ]); -}); - -test('json insert', async () => { - await db.insert(usersTable).values({ name: 'John', jsonb: ['foo', 'bar'] }); - const result = await db.select({ - id: usersTable.id, - name: usersTable.name, - jsonb: usersTable.jsonb, - }).from(usersTable); - - expect(result).toEqual([{ id: 1, name: 'John', jsonb: ['foo', 'bar'] }]); -}); - -test('insert with overridden default values', async () => { - await db.insert(usersTable).values({ name: 'John', verified: true }); - const result = await db.select().from(usersTable); - - expect(result).toEqual([{ id: 1, name: 'John', verified: true, jsonb: null, createdAt: result[0]!.createdAt }]); -}); - -test('insert many', async () => { - await db.insert(usersTable).values([ - { name: 'John' }, - { name: 'Bruce', jsonb: ['foo', 'bar'] }, - { name: 'Jane' }, - { name: 'Austin', verified: true }, - ]); - const result = await db.select({ - id: usersTable.id, - name: usersTable.name, - jsonb: usersTable.jsonb, - verified: usersTable.verified, - }).from(usersTable); - - expect(result).toEqual([ - { id: 1, name: 'John', jsonb: null, verified: false }, - { id: 2, name: 'Bruce', jsonb: ['foo', 'bar'], verified: false }, - { id: 3, name: 'Jane', jsonb: null, verified: false }, - { id: 4, name: 'Austin', jsonb: null, verified: true }, - ]); -}); - -test('insert many with returning', async () => { - const result = await db.insert(usersTable).values([ - { name: 'John' }, - { name: 'Bruce', jsonb: ['foo', 'bar'] }, - { name: 'Jane' }, - { name: 'Austin', verified: true }, - ]); - - expect(result.rowsAffected).toEqual(4); -}); - -test('select with group by as field', async () => { - await db.insert(usersTable).values([{ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }]); - - const result = await db.select({ name: usersTable.name }).from(usersTable) - .groupBy(usersTable.name).orderBy(usersTable.name); - - expect(result).toEqual([{ name: 'Jane' }, { name: 'John' }]); -}); - -test('select with exists', async () => { - await db.insert(usersTable).values([{ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }]); - - const user = alias(usersTable, 'user'); - const result = await db.select({ name: usersTable.name }).from(usersTable).where( - exists(db.select({ one: sql`1` }).from(user).where(and(eq(usersTable.name, 'John'), eq(user.id, usersTable.id)))), - ); - - expect(result).toEqual([{ name: 'John' }]); -}); - -test('select with group by as sql', async () => { - await db.insert(usersTable).values([{ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }]); - - const result = await db.select({ name: usersTable.name }).from(usersTable) - .groupBy(sql`${usersTable.name}`); - - expect(result).toEqual(expect.arrayContaining([{ name: 'Jane' }, { name: 'John' }])); -}); - -test('$default function', async () => { - await db.execute(sql`drop table if exists \`orders\``); - await db.execute( - sql` - create table \`orders\` ( - \`id\` serial primary key, - \`region\` text not null, - \`product\` text not null, - \`amount\` int not null, - \`quantity\` int not null - ) - `, - ); - - await db.insert(orders).values({ id: 1, region: 'Ukraine', amount: 1, quantity: 1 }); - const selectedOrder = await db.select().from(orders); - - expect(selectedOrder).toEqual([{ - id: 1, - amount: 1, - quantity: 1, - region: 'Ukraine', - product: 'random_string', - }]); -}); - -// Default value for TEXT is not supported -test.skip('$default with empty array - text column', async () => { - await db.execute(sql`drop table if exists \`s_orders\``); - await db.execute( - sql` - create table \`s_orders\` ( - \`id\` serial primary key, - \`region\` text default ('Ukraine'), - \`product\` text not null - ) - `, - ); - - const users = mysqlTable('s_orders', { - id: serial('id').primaryKey(), - region: text('region').default('Ukraine'), - product: text('product').$defaultFn(() => 'random_string'), - }); - - await db.insert(users).values({}); - const selectedOrder = await db.select().from(users); - - expect(selectedOrder).toEqual([{ - id: 1, - region: 'Ukraine', - product: 'random_string', - }]); -}); - -test('$default with empty array', async () => { - await db.execute(sql`drop table if exists \`s_orders\``); - await db.execute( - sql` - create table \`s_orders\` ( - \`id\` serial primary key, - \`region\` varchar(255) default 'Ukraine', - \`product\` text not null - ) - `, - ); - - const users = mysqlTable('s_orders', { - id: serial('id').primaryKey(), - region: varchar('region', { length: 255 }).default('Ukraine'), - product: text('product').$defaultFn(() => 'random_string'), - }); - - await db.insert(users).values({}); - const selectedOrder = await db.select().from(users); - - expect(selectedOrder).toEqual([{ - id: 1, - region: 'Ukraine', - product: 'random_string', - }]); -}); - -test('select with group by as sql + column', async () => { - await db.insert(usersTable).values([{ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }]); - - const result = await db.select({ name: usersTable.name }).from(usersTable) - .groupBy(sql`${usersTable.name}`, usersTable.id); - - expect(result).toEqual([{ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }]); -}); - -test('select with group by as column + sql', async () => { - await db.insert(usersTable).values([{ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }]); - - const result = await db.select({ name: usersTable.name }).from(usersTable) - .groupBy(usersTable.id, sql`${usersTable.name}`); - - expect(result).toEqual([{ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }]); -}); - -test('select with group by complex query', async () => { - await db.insert(usersTable).values([{ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }]); - - const result = await db.select({ name: usersTable.name }).from(usersTable) - .groupBy(usersTable.id, sql`${usersTable.name}`) - .orderBy(asc(usersTable.name)) - .limit(1); - - expect(result).toEqual([{ name: 'Jane' }]); -}); - -test('build query', async () => { - const query = db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable) - .groupBy(usersTable.id, usersTable.name) - .toSQL(); - - expect(query).toEqual({ - sql: `select \`id\`, \`name\` from \`userstest\` group by \`userstest\`.\`id\`, \`userstest\`.\`name\``, - params: [], - }); -}); - -test('Query check: Insert all defaults in 1 row', async () => { - const users = mysqlTable('users', { - id: serial('id').primaryKey(), - name: text('name').default('Dan'), - state: text('state'), - }); - - const query = db - .insert(users) - .values({}) - .toSQL(); - - expect(query).toEqual({ - sql: 'insert into `users` (`id`, `name`, `state`) values (default, default, default)', - params: [], - }); -}); - -test('Query check: Insert all defaults in multiple rows', async () => { - const users = mysqlTable('users', { - id: serial('id').primaryKey(), - name: text('name').default('Dan'), - state: text('state').default('UA'), - }); - - const query = db - .insert(users) - .values([{}, {}]) - .toSQL(); - - expect(query).toEqual({ - sql: 'insert into `users` (`id`, `name`, `state`) values (default, default, default), (default, default, default)', - params: [], - }); -}); - -// Default value for TEXT is not supported -test.skip('Insert all defaults in 1 row - text column', async () => { - const users = mysqlTable('empty_insert_single', { - id: serial('id').primaryKey(), - name: text('name').default('Dan'), - state: text('state'), - }); - - await db.execute(sql`drop table if exists ${users}`); - - await db.execute( - sql`create table ${users} (id serial primary key, name text default ('Dan'), state text)`, - ); - - await db.insert(users).values({}); - - const res = await db.select().from(users); - - expect(res).toEqual([{ id: 1, name: 'Dan', state: null }]); -}); - -test('Insert all defaults in 1 row', async () => { - const users = mysqlTable('empty_insert_single', { - id: serial('id').primaryKey(), - name: varchar('name', { length: 255 }).default('Dan'), - state: text('state'), - }); - - await db.execute(sql`drop table if exists ${users}`); - - await db.execute( - sql`create table ${users} (id serial primary key, name varchar(255) default 'Dan', state text)`, - ); - - await db.insert(users).values({}); - - const res = await db.select().from(users); - - expect(res).toEqual([{ id: 1, name: 'Dan', state: null }]); -}); - -// Default value for TEXT is not supported -test.skip('Insert all defaults in multiple rows - text column', async () => { - const users = mysqlTable('empty_insert_multiple', { - id: serial('id').primaryKey(), - name: text('name').default('Dan'), - state: text('state'), - }); - - await db.execute(sql`drop table if exists ${users}`); - - await db.execute( - sql`create table ${users} (id serial primary key, name text default ('Dan'), state text)`, - ); - - await db.insert(users).values([{}, {}]); - - const res = await db.select().from(users); - - expect(res).toEqual([{ id: 1, name: 'Dan', state: null }, { id: 2, name: 'Dan', state: null }]); -}); - -test('Insert all defaults in multiple rows', async () => { - const users = mysqlTable('empty_insert_multiple', { - id: serial('id').primaryKey(), - name: varchar('name', { length: 255 }).default('Dan'), - state: text('state'), - }); - - await db.execute(sql`drop table if exists ${users}`); - - await db.execute( - sql`create table ${users} (id serial primary key, name varchar(255) default 'Dan', state text)`, - ); - - await db.insert(users).values([{}, {}]); - - const res = await db.select().from(users); - - expect(res).toEqual([{ id: 1, name: 'Dan', state: null }, { id: 2, name: 'Dan', state: null }]); -}); - -test('build query insert with onDuplicate', async () => { - const query = db.insert(usersTable) - .values({ name: 'John', jsonb: ['foo', 'bar'] }) - .onDuplicateKeyUpdate({ set: { name: 'John1' } }) - .toSQL(); - - expect(query).toEqual({ - sql: - 'insert into `userstest` (`id`, `name`, `verified`, `jsonb`, `created_at`) values (default, ?, default, ?, default) on duplicate key update `name` = ?', - params: ['John', '["foo","bar"]', 'John1'], - }); -}); - -test('insert with onDuplicate', async () => { - await db.insert(usersTable) - .values({ name: 'John' }); - - await db.insert(usersTable) - .values({ id: 1, name: 'John' }) - .onDuplicateKeyUpdate({ set: { name: 'John1' } }); - - const res = await db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable).where( - eq(usersTable.id, 1), - ); - - expect(res).toEqual([{ id: 1, name: 'John1' }]); -}); - -test('insert conflict', async () => { - await db.insert(usersTable) - .values({ name: 'John' }); - - await expect(() => db.insert(usersTable).values({ id: 1, name: 'John1' })) - .rejects.toThrowError( - "Execute SQL fail: Error 1062 (23000): Duplicate entry '?' for key 'userstest.id'", - ); -}); - -test('insert conflict with ignore', async () => { - await db.insert(usersTable) - .values({ name: 'John' }); - - await db.insert(usersTable) - .ignore() - .values({ id: 1, name: 'John1' }); - - const res = await db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable).where( - eq(usersTable.id, 1), - ); - - expect(res).toEqual([{ id: 1, name: 'John' }]); -}); - -test('insert sql', async () => { - await db.insert(usersTable).values({ name: sql`${'John'}` }); - const result = await db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable); - expect(result).toEqual([{ id: 1, name: 'John' }]); -}); - -test('partial join with alias', async () => { - const customerAlias = alias(usersTable, 'customer'); - - await db.insert(usersTable).values([{ id: 10, name: 'Ivan' }, { id: 11, name: 'Hans' }]); - const result = await db - .select({ - user: { - id: usersTable.id, - name: usersTable.name, - }, - customer: { - id: customerAlias.id, - name: customerAlias.name, - }, - }).from(usersTable) - .leftJoin(customerAlias, eq(customerAlias.id, 11)) - .where(eq(usersTable.id, 10)); - - expect(result).toEqual([{ - user: { id: 10, name: 'Ivan' }, - customer: { id: 11, name: 'Hans' }, - }]); -}); - -test('full join with alias', async () => { - const mysqlTable = mysqlTableCreator((name) => `prefixed_${name}`); - - const users = mysqlTable('users', { - id: serial('id').primaryKey(), - name: text('name').notNull(), - }); - - await db.execute(sql`drop table if exists ${users}`); - await db.execute(sql`create table ${users} (id serial primary key, name text not null)`); - - const customers = alias(users, 'customer'); - - await db.insert(users).values([{ id: 10, name: 'Ivan' }, { id: 11, name: 'Hans' }]); - const result = await db - .select().from(users) - .leftJoin(customers, eq(customers.id, 11)) - .where(eq(users.id, 10)); - - expect(result).toEqual([{ - users: { - id: 10, - name: 'Ivan', - }, - customer: { - id: 11, - name: 'Hans', - }, - }]); - - await db.execute(sql`drop table ${users}`); -}); - -test('select from alias', async () => { - const mysqlTable = mysqlTableCreator((name) => `prefixed_${name}`); - - const users = mysqlTable('users', { - id: serial('id').primaryKey(), - name: text('name').notNull(), - }); - - await db.execute(sql`drop table if exists ${users}`); - await db.execute(sql`create table ${users} (id serial primary key, name text not null)`); - - const user = alias(users, 'user'); - const customers = alias(users, 'customer'); - - await db.insert(users).values([{ id: 10, name: 'Ivan' }, { id: 11, name: 'Hans' }]); - const result = await db - .select() - .from(user) - .leftJoin(customers, eq(customers.id, 11)) - .where(eq(user.id, 10)); - - expect(result).toEqual([{ - user: { - id: 10, - name: 'Ivan', - }, - customer: { - id: 11, - name: 'Hans', - }, - }]); - - await db.execute(sql`drop table ${users}`); -}); - -test('insert with spaces', async () => { - await db.insert(usersTable).values({ name: sql`'Jo h n'` }); - const result = await db.select({ id: usersTable.id, name: usersTable.name }).from(usersTable); - - expect(result).toEqual([{ id: 1, name: 'Jo h n' }]); -}); - -test('prepared statement', async () => { - await db.insert(usersTable).values({ name: 'John' }); - const statement = db.select({ - id: usersTable.id, - name: usersTable.name, - }).from(usersTable) - .prepare(); - const result = await statement.execute(); - - expect(result).toEqual([{ id: 1, name: 'John' }]); -}); - -test('prepared statement reuse', async () => { - const stmt = db.insert(usersTable).values({ - verified: true, - name: sql.placeholder('name'), - }).prepare(); - - for (let i = 0; i < 10; i++) { - await stmt.execute({ name: `John ${i}` }); - } - - const result = await db.select({ - id: usersTable.id, - name: usersTable.name, - verified: usersTable.verified, - }).from(usersTable); - - expect(result).toEqual([ - { id: 1, name: 'John 0', verified: true }, - { id: 2, name: 'John 1', verified: true }, - { id: 3, name: 'John 2', verified: true }, - { id: 4, name: 'John 3', verified: true }, - { id: 5, name: 'John 4', verified: true }, - { id: 6, name: 'John 5', verified: true }, - { id: 7, name: 'John 6', verified: true }, - { id: 8, name: 'John 7', verified: true }, - { id: 9, name: 'John 8', verified: true }, - { id: 10, name: 'John 9', verified: true }, - ]); -}); - -test('prepared statement with placeholder in .where', async () => { - await db.insert(usersTable).values({ name: 'John' }); - const stmt = db.select({ - id: usersTable.id, - name: usersTable.name, - }).from(usersTable) - .where(eq(usersTable.id, sql.placeholder('id'))) - .prepare(); - const result = await stmt.execute({ id: 1 }); - - expect(result).toEqual([{ id: 1, name: 'John' }]); -}); - -test('migrator', async () => { - 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 users12`); - await db.execute(sql`drop table if exists __drizzle_migrations`); - - await migrate(db, { migrationsFolder: './drizzle2/mysql' }); - - await db.insert(usersMigratorTable).values({ name: 'John', email: 'email' }); - - const result = await db.select().from(usersMigratorTable); - - expect(result).toEqual([{ id: 1, 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 users12`); - await db.execute(sql`drop table __drizzle_migrations`); -}); - -test('insert via db.execute + select via db.execute', async () => { - await db.execute(sql`insert into ${usersTable} (${new Name(usersTable.name.name)}) values (${'John'})`); - - const result = await db.execute<{ id: string; name: string }>(sql`select id, name from ${usersTable}`); - expect(result.rows).toEqual([{ id: '1', name: 'John' }]); +beforeEach((ctx) => { + ctx.mysql = { + db, + }; }); -test('insert via db.execute w/ query builder', async () => { - const inserted = await db.execute( - db.insert(usersTable).values({ name: 'John' }), - ); - expect(inserted.rowsAffected).toEqual(1); -}); - -test('insert + select all possible dates', async () => { - await db.execute(sql`drop table if exists \`datestable\``); - await db.execute( - sql` - create table \`datestable\` ( - \`date\` date, - \`date_as_string\` date, - \`time\` time, - \`datetime\` datetime, - \`datetime_as_string\` datetime, - \`timestamp\` timestamp(3), - \`timestamp_as_string\` timestamp(3), - \`year\` year - ) - `, - ); - - const date = new Date('2022-11-11'); - const dateWithMilliseconds = new Date('2022-11-11 12:12:12.123'); - - await db.insert(datesTable).values({ - date: date, - dateAsString: '2022-11-11', - time: '12:12:12', - datetime: date, - year: 22, - datetimeAsString: '2022-11-11 12:12:12', - timestamp: dateWithMilliseconds, - timestampAsString: '2022-11-11 12:12:12.123', - }); - - const res = await db.select().from(datesTable); - - expect(res[0]?.date instanceof Date).toEqual(true); // eslint-disable-line no-instanceof/no-instanceof - expect(res[0]?.datetime instanceof Date).toEqual(true); // eslint-disable-line no-instanceof/no-instanceof - expect(typeof res[0]?.dateAsString === 'string').toEqual(true); - expect(typeof res[0]?.datetimeAsString === 'string').toEqual(true); - - expect(res).toEqual([{ - date: toLocalDate(new Date('2022-11-11')), - dateAsString: '2022-11-11', - time: '12:12:12', - datetime: new Date('2022-11-11'), - year: 2022, - datetimeAsString: '2022-11-11 12:12:12', - timestamp: new Date('2022-11-11 12:12:12.123'), - timestampAsString: '2022-11-11 12:12:12.123', - }]); - - await db.execute(sql`drop table if exists \`datestable\``); -}); - -const tableWithEnums = mysqlTable('enums_test_case', { - id: serial('id').primaryKey(), - enum1: mysqlEnum('enum1', ['a', 'b', 'c']).notNull(), - enum2: mysqlEnum('enum2', ['a', 'b', 'c']).default('a'), - enum3: mysqlEnum('enum3', ['a', 'b', 'c']).notNull().default('b'), -}); - -test('Mysql enum test case #1', async () => { - await db.execute(sql`drop table if exists \`enums_test_case\``); - - await db.execute(sql` - create table \`enums_test_case\` ( - \`id\` serial primary key, - \`enum1\` ENUM('a', 'b', 'c') not null, - \`enum2\` ENUM('a', 'b', 'c') default 'a', - \`enum3\` ENUM('a', 'b', 'c') not null default 'b' - ) - `); - - await db.insert(tableWithEnums).values([ - { id: 1, enum1: 'a', enum2: 'b', enum3: 'c' }, - { id: 2, enum1: 'a', enum3: 'c' }, - { id: 3, enum1: 'a' }, - ]); - - const res = await db.select().from(tableWithEnums); - - await db.execute(sql`drop table \`enums_test_case\``); - - expect(res).toEqual([ - { id: 1, enum1: 'a', enum2: 'b', enum3: 'c' }, - { id: 2, enum1: 'a', enum2: 'a', enum3: 'c' }, - { id: 3, enum1: 'a', enum2: 'a', enum3: 'b' }, - ]); -}); - -test('left join (flat object fields)', async () => { - await db.insert(citiesTable) - .values([{ name: 'Paris' }, { name: 'London' }]); - - await db.insert(users2Table).values([{ name: 'John', cityId: 1 }, { name: 'Jane' }]); - - const res = await db.select({ - userId: users2Table.id, - userName: users2Table.name, - cityId: citiesTable.id, - cityName: citiesTable.name, - }).from(users2Table) - .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)); - - expect(res).toEqual([ - { userId: 1, userName: 'John', cityId: 1, cityName: 'Paris' }, - { userId: 2, userName: 'Jane', cityId: null, cityName: null }, - ]); -}); - -test('left join (grouped fields)', async () => { - await db.insert(citiesTable) - .values([{ name: 'Paris' }, { name: 'London' }]); - - await db.insert(users2Table).values([{ name: 'John', cityId: 1 }, { name: 'Jane' }]); - - const res = await db.select({ - id: users2Table.id, - user: { - name: users2Table.name, - nameUpper: sql`upper(${users2Table.name})`, - }, - city: { - id: citiesTable.id, - name: citiesTable.name, - nameUpper: sql`upper(${citiesTable.name})`, - }, - }).from(users2Table) - .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)); - - expect(res).toEqual([ - { - id: 1, - user: { name: 'John', nameUpper: 'JOHN' }, - city: { id: 1, name: 'Paris', nameUpper: 'PARIS' }, - }, - { - id: 2, - user: { name: 'Jane', nameUpper: 'JANE' }, - city: null, - }, - ]); -}); - -test('left join (all fields)', async () => { - await db.insert(citiesTable) - .values([{ name: 'Paris' }, { name: 'London' }]); - - await db.insert(users2Table).values([{ name: 'John', cityId: 1 }, { name: 'Jane' }]); - - const res = await db.select().from(users2Table) - .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)); - - expect(res).toEqual([ - { - users2: { - id: 1, - name: 'John', - cityId: 1, - }, - cities: { - id: 1, - name: 'Paris', - }, - }, - { - users2: { - id: 2, - name: 'Jane', - cityId: null, - }, - cities: null, - }, - ]); -}); - -test('join subquery', async () => { - await db.execute(sql`drop table if exists \`courses\``); - await db.execute(sql`drop table if exists \`course_categories\``); - - await db.execute( - sql` - create table \`course_categories\` ( - \`id\` serial primary key, - \`name\` text not null - ) - `, - ); - - await db.execute( - sql` - create table \`courses\` ( - \`id\` serial primary key, - \`name\` text not null, - \`category_id\` int references \`course_categories\`(\`id\`) - ) - `, - ); - - await db.insert(courseCategoriesTable).values([ - { name: 'Category 1' }, - { name: 'Category 2' }, - { name: 'Category 3' }, - { name: 'Category 4' }, - ]); - - await db.insert(coursesTable).values([ - { name: 'Development', categoryId: 2 }, - { name: 'IT & Software', categoryId: 3 }, - { name: 'Marketing', categoryId: 4 }, - { name: 'Design', categoryId: 1 }, - ]); - - const sq2 = db - .select({ - categoryId: courseCategoriesTable.id, - category: courseCategoriesTable.name, - total: sql`count(${courseCategoriesTable.id})`, - }) - .from(courseCategoriesTable) - .groupBy(courseCategoriesTable.id, courseCategoriesTable.name) - .as('sq2'); - - const res = await db - .select({ - courseName: coursesTable.name, - categoryId: sq2.categoryId, - }) - .from(coursesTable) - .leftJoin(sq2, eq(coursesTable.categoryId, sq2.categoryId)) - .orderBy(coursesTable.name); - - expect(res).toEqual([ - { courseName: 'Design', categoryId: 1 }, - { courseName: 'Development', categoryId: 2 }, - { courseName: 'IT & Software', categoryId: 3 }, - { courseName: 'Marketing', categoryId: 4 }, - ]); - - await db.execute(sql`drop table if exists \`courses\``); - await db.execute(sql`drop table if exists \`course_categories\``); -}); - -test('with ... select', async () => { - await db.execute(sql`drop table if exists \`orders\``); - await db.execute( - sql` - create table \`orders\` ( - \`id\` serial primary key, - \`region\` text not null, - \`product\` text not null, - \`amount\` int not null, - \`quantity\` int not null - ) - `, - ); - - await db.insert(orders).values([ - { region: 'Europe', product: 'A', amount: 10, quantity: 1 }, - { region: 'Europe', product: 'A', amount: 20, quantity: 2 }, - { region: 'Europe', product: 'B', amount: 20, quantity: 2 }, - { region: 'Europe', product: 'B', amount: 30, quantity: 3 }, - { region: 'US', product: 'A', amount: 30, quantity: 3 }, - { region: 'US', product: 'A', amount: 40, quantity: 4 }, - { region: 'US', product: 'B', amount: 40, quantity: 4 }, - { region: 'US', product: 'B', amount: 50, quantity: 5 }, - ]); - - const regionalSales = db - .$with('regional_sales') - .as( - db - .select({ - region: orders.region, - totalSales: sql`sum(${orders.amount})`.as('total_sales'), - }) - .from(orders) - .groupBy(orders.region), - ); - - const topRegions = db - .$with('top_regions') - .as( - db - .select({ - region: regionalSales.region, - }) - .from(regionalSales) - .where( - gt( - regionalSales.totalSales, - db.select({ sales: sql`sum(${regionalSales.totalSales})/10` }).from(regionalSales), - ), - ), - ); - - const result = await db - .with(regionalSales, topRegions) - .select({ - region: orders.region, - product: orders.product, - productUnits: sql`sum(${orders.quantity})`.mapWith(Number), - productSales: sql`sum(${orders.amount})`.mapWith(Number), - }) - .from(orders) - .where(inArray(orders.region, db.select({ region: topRegions.region }).from(topRegions))) - .groupBy(orders.region, orders.product) - .orderBy(orders.region, orders.product); - - expect(result).toEqual([ - { - region: 'Europe', - product: 'A', - productUnits: 3, - productSales: 30, - }, - { - region: 'Europe', - product: 'B', - productUnits: 5, - productSales: 50, - }, - { - region: 'US', - product: 'A', - productUnits: 7, - productSales: 70, - }, - { - region: 'US', - product: 'B', - productUnits: 9, - productSales: 90, - }, - ]); -}); - -test('with ... update', async () => { - const products = mysqlTable('products', { - id: serial('id').primaryKey(), - price: decimal('price', { - precision: 15, - scale: 2, - }).notNull(), - cheap: boolean('cheap').notNull().default(false), - }); - - await db.execute(sql`drop table if exists ${products}`); - await db.execute(sql` - create table ${products} ( - id serial primary key, - price decimal(15, 2) not null, - cheap boolean not null default false - ) - `); - - await db.insert(products).values([ - { price: '10.99' }, - { price: '25.85' }, - { price: '32.99' }, - { price: '2.50' }, - { price: '4.59' }, - ]); - - const averagePrice = db - .$with('average_price') - .as( - db - .select({ - value: sql`avg(${products.price})`.as('value'), - }) - .from(products), - ); - - await db - .with(averagePrice) - .update(products) - .set({ - cheap: true, - }) - .where(lt(products.price, sql`(select * from ${averagePrice})`)); - - const result = await db - .select({ - id: products.id, - }) - .from(products) - .where(eq(products.cheap, true)); - - expect(result).toEqual([ - { id: 1 }, - { id: 4 }, - { id: 5 }, - ]); -}); - -test('with ... delete', async () => { - await db.execute(sql`drop table if exists \`orders\``); - await db.execute( - sql` - create table \`orders\` ( - \`id\` serial primary key, - \`region\` text not null, - \`product\` text not null, - \`amount\` int not null, - \`quantity\` int not null - ) - `, - ); - - await db.insert(orders).values([ - { region: 'Europe', product: 'A', amount: 10, quantity: 1 }, - { region: 'Europe', product: 'A', amount: 20, quantity: 2 }, - { region: 'Europe', product: 'B', amount: 20, quantity: 2 }, - { region: 'Europe', product: 'B', amount: 30, quantity: 3 }, - { region: 'US', product: 'A', amount: 30, quantity: 3 }, - { region: 'US', product: 'A', amount: 40, quantity: 4 }, - { region: 'US', product: 'B', amount: 40, quantity: 4 }, - { region: 'US', product: 'B', amount: 50, quantity: 5 }, - ]); - - const averageAmount = db - .$with('average_amount') - .as( - db - .select({ - value: sql`avg(${orders.amount})`.as('value'), - }) - .from(orders), - ); - - await db - .with(averageAmount) - .delete(orders) - .where(gt(orders.amount, sql`(select * from ${averageAmount})`)); - - const result = await db - .select({ - id: orders.id, - }) - .from(orders); - - expect(result).toEqual([ - { id: 1 }, - { id: 2 }, - { id: 3 }, - { id: 4 }, - { id: 5 }, - ]); -}); - -test('select from subquery sql', async () => { - await db.insert(users2Table).values([{ name: 'John' }, { name: 'Jane' }]); - - const sq = db - .select({ name: sql`concat(${users2Table.name}, " modified")`.as('name') }) - .from(users2Table) - .as('sq'); - - const res = await db.select({ name: sq.name }).from(sq); - - expect(res).toEqual([{ name: 'John modified' }, { name: 'Jane modified' }]); -}); - -test('select a field without joining its table', () => { - expect(() => db.select({ name: users2Table.name }).from(usersTable).prepare()).toThrowError(); -}); - -test('select all fields from subquery without alias', () => { - const sq = db.$with('sq').as(db.select({ name: sql`upper(${users2Table.name})` }).from(users2Table)); - - expect(() => db.select().from(sq).prepare()).toThrowError(); -}); - -test('select count()', async () => { - await db.insert(usersTable).values([{ name: 'John' }, { name: 'Jane' }]); - - const res = await db.select({ count: sql`count(*)` }).from(usersTable); - - expect(res).toEqual([{ count: '2' }]); -}); - -test('select for ...', () => { - { - const query = db.select().from(users2Table).for('update').toSQL(); - expect(query.sql).toMatch(/ for update$/); - } - { - const query = db.select().from(users2Table).for('share', { skipLocked: true }).toSQL(); - expect(query.sql).toMatch(/ for share skip locked$/); - } - { - const query = db.select().from(users2Table).for('update', { noWait: true }).toSQL(); - expect(query.sql).toMatch(/ for update no wait$/); - } -}); - -test('having', async () => { - await db.insert(citiesTable).values([{ name: 'London' }, { name: 'Paris' }, { name: 'New York' }]); - - await db.insert(users2Table).values([{ name: 'John', cityId: 1 }, { name: 'Jane', cityId: 1 }, { - name: 'Jack', - cityId: 2, - }]); - - const result = await db - .select({ - id: citiesTable.id, - name: sql`upper(${citiesTable.name})`.as('upper_name'), - usersCount: sql`count(${users2Table.id})`.mapWith(Number).as('users_count'), - }) - .from(citiesTable) - .leftJoin(users2Table, eq(users2Table.cityId, citiesTable.id)) - .where(({ name }) => sql`length(${name}) >= 3`) - .groupBy(citiesTable.id) - .having(({ usersCount }) => sql`${usersCount} > 0`) - .orderBy(({ name }) => name); - - expect(result).toEqual([ - { - id: 1, - name: 'LONDON', - usersCount: 2, - }, - { - id: 2, - name: 'PARIS', - usersCount: 1, - }, - ]); -}); - -test('view', async () => { - const newYorkers1 = mysqlView('new_yorkers') - .as((qb) => qb.select().from(users2Table).where(eq(users2Table.cityId, 1))); - - const newYorkers2 = mysqlView('new_yorkers', { - id: serial('id').primaryKey(), - name: text('name').notNull(), - cityId: int('city_id').notNull(), - }).as(sql`select * from ${users2Table} where ${eq(users2Table.cityId, 1)}`); - - const newYorkers3 = mysqlView('new_yorkers', { - id: serial('id').primaryKey(), - name: text('name').notNull(), - cityId: int('city_id').notNull(), - }).existing(); - - await db.execute(sql`drop view if exists ${newYorkers1}`); - - await db.execute(sql`create view ${newYorkers1} as ${getViewConfig(newYorkers1).query}`); - - await db.insert(citiesTable).values([{ name: 'New York' }, { name: 'Paris' }]); - - await db.insert(users2Table).values([ - { name: 'John', cityId: 1 }, - { name: 'Jane', cityId: 1 }, - { name: 'Jack', cityId: 2 }, - ]); - - { - const result = await db.select().from(newYorkers1); - expect(result).toEqual([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 1 }, - ]); - } - - { - const result = await db.select().from(newYorkers2); - expect(result).toEqual([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 1 }, - ]); - } - - { - const result = await db.select().from(newYorkers3); - expect(result).toEqual([ - { id: 1, name: 'John', cityId: 1 }, - { id: 2, name: 'Jane', cityId: 1 }, - ]); - } - - { - const result = await db.select({ name: newYorkers1.name }).from(newYorkers1); - expect(result).toEqual([ - { name: 'John' }, - { name: 'Jane' }, - ]); - } - - await db.execute(sql`drop view ${newYorkers1}`); -}); - -test('select from raw sql', async () => { - const result = await db.select({ - id: sql`id`.mapWith(Number), - name: sql`name`, - }).from(sql`(select 1 as id, 'John' as name) as users`); - - Expect>; - - expect(result).toEqual([ - { id: 1, name: 'John' }, - ]); -}); - -test('select from raw sql with joins', async () => { - const result = await db - .select({ - id: sql`users.id`.mapWith(Number), - name: sql`users.name`, - userCity: sql`users.city`, - cityName: sql`cities.name`, - }) - .from(sql`(select 1 as id, 'John' as name, 'New York' as city) as users`) - .leftJoin(sql`(select 1 as id, 'Paris' as name) as cities`, sql`cities.id = users.id`); - - Expect>; - - expect(result).toEqual([ - { id: 1, name: 'John', userCity: 'New York', cityName: 'Paris' }, - ]); -}); - -test('join on aliased sql from select', async () => { - const result = await db - .select({ - userId: sql`users.id`.mapWith(Number).as('userId'), - name: sql`users.name`, - userCity: sql`users.city`, - cityId: sql`cities.id`.mapWith(Number).as('cityId'), - cityName: sql`cities.name`, - }) - .from(sql`(select 1 as id, 'John' as name, 'New York' as city) as users`) - .leftJoin(sql`(select 1 as id, 'Paris' as name) as cities`, (cols) => eq(cols.cityId, cols.userId)); - - Expect>; - - expect(result).toEqual([ - { userId: 1, name: 'John', userCity: 'New York', cityId: 1, cityName: 'Paris' }, - ]); -}); - -test('join on aliased sql from with clause', async () => { - const users = db.$with('users').as( - db.select({ - id: sql`id`.mapWith(Number).as('userId'), - name: sql`name`.as('userName'), - city: sql`city`.as('city'), - }).from( - sql`(select 1 as id, 'John' as name, 'New York' as city) as users`, - ), - ); - - const cities = db.$with('cities').as( - db.select({ - id: sql`id`.mapWith(Number).as('cityId'), - name: sql`name`.as('cityName'), - }).from( - sql`(select 1 as id, 'Paris' as name) as cities`, - ), - ); - - const result = await db - .with(users, cities) - .select({ - userId: users.id, - name: users.name, - userCity: users.city, - cityId: cities.id, - cityName: cities.name, - }) - .from(users) - .leftJoin(cities, (cols) => eq(cols.cityId, cols.userId)); - - Expect>; - - expect(result).toEqual([ - { userId: 1, name: 'John', userCity: 'New York', cityId: 1, cityName: 'Paris' }, - ]); -}); - -test('prefixed table', async () => { - const mysqlTable = mysqlTableCreator((name) => `myprefix_${name}`); - - const users = mysqlTable('test_prefixed_table_with_unique_name', { - id: int('id').primaryKey(), - name: text('name').notNull(), - }); - - await db.execute(sql`drop table if exists ${users}`); - - await db.execute( - sql`create table myprefix_test_prefixed_table_with_unique_name (id int not null primary key, name text not null)`, - ); - - await db.insert(users).values({ id: 1, name: 'John' }); - - const result = await db.select().from(users); - - expect(result).toEqual([{ id: 1, name: 'John' }]); - - await db.execute(sql`drop table ${users}`); -}); - -test('orderBy with aliased column', () => { - const query = db.select({ - test: sql`something`.as('test'), - }).from(users2Table).orderBy((fields) => fields.test).toSQL(); - - expect(query.sql).toEqual('select something as `test` from `users2` order by `test`'); -}); - -test('timestamp timezone', async () => { - const date = new Date(Date.parse('2020-01-01T12:34:56+07:00')); - - await db.insert(usersTable).values({ name: 'With default times' }); - await db.insert(usersTable).values({ - name: 'Without default times', - createdAt: date, - }); - const users = await db.select().from(usersTable); - - // check that the timestamps are set correctly for default times - expect(Math.abs(users[0]!.createdAt.getTime() - Date.now()) < 2000).toEqual(true); - - // check that the timestamps are set correctly for non default times - expect(Math.abs(users[1]!.createdAt.getTime() - date.getTime()) < 2000).toEqual(true); -}); - -test('transaction', async () => { - const users = mysqlTable('users_transactions', { - id: serial('id').primaryKey(), - balance: int('balance').notNull(), - }); - const products = mysqlTable('products_transactions', { - id: serial('id').primaryKey(), - price: int('price').notNull(), - stock: int('stock').notNull(), - }); - - await db.execute(sql`drop table if exists ${users}`); - await db.execute(sql`drop table if exists ${products}`); - - await db.execute(sql`create table users_transactions (id serial not null primary key, balance int not null)`); - await db.execute( - sql`create table products_transactions (id serial not null primary key, price int not null, stock int not null)`, - ); - - const { lastInsertId: userId } = await db.insert(users).values({ balance: 100 }); - const user = await db.select().from(users).where(eq(users.id, userId!)).then((rows) => rows[0]!); - const { lastInsertId: productId } = await db.insert(products).values({ price: 10, stock: 10 }); - const product = await db.select().from(products).where(eq(products.id, productId!)).then((rows) => rows[0]!); - - await db.transaction(async (tx) => { - await tx.update(users).set({ balance: user.balance - product.price }).where(eq(users.id, user.id)); - await tx.update(products).set({ stock: product.stock - 1 }).where(eq(products.id, product.id)); - }); - - const result = await db.select().from(users); - - expect(result).toEqual([{ id: 1, balance: 90 }]); - - await db.execute(sql`drop table ${users}`); - await db.execute(sql`drop table ${products}`); -}); - -test('transaction rollback', async () => { - const users = mysqlTable('users_transactions_rollback', { - id: serial('id').primaryKey(), - balance: int('balance').notNull(), - }); - - await db.execute(sql`drop table if exists ${users}`); - - await db.execute( - sql`create table users_transactions_rollback (id serial not null primary key, balance int not null)`, - ); - - await expect(async () => - await db.transaction(async (tx) => { - await tx.insert(users).values({ balance: 100 }); - tx.rollback(); - }) - ).rejects.toThrowError(TransactionRollbackError); - - const result = await db.select().from(users); - - expect(result).toEqual([]); - - await db.execute(sql`drop table ${users}`); -}); - -test('nested transaction', async () => { - const users = mysqlTable('users_nested_transactions', { - id: serial('id').primaryKey(), - balance: int('balance').notNull(), - }); - - await db.execute(sql`drop table if exists ${users}`); - - await db.execute( - sql`create table users_nested_transactions (id serial not null primary key, balance int not null)`, - ); - - await db.transaction(async (tx) => { - await tx.insert(users).values({ balance: 100 }); - - await tx.transaction(async (tx) => { - await tx.update(users).set({ balance: 200 }); - }); - }); - - const result = await db.select().from(users); - - expect(result).toEqual([{ id: 1, balance: 200 }]); - - await db.execute(sql`drop table ${users}`); -}); - -test('nested transaction rollback', async () => { - const users = mysqlTable('users_nested_transactions_rollback', { - id: serial('id').primaryKey(), - balance: int('balance').notNull(), - }); - - await db.execute(sql`drop table if exists ${users}`); - - await db.execute( - sql`create table users_nested_transactions_rollback (id serial not null primary key, balance int not null)`, - ); - - await db.transaction(async (tx) => { - await tx.insert(users).values({ balance: 100 }); - - await expect(async () => - await tx.transaction(async (tx) => { - await tx.update(users).set({ balance: 200 }); - tx.rollback(); - }) - ).rejects.toThrowError(TransactionRollbackError); - }); - - const result = await db.select().from(users); - - expect(result).toEqual([{ id: 1, balance: 100 }]); - - await db.execute(sql`drop table ${users}`); -}); - -test('join subquery with join', async () => { - const internalStaff = mysqlTable('internal_staff', { - userId: int('user_id').notNull(), - }); - - const customUser = mysqlTable('custom_user', { - id: int('id').notNull(), - }); - - const ticket = mysqlTable('ticket', { - staffId: int('staff_id').notNull(), - }); - - await db.execute(sql`drop table if exists ${internalStaff}`); - await db.execute(sql`drop table if exists ${customUser}`); - await db.execute(sql`drop table if exists ${ticket}`); - - await db.execute(sql`create table internal_staff (user_id integer not null)`); - await db.execute(sql`create table custom_user (id integer not null)`); - await db.execute(sql`create table ticket (staff_id integer not null)`); - - await db.insert(internalStaff).values({ userId: 1 }); - await db.insert(customUser).values({ id: 1 }); - await db.insert(ticket).values({ staffId: 1 }); - - const subq = db - .select() - .from(internalStaff) - .leftJoin(customUser, eq(internalStaff.userId, customUser.id)) - .as('internal_staff'); - - const mainQuery = await db - .select() - .from(ticket) - .leftJoin(subq, eq(subq.internal_staff.userId, ticket.staffId)); - - expect(mainQuery).toEqual([{ - ticket: { staffId: 1 }, - internal_staff: { - internal_staff: { userId: 1 }, - custom_user: { id: 1 }, - }, - }]); - - await db.execute(sql`drop table ${internalStaff}`); - await db.execute(sql`drop table ${customUser}`); - await db.execute(sql`drop table ${ticket}`); -}); - -test('subquery with view', async () => { - const users = mysqlTable('users_subquery_view', { - id: serial('id').primaryKey(), - name: text('name').notNull(), - cityId: int('city_id').notNull(), - }); - - const newYorkers = mysqlView('new_yorkers').as((qb) => qb.select().from(users).where(eq(users.cityId, 1))); - - await db.execute(sql`drop table if exists ${users}`); - await db.execute(sql`drop view if exists ${newYorkers}`); - - await db.execute( - sql`create table ${users} (id serial not null primary key, name text not null, city_id integer not null)`, - ); - await db.execute(sql`create view ${newYorkers} as select * from ${users} where city_id = 1`); - - await db.insert(users).values([ - { name: 'John', cityId: 1 }, - { name: 'Jane', cityId: 2 }, - { name: 'Jack', cityId: 1 }, - { name: 'Jill', cityId: 2 }, - ]); - - const sq = db.$with('sq').as(db.select().from(newYorkers)); - const result = await db.with(sq).select().from(sq); - - expect(result).toEqual([ - { id: 1, name: 'John', cityId: 1 }, - { id: 3, name: 'Jack', cityId: 1 }, - ]); - - await db.execute(sql`drop view ${newYorkers}`); - await db.execute(sql`drop table ${users}`); -}); - -test('join view as subquery', async () => { - const users = mysqlTable('users_join_view', { - id: serial('id').primaryKey(), - name: text('name').notNull(), - cityId: int('city_id').notNull(), - }); - - const newYorkers = mysqlView('new_yorkers').as((qb) => qb.select().from(users).where(eq(users.cityId, 1))); - - await db.execute(sql`drop table if exists ${users}`); - await db.execute(sql`drop view if exists ${newYorkers}`); - - await db.execute( - sql`create table ${users} (id serial not null primary key, name text not null, city_id integer not null)`, - ); - await db.execute(sql`create view ${newYorkers} as select * from ${users} where city_id = 1`); - - await db.insert(users).values([ - { name: 'John', cityId: 1 }, - { name: 'Jane', cityId: 2 }, - { name: 'Jack', cityId: 1 }, - { name: 'Jill', cityId: 2 }, - ]); - - const sq = db.select().from(newYorkers).as('new_yorkers_sq'); - - const result = await db.select().from(users).leftJoin(sq, eq(users.id, sq.id)); - - expect(result).toEqual([ - { - users_join_view: { id: 1, name: 'John', cityId: 1 }, - new_yorkers_sq: { id: 1, name: 'John', cityId: 1 }, - }, - { - users_join_view: { id: 2, name: 'Jane', cityId: 2 }, - new_yorkers_sq: null, - }, - { - users_join_view: { id: 3, name: 'Jack', cityId: 1 }, - new_yorkers_sq: { id: 3, name: 'Jack', cityId: 1 }, - }, - { - users_join_view: { id: 4, name: 'Jill', cityId: 2 }, - new_yorkers_sq: null, - }, - ]); - - await db.execute(sql`drop view ${newYorkers}`); - await db.execute(sql`drop table ${users}`); -}); - -test('insert undefined', async () => { - const users = mysqlTable('users', { - id: serial('id').primaryKey(), - name: text('name'), - }); - - await db.execute(sql`drop table if exists ${users}`); - - await db.execute( - sql`create table ${users} (id serial not null primary key, name text)`, - ); - - await expect(db.insert(users).values({ name: undefined })).resolves.not.toThrowError(); - - await db.execute(sql`drop table ${users}`); -}); - -test('update undefined', async () => { - const users = mysqlTable('users', { - id: serial('id').primaryKey(), - name: text('name'), - }); - - await db.execute(sql`drop table if exists ${users}`); - - await db.execute( - sql`create table ${users} (id serial not null primary key, name text)`, - ); - - await expect(async () => await db.update(users).set({ name: undefined })).rejects.toThrowError(); - await expect(db.update(users).set({ id: 1, name: undefined })).resolves.not.toThrowError(); - - await db.execute(sql`drop table ${users}`); -}); - -test('utc config for datetime', async () => { - await db.execute(sql`drop table if exists \`datestable\``); - await db.execute( - sql` - create table \`datestable\` ( - \`datetime_utc\` datetime(3), - \`datetime\` datetime(3), - \`datetime_as_string\` datetime - ) - `, - ); - const datesTable = mysqlTable('datestable', { - datetimeUTC: datetime('datetime_utc', { fsp: 3, mode: 'date' }), - datetime: datetime('datetime', { fsp: 3 }), - datetimeAsString: datetime('datetime_as_string', { mode: 'string' }), - }); - - const dateObj = new Date('2022-11-11'); - const dateUtc = new Date('2022-11-11T12:12:12.122Z'); - - await db.insert(datesTable).values({ - datetimeUTC: dateUtc, - datetime: dateObj, - datetimeAsString: '2022-11-11 12:12:12', - }); - - const res = await db.select().from(datesTable); - - const rawSelect = await db.execute(sql`select \`datetime_utc\` from \`datestable\``); - const selectedRow = (rawSelect.rows as [{ datetime_utc: string }])[0]; - - expect(selectedRow.datetime_utc).toEqual('2022-11-11 12:12:12.122'); - expect(new Date(selectedRow.datetime_utc.replace(' ', 'T') + 'Z')).toEqual(dateUtc); - - expect(res[0]?.datetime instanceof Date).toEqual(true); // eslint-disable-line no-instanceof/no-instanceof - expect(res[0]?.datetimeUTC instanceof Date).toEqual(true); // eslint-disable-line no-instanceof/no-instanceof - expect(typeof res[0]?.datetimeAsString === 'string').toEqual(true); - - expect(res).toEqual([{ - datetimeUTC: dateUtc, - datetime: new Date('2022-11-11'), - datetimeAsString: '2022-11-11 12:12:12', - }]); - - await db.execute(sql`drop table if exists \`datestable\``); -}); - -test('set operations (union) from query builder with subquery', async () => { - await setupSetOperationTest(db); - const sq = db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table).as('sq'); - - const result = await db - .select({ id: citiesTable.id, name: citiesTable.name }) - .from(citiesTable).union( - db.select().from(sq).orderBy(sq.id), - ) - .orderBy(citiesTable.id) - .limit(8); - - expect(result).toHaveLength(8); - - expect(result).toEqual(expect.arrayContaining([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - { id: 1, name: 'John' }, - { id: 2, name: 'Jane' }, - { id: 3, name: 'Jack' }, - { id: 4, name: 'Peter' }, - { id: 5, name: 'Ben' }, - ])); - - // union should throw if selected fields are not in the same order - expect(() => - db - .select({ id: citiesTable.id, name: citiesTable.name }) - .from(citiesTable).union( - db - .select({ name: users2Table.name, id: users2Table.id }) - .from(users2Table), - ) - ).toThrow(); -}); - -test('set operations (union) as function', async () => { - await setupSetOperationTest(db); - - const result = await union( - db - .select({ id: citiesTable.id, name: citiesTable.name }) - .from(citiesTable).where(eq(citiesTable.id, 1)), - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table).where(eq(users2Table.id, 1)), - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table).where(eq(users2Table.id, 1)), - ); - - expect(result).toHaveLength(2); - - expect(result).toEqual(expect.arrayContaining([ - { id: 1, name: 'New York' }, - { id: 1, name: 'John' }, - ])); - - expect(() => { - union( - db - .select({ id: citiesTable.id, name: citiesTable.name }) - .from(citiesTable).where(eq(citiesTable.id, 1)), - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table).where(eq(users2Table.id, 1)), - db - .select({ name: users2Table.name, id: users2Table.id }) - .from(users2Table).where(eq(users2Table.id, 1)), - ); - }).toThrow(); -}); - -test('set operations (union all) from query builder', async () => { - await setupSetOperationTest(db); - - const result = await db - .select({ id: citiesTable.id, name: citiesTable.name }) - .from(citiesTable).limit(2).unionAll( - db - .select({ id: citiesTable.id, name: citiesTable.name }) - .from(citiesTable).limit(2), - ).orderBy(asc(sql`id`)).limit(3); - - expect(result.length === 3).toEqual(true); - - expect(result).toEqual([ - { id: 1, name: 'New York' }, - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - ]); - - expect(() => { - db - .select({ id: citiesTable.id, name: citiesTable.name }) - .from(citiesTable).limit(2).unionAll( - db - .select({ name: citiesTable.name, id: citiesTable.id }) - .from(citiesTable).limit(2), - ).orderBy(asc(sql`id`)); - }).toThrow(); -}); - -test('set operations (union all) as function', async () => { - await setupSetOperationTest(db); - - const result = await unionAll( - db - .select({ id: citiesTable.id, name: citiesTable.name }) - .from(citiesTable).where(eq(citiesTable.id, 1)) - .limit(1), - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table).where(eq(users2Table.id, 1)) - .limit(1), - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table).where(eq(users2Table.id, 1)) - .limit(1), - ); - - expect(result).toHaveLength(3); - - expect(result).toEqual(expect.arrayContaining([ - { id: 1, name: 'New York' }, - { id: 1, name: 'John' }, - { id: 1, name: 'John' }, - ])); - - expect(() => { - unionAll( - db - .select({ id: citiesTable.id, name: citiesTable.name }) - .from(citiesTable).where(eq(citiesTable.id, 1)), - db - .select({ name: users2Table.name, id: users2Table.id }) - .from(users2Table).where(eq(users2Table.id, 1)), - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table).where(eq(users2Table.id, 1)), - ).limit(1); - }).toThrow(); -}); - -test('set operations (intersect) from query builder', async () => { - await setupSetOperationTest(db); - - const result = await db - .select({ id: citiesTable.id, name: citiesTable.name }) - .from(citiesTable).intersect( - db - .select({ id: citiesTable.id, name: citiesTable.name }) - .from(citiesTable).where(gt(citiesTable.id, 1)), - ); - - expect(result.length === 2).toEqual(true); - - expect(result).toEqual([ - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - expect(() => { - db - .select({ name: citiesTable.name, id: citiesTable.id }) - .from(citiesTable).intersect( - db - .select({ id: citiesTable.id, name: citiesTable.name }) - .from(citiesTable).where(gt(citiesTable.id, 1)), - ); - }).toThrow(); -}); - -test('set operations (intersect) as function', async () => { - await setupSetOperationTest(db); - - const result = await intersect( - db - .select({ id: citiesTable.id, name: citiesTable.name }) - .from(citiesTable).where(eq(citiesTable.id, 1)), - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table).where(eq(users2Table.id, 1)), - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table).where(eq(users2Table.id, 1)), - ).limit(1); - - expect(result.length === 0).toEqual(true); - - expect(result).toEqual([]); - - expect(() => { - intersect( - db - .select({ id: citiesTable.id, name: citiesTable.name }) - .from(citiesTable).where(eq(citiesTable.id, 1)), - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table).where(eq(users2Table.id, 1)), - db - .select({ name: users2Table.name, id: users2Table.id }) - .from(users2Table).where(eq(users2Table.id, 1)), - ).limit(1); - }).toThrow(); -}); - -// "intersect all" is not supported in TiDB -test.skip('set operations (intersect all) from query builder', async () => { - await setupSetOperationTest(db); - - const result = await db - .select({ id: citiesTable.id, name: citiesTable.name }) - .from(citiesTable).limit(2).intersectAll( - db - .select({ id: citiesTable.id, name: citiesTable.name }) - .from(citiesTable).limit(2), - ).orderBy(asc(sql`id`)); - - expect(result.length === 2).toEqual(true); - - expect(result).toEqual([ - { id: 1, name: 'New York' }, - { id: 2, name: 'London' }, - ]); - - expect(() => { - db - .select({ id: citiesTable.id, name: citiesTable.name }) - .from(citiesTable).limit(2).intersectAll( - db - .select({ name: citiesTable.name, id: citiesTable.id }) - .from(citiesTable).limit(2), - ).orderBy(asc(sql`id`)); - }).toThrow(); -}); - -// "intersect all" is not supported in TiDB -test.skip('set operations (intersect all) as function', async () => { - await setupSetOperationTest(db); - - const result = await intersectAll( - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table).where(eq(users2Table.id, 1)), - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table).where(eq(users2Table.id, 1)), - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table).where(eq(users2Table.id, 1)), - ); - - expect(result.length === 1).toEqual(true); - - expect(result).toEqual([ - { id: 1, name: 'John' }, - ]); - - expect(() => { - intersectAll( - db - .select({ name: users2Table.name, id: users2Table.id }) - .from(users2Table).where(eq(users2Table.id, 1)), - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table).where(eq(users2Table.id, 1)), - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table).where(eq(users2Table.id, 1)), - ); - }).toThrow(); -}); - -test('set operations (except) from query builder', async () => { - await setupSetOperationTest(db); - - const result = await db - .select() - .from(citiesTable).except( - db - .select() - .from(citiesTable).where(gt(citiesTable.id, 1)), - ); - - expect(result.length === 1).toEqual(true); - - expect(result).toEqual([ - { id: 1, name: 'New York' }, - ]); -}); - -test('set operations (except) as function', async () => { - await setupSetOperationTest(db); - - const result = await except( - db - .select({ id: citiesTable.id, name: citiesTable.name }) - .from(citiesTable), - db - .select({ id: citiesTable.id, name: citiesTable.name }) - .from(citiesTable).where(eq(citiesTable.id, 1)), - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table).where(eq(users2Table.id, 1)), - ).limit(3); - - expect(result.length === 2).toEqual(true); - - expect(result).toEqual([ - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - expect(() => { - except( - db - .select({ name: citiesTable.name, id: citiesTable.id }) - .from(citiesTable), - db - .select({ id: citiesTable.id, name: citiesTable.name }) - .from(citiesTable).where(eq(citiesTable.id, 1)), - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table).where(eq(users2Table.id, 1)), - ).limit(3); - }).toThrow(); -}); - -// "except all" is not supported in TiDB -test.skip('set operations (except all) from query builder', async () => { - await setupSetOperationTest(db); - - const result = await db - .select() - .from(citiesTable).exceptAll( - db - .select({ id: citiesTable.id, name: citiesTable.name }) - .from(citiesTable).where(eq(citiesTable.id, 1)), - ).orderBy(asc(sql`id`)); - - expect(result.length === 2).toEqual(true); - - expect(result).toEqual([ - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ]); - - expect(() => { - db - .select() - .from(citiesTable).exceptAll( - db - .select({ name: citiesTable.name, id: citiesTable.id }) - .from(citiesTable).where(eq(citiesTable.id, 1)), - ).orderBy(asc(sql`id`)); - }).toThrow(); -}); - -// "except all" is not supported in TiDB -test.skip('set operations (except all) as function', async () => { - await setupSetOperationTest(db); - - const result = await exceptAll( - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table), - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table).where(gt(users2Table.id, 7)), - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table).where(eq(users2Table.id, 1)), - ).limit(6).orderBy(asc(sql.identifier('id'))); - - expect(result.length === 6).toEqual(true); - - expect(result).toEqual([ - { id: 2, name: 'Jane' }, - { id: 3, name: 'Jack' }, - { id: 4, name: 'Peter' }, - { id: 5, name: 'Ben' }, - { id: 6, name: 'Jill' }, - { id: 7, name: 'Mary' }, - ]); - - expect(() => { - exceptAll( - db - .select({ name: users2Table.name, id: users2Table.id }) - .from(users2Table), - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table).where(gt(users2Table.id, 7)), - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table).where(eq(users2Table.id, 1)), - ).limit(6); - }).toThrow(); -}); - -test('set operations (mixed) from query builder', async () => { - await setupSetOperationTest(db); - - const result = await db - .select() - .from(citiesTable).except( - ({ unionAll }) => - unionAll( - db - .select() - .from(citiesTable).where(gt(citiesTable.id, 1)), - db.select().from(citiesTable).where(eq(citiesTable.id, 2)), - ).orderBy(asc(citiesTable.id)).limit(1).offset(1), - ); - - expect(result.length === 2).toEqual(true); - - expect(result).toEqual([ - { id: 1, name: 'New York' }, - { id: 3, name: 'Tampa' }, - ]); - - expect(() => { - db - .select() - .from(citiesTable).except( - ({ unionAll }) => - unionAll( - db - .select({ name: citiesTable.name, id: citiesTable.id }) - .from(citiesTable).where(gt(citiesTable.id, 1)), - db.select().from(citiesTable).where(eq(citiesTable.id, 2)), - ), - ); - }).toThrow(); -}); - -test('set operations (mixed all) as function with subquery', async () => { - await setupSetOperationTest(db); - - const sq = except( - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table).where(gte(users2Table.id, 5)), - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table).where(eq(users2Table.id, 7)), - ).orderBy(asc(sql.identifier('id'))).as('sq'); - - const result = await union( - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table).where(eq(users2Table.id, 1)).orderBy(users2Table.id), - db.select().from(sq).limit(1).orderBy(sq.id), - db - .select().from(citiesTable).where(gt(citiesTable.id, 1)).orderBy(citiesTable.id), - ); - - expect(result).toHaveLength(4); - - expect(result).toEqual(expect.arrayContaining([ - { id: 1, name: 'John' }, - { id: 5, name: 'Ben' }, - { id: 2, name: 'London' }, - { id: 3, name: 'Tampa' }, - ])); - - expect(() => { - union( - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table).where(eq(users2Table.id, 1)), - except( - db - .select({ id: users2Table.id, name: users2Table.name }) - .from(users2Table).where(gte(users2Table.id, 5)), - db - .select({ name: users2Table.name, id: users2Table.id }) - .from(users2Table).where(eq(users2Table.id, 7)), - ).limit(1), - db - .select().from(citiesTable).where(gt(citiesTable.id, 1)), - ); - }).toThrow(); -}); - -test('aggregate function: count', async () => { - 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); - - expect(result1[0]?.value).toEqual(7); - expect(result2[0]?.value).toEqual(5); - expect(result3[0]?.value).toEqual(6); -}); - -test('aggregate function: avg', async () => { - 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); - - expect(result1[0]?.value).toEqual('33.3333'); - expect(result2[0]?.value).toEqual(null); - expect(result3[0]?.value).toEqual('42.5000'); -}); - -test('aggregate function: sum', async () => { - 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); - - expect(result1[0]?.value).toEqual('200'); - expect(result2[0]?.value).toEqual(null); - expect(result3[0]?.value).toEqual('170'); -}); - -test('aggregate function: max', async () => { - 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); - - expect(result1[0]?.value).toEqual(90); - expect(result2[0]?.value).toEqual(null); -}); - -test('aggregate function: min', async () => { - 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); - - expect(result1[0]?.value).toEqual(10); - expect(result2[0]?.value).toEqual(null); -}); - -test('test $onUpdateFn and $onUpdate works as $default', async () => { - await db.execute(sql`drop table if exists ${usersOnUpdate}`); - - await db.execute( - sql` - create table ${usersOnUpdate} ( - id serial not null primary key, - name text not null, - update_counter integer default 1 not null, - updated_at datetime(3), - uppercase_name text, - always_null text - ) - `, - ); - - await db.insert(usersOnUpdate).values([ - { name: 'John' }, - { name: 'Jane' }, - { name: 'Jack' }, - { name: 'Jill' }, - ]); - const { updatedAt, ...rest } = getTableColumns(usersOnUpdate); - - const justDates = await db.select({ updatedAt }).from(usersOnUpdate); - - const response = await db.select({ ...rest }).from(usersOnUpdate); - - expect(response).toEqual([ - { name: 'John', id: 1, updateCounter: 1, uppercaseName: 'JOHN', alwaysNull: null }, - { name: 'Jane', id: 2, updateCounter: 1, uppercaseName: 'JANE', alwaysNull: null }, - { name: 'Jack', id: 3, updateCounter: 1, uppercaseName: 'JACK', alwaysNull: null }, - { name: 'Jill', id: 4, updateCounter: 1, uppercaseName: 'JILL', alwaysNull: null }, - ]); - const msDelay = 2000; - - for (const eachUser of justDates) { - expect(eachUser.updatedAt!.valueOf() > Date.now() - msDelay).toEqual(true); - } -}); - -test('test $onUpdateFn and $onUpdate works updating', async () => { - await db.execute(sql`drop table if exists ${usersOnUpdate}`); - - await db.execute( - sql` - create table ${usersOnUpdate} ( - id serial not null primary key, - name text not null, - update_counter integer default 1 not null, - updated_at datetime(3), - uppercase_name text, - always_null text - ) - `, - ); - - await db.insert(usersOnUpdate).values([ - { name: 'John', alwaysNull: 'this will will be null after updating' }, - { name: 'Jane' }, - { name: 'Jack' }, - { name: 'Jill' }, - ]); - const { updatedAt, ...rest } = getTableColumns(usersOnUpdate); - const initial = await db.select({ updatedAt }).from(usersOnUpdate); - - await db.update(usersOnUpdate).set({ name: 'Angel', uppercaseName: null }).where(eq(usersOnUpdate.id, 1)); - - const justDates = await db.select({ updatedAt }).from(usersOnUpdate); - - const response = await db.select({ ...rest }).from(usersOnUpdate); - - expect(response).toEqual([ - { name: 'Angel', id: 1, updateCounter: 2, uppercaseName: null, alwaysNull: null }, - { name: 'Jane', id: 2, updateCounter: 1, uppercaseName: 'JANE', alwaysNull: null }, - { name: 'Jack', id: 3, updateCounter: 1, uppercaseName: 'JACK', alwaysNull: null }, - { name: 'Jill', id: 4, updateCounter: 1, uppercaseName: 'JILL', alwaysNull: null }, - ]); - const msDelay = 2000; - - expect(initial[0]?.updatedAt?.valueOf() !== justDates[0]?.updatedAt?.valueOf()).toEqual(true); - - for (const eachUser of justDates) { - expect(eachUser.updatedAt!.valueOf() > Date.now() - msDelay).toEqual(true); - } -}); +skipTests([ + 'mySchema :: select with group by as field', + 'mySchema :: delete with returning all fields', + 'mySchema :: update with returning partial', + 'mySchema :: delete returning sql', + 'mySchema :: insert returning sql', + 'test $onUpdateFn and $onUpdate works updating', + 'set operations (mixed all) as function with subquery', + 'set operations (union) from query builder with subquery', + 'join on aliased sql from with clause', + 'join on aliased sql from select', + 'select from raw sql with joins', + 'select from raw sql', + 'having', + 'select count()', + 'with ... select', + 'insert via db.execute w/ query builder', + 'insert via db.execute + select via db.execute', + 'select with group by as sql', + 'select with group by as field', + 'insert many with returning', + 'delete with returning partial', + 'delete with returning all fields', + 'update with returning partial', + 'update with returning all fields', + 'update returning sql', + 'delete returning sql', + 'insert returning sql', + + // not supported + 'set operations (except all) as function', + 'set operations (except all) from query builder', + 'set operations (intersect all) as function', + 'set operations (intersect all) from query builder', + 'set operations (union all) as function', + 'tc config for datetime', + 'select iterator w/ prepared statement', + 'select iterator', + 'transaction', + 'Insert all defaults in multiple rows', + 'Insert all defaults in 1 row', + '$default with empty array', + 'utc config for datetime', +]); + +tests(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 38a6aaa6a..8cff3f864 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -446,6 +446,12 @@ importers: '@neondatabase/serverless': specifier: 0.9.0 version: 0.9.0 + '@originjs/vite-plugin-commonjs': + specifier: ^1.0.3 + version: 1.0.3 + '@paralleldrive/cuid2': + specifier: ^2.2.2 + version: 2.2.2 '@types/async-retry': specifier: ^1.4.8 version: 1.4.8 @@ -2601,6 +2607,10 @@ packages: '@neondatabase/serverless@0.9.3': resolution: {integrity: sha512-6ZBK8asl2Z3+ADEaELvbaVVGVlmY1oAzkxxZfpmXPKFuJhbDN+5fU3zYBamsahS/Ch1zE+CVWB3R+8QEI2LMSw==} + '@noble/hashes@1.4.0': + resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} + engines: {node: '>= 16'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2638,6 +2648,9 @@ packages: '@originjs/vite-plugin-commonjs@1.0.3': resolution: {integrity: sha512-KuEXeGPptM2lyxdIEJ4R11+5ztipHoE7hy8ClZt3PYaOVQ/pyngd2alaSrPnwyFeOW1UagRBaQ752aA1dTMdOQ==} + '@paralleldrive/cuid2@2.2.2': + resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -9204,8 +9217,8 @@ snapshots: dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.583.0(@aws-sdk/client-sts@3.583.0) - '@aws-sdk/client-sts': 3.583.0 + '@aws-sdk/client-sso-oidc': 3.583.0 + '@aws-sdk/client-sts': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0) '@aws-sdk/core': 3.582.0 '@aws-sdk/credential-provider-node': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0)(@aws-sdk/client-sts@3.583.0) '@aws-sdk/middleware-host-header': 3.577.0 @@ -9291,11 +9304,11 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sso-oidc@3.583.0(@aws-sdk/client-sts@3.583.0)': + '@aws-sdk/client-sso-oidc@3.583.0': dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sts': 3.583.0 + '@aws-sdk/client-sts': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0) '@aws-sdk/core': 3.582.0 '@aws-sdk/credential-provider-node': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0)(@aws-sdk/client-sts@3.583.0) '@aws-sdk/middleware-host-header': 3.577.0 @@ -9334,7 +9347,6 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.6.2 transitivePeerDependencies: - - '@aws-sdk/client-sts' - aws-crt '@aws-sdk/client-sso@3.478.0': @@ -9601,11 +9613,11 @@ snapshots: - '@aws-sdk/client-sso-oidc' - aws-crt - '@aws-sdk/client-sts@3.583.0': + '@aws-sdk/client-sts@3.583.0(@aws-sdk/client-sso-oidc@3.583.0)': dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.583.0(@aws-sdk/client-sts@3.583.0) + '@aws-sdk/client-sso-oidc': 3.583.0 '@aws-sdk/core': 3.582.0 '@aws-sdk/credential-provider-node': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0)(@aws-sdk/client-sts@3.583.0) '@aws-sdk/middleware-host-header': 3.577.0 @@ -9644,6 +9656,7 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.6.2 transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' - aws-crt '@aws-sdk/core@3.477.0': @@ -9798,7 +9811,7 @@ snapshots: '@aws-sdk/credential-provider-ini@3.583.0(@aws-sdk/client-sso-oidc@3.583.0)(@aws-sdk/client-sts@3.583.0)': dependencies: - '@aws-sdk/client-sts': 3.583.0 + '@aws-sdk/client-sts': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0) '@aws-sdk/credential-provider-env': 3.577.0 '@aws-sdk/credential-provider-process': 3.577.0 '@aws-sdk/credential-provider-sso': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0) @@ -10005,7 +10018,7 @@ snapshots: '@aws-sdk/credential-provider-web-identity@3.577.0(@aws-sdk/client-sts@3.583.0)': dependencies: - '@aws-sdk/client-sts': 3.583.0 + '@aws-sdk/client-sts': 3.583.0(@aws-sdk/client-sso-oidc@3.583.0) '@aws-sdk/types': 3.577.0 '@smithy/property-provider': 3.0.0 '@smithy/types': 3.0.0 @@ -10206,7 +10219,7 @@ snapshots: '@aws-sdk/token-providers@3.568.0(@aws-sdk/client-sso-oidc@3.583.0)': dependencies: - '@aws-sdk/client-sso-oidc': 3.583.0(@aws-sdk/client-sts@3.583.0) + '@aws-sdk/client-sso-oidc': 3.583.0 '@aws-sdk/types': 3.567.0 '@smithy/property-provider': 2.2.0 '@smithy/shared-ini-file-loader': 2.4.0 @@ -10215,7 +10228,7 @@ snapshots: '@aws-sdk/token-providers@3.577.0(@aws-sdk/client-sso-oidc@3.583.0)': dependencies: - '@aws-sdk/client-sso-oidc': 3.583.0(@aws-sdk/client-sts@3.583.0) + '@aws-sdk/client-sso-oidc': 3.583.0 '@aws-sdk/types': 3.577.0 '@smithy/property-provider': 3.0.0 '@smithy/shared-ini-file-loader': 3.0.0 @@ -12250,6 +12263,8 @@ snapshots: '@types/pg': 8.11.6 optional: true + '@noble/hashes@1.4.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -12289,6 +12304,10 @@ snapshots: dependencies: esbuild: 0.14.54 + '@paralleldrive/cuid2@2.2.2': + dependencies: + '@noble/hashes': 1.4.0 + '@pkgjs/parseargs@0.11.0': optional: true