From 5ae8245736e73da9865cd7588bde5a727e4bcfc5 Mon Sep 17 00:00:00 2001 From: Dan Kochetov Date: Fri, 3 Feb 2023 12:05:16 +0200 Subject: [PATCH 1/7] [WIP] Improve joins types and runtime logic, remove dot from folder names --- changelogs/drizzle-orm-pg/0.16.0.md | 2 +- drizzle-orm/src/better-sqlite3/session.ts | 2 +- drizzle-orm/src/bun-sqlite/session.ts | 2 +- drizzle-orm/src/d1/session.ts | 2 +- drizzle-orm/src/mysql-core/columns/custom.ts | 8 +- drizzle-orm/src/mysql-core/db.ts | 2 +- drizzle-orm/src/mysql-core/dialect.ts | 3 +- drizzle-orm/src/mysql-core/index.ts | 1 - drizzle-orm/src/mysql-core/operations.ts | 28 - .../src/mysql-core/query-builders/delete.ts | 2 +- .../src/mysql-core/query-builders/insert.ts | 2 +- .../src/mysql-core/query-builders/select.ts | 8 +- .../mysql-core/query-builders/select.types.ts | 146 ++-- .../src/mysql-core/query-builders/update.ts | 2 +- drizzle-orm/src/mysql-core/session.ts | 2 +- drizzle-orm/src/mysql-core/utils.ts | 32 +- drizzle-orm/src/mysql2/session.ts | 2 +- drizzle-orm/src/neon-serverless/session.ts | 2 +- drizzle-orm/src/node-postgres/session.ts | 12 +- drizzle-orm/src/operations.ts | 14 +- drizzle-orm/src/pg-core/README.md | 2 +- drizzle-orm/src/pg-core/db.ts | 2 +- drizzle-orm/src/pg-core/dialect.ts | 19 +- drizzle-orm/src/pg-core/index.ts | 1 - drizzle-orm/src/pg-core/operations.ts | 29 - .../src/pg-core/query-builders/delete.ts | 4 +- .../src/pg-core/query-builders/insert.ts | 4 +- .../src/pg-core/query-builders/select.ts | 9 +- .../pg-core/query-builders/select.types.ts | 122 ++-- .../src/pg-core/query-builders/update.ts | 11 +- drizzle-orm/src/pg-core/session.ts | 7 +- drizzle-orm/src/pg-core/utils.ts | 44 +- .../{postgres.js => postgres-js}/README.md | 4 +- .../{postgres.js => postgres-js}/driver.ts | 0 .../src/{postgres.js => postgres-js}/index.ts | 0 .../{postgres.js => postgres-js}/migrator.ts | 0 .../{postgres.js => postgres-js}/session.ts | 2 +- drizzle-orm/src/{sql.js => sql-js}/driver.ts | 0 drizzle-orm/src/{sql.js => sql-js}/index.ts | 0 .../src/{sql.js => sql-js}/migrator.ts | 0 drizzle-orm/src/{sql.js => sql-js}/session.ts | 2 +- drizzle-orm/src/sqlite-core/db.ts | 2 +- drizzle-orm/src/sqlite-core/dialect.ts | 3 +- drizzle-orm/src/sqlite-core/index.ts | 3 +- drizzle-orm/src/sqlite-core/operations.ts | 29 - .../src/sqlite-core/query-builders/delete.ts | 4 +- .../src/sqlite-core/query-builders/insert.ts | 5 +- .../src/sqlite-core/query-builders/select.ts | 7 +- .../query-builders/select.types.ts | 151 ++-- .../src/sqlite-core/query-builders/update.ts | 5 +- drizzle-orm/src/sqlite-core/session.ts | 2 +- drizzle-orm/src/sqlite-core/utils.ts | 44 -- drizzle-orm/src/utils.ts | 91 ++- drizzle-orm/tests/mysql/dan/select.ts | 207 +----- drizzle-orm/tests/pg/dan/select.ts | 644 +++++++++++------- drizzle-orm/tests/sqlite/dan/select.ts | 123 ++-- integration-tests/tests/better-sqlite.test.ts | 108 +++ integration-tests/tests/mysql.test.ts | 106 +++ integration-tests/tests/pg.test.ts | 123 +++- integration-tests/tests/postgres.js.test.ts | 114 +++- integration-tests/tests/sql.js.test.ts | 112 ++- package.json | 2 +- pnpm-lock.yaml | 44 +- 63 files changed, 1477 insertions(+), 988 deletions(-) delete mode 100644 drizzle-orm/src/mysql-core/operations.ts delete mode 100644 drizzle-orm/src/pg-core/operations.ts rename drizzle-orm/src/{postgres.js => postgres-js}/README.md (94%) rename drizzle-orm/src/{postgres.js => postgres-js}/driver.ts (100%) rename drizzle-orm/src/{postgres.js => postgres-js}/index.ts (100%) rename drizzle-orm/src/{postgres.js => postgres-js}/migrator.ts (100%) rename drizzle-orm/src/{postgres.js => postgres-js}/session.ts (97%) rename drizzle-orm/src/{sql.js => sql-js}/driver.ts (100%) rename drizzle-orm/src/{sql.js => sql-js}/index.ts (100%) rename drizzle-orm/src/{sql.js => sql-js}/migrator.ts (100%) rename drizzle-orm/src/{sql.js => sql-js}/session.ts (98%) delete mode 100644 drizzle-orm/src/sqlite-core/operations.ts diff --git a/changelogs/drizzle-orm-pg/0.16.0.md b/changelogs/drizzle-orm-pg/0.16.0.md index 19d897e15..abc6b8dac 100644 --- a/changelogs/drizzle-orm-pg/0.16.0.md +++ b/changelogs/drizzle-orm-pg/0.16.0.md @@ -1,3 +1,3 @@ # drizzle-orm-pg 0.16.0 -- Implemented [postgres.js](https://github.com/porsager/postgres) driver support ([docs](/drizzle-orm-pg/src/postgres.js/README.md)) +- Implemented [postgres.js](https://github.com/porsager/postgres) driver support ([docs](/drizzle-orm-pg/src/postgres-js/README.md)) diff --git a/drizzle-orm/src/better-sqlite3/session.ts b/drizzle-orm/src/better-sqlite3/session.ts index a149ee503..dbcb7ae40 100644 --- a/drizzle-orm/src/better-sqlite3/session.ts +++ b/drizzle-orm/src/better-sqlite3/session.ts @@ -2,7 +2,7 @@ import { Database, RunResult, Statement } from 'better-sqlite3'; import { Logger, NoopLogger } from '~/logger'; import { fillPlaceholders, Query } from '~/sql'; import { SQLiteSyncDialect } from '~/sqlite-core/dialect'; -import { SelectFieldsOrdered } from '~/sqlite-core/operations'; +import { SelectFieldsOrdered } from '~/sqlite-core/query-builders/select.types'; import { PreparedQuery as PreparedQueryBase, PreparedQueryConfig as PreparedQueryConfigBase, diff --git a/drizzle-orm/src/bun-sqlite/session.ts b/drizzle-orm/src/bun-sqlite/session.ts index 6b81919c3..8f2f4d860 100644 --- a/drizzle-orm/src/bun-sqlite/session.ts +++ b/drizzle-orm/src/bun-sqlite/session.ts @@ -4,7 +4,7 @@ import { Database, Statement as BunStatement } from 'bun:sqlite'; import { Logger, NoopLogger } from '~/logger'; import { fillPlaceholders, Query } from '~/sql'; import { SQLiteSyncDialect } from '~/sqlite-core/dialect'; -import { SelectFieldsOrdered } from '~/sqlite-core/operations'; +import { SelectFieldsOrdered } from '~/sqlite-core/query-builders/select.types'; import { PreparedQuery as PreparedQueryBase, PreparedQueryConfig as PreparedQueryConfigBase, diff --git a/drizzle-orm/src/d1/session.ts b/drizzle-orm/src/d1/session.ts index c115c74b3..f9240613d 100644 --- a/drizzle-orm/src/d1/session.ts +++ b/drizzle-orm/src/d1/session.ts @@ -3,7 +3,7 @@ import { Logger, NoopLogger } from '~/logger'; import { fillPlaceholders, Query } from '~/sql'; import { SQLiteAsyncDialect } from '~/sqlite-core/dialect'; -import { SelectFieldsOrdered } from '~/sqlite-core/operations'; +import { SelectFieldsOrdered } from '~/sqlite-core/query-builders/select.types'; import { PreparedQuery as PreparedQueryBase, PreparedQueryConfig as PreparedQueryConfigBase, diff --git a/drizzle-orm/src/mysql-core/columns/custom.ts b/drizzle-orm/src/mysql-core/columns/custom.ts index cafbafcfa..b64bc7d76 100644 --- a/drizzle-orm/src/mysql-core/columns/custom.ts +++ b/drizzle-orm/src/mysql-core/columns/custom.ts @@ -3,7 +3,7 @@ import { ColumnBuilderConfig } from '~/column-builder'; import { AnyMySqlTable } from '~/mysql-core/table'; import { MySqlColumn, MySqlColumnBuilder } from './common'; -export type CustomColumnBuildeConfig = { +export type CustomColumnBuilderConfig = { data: T['data']; driverParam: T['driverData']; notNull: T['notNull'] extends undefined ? false : T['notNull'] extends true ? true : false; @@ -184,12 +184,12 @@ export function customType< dbName: string, fieldConfig?: T['config'], ) => MySqlColumnBuilder< - ColumnBuilderConfig>, + ColumnBuilderConfig>, Record > { return (dbName: string, fieldConfig?: T['config']) => new class extends MySqlColumnBuilder< - ColumnBuilderConfig>, + ColumnBuilderConfig>, Record > { protected $pgColumnBuilderBrand!: 'CustomColumnBuilderBrand'; @@ -198,7 +198,7 @@ export function customType< build( table: AnyMySqlTable<{ name: TTableName }>, ): MySqlColumn< - ColumnConfig & { tableName: TTableName }> + ColumnConfig & { tableName: TTableName }> > { return returnColumn( table, diff --git a/drizzle-orm/src/mysql-core/db.ts b/drizzle-orm/src/mysql-core/db.ts index b55e84209..5d8920cf0 100644 --- a/drizzle-orm/src/mysql-core/db.ts +++ b/drizzle-orm/src/mysql-core/db.ts @@ -1,10 +1,10 @@ import { ResultSetHeader } from 'mysql2/promise'; import { SQLWrapper } from '~/sql'; +import { orderSelectedFields } from '~/utils'; import { MySqlDialect } from './dialect'; import { MySqlDelete, MySqlInsertBuilder, MySqlSelect, MySqlUpdateBuilder } from './query-builders'; import { MySqlQueryResult, MySqlSession } from './session'; import { AnyMySqlTable, MySqlTable } from './table'; -import { orderSelectedFields } from './utils'; export class MySqlDatabase { constructor( diff --git a/drizzle-orm/src/mysql-core/dialect.ts b/drizzle-orm/src/mysql-core/dialect.ts index 39a5491e6..0c9da42c6 100644 --- a/drizzle-orm/src/mysql-core/dialect.ts +++ b/drizzle-orm/src/mysql-core/dialect.ts @@ -1,13 +1,12 @@ import { AnyColumn, Column } from '~/column'; import { MigrationMeta } from '~/migrator'; -import { SelectFieldsOrdered } from '~/operations'; import { Name, Query, SQL, sql, SQLResponse, SQLSourceParam } from '~/sql'; import { Table } from '~/table'; import { AnyMySqlColumn, MySqlColumn } from './columns/common'; import { MySqlDatabase } from './db'; import { MySqlDeleteConfig } from './query-builders/delete'; import { MySqlInsertConfig } from './query-builders/insert'; -import { MySqlSelectConfig } from './query-builders/select.types'; +import { MySqlSelectConfig, SelectFieldsOrdered } from './query-builders/select.types'; import { MySqlUpdateConfig, MySqlUpdateSet } from './query-builders/update'; import { MySqlSession } from './session'; import { AnyMySqlTable, MySqlTable } from './table'; diff --git a/drizzle-orm/src/mysql-core/index.ts b/drizzle-orm/src/mysql-core/index.ts index df9e7ba37..5d7f4fad0 100644 --- a/drizzle-orm/src/mysql-core/index.ts +++ b/drizzle-orm/src/mysql-core/index.ts @@ -5,6 +5,5 @@ export * from './db'; export * from './dialect'; export * from './foreign-keys'; export * from './indexes'; -export * from './operations'; export * from './session'; export * from './table'; diff --git a/drizzle-orm/src/mysql-core/operations.ts b/drizzle-orm/src/mysql-core/operations.ts deleted file mode 100644 index b2a7481a7..000000000 --- a/drizzle-orm/src/mysql-core/operations.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { GetColumnData } from '~/column'; -import { SelectFieldsOrdered as SelectFieldsOrderedBase } from '~/operations'; -import { SQL, SQLResponse } from '~/sql'; -import { Simplify } from '~/utils'; -import { AnyMySqlColumn } from './columns/common'; -import { AnyMySqlTable, GetTableConfig } from './table'; - -export type SelectFields = { - [Key: string]: SQL | SQLResponse | AnyMySqlColumn | SelectFields | AnyMySqlTable; -}; - -export type SelectFieldsOrdered = ( - & Omit - & { field: AnyMySqlColumn | SQL | SQLResponse } -)[]; - -export type SelectResultField = T extends AnyMySqlTable ? SelectResultField> - : T extends AnyMySqlColumn ? GetColumnData - : T extends SQLResponse ? TDriverParam - : T extends SQL ? unknown - : T extends Record ? { [Key in keyof T]: SelectResultField } - : never; - -export type SelectResultFields = Simplify< - { - [Key in keyof TSelectedFields & string]: SelectResultField; - } ->; diff --git a/drizzle-orm/src/mysql-core/query-builders/delete.ts b/drizzle-orm/src/mysql-core/query-builders/delete.ts index e83837ac8..4d5a6ba08 100644 --- a/drizzle-orm/src/mysql-core/query-builders/delete.ts +++ b/drizzle-orm/src/mysql-core/query-builders/delete.ts @@ -1,9 +1,9 @@ import { MySqlDialect } from '~/mysql-core/dialect'; -import { SelectFieldsOrdered } from '~/mysql-core/operations'; import { MySqlRawQueryResult, MySqlSession, PreparedQuery, PreparedQueryConfig } from '~/mysql-core/session'; import { AnyMySqlTable } from '~/mysql-core/table'; import { QueryPromise } from '~/query-promise'; import { Query, SQL, SQLWrapper } from '~/sql'; +import { SelectFieldsOrdered } from './select.types'; export interface MySqlDeleteConfig { where?: SQL | undefined; diff --git a/drizzle-orm/src/mysql-core/query-builders/insert.ts b/drizzle-orm/src/mysql-core/query-builders/insert.ts index 21a326e8f..f85e20ed6 100644 --- a/drizzle-orm/src/mysql-core/query-builders/insert.ts +++ b/drizzle-orm/src/mysql-core/query-builders/insert.ts @@ -1,11 +1,11 @@ import { MySqlDialect } from '~/mysql-core/dialect'; -import { SelectFieldsOrdered } from '~/mysql-core/operations'; import { MySqlRawQueryResult, MySqlSession, PreparedQuery, PreparedQueryConfig } from '~/mysql-core/session'; import { AnyMySqlTable, InferModel } from '~/mysql-core/table'; import { mapUpdateSet } from '~/mysql-core/utils'; import { QueryPromise } from '~/query-promise'; import { Param, Placeholder, Query, SQL, sql, SQLWrapper } from '~/sql'; import { Table } from '~/table'; +import { SelectFieldsOrdered } from './select.types'; import { MySqlUpdateSetSource } from './update'; export interface MySqlInsertConfig { table: TTable; diff --git a/drizzle-orm/src/mysql-core/query-builders/select.ts b/drizzle-orm/src/mysql-core/query-builders/select.ts index f5a7b8b60..49685c4da 100644 --- a/drizzle-orm/src/mysql-core/query-builders/select.ts +++ b/drizzle-orm/src/mysql-core/query-builders/select.ts @@ -1,12 +1,11 @@ import { AnyMySqlColumn } from '~/mysql-core/columns/common'; import { MySqlDialect } from '~/mysql-core/dialect'; -import { SelectFields } from '~/mysql-core/operations'; import { MySqlSession, PreparedQuery, PreparedQueryConfig } from '~/mysql-core/session'; import { AnyMySqlTable, GetTableConfig, InferModel } from '~/mysql-core/table'; -import { orderSelectedFields } from '~/mysql-core/utils'; import { QueryPromise } from '~/query-promise'; import { Query, SQL, SQLWrapper } from '~/sql'; import { Table } from '~/table'; +import { orderSelectedFields } from '~/utils'; import { AnyMySqlSelect, @@ -14,6 +13,7 @@ import { JoinNullability, JoinType, MySqlSelectConfig, + SelectFields, SelectMode, SelectResult, } from './select.types'; @@ -56,6 +56,7 @@ export class MySqlSelect< }; this.joinsNotNullable = { [table[Table.Symbol.Name]]: true }; } + private createJoin( joinType: TJoinType, ): JoinFn { @@ -86,9 +87,6 @@ export class MySqlSelect< this.joinsNotNullable[tableName] = true; break; case 'inner': - this.joinsNotNullable = Object.fromEntries( - Object.entries(this.joinsNotNullable).map(([key]) => [key, true]), - ); this.joinsNotNullable[tableName] = true; break; case 'full': diff --git a/drizzle-orm/src/mysql-core/query-builders/select.types.ts b/drizzle-orm/src/mysql-core/query-builders/select.types.ts index 2c1d613bf..18bc90ec5 100644 --- a/drizzle-orm/src/mysql-core/query-builders/select.types.ts +++ b/drizzle-orm/src/mysql-core/query-builders/select.types.ts @@ -1,10 +1,9 @@ -import { GetColumnConfig } from '~/column'; +import { GetColumnConfig, GetColumnData } from '~/column'; import { Placeholder, SQL, SQLResponse } from '~/sql'; -import { Simplify } from '~/utils'; +import { Equal, Simplify } from '~/utils'; import { AnyMySqlColumn } from '~/mysql-core/columns'; import { ChangeColumnTableName } from '~/mysql-core/columns/common'; -import { SelectFields, SelectFieldsOrdered, SelectResultField, SelectResultFields } from '~/mysql-core/operations'; import { AnyMySqlTable, GetTableConfig, @@ -12,6 +11,7 @@ import { TableConfig, UpdateTableConfig, } from '~/mysql-core/table'; +import { SelectFields as SelectFieldsBase, SelectFieldsOrdered as SelectFieldsOrderedBase } from '~/operations'; import { MySqlSelect } from './select'; @@ -36,57 +36,53 @@ export type ApplyNullabilityNested = T } : ApplyNullability; -export type ApplyNotNullMapToJoins> = - TJoinsNotNullable extends TJoinsNotNullable ? { - [TTableName in keyof TResult & keyof TJoinsNotNullable & string]: ApplyNullability< - TResult[TTableName], - TJoinsNotNullable[TTableName] - >; - } - : never; +export type ApplyNotNullMapToJoins> = { + [TTableName in keyof TResult & keyof TNullabilityMap & string]: ApplyNullability< + TResult[TTableName], + TNullabilityMap[TTableName] + >; +}; export type SelectResult< TResult, TSelectMode extends SelectMode, TJoinsNotNullable extends Record, -> = TSelectMode extends 'partial' ? SelectPartialResult +> = TSelectMode extends 'partial' + ? RemoveDuplicates>> : TSelectMode extends 'single' ? TResult : RemoveDuplicates>>; -type GetNullableKeys> = { - [Key in keyof T]: T[Key] extends 'nullable' ? Key : never; -}[keyof T]; +type IsUnion = (T extends any ? (U extends T ? false : true) + : never) extends false ? false : true; -// Splits a single variant with 'nullable' into two variants with 'null' and 'not-null' -type SplitNullability> = RemoveDuplicates< - 'nullable' extends T[keyof T] - ? T extends T ? GetNullableKeys extends infer TKey extends string ? [TKey] extends [TKey] ? TKey extends TKey ? - | Simplify> & { [Key in TKey]: 'not-null' }> - | Simplify> & { [Key in TKey]: 'null' }> - : never - : never - : T - : never - : T ->; +type Not = T extends true ? false : true; type SelectPartialResult< TFields, TNullability extends Record, -> = SplitNullability extends infer TNullability extends Record - ? TNullability extends TNullability ? { - [Key in keyof TFields as Key extends string ? Key : never]: TFields[Key] extends infer TField - ? TField extends AnyMySqlTable ? SelectPartialResult, TNullability> - : TField extends AnyMySqlColumn - ? GetColumnConfig extends infer TTableName extends keyof TNullability - ? ApplyNullability, TNullability[TTableName]> + TIsSimpleFields extends boolean, +> = TNullability extends TNullability ? { + [Key in keyof TFields]: TFields[Key] extends infer TField + ? TField extends AnyMySqlTable + ? TIsSimpleFields extends true ? GetTableConfig extends keyof TNullability ? ApplyNullability< + SelectResultFields>, + TNullability[GetTableConfig] + > : never - : TField extends SQL | SQLResponse ? SelectResultField - : TField extends Record ? SelectPartialResult - : SelectResultField - : never; - } - : never + : SelectPartialResult, TNullability, TIsSimpleFields> + : TField extends AnyMySqlColumn + ? GetColumnConfig extends infer TTableName extends keyof TNullability + ? ApplyNullability, TNullability[TTableName]> + : never + : TField extends SQL | SQLResponse ? SelectResultField + : TField extends Record + ? [TIsSimpleFields, TField[keyof TField]] extends + [true, AnyMySqlColumn<{ tableName: infer TTableName extends string }>] + ? ApplyNullability, TNullability[TTableName]> + : SelectPartialResult + : SelectResultField + : never; + } : never; export type AnyMySqlSelect = MySqlSelect; @@ -115,8 +111,8 @@ export type AppendToResult< ? Record, TResult> & Record> : Simplify>>; -type SetJoinsNotNull, TValue extends JoinNullability> = { - [Key in keyof TJoinsNotNull]: TValue; +type SetJoinsNullability, TValue extends JoinNullability> = { + [Key in keyof TNullabilityMap]: TValue; }; // https://stackoverflow.com/a/70061272/9929789 @@ -133,15 +129,39 @@ export type AppendToJoinsNotNull< TJoinsNotNull extends Record, TJoinedName extends string, TJoinType extends JoinType, -> = Simplify< - 'left' extends TJoinType ? TJoinsNotNull & { [name in TJoinedName]: 'nullable' } - : 'right' extends TJoinType ? SetJoinsNotNull & { [name in TJoinedName]: 'not-null' } - : 'inner' extends TJoinType ? SetJoinsNotNull & { [name in TJoinedName]: 'not-null' } - : 'full' extends TJoinType ? - | (TJoinsNotNull & { [name in TJoinedName]: 'not-null' }) - | (TJoinsNotNull & { [name in TJoinedName]: 'null' }) - | (SetJoinsNotNull & { [name in TJoinedName]: 'not-null' }) - : never + TIsSimpleFields extends boolean, +> = 'left' extends TJoinType ? TIsSimpleFields extends true ? TJoinsNotNull & { [name in TJoinedName]: 'nullable' } + : TJoinsNotNull & { [name in TJoinedName]: 'not-null' } | TJoinsNotNull & { [name in TJoinedName]: 'null' } + : 'right' extends TJoinType + ? [TIsSimpleFields, Not>] extends [true, true] + ? SetJoinsNullability & { [name in TJoinedName]: 'not-null' } + : + | TJoinsNotNull & { [name in TJoinedName]: 'not-null' } + | SetJoinsNullability & { [name in TJoinedName]: 'not-null' } + : 'inner' extends TJoinType ? TJoinsNotNull & { [name in TJoinedName]: 'not-null' } + : 'full' extends TJoinType ? + | (TJoinsNotNull & { [name in TJoinedName]: 'not-null' }) + | (TJoinsNotNull & { [name in TJoinedName]: 'null' }) + | (SetJoinsNullability & { [name in TJoinedName]: 'not-null' }) + : never; + +// Field selection is considered "simple" if it's either a flat object of columns from the same table (select w/o joins), or nested objects, where each object only has columns from the same table. +// If we are dealing with a simple field selection, the resulting type will be much easier to understand, and you'll be able to use more joins in a single statement, +// because in that case we can just mark the whole nested object as nullable instead of creating unions, where all fields of a certain table are either null or not null. +export type IsSimpleObject = T[keyof T] extends + AnyMySqlColumn<{ tableName: infer TTableName extends string }> | SQL | SQLResponse + ? Not> extends true ? true : false + : false; + +export type IsSimpleFields = IsSimpleObject extends true ? true : Equal< + true, + { + [Key in keyof TFields]: TFields[Key] extends AnyMySqlTable ? true + : TFields[Key] extends + Record | SQL | SQLResponse> + ? Not> + : false; + }[keyof TFields] >; export interface MySqlSelectConfig { @@ -168,5 +188,27 @@ export type JoinFn< TTable, AppendToResult, TSelectMode>, TSelectMode extends 'partial' ? TSelectMode : 'multiple', - AppendToJoinsNotNull + AppendToJoinsNotNull< + TJoinsNotNullable, + TJoinedName, + TJoinType, + TSelectMode extends 'partial' ? IsSimpleFields : true + > +>; + +export type SelectFields = SelectFieldsBase; + +export type SelectFieldsOrdered = SelectFieldsOrderedBase; + +export type SelectResultField = T extends AnyMySqlTable ? SelectResultField> + : T extends AnyMySqlColumn ? GetColumnData + : T extends SQLResponse ? TDriverParam + : T extends SQL ? unknown + : T extends Record ? { [Key in keyof T]: SelectResultField } + : never; + +export type SelectResultFields = Simplify< + { + [Key in keyof TSelectedFields & string]: SelectResultField; + } >; diff --git a/drizzle-orm/src/mysql-core/query-builders/update.ts b/drizzle-orm/src/mysql-core/query-builders/update.ts index 64f5aa472..80d5f0822 100644 --- a/drizzle-orm/src/mysql-core/query-builders/update.ts +++ b/drizzle-orm/src/mysql-core/query-builders/update.ts @@ -1,12 +1,12 @@ import { GetColumnData } from '~/column'; import { MySqlDialect } from '~/mysql-core/dialect'; -import { SelectFieldsOrdered } from '~/mysql-core/operations'; import { MySqlRawQueryResult, MySqlSession, PreparedQuery, PreparedQueryConfig } from '~/mysql-core/session'; import { AnyMySqlTable, GetTableConfig } from '~/mysql-core/table'; import { mapUpdateSet } from '~/mysql-core/utils'; import { QueryPromise } from '~/query-promise'; import { Param, Query, SQL, SQLWrapper } from '~/sql'; import { Simplify } from '~/utils'; +import { SelectFieldsOrdered } from './select.types'; export interface MySqlUpdateConfig { where?: SQL | undefined; diff --git a/drizzle-orm/src/mysql-core/session.ts b/drizzle-orm/src/mysql-core/session.ts index fe07a16ae..74ebec7a5 100644 --- a/drizzle-orm/src/mysql-core/session.ts +++ b/drizzle-orm/src/mysql-core/session.ts @@ -1,7 +1,7 @@ import { FieldPacket, OkPacket, ResultSetHeader, RowDataPacket } from 'mysql2/promise'; import { Query, SQL } from '~/sql'; import { MySqlDialect } from './dialect'; -import { SelectFieldsOrdered } from './operations'; +import { SelectFieldsOrdered } from './query-builders/select.types'; // TODO: improve type export type MySqlRawQueryResult = [ResultSetHeader, FieldPacket[]]; diff --git a/drizzle-orm/src/mysql-core/utils.ts b/drizzle-orm/src/mysql-core/utils.ts index 88b1f65c3..8f9d55c31 100644 --- a/drizzle-orm/src/mysql-core/utils.ts +++ b/drizzle-orm/src/mysql-core/utils.ts @@ -1,8 +1,6 @@ -import { SelectFields, SelectFieldsOrdered } from '~/mysql-core/operations'; -import { Param, SQL, SQLResponse } from '~/sql'; +import { Param, SQL } from '~/sql'; import { Table } from '~/table'; import { Check, CheckBuilder } from './checks'; -import { MySqlColumn } from './columns/common'; import { ForeignKey, ForeignKeyBuilder } from './foreign-keys'; import { Index, IndexBuilder } from './indexes'; import { MySqlUpdateSet } from './query-builders/update'; @@ -95,31 +93,3 @@ export function mapUpdateSet(table: AnyMySqlTable, values: Record = T extends U ? T : U; - -export function orderSelectedFields(fields: SelectFields, pathPrefix?: string[]): SelectFieldsOrdered { - return Object.entries(fields).reduce((result, [name, field]) => { - if (typeof name !== 'string') { - return result; - } - - const newPath = pathPrefix ? [...pathPrefix, name] : [name]; - if ( - field instanceof MySqlColumn - || field instanceof SQL - || field instanceof SQLResponse - ) { - result.push({ path: newPath, field }); - } else if (field instanceof MySqlTable) { - result.push( - ...orderSelectedFields(field[Table.Symbol.Columns], newPath), - ); - } else { - result.push( - ...orderSelectedFields(field, newPath), - ); - } - return result; - }, []); -} diff --git a/drizzle-orm/src/mysql2/session.ts b/drizzle-orm/src/mysql2/session.ts index e914ee8eb..3b5de191a 100644 --- a/drizzle-orm/src/mysql2/session.ts +++ b/drizzle-orm/src/mysql2/session.ts @@ -1,7 +1,7 @@ import { Connection, Pool, QueryOptions } from 'mysql2/promise'; import { Logger, NoopLogger } from '~/logger'; import { MySqlDialect } from '~/mysql-core/dialect'; -import { SelectFieldsOrdered } from '~/mysql-core/operations'; +import { SelectFieldsOrdered } from '~/mysql-core/query-builders/select.types'; import { MySqlQueryResult, MySqlQueryResultType, diff --git a/drizzle-orm/src/neon-serverless/session.ts b/drizzle-orm/src/neon-serverless/session.ts index 6b05dd6f7..1d5aef2c2 100644 --- a/drizzle-orm/src/neon-serverless/session.ts +++ b/drizzle-orm/src/neon-serverless/session.ts @@ -9,7 +9,7 @@ import { } from '@neondatabase/serverless'; import { Logger, NoopLogger } from '~/logger'; import { PgDialect } from '~/pg-core/dialect'; -import { SelectFieldsOrdered } from '~/pg-core/operations'; +import { SelectFieldsOrdered } from '~/pg-core/query-builders/select.types'; import { PgSession, PreparedQuery, PreparedQueryConfig, QueryResultHKT } from '~/pg-core/session'; import { fillPlaceholders, Query } from '~/sql'; import { Assume } from '~/utils'; diff --git a/drizzle-orm/src/node-postgres/session.ts b/drizzle-orm/src/node-postgres/session.ts index 67cce4008..c48c18b83 100644 --- a/drizzle-orm/src/node-postgres/session.ts +++ b/drizzle-orm/src/node-postgres/session.ts @@ -1,7 +1,7 @@ import { Client, Pool, PoolClient, QueryArrayConfig, QueryConfig, QueryResult, QueryResultRow } from 'pg'; import { Logger, NoopLogger } from '~/logger'; -import { SelectFieldsOrdered } from '~/operations'; import { PgDialect } from '~/pg-core/dialect'; +import { SelectFieldsOrdered } from '~/pg-core/query-builders/select.types'; import { PgSession, PreparedQuery, PreparedQueryConfig, QueryResultHKT } from '~/pg-core/session'; import { fillPlaceholders, Query } from '~/sql'; import { mapResultRow } from '~/utils'; @@ -20,8 +20,9 @@ export class NodePgPreparedQuery extends Prepared private logger: Logger, private fields: SelectFieldsOrdered | undefined, name: string | undefined, + joinsNotNullable?: Record, ) { - super(); + super(joinsNotNullable); this.rawQuery = { name, text: queryString, @@ -45,7 +46,9 @@ export class NodePgPreparedQuery extends Prepared const result = this.client.query(this.query, params); - return result.then((result) => result.rows.map((row) => mapResultRow(fields, row))); + return result.then((result) => + result.rows.map((row) => mapResultRow(fields, row, this.joinsNotNullable)) + ); } all(placeholderValues: Record | undefined = {}): Promise { @@ -81,8 +84,9 @@ export class NodePgSession extends PgSession { query: Query, fields: SelectFieldsOrdered | undefined, name: string | undefined, + joinsNotNullable?: Record, ): PreparedQuery { - return new NodePgPreparedQuery(this.client, query.sql, query.params, this.logger, fields, name); + return new NodePgPreparedQuery(this.client, query.sql, query.params, this.logger, fields, name, joinsNotNullable); } async query(query: string, params: unknown[]): Promise { diff --git a/drizzle-orm/src/operations.ts b/drizzle-orm/src/operations.ts index c9392eb6a..ba5443190 100644 --- a/drizzle-orm/src/operations.ts +++ b/drizzle-orm/src/operations.ts @@ -1,5 +1,6 @@ import { AnyColumn } from './column'; import { SQL, SQLResponse } from './sql'; +import { Table } from './table'; export type RequiredKeyOnly = T extends AnyColumn<{ notNull: true; hasDefault: false }> ? TKey @@ -8,14 +9,13 @@ export type RequiredKeyOnly = T extend export type OptionalKeyOnly = TKey extends RequiredKeyOnly ? never : TKey; -export type SelectFields = { - [key: string]: - | SQL - | SQLResponse - | AnyColumn<{ tableName: TTableName; driverParam: TColumnDriverParam }>; +export type SelectFields = { + [Key: string]: TColumn | SQL | SQLResponse | TTable | { + [Subkey: string]: TColumn | SQL | SQLResponse; + }; }; -export type SelectFieldsOrdered = { +export type SelectFieldsOrdered = { path: string[]; - field: AnyColumn | SQL | SQLResponse; + field: TColumn | SQL | SQLResponse; }[]; diff --git a/drizzle-orm/src/pg-core/README.md b/drizzle-orm/src/pg-core/README.md index 777c544c5..acd602104 100644 --- a/drizzle-orm/src/pg-core/README.md +++ b/drizzle-orm/src/pg-core/README.md @@ -13,7 +13,7 @@ Drizzle ORM is a TypeScript ORM for SQL databases designed with maximum type saf | Driver | Support | | Driver version | | :- | :-: | :-: | :-: | | [node-postgres](https://github.com/brianc/node-postgres) | ✅ | | driver version | -| [postgres.js](https://github.com/porsager/postgres) | ✅ | [Docs](/drizzle-orm/src/postgres.js/README.md) | driver version | +| [postgres.js](https://github.com/porsager/postgres) | ✅ | [Docs](/drizzle-orm/src/postgres-js/README.md) | driver version | | [NeonDB Serverless](https://github.com/neondatabase/serverless) | ✅ | | driver version | ## Installation diff --git a/drizzle-orm/src/pg-core/db.ts b/drizzle-orm/src/pg-core/db.ts index aba436732..611dc1acb 100644 --- a/drizzle-orm/src/pg-core/db.ts +++ b/drizzle-orm/src/pg-core/db.ts @@ -2,8 +2,8 @@ import { PgDialect } from '~/pg-core/dialect'; import { PgDelete, PgInsertBuilder, PgSelect, PgUpdateBuilder } from '~/pg-core/query-builders'; import { PgSession, QueryResultHKT, QueryResultKind } from '~/pg-core/session'; import { AnyPgTable, PgTable } from '~/pg-core/table'; -import { orderSelectedFields } from '~/pg-core/utils'; import { SQLWrapper } from '~/sql'; +import { orderSelectedFields } from '~/utils'; export class PgDatabase { constructor( diff --git a/drizzle-orm/src/pg-core/dialect.ts b/drizzle-orm/src/pg-core/dialect.ts index 4b4135e83..357f2b8bc 100644 --- a/drizzle-orm/src/pg-core/dialect.ts +++ b/drizzle-orm/src/pg-core/dialect.ts @@ -1,13 +1,12 @@ import { AnyColumn, Column } from '~/column'; import { MigrationMeta } from '~/migrator'; -import { SelectFieldsOrdered } from '~/operations'; import { AnyPgColumn, PgColumn } from '~/pg-core/columns'; -import { PgDatabase } from '~/pg-core/db'; -import { PgDeleteConfig, PgInsertConfig, PgUpdateConfig, PgUpdateSet } from '~/pg-core/query-builders'; -import { PgSelectConfig } from '~/pg-core/query-builders/select.types'; +import { PgDeleteConfig, PgInsertConfig, PgUpdateConfig } from '~/pg-core/query-builders'; +import { PgSelectConfig, SelectFieldsOrdered } from '~/pg-core/query-builders/select.types'; import { AnyPgTable, PgTable } from '~/pg-core/table'; import { Name, Query, SQL, sql, SQLResponse, SQLSourceParam } from '~/sql'; -import { Table } from '~/table'; +import { getTableName, Table } from '~/table'; +import { UpdateSet } from '~/utils'; import { PgSession } from './session'; export class PgDialect { @@ -65,7 +64,7 @@ export class PgDialect { return sql`delete from ${table}${whereSql}${returningSql}`; } - buildUpdateSet(table: AnyPgTable, set: PgUpdateSet): SQL { + buildUpdateSet(table: AnyPgTable, set: UpdateSet): SQL { const setEntries = Object.entries(set); const setSize = setEntries.length; @@ -153,6 +152,14 @@ export class PgDialect { } buildSelectQuery({ fields, where, table, joins, orderBy, groupBy, limit, offset }: PgSelectConfig): SQL { + fields.forEach((f) => { + if (f.field instanceof Column && f.field.table !== table && !(getTableName(f.field.table) in joins)) { + throw new Error( + `Column "${f.path.join('.')}" was selected, but its table "${getTableName(f.field.table)}" was not joined`, + ); + } + }); + const joinKeys = Object.keys(joins); const selection = this.buildSelection(fields, { isSingleTable: joinKeys.length === 0 }); diff --git a/drizzle-orm/src/pg-core/index.ts b/drizzle-orm/src/pg-core/index.ts index df9e7ba37..5d7f4fad0 100644 --- a/drizzle-orm/src/pg-core/index.ts +++ b/drizzle-orm/src/pg-core/index.ts @@ -5,6 +5,5 @@ export * from './db'; export * from './dialect'; export * from './foreign-keys'; export * from './indexes'; -export * from './operations'; export * from './session'; export * from './table'; diff --git a/drizzle-orm/src/pg-core/operations.ts b/drizzle-orm/src/pg-core/operations.ts deleted file mode 100644 index 417fd04ce..000000000 --- a/drizzle-orm/src/pg-core/operations.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { GetColumnData } from '~/column'; -import { SelectFieldsOrdered as SelectFieldsOrderedBase } from '~/operations'; -import { SQL, SQLResponse } from '~/sql'; -import { Simplify } from '~/utils'; - -import { AnyPgColumn } from '~/pg-core/columns/common'; -import { AnyPgTable, GetTableConfig } from '~/pg-core/table'; - -export type SelectFields = { - [Key: string]: SQL | SQLResponse | AnyPgColumn | SelectFields | AnyPgTable; -}; - -export type SelectFieldsOrdered = ( - & Omit - & { field: AnyPgColumn | SQL | SQLResponse } -)[]; - -export type SelectResultField = T extends AnyPgTable ? SelectResultField> - : T extends AnyPgColumn ? GetColumnData - : T extends SQLResponse ? TDriverParam - : T extends SQL ? unknown - : T extends Record ? { [Key in keyof T]: SelectResultField } - : never; - -export type SelectResultFields = Simplify< - { - [Key in keyof TSelectedFields & string]: SelectResultField; - } ->; diff --git a/drizzle-orm/src/pg-core/query-builders/delete.ts b/drizzle-orm/src/pg-core/query-builders/delete.ts index 93985aa7e..be4ba0301 100644 --- a/drizzle-orm/src/pg-core/query-builders/delete.ts +++ b/drizzle-orm/src/pg-core/query-builders/delete.ts @@ -2,10 +2,10 @@ import { QueryPromise } from '~/query-promise'; import { Query, SQL, SQLWrapper } from '~/sql'; import { PgDialect } from '~/pg-core/dialect'; -import { SelectFields, SelectFieldsOrdered, SelectResultFields } from '~/pg-core/operations'; import { PgSession, PreparedQuery, PreparedQueryConfig, QueryResultHKT, QueryResultKind } from '~/pg-core/session'; import { AnyPgTable, InferModel, PgTable } from '~/pg-core/table'; -import { orderSelectedFields } from '~/pg-core/utils'; +import { orderSelectedFields } from '~/utils'; +import { SelectFields, SelectFieldsOrdered, SelectResultFields } from './select.types'; export interface PgDeleteConfig { where?: SQL | undefined; diff --git a/drizzle-orm/src/pg-core/query-builders/insert.ts b/drizzle-orm/src/pg-core/query-builders/insert.ts index b3cf594da..51d59449e 100644 --- a/drizzle-orm/src/pg-core/query-builders/insert.ts +++ b/drizzle-orm/src/pg-core/query-builders/insert.ts @@ -4,10 +4,10 @@ import { Table } from '~/table'; import { PgDialect } from '~/pg-core/dialect'; import { IndexColumn } from '~/pg-core/indexes'; -import { SelectFields, SelectFieldsOrdered, SelectResultFields } from '~/pg-core/operations'; import { PgSession, PreparedQuery, PreparedQueryConfig, QueryResultHKT, QueryResultKind } from '~/pg-core/session'; import { AnyPgTable, InferModel, PgTable } from '~/pg-core/table'; -import { mapUpdateSet, orderSelectedFields } from '~/pg-core/utils'; +import { mapUpdateSet, orderSelectedFields } from '~/utils'; +import { SelectFields, SelectFieldsOrdered, SelectResultFields } from './select.types'; import { PgUpdateSetSource } from './update'; export interface PgInsertConfig { diff --git a/drizzle-orm/src/pg-core/query-builders/select.ts b/drizzle-orm/src/pg-core/query-builders/select.ts index 9cbae76fa..315df940c 100644 --- a/drizzle-orm/src/pg-core/query-builders/select.ts +++ b/drizzle-orm/src/pg-core/query-builders/select.ts @@ -4,17 +4,17 @@ import { Table } from '~/table'; import { AnyPgColumn } from '~/pg-core/columns'; import { PgDialect } from '~/pg-core/dialect'; -import { SelectFields } from '~/pg-core/operations'; import { PgSession, PreparedQuery, PreparedQueryConfig } from '~/pg-core/session'; import { AnyPgTable, GetTableConfig, InferModel } from '~/pg-core/table'; -import { orderSelectedFields } from '~/pg-core/utils'; +import { orderSelectedFields } from '~/utils'; import { AnyPgSelect, JoinFn, JoinNullability, JoinType, PgSelectConfig, + SelectFields, SelectMode, SelectResult, } from './select.types'; @@ -88,9 +88,6 @@ export class PgSelect< this.joinsNotNullable[tableName] = true; break; case 'inner': - this.joinsNotNullable = Object.fromEntries( - Object.entries(this.joinsNotNullable).map(([key]) => [key, true]), - ); this.joinsNotNullable[tableName] = true; break; case 'full': @@ -160,7 +157,7 @@ export class PgSelect< execute: SelectResult[]; } > { - return this.session.prepareQuery(this.toSQL(), this.config.fields, name); + return this.session.prepareQuery(this.toSQL(), this.config.fields, name, this.joinsNotNullable); } prepare(name: string): PreparedQuery< diff --git a/drizzle-orm/src/pg-core/query-builders/select.types.ts b/drizzle-orm/src/pg-core/query-builders/select.types.ts index b4fb9ca59..eecd3dda2 100644 --- a/drizzle-orm/src/pg-core/query-builders/select.types.ts +++ b/drizzle-orm/src/pg-core/query-builders/select.types.ts @@ -1,10 +1,10 @@ -import { GetColumnConfig } from '~/column'; +import { GetColumnConfig, GetColumnData } from '~/column'; import { Placeholder, SQL, SQLResponse } from '~/sql'; import { Simplify } from '~/utils'; +import { SelectFields as SelectFieldsBase, SelectFieldsOrdered as SelectFieldsOrderedBase } from '~/operations'; import { AnyPgColumn } from '~/pg-core/columns'; import { ChangeColumnTableName } from '~/pg-core/columns/common'; -import { SelectFields, SelectFieldsOrdered, SelectResultField, SelectResultFields } from '~/pg-core/operations'; import { AnyPgTable, GetTableConfig, PgTableWithColumns, TableConfig, UpdateTableConfig } from '~/pg-core/table'; import { PgSelect } from './select'; @@ -25,19 +25,12 @@ export type ApplyNullability = TNullabi : TNullability extends 'null' ? null : T; -export type ApplyNullabilityNested = T extends Record ? { - [Key in keyof T]: ApplyNullabilityNested; - } - : ApplyNullability; - -export type ApplyNotNullMapToJoins> = - TJoinsNotNullable extends TJoinsNotNullable ? { - [TTableName in keyof TResult & keyof TJoinsNotNullable & string]: ApplyNullability< - TResult[TTableName], - TJoinsNotNullable[TTableName] - >; - } - : never; +export type ApplyNotNullMapToJoins> = { + [TTableName in keyof TResult & keyof TNullabilityMap & string]: ApplyNullability< + TResult[TTableName], + TNullabilityMap[TTableName] + >; +}; export type SelectResult< TResult, @@ -45,42 +38,35 @@ export type SelectResult< TJoinsNotNullable extends Record, > = TSelectMode extends 'partial' ? SelectPartialResult : TSelectMode extends 'single' ? TResult - : RemoveDuplicates>>; - -type GetNullableKeys> = { - [Key in keyof T]: T[Key] extends 'nullable' ? Key : never; -}[keyof T]; - -// Splits a single variant with 'nullable' into two variants with 'null' and 'not-null' -type SplitNullability> = RemoveDuplicates< - 'nullable' extends T[keyof T] - ? T extends T ? GetNullableKeys extends infer TKey extends string ? [TKey] extends [TKey] ? TKey extends TKey ? - | Simplify> & { [Key in TKey]: 'not-null' }> - | Simplify> & { [Key in TKey]: 'null' }> - : never - : never - : T - : never - : T ->; + : Simplify>; -type SelectPartialResult< - TFields, - TNullability extends Record, -> = SplitNullability extends infer TNullability extends Record - ? TNullability extends TNullability ? { - [Key in keyof TFields as Key extends string ? Key : never]: TFields[Key] extends infer TField - ? TField extends AnyPgTable ? SelectPartialResult, TNullability> - : TField extends AnyPgColumn - ? GetColumnConfig extends infer TTableName extends keyof TNullability - ? ApplyNullability, TNullability[TTableName]> - : never - : TField extends SQL | SQLResponse ? SelectResultField - : TField extends Record ? SelectPartialResult - : SelectResultField - : never; - } - : never +type IsUnion = (T extends any ? (U extends T ? false : true) + : never) extends false ? false : true; + +type Not = T extends true ? false : true; + +type SelectPartialResult> = TNullability extends + TNullability ? { + [Key in keyof TFields]: TFields[Key] extends infer TField + ? TField extends AnyPgTable ? GetTableConfig extends keyof TNullability ? ApplyNullability< + SelectResultFields>, + TNullability[GetTableConfig] + > + : never + : TField extends AnyPgColumn + ? GetColumnConfig extends infer TTableName extends keyof TNullability + ? ApplyNullability, TNullability[TTableName]> + : never + : TField extends SQL | SQLResponse ? SelectResultField + : TField extends Record + ? TField[keyof TField] extends AnyPgColumn<{ tableName: infer TTableName extends string }> | SQL | SQLResponse + ? Not> extends true + ? ApplyNullability, TNullability[TTableName]> + : SelectPartialResult + : never + : never + : never; + } : never; export type AnyPgSelect = PgSelect; @@ -109,8 +95,8 @@ export type AppendToResult< ? Record, TResult> & Record> : Simplify>>; -type SetJoinsNotNull, TValue extends JoinNullability> = { - [Key in keyof TJoinsNotNull]: TValue; +type SetJoinsNullability, TValue extends JoinNullability> = { + [Key in keyof TNullabilityMap]: TValue; }; // https://stackoverflow.com/a/70061272/9929789 @@ -127,16 +113,11 @@ export type AppendToJoinsNotNull< TJoinsNotNull extends Record, TJoinedName extends string, TJoinType extends JoinType, -> = Simplify< - 'left' extends TJoinType ? TJoinsNotNull & { [name in TJoinedName]: 'nullable' } - : 'right' extends TJoinType ? SetJoinsNotNull & { [name in TJoinedName]: 'not-null' } - : 'inner' extends TJoinType ? SetJoinsNotNull & { [name in TJoinedName]: 'not-null' } - : 'full' extends TJoinType ? - | (TJoinsNotNull & { [name in TJoinedName]: 'not-null' }) - | (TJoinsNotNull & { [name in TJoinedName]: 'null' }) - | (SetJoinsNotNull & { [name in TJoinedName]: 'not-null' }) - : never ->; +> = 'left' extends TJoinType ? TJoinsNotNull & { [name in TJoinedName]: 'nullable' } + : 'right' extends TJoinType ? SetJoinsNullability & { [name in TJoinedName]: 'not-null' } + : 'inner' extends TJoinType ? TJoinsNotNull & { [name in TJoinedName]: 'not-null' } + : 'full' extends TJoinType ? SetJoinsNullability & { [name in TJoinedName]: 'nullable' } + : never; export interface PgSelectConfig { fields: SelectFieldsOrdered; @@ -164,3 +145,20 @@ export type JoinFn< TSelectMode extends 'partial' ? TSelectMode : 'multiple', AppendToJoinsNotNull >; + +export type SelectFields = SelectFieldsBase; + +export type SelectFieldsOrdered = SelectFieldsOrderedBase; + +export type SelectResultField = T extends AnyPgTable ? SelectResultField> + : T extends AnyPgColumn ? GetColumnData + : T extends SQLResponse ? TDriverParam + : T extends SQL ? unknown + : T extends Record ? { [Key in keyof T]: SelectResultField } + : never; + +export type SelectResultFields = Simplify< + { + [Key in keyof TSelectedFields & string]: SelectResultField; + } +>; diff --git a/drizzle-orm/src/pg-core/query-builders/update.ts b/drizzle-orm/src/pg-core/query-builders/update.ts index 704eca677..4d81cdf5e 100644 --- a/drizzle-orm/src/pg-core/query-builders/update.ts +++ b/drizzle-orm/src/pg-core/query-builders/update.ts @@ -2,16 +2,15 @@ import { GetColumnData } from '~/column'; import { PgDialect } from '~/pg-core/dialect'; import { QueryPromise } from '~/query-promise'; import { Param, Query, SQL, SQLWrapper } from '~/sql'; -import { Simplify } from '~/utils'; +import { mapUpdateSet, orderSelectedFields, Simplify, UpdateSet } from '~/utils'; -import { SelectFields, SelectFieldsOrdered, SelectResultFields } from '~/pg-core/operations'; import { PgSession, PreparedQuery, PreparedQueryConfig, QueryResultHKT, QueryResultKind } from '~/pg-core/session'; import { AnyPgTable, GetTableConfig, InferModel, PgTable } from '~/pg-core/table'; -import { mapUpdateSet, orderSelectedFields } from '~/pg-core/utils'; +import { SelectFields, SelectFieldsOrdered, SelectResultFields } from './select.types'; export interface PgUpdateConfig { where?: SQL | undefined; - set: PgUpdateSet; + set: UpdateSet; table: AnyPgTable; returning?: SelectFieldsOrdered; } @@ -24,8 +23,6 @@ export type PgUpdateSetSource = Simplify< } >; -export type PgUpdateSet = Record; - export class PgUpdateBuilder { declare protected $table: TTable; @@ -63,7 +60,7 @@ export class PgUpdate< constructor( table: TTable, - set: PgUpdateSet, + set: UpdateSet, private session: PgSession, private dialect: PgDialect, ) { diff --git a/drizzle-orm/src/pg-core/session.ts b/drizzle-orm/src/pg-core/session.ts index ee270b606..037a8b2ea 100644 --- a/drizzle-orm/src/pg-core/session.ts +++ b/drizzle-orm/src/pg-core/session.ts @@ -1,6 +1,6 @@ import { Query, SQL } from '~/sql'; import { PgDialect } from './dialect'; -import { SelectFieldsOrdered } from './operations'; +import { SelectFieldsOrdered } from './query-builders/select.types'; export interface PreparedQueryConfig { execute: unknown; @@ -9,6 +9,10 @@ export interface PreparedQueryConfig { } export abstract class PreparedQuery { + constructor( + protected joinsNotNullable?: Record, + ) {} + abstract execute(placeholderValues?: Record): Promise; /** @internal */ @@ -25,6 +29,7 @@ export abstract class PgSession { query: Query, fields: SelectFieldsOrdered | undefined, name: string | undefined, + joinsNotNullable?: Record, ): PreparedQuery; execute(query: SQL): Promise { diff --git a/drizzle-orm/src/pg-core/utils.ts b/drizzle-orm/src/pg-core/utils.ts index ad3b9d68d..74162aa54 100644 --- a/drizzle-orm/src/pg-core/utils.ts +++ b/drizzle-orm/src/pg-core/utils.ts @@ -1,8 +1,5 @@ -import { AnyPgColumn, PgColumn } from '~/pg-core/columns'; -import { SelectFields, SelectFieldsOrdered } from '~/pg-core/operations'; -import { PgUpdateSet } from '~/pg-core/query-builders'; +import { AnyPgColumn } from '~/pg-core/columns'; import { AnyPgTable, GetTableConfig, PgTable } from '~/pg-core/table'; -import { Param, SQL, SQLResponse } from '~/sql'; import { Table } from '~/table'; import { Check, CheckBuilder } from './checks'; import { ForeignKey, ForeignKeyBuilder } from './foreign-keys'; @@ -91,42 +88,3 @@ export function getTableChecks(table: TTable) { const keys = Reflect.ownKeys(checks); return keys.map((key) => checks[key]!); } - -/** @internal */ -export function mapUpdateSet(table: AnyPgTable, values: Record): PgUpdateSet { - return Object.fromEntries( - Object.entries(values).map(([key, value]) => { - if (value instanceof SQL || value === null || value === undefined) { - return [key, value]; - } else { - return [key, new Param(value, table[PgTable.Symbol.Columns][key])]; - } - }), - ); -} - -export function orderSelectedFields(fields: SelectFields, pathPrefix?: string[]): SelectFieldsOrdered { - return Object.entries(fields).reduce((result, [name, field]) => { - if (typeof name !== 'string') { - return result; - } - - const newPath = pathPrefix ? [...pathPrefix, name] : [name]; - if ( - field instanceof PgColumn - || field instanceof SQL - || field instanceof SQLResponse - ) { - result.push({ path: newPath, field }); - } else if (field instanceof PgTable) { - result.push( - ...orderSelectedFields(field[Table.Symbol.Columns], newPath), - ); - } else { - result.push( - ...orderSelectedFields(field, newPath), - ); - } - return result; - }, []); -} diff --git a/drizzle-orm/src/postgres.js/README.md b/drizzle-orm/src/postgres-js/README.md similarity index 94% rename from drizzle-orm/src/postgres.js/README.md rename to drizzle-orm/src/postgres-js/README.md index 914860c69..12b421f18 100644 --- a/drizzle-orm/src/postgres.js/README.md +++ b/drizzle-orm/src/postgres-js/README.md @@ -19,7 +19,7 @@ pnpm add -D drizzle-kit ## Connection ```typescript -import { drizzle, PostgresJsDatabase } from 'drizzle-orm/postgres.js'; +import { drizzle, PostgresJsDatabase } from 'drizzle-orm/postgres-js'; import postgres from 'postgres'; const client = postgres(connectionString); @@ -34,7 +34,7 @@ In order to run the migrations, [you need to use `max: 1` in the postgres.js con ```typescript import postgres from 'postgres'; -import { migrate } from 'drizzle-orm/postgres.js/migrator'; +import { migrate } from 'drizzle-orm/postgres-js/migrator'; const migrationsClient = postgres(connectionString, { max: 1, diff --git a/drizzle-orm/src/postgres.js/driver.ts b/drizzle-orm/src/postgres-js/driver.ts similarity index 100% rename from drizzle-orm/src/postgres.js/driver.ts rename to drizzle-orm/src/postgres-js/driver.ts diff --git a/drizzle-orm/src/postgres.js/index.ts b/drizzle-orm/src/postgres-js/index.ts similarity index 100% rename from drizzle-orm/src/postgres.js/index.ts rename to drizzle-orm/src/postgres-js/index.ts diff --git a/drizzle-orm/src/postgres.js/migrator.ts b/drizzle-orm/src/postgres-js/migrator.ts similarity index 100% rename from drizzle-orm/src/postgres.js/migrator.ts rename to drizzle-orm/src/postgres-js/migrator.ts diff --git a/drizzle-orm/src/postgres.js/session.ts b/drizzle-orm/src/postgres-js/session.ts similarity index 97% rename from drizzle-orm/src/postgres.js/session.ts rename to drizzle-orm/src/postgres-js/session.ts index d5e30b642..3386fa2d2 100644 --- a/drizzle-orm/src/postgres.js/session.ts +++ b/drizzle-orm/src/postgres-js/session.ts @@ -1,7 +1,7 @@ import { Row, RowList, Sql } from 'postgres'; import { Logger, NoopLogger } from '~/logger'; import { PgDialect } from '~/pg-core/dialect'; -import { SelectFieldsOrdered } from '~/pg-core/operations'; +import { SelectFieldsOrdered } from '~/pg-core/query-builders/select.types'; import { PgSession, PreparedQuery, PreparedQueryConfig, QueryResultHKT } from '~/pg-core/session'; import { fillPlaceholders, Query } from '~/sql'; import { Assume } from '~/utils'; diff --git a/drizzle-orm/src/sql.js/driver.ts b/drizzle-orm/src/sql-js/driver.ts similarity index 100% rename from drizzle-orm/src/sql.js/driver.ts rename to drizzle-orm/src/sql-js/driver.ts diff --git a/drizzle-orm/src/sql.js/index.ts b/drizzle-orm/src/sql-js/index.ts similarity index 100% rename from drizzle-orm/src/sql.js/index.ts rename to drizzle-orm/src/sql-js/index.ts diff --git a/drizzle-orm/src/sql.js/migrator.ts b/drizzle-orm/src/sql-js/migrator.ts similarity index 100% rename from drizzle-orm/src/sql.js/migrator.ts rename to drizzle-orm/src/sql-js/migrator.ts diff --git a/drizzle-orm/src/sql.js/session.ts b/drizzle-orm/src/sql-js/session.ts similarity index 98% rename from drizzle-orm/src/sql.js/session.ts rename to drizzle-orm/src/sql-js/session.ts index 9b3618eb2..0ff790e6c 100644 --- a/drizzle-orm/src/sql.js/session.ts +++ b/drizzle-orm/src/sql-js/session.ts @@ -2,7 +2,7 @@ import { BindParams, Database, Statement } from 'sql.js'; import { Logger, NoopLogger } from '~/logger'; import { fillPlaceholders, Query } from '~/sql'; import { SQLiteSyncDialect } from '~/sqlite-core/dialect'; -import { SelectFieldsOrdered } from '~/sqlite-core/operations'; +import { SelectFieldsOrdered } from '~/sqlite-core/query-builders/select.types'; import { PreparedQuery as PreparedQueryBase, PreparedQueryConfig as PreparedQueryConfigBase, diff --git a/drizzle-orm/src/sqlite-core/db.ts b/drizzle-orm/src/sqlite-core/db.ts index 30904f4a3..0fbc363ab 100644 --- a/drizzle-orm/src/sqlite-core/db.ts +++ b/drizzle-orm/src/sqlite-core/db.ts @@ -5,7 +5,7 @@ import { SQLiteAsyncDialect, SQLiteSyncDialect } from '~/sqlite-core/dialect'; import { SQLiteDelete, SQLiteInsertBuilder, SQLiteSelect, SQLiteUpdateBuilder } from '~/sqlite-core/query-builders'; import { ResultKind, SQLiteSession } from '~/sqlite-core/session'; import { AnySQLiteTable } from '~/sqlite-core/table'; -import { orderSelectedFields } from './utils'; +import { orderSelectedFields } from '~/utils'; export class BaseSQLiteDatabase { constructor( diff --git a/drizzle-orm/src/sqlite-core/dialect.ts b/drizzle-orm/src/sqlite-core/dialect.ts index 5615c2907..548c14370 100644 --- a/drizzle-orm/src/sqlite-core/dialect.ts +++ b/drizzle-orm/src/sqlite-core/dialect.ts @@ -2,7 +2,6 @@ import { AnyColumn, Column } from '~/column'; import { MigrationMeta } from '~/migrator'; import { Name, param, Query, SQL, sql, SQLResponse, SQLSourceParam } from '~/sql'; import { AnySQLiteColumn, SQLiteColumn } from '~/sqlite-core/columns'; -import { SelectFieldsOrdered } from '~/sqlite-core/operations'; import { SQLiteDeleteConfig, SQLiteInsertConfig, @@ -11,7 +10,7 @@ import { } from '~/sqlite-core/query-builders'; import { AnySQLiteTable, SQLiteTable } from '~/sqlite-core/table'; import { Table } from '~/table'; -import { SQLiteSelectConfig } from './query-builders/select.types'; +import { SelectFieldsOrdered, SQLiteSelectConfig } from './query-builders/select.types'; import { SQLiteSession } from './session'; export abstract class SQLiteDialect { diff --git a/drizzle-orm/src/sqlite-core/index.ts b/drizzle-orm/src/sqlite-core/index.ts index c26e17780..30e5a4bc3 100644 --- a/drizzle-orm/src/sqlite-core/index.ts +++ b/drizzle-orm/src/sqlite-core/index.ts @@ -4,7 +4,6 @@ export * from './columns'; export * from './db'; export * from './dialect'; export * from './foreign-keys'; -export * from './primary-keys'; export * from './indexes'; -export * from './operations'; +export * from './primary-keys'; export * from './table'; diff --git a/drizzle-orm/src/sqlite-core/operations.ts b/drizzle-orm/src/sqlite-core/operations.ts deleted file mode 100644 index f6a7c2823..000000000 --- a/drizzle-orm/src/sqlite-core/operations.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { GetColumnData } from '~/column'; -import { SelectFieldsOrdered as SelectFieldsOrderedBase } from '~/operations'; -import { SQL, SQLResponse } from '~/sql'; -import { Simplify } from '~/utils'; - -import { AnySQLiteColumn } from '~/sqlite-core/columns/common'; -import { AnySQLiteTable, GetTableConfig } from '~/sqlite-core/table'; - -export type SQLiteSelectFields = { - [Key: string]: SQL | SQLResponse | AnySQLiteColumn | SQLiteSelectFields | AnySQLiteTable; -}; - -export type SelectFieldsOrdered = ( - & Omit - & { field: AnySQLiteColumn | SQL | SQLResponse } -)[]; - -export type SelectResultField = T extends AnySQLiteTable ? SelectResultField> - : T extends AnySQLiteColumn ? GetColumnData - : T extends SQLResponse ? TDriverParam - : T extends SQL ? unknown - : T extends Record ? { [Key in keyof T]: SelectResultField } - : never; - -export type SelectResultFields = Simplify< - { - [Key in keyof TSelectedFields & string]: SelectResultField; - } ->; diff --git a/drizzle-orm/src/sqlite-core/query-builders/delete.ts b/drizzle-orm/src/sqlite-core/query-builders/delete.ts index 6430c3559..6efe4822a 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/delete.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/delete.ts @@ -1,10 +1,10 @@ import { Query, SQL, SQLWrapper } from '~/sql'; import { SQLiteDialect } from '~/sqlite-core/dialect'; -import { SelectFieldsOrdered, SelectResultFields, SQLiteSelectFields } from '~/sqlite-core/operations'; import { PreparedQuery, SQLiteSession } from '~/sqlite-core/session'; import { AnySQLiteTable, InferModel, SQLiteTable } from '~/sqlite-core/table'; -import { orderSelectedFields } from '~/sqlite-core/utils'; +import { orderSelectedFields } from '~/utils'; +import { SelectFieldsOrdered, SelectResultFields, SQLiteSelectFields } from './select.types'; export interface SQLiteDeleteConfig { where?: SQL | undefined; diff --git a/drizzle-orm/src/sqlite-core/query-builders/insert.ts b/drizzle-orm/src/sqlite-core/query-builders/insert.ts index 559360ebc..233617e17 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/insert.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/insert.ts @@ -1,13 +1,12 @@ import { Param, Placeholder, Query, SQL, sql, SQLWrapper } from '~/sql'; import { Table } from '~/table'; -import { Simplify } from '~/utils'; +import { mapUpdateSet, orderSelectedFields, Simplify } from '~/utils'; import { SQLiteDialect } from '~/sqlite-core/dialect'; import { IndexColumn } from '~/sqlite-core/indexes'; -import { SelectFieldsOrdered, SelectResultFields, SQLiteSelectFields } from '~/sqlite-core/operations'; import { PreparedQuery, SQLiteSession } from '~/sqlite-core/session'; import { AnySQLiteTable, InferModel, SQLiteTable } from '~/sqlite-core/table'; -import { mapUpdateSet, orderSelectedFields } from '~/sqlite-core/utils'; +import { SelectFieldsOrdered, SelectResultFields, SQLiteSelectFields } from './select.types'; import { SQLiteUpdateSetSource } from './update'; export interface SQLiteInsertConfig { diff --git a/drizzle-orm/src/sqlite-core/query-builders/select.ts b/drizzle-orm/src/sqlite-core/query-builders/select.ts index 03e44aeee..bb430803b 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/select.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/select.ts @@ -3,11 +3,10 @@ import { AnySQLiteColumn } from '~/sqlite-core/columns'; import { SQLiteDialect } from '~/sqlite-core/dialect'; import { Table } from '~/table'; -import { SQLiteSelectFields } from '~/sqlite-core/operations'; import { PreparedQuery, SQLiteSession } from '~/sqlite-core/session'; import { AnySQLiteTable, GetTableConfig, InferModel } from '~/sqlite-core/table'; -import { orderSelectedFields } from '~/sqlite-core/utils'; +import { orderSelectedFields } from '~/utils'; import { AnySQLiteSelect, JoinFn, @@ -16,6 +15,7 @@ import { SelectMode, SelectResult, SQLiteSelectConfig, + SQLiteSelectFields, } from './select.types'; export interface SQLiteSelect< @@ -90,9 +90,6 @@ export class SQLiteSelect< this.joinsNotNullable[tableName] = true; break; case 'inner': - this.joinsNotNullable = Object.fromEntries( - Object.entries(this.joinsNotNullable).map(([key]) => [key, true]), - ); this.joinsNotNullable[tableName] = true; break; case 'full': diff --git a/drizzle-orm/src/sqlite-core/query-builders/select.types.ts b/drizzle-orm/src/sqlite-core/query-builders/select.types.ts index b60e3bca4..928945406 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/select.types.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/select.types.ts @@ -1,15 +1,9 @@ -import { GetColumnConfig } from '~/column'; +import { GetColumnConfig, GetColumnData } from '~/column'; import { Placeholder, SQL, SQLResponse } from '~/sql'; -import { Simplify } from '~/utils'; +import { Equal, Simplify } from '~/utils'; import { AnySQLiteColumn } from '~/sqlite-core/columns'; import { ChangeColumnTableName } from '~/sqlite-core/columns/common'; -import { - SelectFieldsOrdered, - SelectResultField, - SelectResultFields, - SQLiteSelectFields, -} from '~/sqlite-core/operations'; import { AnySQLiteTable, GetTableConfig, @@ -18,6 +12,7 @@ import { UpdateTableConfig, } from '~/sqlite-core/table'; +import { SelectFields, SelectFieldsOrdered as SelectFieldsOrderedBase } from '~/operations'; import { SQLiteSelect } from './select'; export type JoinType = 'inner' | 'left' | 'right' | 'full'; @@ -41,57 +36,53 @@ export type ApplyNullabilityNested = T } : ApplyNullability; -export type ApplyNotNullMapToJoins> = - TJoinsNotNullable extends TJoinsNotNullable ? { - [TTableName in keyof TResult & keyof TJoinsNotNullable & string]: ApplyNullability< - TResult[TTableName], - TJoinsNotNullable[TTableName] - >; - } - : never; +export type ApplyNotNullMapToJoins> = { + [TTableName in keyof TResult & keyof TNullabilityMap & string]: ApplyNullability< + TResult[TTableName], + TNullabilityMap[TTableName] + >; +}; export type SelectResult< TResult, TSelectMode extends SelectMode, TJoinsNotNullable extends Record, -> = TSelectMode extends 'partial' ? SelectPartialResult +> = TSelectMode extends 'partial' + ? RemoveDuplicates>> : TSelectMode extends 'single' ? TResult : RemoveDuplicates>>; -type GetNullableKeys> = { - [Key in keyof T]: T[Key] extends 'nullable' ? Key : never; -}[keyof T]; +type IsUnion = (T extends any ? (U extends T ? false : true) + : never) extends false ? false : true; -// Splits a single variant with 'nullable' into two variants with 'null' and 'not-null' -type SplitNullability> = RemoveDuplicates< - 'nullable' extends T[keyof T] - ? T extends T ? GetNullableKeys extends infer TKey extends string ? [TKey] extends [TKey] ? TKey extends TKey ? - | Simplify> & { [Key in TKey]: 'not-null' }> - | Simplify> & { [Key in TKey]: 'null' }> - : never - : never - : T - : never - : T ->; +type Not = T extends true ? false : true; type SelectPartialResult< TFields, TNullability extends Record, -> = SplitNullability extends infer TNullability extends Record - ? TNullability extends TNullability ? { - [Key in keyof TFields as Key extends string ? Key : never]: TFields[Key] extends infer TField - ? TField extends AnySQLiteTable ? SelectPartialResult, TNullability> - : TField extends AnySQLiteColumn - ? GetColumnConfig extends infer TTableName extends keyof TNullability - ? ApplyNullability, TNullability[TTableName]> + TIsSimpleFields extends boolean, +> = TNullability extends TNullability ? { + [Key in keyof TFields]: TFields[Key] extends infer TField + ? TField extends AnySQLiteTable + ? TIsSimpleFields extends true ? GetTableConfig extends keyof TNullability ? ApplyNullability< + SelectResultFields>, + TNullability[GetTableConfig] + > : never - : TField extends SQL | SQLResponse ? SelectResultField - : TField extends Record ? SelectPartialResult - : SelectResultField - : never; - } - : never + : SelectPartialResult, TNullability, TIsSimpleFields> + : TField extends AnySQLiteColumn + ? GetColumnConfig extends infer TTableName extends keyof TNullability + ? ApplyNullability, TNullability[TTableName]> + : never + : TField extends SQL | SQLResponse ? SelectResultField + : TField extends Record + ? [TIsSimpleFields, TField[keyof TField]] extends + [true, AnySQLiteColumn<{ tableName: infer TTableName extends string }>] + ? ApplyNullability, TNullability[TTableName]> + : SelectPartialResult + : SelectResultField + : never; + } : never; export type AnySQLiteSelect = SQLiteSelect; @@ -120,8 +111,8 @@ export type AppendToResult< ? Record, TResult> & Record> : Simplify>>; -type SetJoinsNotNull, TValue extends JoinNullability> = { - [Key in keyof TJoinsNotNull]: TValue; +type SetJoinsNullability, TValue extends JoinNullability> = { + [Key in keyof TNullabilityMap]: TValue; }; // https://stackoverflow.com/a/70061272/9929789 @@ -138,15 +129,39 @@ export type AppendToJoinsNotNull< TJoinsNotNull extends Record, TJoinedName extends string, TJoinType extends JoinType, -> = Simplify< - 'left' extends TJoinType ? TJoinsNotNull & { [name in TJoinedName]: 'nullable' } - : 'right' extends TJoinType ? SetJoinsNotNull & { [name in TJoinedName]: 'not-null' } - : 'inner' extends TJoinType ? SetJoinsNotNull & { [name in TJoinedName]: 'not-null' } - : 'full' extends TJoinType ? - | (TJoinsNotNull & { [name in TJoinedName]: 'not-null' }) - | (TJoinsNotNull & { [name in TJoinedName]: 'null' }) - | (SetJoinsNotNull & { [name in TJoinedName]: 'not-null' }) - : never + TIsSimpleFields extends boolean, +> = 'left' extends TJoinType ? TIsSimpleFields extends true ? TJoinsNotNull & { [name in TJoinedName]: 'nullable' } + : TJoinsNotNull & { [name in TJoinedName]: 'not-null' } | TJoinsNotNull & { [name in TJoinedName]: 'null' } + : 'right' extends TJoinType + ? [TIsSimpleFields, Not>] extends [true, true] + ? SetJoinsNullability & { [name in TJoinedName]: 'not-null' } + : + | TJoinsNotNull & { [name in TJoinedName]: 'not-null' } + | SetJoinsNullability & { [name in TJoinedName]: 'not-null' } + : 'inner' extends TJoinType ? TJoinsNotNull & { [name in TJoinedName]: 'not-null' } + : 'full' extends TJoinType ? + | (TJoinsNotNull & { [name in TJoinedName]: 'not-null' }) + | (TJoinsNotNull & { [name in TJoinedName]: 'null' }) + | (SetJoinsNullability & { [name in TJoinedName]: 'not-null' }) + : never; + +// Field selection is considered "simple" if it's either a flat object of columns from the same table (select w/o joins), or nested objects, where each object only has columns from the same table. +// If we are dealing with a simple field selection, the resulting type will be much easier to understand, and you'll be able to use more joins in a single statement, +// because in that case we can just mark the whole nested object as nullable instead of creating unions, where all fields of a certain table are either null or not null. +export type IsSimpleObject = T[keyof T] extends + AnySQLiteColumn<{ tableName: infer TTableName extends string }> | SQL | SQLResponse + ? Not> extends true ? true : false + : false; + +export type IsSimpleFields = IsSimpleObject extends true ? true : Equal< + true, + { + [Key in keyof TFields]: TFields[Key] extends AnySQLiteTable ? true + : TFields[Key] extends + Record | SQL | SQLResponse> + ? Not> + : false; + }[keyof TFields] >; export interface SQLiteSelectConfig { @@ -177,5 +192,27 @@ export type JoinFn< TRunResult, AppendToResult, TSelectMode>, TSelectMode extends 'partial' ? TSelectMode : 'multiple', - AppendToJoinsNotNull + AppendToJoinsNotNull< + TJoinsNotNullable, + TJoinedName, + TJoinType, + TSelectMode extends 'partial' ? IsSimpleFields : true + > +>; + +export type SQLiteSelectFields = SelectFields; + +export type SelectFieldsOrdered = SelectFieldsOrderedBase; + +export type SelectResultField = T extends AnySQLiteTable ? SelectResultField> + : T extends AnySQLiteColumn ? GetColumnData + : T extends SQLResponse ? TDriverParam + : T extends SQL ? unknown + : T extends Record ? { [Key in keyof T]: SelectResultField } + : never; + +export type SelectResultFields = Simplify< + { + [Key in keyof TSelectedFields & string]: SelectResultField; + } >; diff --git a/drizzle-orm/src/sqlite-core/query-builders/update.ts b/drizzle-orm/src/sqlite-core/query-builders/update.ts index 68abeaabe..414283e63 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/update.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/update.ts @@ -1,11 +1,10 @@ import { GetColumnData } from '~/column'; import { Param, Query, SQL, SQLWrapper } from '~/sql'; import { SQLiteDialect } from '~/sqlite-core/dialect'; -import { SelectFieldsOrdered, SelectResultFields, SQLiteSelectFields } from '~/sqlite-core/operations'; import { PreparedQuery, SQLiteSession } from '~/sqlite-core/session'; import { AnySQLiteTable, GetTableConfig, InferModel, SQLiteTable } from '~/sqlite-core/table'; -import { mapUpdateSet, orderSelectedFields } from '~/sqlite-core/utils'; -import { Simplify } from '~/utils'; +import { mapUpdateSet, orderSelectedFields, Simplify } from '~/utils'; +import { SelectFieldsOrdered, SelectResultFields, SQLiteSelectFields } from './select.types'; export interface SQLiteUpdateConfig { where?: SQL | undefined; diff --git a/drizzle-orm/src/sqlite-core/session.ts b/drizzle-orm/src/sqlite-core/session.ts index 68e213334..d127d0c87 100644 --- a/drizzle-orm/src/sqlite-core/session.ts +++ b/drizzle-orm/src/sqlite-core/session.ts @@ -1,6 +1,6 @@ import { Query, SQL } from '~/sql'; import { SQLiteDialect } from '~/sqlite-core/dialect'; -import { SelectFieldsOrdered } from './operations'; +import { SelectFieldsOrdered } from './query-builders/select.types'; export interface PreparedQueryConfig { type: 'sync' | 'async'; diff --git a/drizzle-orm/src/sqlite-core/utils.ts b/drizzle-orm/src/sqlite-core/utils.ts index 1b5d82657..60f4ce7dc 100644 --- a/drizzle-orm/src/sqlite-core/utils.ts +++ b/drizzle-orm/src/sqlite-core/utils.ts @@ -1,9 +1,4 @@ -import { Param, SQL, SQLResponse } from '~/sql'; -import { SQLiteColumn } from '~/sqlite-core/columns'; -import { SelectFieldsOrdered, SQLiteSelectFields } from '~/sqlite-core/operations'; -import { SQLiteUpdateSet } from '~/sqlite-core/query-builders'; import { AnySQLiteTable, SQLiteTable } from '~/sqlite-core/table'; -import { Table } from '~/table'; export function getTableColumns(table: TTable) { const columns = table[SQLiteTable.Symbol.Columns]; @@ -35,43 +30,4 @@ export function getTableChecks(table: TTable) { return keys.map((key) => checks[key]!); } -/** @internal */ -export function mapUpdateSet(table: AnySQLiteTable, values: Record): SQLiteUpdateSet { - return Object.fromEntries( - Object.entries(values).map(([key, value]) => { - if (value instanceof SQL || value === null || value === undefined) { - return [key, value]; - } else { - return [key, new Param(value, table[SQLiteTable.Symbol.Columns][key])]; - } - }), - ); -} - export type OnConflict = 'rollback' | 'abort' | 'fail' | 'ignore' | 'replace'; - -export function orderSelectedFields(fields: SQLiteSelectFields, pathPrefix?: string[]): SelectFieldsOrdered { - return Object.entries(fields).reduce((result, [name, field]) => { - if (typeof name !== 'string') { - return result; - } - - const newPath = pathPrefix ? [...pathPrefix, name] : [name]; - if ( - field instanceof SQLiteColumn - || field instanceof SQL - || field instanceof SQLResponse - ) { - result.push({ path: newPath, field }); - } else if (field instanceof SQLiteTable) { - result.push( - ...orderSelectedFields(field[Table.Symbol.Columns], newPath), - ); - } else { - result.push( - ...orderSelectedFields(field, newPath), - ); - } - return result; - }, []); -} diff --git a/drizzle-orm/src/utils.ts b/drizzle-orm/src/utils.ts index 988d9ea62..3e5fa9e21 100644 --- a/drizzle-orm/src/utils.ts +++ b/drizzle-orm/src/utils.ts @@ -1,6 +1,7 @@ -import { Column } from './column'; -import { SelectFieldsOrdered } from './operations'; -import { DriverValueDecoder, noopDecoder, SQL } from './sql'; +import { AnyColumn, Column } from './column'; +import { SelectFields, SelectFieldsOrdered } from './operations'; +import { DriverValueDecoder, noopDecoder, Param, SQL, SQLResponse } from './sql'; +import { getTableName, Table } from './table'; /** * @deprecated @@ -14,10 +15,13 @@ export const apiVersion = 2; export const npmVersion = '0.17.0'; export function mapResultRow( - columns: SelectFieldsOrdered, + columns: SelectFieldsOrdered, row: unknown[], - joinsNotNullable?: Record, + joinsNotNullableMap?: Record, ): TResult { + // Key -> nested object key, value -> table name if all fields in the nested object are from the same table, false otherwise + const nullifyMap: Record = {}; + const result = columns.reduce>( (result, { path, field }, columnIndex) => { let decoder: DriverValueDecoder; @@ -37,7 +41,22 @@ export function mapResultRow( node = node[pathChunk]; } else { const rawValue = row[columnIndex]!; - node[pathChunk] = rawValue === null ? null : decoder.mapFromDriverValue(rawValue); + const value = node[pathChunk] = rawValue === null ? null : decoder.mapFromDriverValue(rawValue); + + if (joinsNotNullableMap && field instanceof Column && path.length === 2) { + const objectName = path[0]!; + if (!(objectName in nullifyMap)) { + if (value === null) { + nullifyMap[objectName] = getTableName(field.table); + } else { + nullifyMap[objectName] = false; + } + } else if ( + typeof nullifyMap[objectName] === 'string' && nullifyMap[objectName] !== getTableName(field.table) + ) { + nullifyMap[objectName] = false; + } + } } }); return result; @@ -45,24 +64,58 @@ export function mapResultRow( {}, ); - if (!joinsNotNullable) { - return result as TResult; + // Nullify all nested objects from nullifyMap that are nullable + if (joinsNotNullableMap && Object.keys(nullifyMap).length > 0) { + Object.entries(nullifyMap).forEach(([objectName, tableName]) => { + if (typeof tableName === 'string' && !joinsNotNullableMap[tableName]) { + result[objectName] = null; + } + }); } - // If all fields in a table are null, return null for the table - return Object.fromEntries( - Object.entries(result).map(([tableName, tableResult]) => { - if (!joinsNotNullable[tableName]) { - const hasNotNull = Object.values(tableResult).some((value) => value !== null); - if (!hasNotNull) { - return [tableName, null]; - } + return result as TResult; +} + +export function orderSelectedFields( + fields: SelectFields, + pathPrefix?: string[], +): SelectFieldsOrdered { + return Object.entries(fields).reduce>((result, [name, field]) => { + if (typeof name !== 'string') { + return result; + } + + const newPath = pathPrefix ? [...pathPrefix, name] : [name]; + if ( + field instanceof Column + || field instanceof SQL + || field instanceof SQLResponse + ) { + result.push({ path: newPath, field }); + } else if (field instanceof Table) { + result.push(...orderSelectedFields(field[Table.Symbol.Columns], newPath)); + } else { + result.push(...orderSelectedFields(field, newPath)); + } + return result; + }, []) as SelectFieldsOrdered; +} + +/** @internal */ +export function mapUpdateSet(table: Table, values: Record): UpdateSet { + return Object.fromEntries( + Object.entries(values).map(([key, value]) => { + if (value instanceof SQL || value === null || value === undefined) { + return [key, value]; + } else { + return [key, new Param(value, table[Table.Symbol.Columns][key])]; } - return [tableName, tableResult]; }), - ) as TResult; + ); } +export type UpdateSet = Record; + export type OneOrMany = T | T[]; export type Update = Simplify< @@ -155,3 +208,5 @@ export type Simplify< : AnyType; export type Assume = T extends U ? T : U; + +export type Equal = (() => T extends X ? 1 : 2) extends (() => T extends Y ? 1 : 2) ? true : false; diff --git a/drizzle-orm/tests/mysql/dan/select.ts b/drizzle-orm/tests/mysql/dan/select.ts index 5eb977e36..4cb8abe4a 100644 --- a/drizzle-orm/tests/mysql/dan/select.ts +++ b/drizzle-orm/tests/mysql/dan/select.ts @@ -49,67 +49,13 @@ Expect< ({ users: { id: number; - homeCity: number; - currentCity: number | null; - serialNullable: number; - serialNotNull: number; - class: 'A' | 'C'; - subClass: 'B' | 'D' | null; text: string | null; - age1: number; - createdAt: Date; - enumCol: 'a' | 'b' | 'c'; - }; - cities: { - id: number; - name: string; - population: number | null; - }; - city: { - id: number; - name: string; - population: number | null; - }; - city1: { - id: number; - }; - } | { - users: { - id: null; - homeCity: null; - currentCity: null; - serialNullable: null; - serialNotNull: null; - class: null; - subClass: null; - text: null; - age1: null; - createdAt: null; - enumCol: null; - }; - cities: { - id: number; - name: string; - population: number | null; - }; - city: { - id: number; - name: string; - population: number | null; - }; - city1: { - id: number; - }; - } | { - users: { - id: number; homeCity: number; currentCity: number | null; serialNullable: number; serialNotNull: number; class: 'A' | 'C'; subClass: 'B' | 'D' | null; - text: string | null; age1: number; createdAt: Date; enumCol: 'a' | 'b' | 'c'; @@ -118,61 +64,7 @@ Expect< id: number; name: string; population: number | null; - }; - city: { - id: null; - name: null; - population: null; - }; - city1: { - id: number; - }; - } | { - users: { - id: null; - homeCity: null; - currentCity: null; - serialNullable: null; - serialNotNull: null; - class: null; - subClass: null; - text: null; - age1: null; - createdAt: null; - enumCol: null; - }; - cities: { - id: number; - name: string; - population: number | null; - }; - city: { - id: null; - name: null; - population: null; - }; - city1: { - id: number; - }; - } | { - users: { - id: number; - homeCity: number; - currentCity: number | null; - serialNullable: number; - serialNotNull: number; - class: 'A' | 'C'; - subClass: 'B' | 'D' | null; - text: string | null; - age1: number; - createdAt: Date; - enumCol: 'a' | 'b' | 'c'; - }; - cities: { - id: null; - name: null; - population: null; - }; + } | null; city: { id: number; name: string; @@ -182,24 +74,8 @@ Expect< id: number; }; } | { - users: { - id: null; - homeCity: null; - currentCity: null; - serialNullable: null; - serialNotNull: null; - class: null; - subClass: null; - text: null; - age1: null; - createdAt: null; - enumCol: null; - }; - cities: { - id: null; - name: null; - population: null; - }; + users: null; + cities: null; city: { id: number; name: string; @@ -209,56 +85,9 @@ Expect< id: number; }; } | { - users: { - id: number; - homeCity: number; - currentCity: number | null; - serialNullable: number; - serialNotNull: number; - class: 'A' | 'C'; - subClass: 'B' | 'D' | null; - text: string | null; - age1: number; - createdAt: Date; - enumCol: 'a' | 'b' | 'c'; - }; - cities: { - id: null; - name: null; - population: null; - }; - city: { - id: null; - name: null; - population: null; - }; - city1: { - id: number; - }; - } | { - users: { - id: null; - homeCity: null; - currentCity: null; - serialNullable: null; - serialNotNull: null; - class: null; - subClass: null; - text: null; - age1: null; - createdAt: null; - enumCol: null; - }; - cities: { - id: null; - name: null; - population: null; - }; - city: { - id: null; - name: null; - population: null; - }; + users: null; + cities: null; + city: null; city1: { id: number; }; @@ -425,7 +254,9 @@ const megaJoin = await db.select(users) id: users.id, maxAge: sql`max(${users.age1})`, }, - cityId: cities.id, + city: { + id: cities.id, + }, homeCity, c, otherClass, @@ -455,7 +286,9 @@ Expect< id: number; maxAge: unknown; }; - cityId: number; + city: { + id: number; + }; homeCity: { id: number; name: string; @@ -521,8 +354,12 @@ const friends = alias(users, 'friends'); const join4 = await db.select(users) .fields({ - userId: users.id, - cityId: cities.id, + user: { + id: users.id, + }, + city: { + id: cities.id, + }, class: classes, friend: friends, }) @@ -533,8 +370,12 @@ const join4 = await db.select(users) Expect< Equal<{ - userId: number; - cityId: number; + user: { + id: number; + }; + city: { + id: number; + }; class: { id: number; class: 'A' | 'C' | null; diff --git a/drizzle-orm/tests/pg/dan/select.ts b/drizzle-orm/tests/pg/dan/select.ts index 268bcb144..74f246b25 100644 --- a/drizzle-orm/tests/pg/dan/select.ts +++ b/drizzle-orm/tests/pg/dan/select.ts @@ -31,6 +31,159 @@ import { cities, classes, users } from './tables'; const city = alias(cities, 'city'); const city1 = alias(cities, 'city1'); +const leftJoinFlat = await db.select(users) + .fields({ + userId: users.id, + userText: users.text, + cityId: city.id, + cityName: city.name, + }) + .leftJoin(city, eq(users.id, city.id)); + +Expect< + Equal<{ + userId: number; + userText: string | null; + cityId: number | null; + cityName: string | null; + }[], typeof leftJoinFlat> +>; + +const rightJoinFlat = await db.select(users) + .fields({ + userId: users.id, + userText: users.text, + cityId: city.id, + cityName: city.name, + }) + .rightJoin(city, eq(users.id, city.id)); + +Expect< + Equal<{ + userId: number | null; + userText: string | null; + cityId: number; + cityName: string; + }[], typeof rightJoinFlat> +>; + +const innerJoinFlat = await db.select(users) + .fields({ + userId: users.id, + userText: users.text, + cityId: city.id, + cityName: city.name, + }) + .innerJoin(city, eq(users.id, city.id)); + +Expect< + Equal<{ + userId: number; + userText: string | null; + cityId: number; + cityName: string; + }[], typeof innerJoinFlat> +>; + +const fullJoinFlat = await db.select(users) + .fields({ + userId: users.id, + userText: users.text, + cityId: city.id, + cityName: city.name, + }) + .fullJoin(city, eq(users.id, city.id)); + +Expect< + Equal<{ + userId: number | null; + userText: string | null; + cityId: number | null; + cityName: string | null; + }[], typeof fullJoinFlat> +>; + +const leftJoinMixed = await db.select(users) + .fields({ + id: users.id, + text: users.text, + textUpper: sql`upper(${users.text})`.as(), + idComplex: sql`${users.id}::text || ${city.id}::text`.as(), + city: { + id: city.id, + name: city.name, + }, + }) + .leftJoin(city, eq(users.id, city.id)); + +Expect< + Equal< + { + id: number; + text: string | null; + textUpper: string | null; + idComplex: string | null; + city: { + id: number; + name: string; + } | null; + }[], + typeof leftJoinMixed + > +>; + +const leftJoinMixed2 = await db.select(users) + .fields({ + id: users.id, + text: users.text, + foo: { + bar: users.uuid, + baz: cities.id, + }, + }) + .leftJoin(cities, eq(users.id, cities.id)); + +Expect< + Equal< + { + id: number; + text: string | null; + foo: { + bar: string; + baz: number | null; + }; + }[], + typeof leftJoinMixed2 + > +>; + +const join1 = await db.select(users) + .fields({ + user: { + id: users.id, + text: users.text, + }, + city: { + id: city.id, + name: city.name, + nameUpper: sql`upper(${city.name})`.as(), + }, + }).leftJoin(city, eq(users.id, city.id)); + +Expect< + Equal<{ + user: { + id: number; + text: string | null; + }; + city: { + id: number; + name: string; + nameUpper: string; + } | null; + }[], typeof join1> +>; + const join = await db.select(users) .fields({ users, @@ -46,119 +199,7 @@ const join = await db.select(users) Expect< Equal< - ({ - users: { - id: number; - uuid: string; - homeCity: number; - currentCity: number | null; - serialNullable: number; - serialNotNull: number; - class: 'A' | 'C'; - subClass: 'B' | 'D' | null; - text: string | null; - age1: number; - createdAt: Date; - enumCol: 'a' | 'b' | 'c'; - }; - cities: { - id: number; - name: string; - population: number | null; - }; - city: { - id: number; - name: string; - population: number | null; - }; - city1: { - id: number; - }; - } | { - users: { - id: null; - uuid: null; - homeCity: null; - currentCity: null; - serialNullable: null; - serialNotNull: null; - class: null; - subClass: null; - text: null; - age1: null; - createdAt: null; - enumCol: null; - }; - cities: { - id: number; - name: string; - population: number | null; - }; - city: { - id: number; - name: string; - population: number | null; - }; - city1: { - id: number; - }; - } | { - users: { - id: number; - uuid: string; - homeCity: number; - currentCity: number | null; - serialNullable: number; - serialNotNull: number; - class: 'A' | 'C'; - subClass: 'B' | 'D' | null; - text: string | null; - age1: number; - createdAt: Date; - enumCol: 'a' | 'b' | 'c'; - }; - cities: { - id: number; - name: string; - population: number | null; - }; - city: { - id: null; - name: null; - population: null; - }; - city1: { - id: number; - }; - } | { - users: { - id: null; - uuid: null; - homeCity: null; - currentCity: null; - serialNullable: null; - serialNotNull: null; - class: null; - subClass: null; - text: null; - age1: null; - createdAt: null; - enumCol: null; - }; - cities: { - id: number; - name: string; - population: number | null; - }; - city: { - id: null; - name: null; - population: null; - }; - city1: { - id: number; - }; - } | { + { users: { id: number; uuid: string; @@ -172,160 +213,78 @@ Expect< age1: number; createdAt: Date; enumCol: 'a' | 'b' | 'c'; - }; + } | null; cities: { - id: null; - name: null; - population: null; - }; - city: { id: number; name: string; population: number | null; - }; - city1: { - id: number; - }; - } | { - users: { - id: null; - uuid: null; - homeCity: null; - currentCity: null; - serialNullable: null; - serialNotNull: null; - class: null; - subClass: null; - text: null; - age1: null; - createdAt: null; - enumCol: null; - }; - cities: { - id: null; - name: null; - population: null; - }; + } | null; city: { id: number; name: string; population: number | null; - }; - city1: { - id: number; - }; - } | { - users: { - id: number; - uuid: string; - homeCity: number; - currentCity: number | null; - serialNullable: number; - serialNotNull: number; - class: 'A' | 'C'; - subClass: 'B' | 'D' | null; - text: string | null; - age1: number; - createdAt: Date; - enumCol: 'a' | 'b' | 'c'; - }; - cities: { - id: null; - name: null; - population: null; - }; - city: { - id: null; - name: null; - population: null; - }; + } | null; city1: { id: number; }; - } | { - users: { - id: null; - uuid: null; - homeCity: null; - currentCity: null; - serialNullable: null; - serialNotNull: null; - class: null; - subClass: null; - text: null; - age1: null; - createdAt: null; - enumCol: null; - }; - cities: { - id: null; - name: null; - population: null; - }; - city: { - id: null; - name: null; - population: null; - }; - city1: { - id: number; - }; - })[], + }[], typeof join > >; const join2 = await db.select(users) .fields({ - userId: users.id, - cityId: cities.id, + user: { + id: users.id, + }, + city: { + id: cities.id, + }, }) .fullJoin(cities, eq(users.id, cities.id)); Expect< Equal< - ({ - userId: number; - cityId: number; - } | { - userId: number; - cityId: null; - } | { - userId: null; - cityId: number; - })[], + { + user: { + id: number; + } | null; + city: { + id: number; + } | null; + }[], typeof join2 > >; const join3 = await db.select(users) .fields({ - userId: users.id, - cityId: cities.id, - classId: classes.id, + user: { + id: users.id, + }, + city: { + id: cities.id, + }, + class: { + id: classes.id, + }, }) .fullJoin(cities, eq(users.id, cities.id)) .rightJoin(classes, eq(users.id, classes.id)); Expect< Equal< - ({ - userId: number; - cityId: number; - classId: number; - } | { - userId: number; - cityId: null; - classId: number; - } | { - userId: null; - cityId: number; - classId: number; - } | { - userId: null; - cityId: null; - classId: number; - })[], + { + user: { + id: number; + } | null; + city: { + id: number; + } | null; + class: { + id: number; + }; + }[], typeof join3 > >; @@ -426,6 +385,12 @@ const friend = alias(users, 'friend'); const currentCity = alias(cities, 'currentCity'); const subscriber = alias(users, 'subscriber'); const closestCity = alias(cities, 'closestCity'); +const closestCity2 = alias(cities, 'closestCity2'); +const closestCity3 = alias(cities, 'closestCity3'); +const closestCity4 = alias(cities, 'closestCity4'); +const closestCity5 = alias(cities, 'closestCity5'); +const closestCity6 = alias(cities, 'closestCity6'); +const closestCity7 = alias(cities, 'closestCity7'); const megaJoin = await db.select(users) .fields({ @@ -433,7 +398,9 @@ const megaJoin = await db.select(users) id: users.id, maxAge: sql`max(${users.age1})`, }, - cityId: cities.id, + city: { + id: cities.id, + }, homeCity, c, otherClass, @@ -463,7 +430,9 @@ Expect< id: number; maxAge: unknown; }; - cityId: number; + city: { + id: number; + }; homeCity: { id: number; name: string; @@ -527,12 +496,205 @@ Expect< > >; +const megaLeftJoin = await db.select(users) + .fields({ + user: { + id: users.id, + maxAge: sql`max(${users.age1})`, + }, + city: { + id: cities.id, + }, + homeCity, + c, + otherClass, + anotherClass, + friend, + currentCity, + subscriber, + closestCity, + closestCity2, + closestCity3, + closestCity4, + closestCity5, + closestCity6, + closestCity7, + }) + .leftJoin(cities, sql`${users.id} = ${cities.id}`) + .leftJoin(homeCity, sql`${users.homeCity} = ${homeCity.id}`) + .leftJoin(c, eq(c.id, users.class)) + .leftJoin(otherClass, sql`${c.id} = ${otherClass.id}`) + .leftJoin(anotherClass, sql`${users.class} = ${anotherClass.id}`) + .leftJoin(friend, sql`${users.id} = ${friend.id}`) + .leftJoin(currentCity, sql`${homeCity.id} = ${currentCity.id}`) + .leftJoin(subscriber, sql`${users.class} = ${subscriber.id}`) + .leftJoin(closestCity, sql`${users.currentCity} = ${closestCity.id}`) + .leftJoin(closestCity2, sql`${users.currentCity} = ${closestCity.id}`) + .leftJoin(closestCity3, sql`${users.currentCity} = ${closestCity.id}`) + .leftJoin(closestCity4, sql`${users.currentCity} = ${closestCity.id}`) + .leftJoin(closestCity5, sql`${users.currentCity} = ${closestCity.id}`) + .leftJoin(closestCity6, sql`${users.currentCity} = ${closestCity.id}`) + .leftJoin(closestCity7, sql`${users.currentCity} = ${closestCity.id}`) + .where(and(sql`${users.age1} > 0`, eq(cities.id, 1))) + .limit(1) + .offset(1); + +Expect< + Equal< + { + user: { + id: number; + maxAge: unknown; + }; + city: { + id: number; + } | null; + homeCity: { + id: number; + name: string; + population: number | null; + } | null; + c: { + id: number; + class: 'A' | 'C' | null; + subClass: 'B' | 'D'; + } | null; + otherClass: { + id: number; + class: 'A' | 'C' | null; + subClass: 'B' | 'D'; + } | null; + anotherClass: { + id: number; + class: 'A' | 'C' | null; + subClass: 'B' | 'D'; + } | null; + friend: { + id: number; + uuid: string; + homeCity: number; + currentCity: number | null; + serialNullable: number; + serialNotNull: number; + class: 'A' | 'C'; + subClass: 'B' | 'D' | null; + text: string | null; + age1: number; + createdAt: Date; + enumCol: 'a' | 'b' | 'c'; + } | null; + currentCity: { + id: number; + name: string; + population: number | null; + } | null; + subscriber: { + id: number; + uuid: string; + homeCity: number; + currentCity: number | null; + serialNullable: number; + serialNotNull: number; + class: 'A' | 'C'; + subClass: 'B' | 'D' | null; + text: string | null; + age1: number; + createdAt: Date; + enumCol: 'a' | 'b' | 'c'; + } | null; + closestCity: { + id: number; + name: string; + population: number | null; + } | null; + closestCity2: { + id: number; + name: string; + population: number | null; + } | null; + closestCity3: { + id: number; + name: string; + population: number | null; + } | null; + closestCity4: { + id: number; + name: string; + population: number | null; + } | null; + closestCity5: { + id: number; + name: string; + population: number | null; + } | null; + closestCity6: { + id: number; + name: string; + population: number | null; + } | null; + closestCity7: { + id: number; + name: string; + population: number | null; + } | null; + }[], + typeof megaLeftJoin + > +>; + +const megaFullJoin = await db.select(users) + .fields({ + user: { + id: users.id, + maxAge: sql`max(${users.age1})`, + }, + city: { + id: cities.id, + }, + homeCity, + c, + otherClass, + anotherClass, + friend, + currentCity, + subscriber, + closestCity, + closestCity2, + closestCity3, + closestCity4, + closestCity5, + closestCity6, + closestCity7, + }) + .fullJoin(cities, sql`${users.id} = ${cities.id}`) + .fullJoin(homeCity, sql`${users.homeCity} = ${homeCity.id}`) + .fullJoin(c, eq(c.id, users.class)) + .fullJoin(otherClass, sql`${c.id} = ${otherClass.id}`) + .fullJoin(anotherClass, sql`${users.class} = ${anotherClass.id}`) + .fullJoin(friend, sql`${users.id} = ${friend.id}`) + .fullJoin(currentCity, sql`${homeCity.id} = ${currentCity.id}`) + .fullJoin(subscriber, sql`${users.class} = ${subscriber.id}`) + .fullJoin(closestCity, sql`${users.currentCity} = ${closestCity.id}`) + .fullJoin(closestCity2, sql`${users.currentCity} = ${closestCity.id}`) + .fullJoin(closestCity3, sql`${users.currentCity} = ${closestCity.id}`) + .fullJoin(closestCity4, sql`${users.currentCity} = ${closestCity.id}`) + .fullJoin(closestCity5, sql`${users.currentCity} = ${closestCity.id}`) + .fullJoin(closestCity6, sql`${users.currentCity} = ${closestCity.id}`) + .fullJoin(closestCity7, sql`${users.currentCity} = ${closestCity.id}`) + .where(and(sql`${users.age1} > 0`, eq(cities.id, 1))) + .limit(1) + .offset(1); + const friends = alias(users, 'friends'); const join4 = await db.select(users) .fields({ - userId: users.id, - cityId: cities.id, + user: { + id: users.id, + }, + city: { + id: cities.id, + }, class: classes, friend: friends, }) @@ -543,8 +705,12 @@ const join4 = await db.select(users) Expect< Equal<{ - userId: number; - cityId: number; + user: { + id: number; + }; + city: { + id: number; + }; class: { id: number; class: 'A' | 'C' | null; diff --git a/drizzle-orm/tests/sqlite/dan/select.ts b/drizzle-orm/tests/sqlite/dan/select.ts index 060a6b45c..be913a399 100644 --- a/drizzle-orm/tests/sqlite/dan/select.ts +++ b/drizzle-orm/tests/sqlite/dan/select.ts @@ -83,37 +83,33 @@ const joinPartial = db.select(users) .all(); Expect< - Equal<({ - user: { - id: number; - age: number; - name: string | null; - }; - city: { - id: number; - name: string; - }; - } | { - user: { - id: number; - age: number; - name: string | null; - }; - city: { - id: null; - name: null; - }; - } | { - user: { - id: null; - age: null; - name: null; - }; - city: { - id: number; - name: string; - }; - })[], typeof joinPartial> + Equal< + ({ + user: { + id: number; + name: string | null; + age: number; + }; + city: { + id: number; + name: string; + }; + } | { + user: { + id: number; + name: string | null; + age: number; + }; + city: null; + } | { + user: null; + city: { + id: number; + name: string; + }; + })[], + typeof joinPartial + > >; const join3 = db.select(users) @@ -348,7 +344,9 @@ const friends = alias(users, 'friends'); const join4 = db.select(users) .fields({ - userId: users.id, + user: { + id: users.id, + }, users123: { id: users.id, }, @@ -365,32 +363,37 @@ const join4 = db.select(users) .where(sql`${users.age1} > 0`).all(); Expect< - Equal<{ - userId: number; - users123: { - id: number; - }; - city: { - name: string; - population: number | null; - }; - class: { - id: number; - class: 'A' | 'C' | null; - subClass: 'B' | 'D'; - }; - friend: { - id: number; - homeCity: number; - currentCity: number | null; - serialNullable: number | null; - serialNotNull: number; - class: 'A' | 'C'; - subClass: 'B' | 'D' | null; - name: string | null; - age1: number; - createdAt: Date; - enumCol: 'a' | 'b' | 'c'; - }; - }[], typeof join4> + Equal< + { + user: { + id: number; + }; + users123: { + id: number; + }; + city: { + name: string; + population: number | null; + }; + class: { + id: number; + class: 'A' | 'C' | null; + subClass: 'B' | 'D'; + }; + friend: { + id: number; + name: string | null; + homeCity: number; + currentCity: number | null; + serialNullable: number | null; + serialNotNull: number; + class: 'A' | 'C'; + subClass: 'B' | 'D' | null; + age1: number; + createdAt: Date; + enumCol: 'a' | 'b' | 'c'; + }; + }[], + typeof join4 + > >; diff --git a/integration-tests/tests/better-sqlite.test.ts b/integration-tests/tests/better-sqlite.test.ts index 237c104e5..17e7fc8a0 100644 --- a/integration-tests/tests/better-sqlite.test.ts +++ b/integration-tests/tests/better-sqlite.test.ts @@ -15,6 +15,17 @@ const usersTable = sqliteTable('users', { createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(), }); +const users2Table = sqliteTable('users2', { + id: integer('id').primaryKey(), + name: text('name').notNull(), + cityId: integer('city_id').references(() => citiesTable.id), +}); + +const citiesTable = sqliteTable('cities', { + id: integer('id').primaryKey(), + name: text('name').notNull(), +}); + const usersMigratorTable = sqliteTable('users12', { id: integer('id').primaryKey(), name: text('name').notNull(), @@ -560,6 +571,103 @@ test.serial('insert via db.get w/ query builder', (t) => { t.deepEqual(inserted, { id: 1, name: 'John' }); }); +test.serial('left join (flat object fields)', (t) => { + const { db } = t.context; + + const { id: cityId } = db.insert(citiesTable) + .values({ name: 'Paris' }, { name: 'London' }) + .returning({ id: citiesTable.id }).all()[0]!; + + db.insert(users2Table).values({ name: 'John', cityId }, { name: 'Jane' }); + + const res = db.select(users2Table) + .fields({ + userId: users2Table.id, + userName: users2Table.name, + cityId: citiesTable.id, + cityName: citiesTable.name, + }) + .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)); + + t.deepEqual(res, [ + { userId: 1, userName: 'John', cityId, cityName: 'Paris' }, + { userId: 2, userName: 'Jane', cityId: null, cityName: null }, + ]); +}); + +test.serial('left join (grouped fields)', (t) => { + const { db } = t.context; + + const { id: cityId } = db.insert(citiesTable) + .values({ name: 'Paris' }, { name: 'London' }) + .returning({ id: citiesTable.id }).all()[0]!; + + db.insert(users2Table).values({ name: 'John', cityId }, { name: 'Jane' }); + + const res = db.select(users2Table) + .fields({ + id: users2Table.id, + user: { + name: users2Table.name, + nameUpper: sql`upper(${users2Table.name})`.as(), + }, + city: { + id: citiesTable.id, + name: citiesTable.name, + nameUpper: sql`upper(${citiesTable.name})`.as(), + }, + }) + .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)); + + t.deepEqual(res, [ + { + id: 1, + user: { name: 'John', nameUpper: 'JOHN' }, + city: { id: cityId, name: 'Paris', nameUpper: 'PARIS' }, + }, + { + id: 2, + user: { name: 'Jane', nameUpper: 'JANE' }, + city: null, + }, + ]); +}); + +test.serial('left join (all fields)', (t) => { + const { db } = t.context; + + const { id: cityId } = db.insert(citiesTable) + .values({ name: 'Paris' }, { name: 'London' }) + .returning({ id: citiesTable.id }).all()[0]!; + + db.insert(users2Table).values({ name: 'John', cityId }, { name: 'Jane' }); + + const res = db.select(users2Table) + .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)); + + t.deepEqual(res, [ + { + users2: { + id: 1, + name: 'John', + cityId, + }, + cities: { + id: cityId, + name: 'Paris', + }, + }, + { + users2: { + id: 2, + name: 'Jane', + cityId: null, + }, + cities: null, + }, + ]); +}); + test.after.always((t) => { const ctx = t.context; ctx.client?.close(); diff --git a/integration-tests/tests/mysql.test.ts b/integration-tests/tests/mysql.test.ts index 2a1093364..f25ead156 100644 --- a/integration-tests/tests/mysql.test.ts +++ b/integration-tests/tests/mysql.test.ts @@ -7,6 +7,7 @@ import { boolean, date, datetime, + int, json, MySqlDatabase, mysqlEnum, @@ -33,6 +34,17 @@ const usersTable = mysqlTable('userstest', { 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 datesTable = mysqlTable('datestable', { date: date('date'), dateAsString: date('date_as_string', { mode: 'string' }), @@ -666,6 +678,100 @@ test.serial('Mysql enum test case #1', async (t) => { ]); }); +test.serial('left join (flat object fields)', async (t) => { + const { db } = t.context; + + 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(users2Table) + .fields({ + userId: users2Table.id, + userName: users2Table.name, + cityId: citiesTable.id, + cityName: citiesTable.name, + }) + .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)); + + t.deepEqual(res, [ + { userId: 1, userName: 'John', cityId: 1, cityName: 'Paris' }, + { userId: 2, userName: 'Jane', cityId: null, cityName: null }, + ]); +}); + +test.serial('left join (grouped fields)', async (t) => { + const { db } = t.context; + + 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(users2Table) + .fields({ + id: users2Table.id, + user: { + name: users2Table.name, + nameUpper: sql`upper(${users2Table.name})`.as(), + }, + city: { + id: citiesTable.id, + name: citiesTable.name, + nameUpper: sql`upper(${citiesTable.name})`.as(), + }, + }) + .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)); + + t.deepEqual(res, [ + { + id: 1, + user: { name: 'John', nameUpper: 'JOHN' }, + city: { id: 1, name: 'Paris', nameUpper: 'PARIS' }, + }, + { + id: 2, + user: { name: 'Jane', nameUpper: 'JANE' }, + city: null, + }, + ]); +}); + +test.serial('left join (all fields)', async (t) => { + const { db } = t.context; + + 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(users2Table) + .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)); + + t.deepEqual(res, [ + { + users2: { + id: 1, + name: 'John', + cityId: 1, + }, + cities: { + id: 1, + name: 'Paris', + }, + }, + { + users2: { + id: 2, + name: 'Jane', + cityId: null, + }, + cities: null, + }, + ]); +}); + test.after.always(async (t) => { const ctx = t.context; await ctx.client?.end().catch(console.error); diff --git a/integration-tests/tests/pg.test.ts b/integration-tests/tests/pg.test.ts index e970371c6..af42e12f8 100644 --- a/integration-tests/tests/pg.test.ts +++ b/integration-tests/tests/pg.test.ts @@ -4,7 +4,7 @@ import { sql } from 'drizzle-orm'; import { asc, eq } from 'drizzle-orm/expressions'; import { drizzle, NodePgDatabase } from 'drizzle-orm/node-postgres'; import { migrate } from 'drizzle-orm/node-postgres/migrator'; -import { alias, boolean, InferModel, jsonb, pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core'; +import { alias, boolean, InferModel, integer, jsonb, pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core'; import { name, placeholder } from 'drizzle-orm/sql'; import getPort from 'get-port'; import { Client } from 'pg'; @@ -18,6 +18,17 @@ const usersTable = pgTable('users', { createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), }); +const users2Table = pgTable('users2', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + cityId: integer('city_id').references(() => citiesTable.id), +}); + +const citiesTable = pgTable('cities', { + id: serial('id').primaryKey(), + name: text('name').notNull(), +}); + const usersMigratorTable = pgTable('users12', { id: serial('id').primaryKey(), name: text('name').notNull(), @@ -100,6 +111,19 @@ test.beforeEach(async (t) => { created_at timestamptz not null default now() )`, ); + await ctx.db.execute( + sql`create table cities ( + id serial primary key, + name text not null + )`, + ); + await ctx.db.execute( + sql`create table users2 ( + id serial primary key, + name text not null, + city_id integer references cities(id) + )`, + ); }); test.serial('select all fields', async (t) => { @@ -662,6 +686,103 @@ test.serial('insert with onConflict do nothing + target', async (t) => { t.deepEqual(res, [{ id: 1, name: 'John' }]); }); +test.serial('left join (flat object fields)', async (t) => { + const { db } = t.context; + + const { id: cityId } = await db.insert(citiesTable) + .values({ name: 'Paris' }, { name: 'London' }) + .returning({ id: citiesTable.id }).then((rows) => rows[0]!); + + await db.insert(users2Table).values({ name: 'John', cityId }, { name: 'Jane' }); + + const res = await db.select(users2Table) + .fields({ + userId: users2Table.id, + userName: users2Table.name, + cityId: citiesTable.id, + cityName: citiesTable.name, + }) + .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)); + + t.deepEqual(res, [ + { userId: 1, userName: 'John', cityId, cityName: 'Paris' }, + { userId: 2, userName: 'Jane', cityId: null, cityName: null }, + ]); +}); + +test.serial('left join (grouped fields)', async (t) => { + const { db } = t.context; + + const { id: cityId } = await db.insert(citiesTable) + .values({ name: 'Paris' }, { name: 'London' }) + .returning({ id: citiesTable.id }).then((rows) => rows[0]!); + + await db.insert(users2Table).values({ name: 'John', cityId }, { name: 'Jane' }); + + const res = await db.select(users2Table) + .fields({ + id: users2Table.id, + user: { + name: users2Table.name, + nameUpper: sql`upper(${users2Table.name})`.as(), + }, + city: { + id: citiesTable.id, + name: citiesTable.name, + nameUpper: sql`upper(${citiesTable.name})`.as(), + }, + }) + .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)); + + t.deepEqual(res, [ + { + id: 1, + user: { name: 'John', nameUpper: 'JOHN' }, + city: { id: cityId, name: 'Paris', nameUpper: 'PARIS' }, + }, + { + id: 2, + user: { name: 'Jane', nameUpper: 'JANE' }, + city: null, + }, + ]); +}); + +test.serial('left join (all fields)', async (t) => { + const { db } = t.context; + + const { id: cityId } = await db.insert(citiesTable) + .values({ name: 'Paris' }, { name: 'London' }) + .returning({ id: citiesTable.id }).then((rows) => rows[0]!); + + await db.insert(users2Table).values({ name: 'John', cityId }, { name: 'Jane' }); + + const res = await db.select(users2Table) + .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)); + + t.deepEqual(res, [ + { + users2: { + id: 1, + name: 'John', + cityId, + }, + cities: { + id: cityId, + name: 'Paris', + }, + }, + { + users2: { + id: 2, + name: 'Jane', + cityId: null, + }, + cities: null, + }, + ]); +}); + test.after.always(async (t) => { const ctx = t.context; await ctx.client?.end().catch(console.error); diff --git a/integration-tests/tests/postgres.js.test.ts b/integration-tests/tests/postgres.js.test.ts index 5000c0b7a..466b6c6fc 100644 --- a/integration-tests/tests/postgres.js.test.ts +++ b/integration-tests/tests/postgres.js.test.ts @@ -2,9 +2,9 @@ import anyTest, { TestFn } from 'ava'; import Docker from 'dockerode'; import { sql } from 'drizzle-orm'; import { asc, eq } from 'drizzle-orm/expressions'; -import { alias, boolean, InferModel, jsonb, pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core'; -import { drizzle, PostgresJsDatabase } from 'drizzle-orm/postgres.js'; -import { migrate } from 'drizzle-orm/postgres.js/migrator'; +import { alias, boolean, InferModel, integer, jsonb, pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core'; +import { drizzle, PostgresJsDatabase } from 'drizzle-orm/postgres-js'; +import { migrate } from 'drizzle-orm/postgres-js/migrator'; import { name, placeholder } from 'drizzle-orm/sql'; import getPort from 'get-port'; import postgres, { Sql } from 'postgres'; @@ -18,6 +18,17 @@ const usersTable = pgTable('users', { createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), }); +const users2Table = pgTable('users2', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + cityId: integer('city_id').references(() => citiesTable.id), +}); + +const citiesTable = pgTable('cities', { + id: serial('id').primaryKey(), + name: text('name').notNull(), +}); + const usersMigratorTable = pgTable('users12', { id: serial('id').primaryKey(), name: text('name').notNull(), @@ -665,6 +676,103 @@ test.serial('insert with onConflict do nothing + target', async (t) => { t.deepEqual(res, [{ id: 1, name: 'John' }]); }); +test.serial('left join (flat object fields)', async (t) => { + const { db } = t.context; + + const { id: cityId } = await db.insert(citiesTable) + .values({ name: 'Paris' }, { name: 'London' }) + .returning({ id: citiesTable.id }).then((rows) => rows[0]!); + + await db.insert(users2Table).values({ name: 'John', cityId }, { name: 'Jane' }); + + const res = await db.select(users2Table) + .fields({ + userId: users2Table.id, + userName: users2Table.name, + cityId: citiesTable.id, + cityName: citiesTable.name, + }) + .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)); + + t.deepEqual(res, [ + { userId: 1, userName: 'John', cityId, cityName: 'Paris' }, + { userId: 2, userName: 'Jane', cityId: null, cityName: null }, + ]); +}); + +test.serial('left join (grouped fields)', async (t) => { + const { db } = t.context; + + const { id: cityId } = await db.insert(citiesTable) + .values({ name: 'Paris' }, { name: 'London' }) + .returning({ id: citiesTable.id }).then((rows) => rows[0]!); + + await db.insert(users2Table).values({ name: 'John', cityId }, { name: 'Jane' }); + + const res = await db.select(users2Table) + .fields({ + id: users2Table.id, + user: { + name: users2Table.name, + nameUpper: sql`upper(${users2Table.name})`.as(), + }, + city: { + id: citiesTable.id, + name: citiesTable.name, + nameUpper: sql`upper(${citiesTable.name})`.as(), + }, + }) + .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)); + + t.deepEqual(res, [ + { + id: 1, + user: { name: 'John', nameUpper: 'JOHN' }, + city: { id: cityId, name: 'Paris', nameUpper: 'PARIS' }, + }, + { + id: 2, + user: { name: 'Jane', nameUpper: 'JANE' }, + city: null, + }, + ]); +}); + +test.serial('left join (all fields)', async (t) => { + const { db } = t.context; + + const { id: cityId } = await db.insert(citiesTable) + .values({ name: 'Paris' }, { name: 'London' }) + .returning({ id: citiesTable.id }).then((rows) => rows[0]!); + + await db.insert(users2Table).values({ name: 'John', cityId }, { name: 'Jane' }); + + const res = await db.select(users2Table) + .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)); + + t.deepEqual(res, [ + { + users2: { + id: 1, + name: 'John', + cityId, + }, + cities: { + id: cityId, + name: 'Paris', + }, + }, + { + users2: { + id: 2, + name: 'Jane', + cityId: null, + }, + cities: null, + }, + ]); +}); + test.after.always(async (t) => { const ctx = t.context; await ctx.client?.end().catch(console.error); diff --git a/integration-tests/tests/sql.js.test.ts b/integration-tests/tests/sql.js.test.ts index 74b7d66b4..8596d3542 100644 --- a/integration-tests/tests/sql.js.test.ts +++ b/integration-tests/tests/sql.js.test.ts @@ -2,8 +2,8 @@ import anyTest, { TestFn } from 'ava'; import { sql } from 'drizzle-orm'; import { asc, eq } from 'drizzle-orm/expressions'; import { name, placeholder } from 'drizzle-orm/sql'; -import { drizzle, SQLJsDatabase } from 'drizzle-orm/sql.js'; -import { migrate } from 'drizzle-orm/sql.js/migrator'; +import { drizzle, SQLJsDatabase } from 'drizzle-orm/sql-js'; +import { migrate } from 'drizzle-orm/sql-js/migrator'; import { alias, blob, InferModel, integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core'; import initSqlJs, { Database } from 'sql.js'; @@ -15,6 +15,17 @@ const usersTable = sqliteTable('users', { createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(), }); +const users2Table = sqliteTable('users2', { + id: integer('id').primaryKey(), + name: text('name').notNull(), + cityId: integer('city_id').references(() => citiesTable.id), +}); + +const citiesTable = sqliteTable('cities', { + id: integer('id').primaryKey(), + name: text('name').notNull(), +}); + const usersMigratorTable = sqliteTable('users12', { id: integer('id').primaryKey(), name: text('name').notNull(), @@ -560,6 +571,103 @@ test.serial('insert via db.get w/ query builder', (t) => { t.deepEqual(inserted, { id: 1, name: 'John' }); }); +test.serial('left join (flat object fields)', (t) => { + const { db } = t.context; + + const { id: cityId } = db.insert(citiesTable) + .values({ name: 'Paris' }, { name: 'London' }) + .returning({ id: citiesTable.id }).all()[0]!; + + db.insert(users2Table).values({ name: 'John', cityId }, { name: 'Jane' }); + + const res = db.select(users2Table) + .fields({ + userId: users2Table.id, + userName: users2Table.name, + cityId: citiesTable.id, + cityName: citiesTable.name, + }) + .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)); + + t.deepEqual(res, [ + { userId: 1, userName: 'John', cityId, cityName: 'Paris' }, + { userId: 2, userName: 'Jane', cityId: null, cityName: null }, + ]); +}); + +test.serial('left join (grouped fields)', (t) => { + const { db } = t.context; + + const { id: cityId } = db.insert(citiesTable) + .values({ name: 'Paris' }, { name: 'London' }) + .returning({ id: citiesTable.id }).all()[0]!; + + db.insert(users2Table).values({ name: 'John', cityId }, { name: 'Jane' }); + + const res = db.select(users2Table) + .fields({ + id: users2Table.id, + user: { + name: users2Table.name, + nameUpper: sql`upper(${users2Table.name})`.as(), + }, + city: { + id: citiesTable.id, + name: citiesTable.name, + nameUpper: sql`upper(${citiesTable.name})`.as(), + }, + }) + .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)); + + t.deepEqual(res, [ + { + id: 1, + user: { name: 'John', nameUpper: 'JOHN' }, + city: { id: cityId, name: 'Paris', nameUpper: 'PARIS' }, + }, + { + id: 2, + user: { name: 'Jane', nameUpper: 'JANE' }, + city: null, + }, + ]); +}); + +test.serial('left join (all fields)', (t) => { + const { db } = t.context; + + const { id: cityId } = db.insert(citiesTable) + .values({ name: 'Paris' }, { name: 'London' }) + .returning({ id: citiesTable.id }).all()[0]!; + + db.insert(users2Table).values({ name: 'John', cityId }, { name: 'Jane' }); + + const res = db.select(users2Table) + .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)); + + t.deepEqual(res, [ + { + users2: { + id: 1, + name: 'John', + cityId, + }, + cities: { + id: cityId, + name: 'Paris', + }, + }, + { + users2: { + id: 2, + name: 'Jane', + cityId: null, + }, + cities: null, + }, + ]); +}); + test.after.always((t) => { const ctx = t.context; ctx.client?.close(); diff --git a/package.json b/package.json index bf04e78df..40d4a7644 100755 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "dprint": "^0.32.2", "prettier": "^2.7.1", "resolve-tspaths": "^0.8.3", - "turbo": "^1.6.3", + "turbo": "^1.7.2", "typescript": "4.8.4" }, "pnpm": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39438a9a3..e729883c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,7 +15,7 @@ importers: dprint: ^0.32.2 prettier: ^2.7.1 resolve-tspaths: ^0.8.3 - turbo: ^1.6.3 + turbo: ^1.7.2 typescript: 4.8.4 devDependencies: '@trivago/prettier-plugin-sort-imports': 3.4.0_prettier@2.8.3 @@ -24,7 +24,7 @@ importers: dprint: 0.32.2 prettier: 2.8.3 resolve-tspaths: 0.8.3_typescript@4.8.4 - turbo: 1.7.0 + turbo: 1.7.2 typescript: 4.8.4_cooyevkj4js3zgkcwfb6v3qsqu drizzle-orm: @@ -3825,65 +3825,65 @@ packages: dependencies: safe-buffer: 5.2.1 - /turbo-darwin-64/1.7.0: - resolution: {integrity: sha512-hSGAueSf5Ko8J67mpqjpt9FsP6ePn1nMcl7IVPoJq5dHsgX3anCP/BPlexJ502bNK+87DDyhQhJ/LPSJXKrSYQ==} + /turbo-darwin-64/1.7.2: + resolution: {integrity: sha512-Sml3WR8MSu80W+gS8SnoKNImcDOlIX7zlvezzds65mW11yGniIFfZ18aKWGOm92Nj2SvXCQ2+UmyGghbFaHNmQ==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-darwin-arm64/1.7.0: - resolution: {integrity: sha512-BLLOW5W6VZxk5+0ZOj5AO1qjM0P5isIgjbEuyAl8lHZ4s9antUbY4CtFrspT32XxPTYoDl4UjviPMcSsbcl3WQ==} + /turbo-darwin-arm64/1.7.2: + resolution: {integrity: sha512-JnlgGLScboUJGJxvmSsF+5xkImEDTMPg2FHzX4n8AMB9az9ZlPQAMtc+xu4p6Xp9eaykKiV2RG81YS3H0fxDLA==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-linux-64/1.7.0: - resolution: {integrity: sha512-aw2qxmfZa+kT87SB3GNUoFimqEPzTlzlRqhPgHuAAT6Uf0JHnmebPt4K+ZPtDNl5yfVmtB05bhHPqw+5QV97Yg==} + /turbo-linux-64/1.7.2: + resolution: {integrity: sha512-vbLJw6ovG+lpiPqxniscBjljKJ2jbsHuKp8uK4j/wqgp68wAVKeAZW77GGDAUgDb88XH6Kvhh2hcizL+iWduww==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-linux-arm64/1.7.0: - resolution: {integrity: sha512-AJEx2jX+zO5fQtJpO3r6uhTabj4oSA5ZhB7zTs/rwu/XqoydsvStA4X8NDW4poTbOjF7DcSHizqwi04tSMzpJw==} + /turbo-linux-arm64/1.7.2: + resolution: {integrity: sha512-zLnuS8WdHonKL74KqOopOH/leBOWumlVGF8/8hldbDPq0mwY+6myRR5/5LdveB51rkG4UJh/sQ94xV67tjBoyw==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-windows-64/1.7.0: - resolution: {integrity: sha512-ewj7PPv2uxqv0r31hgnBa3E5qwUu7eyVRP5M1gB/TJXfSHduU79gbxpKCyxIZv2fL/N2/3U7EPOQPSZxBAoljA==} + /turbo-windows-64/1.7.2: + resolution: {integrity: sha512-oE5PMoXjmR09okvVzteFb6FjA6yo+nMsacsgKH2yLNq4sOrVo9tG98JkRurOv5+L6ZQ3yGXPxWHiqeH7hLkAVQ==} cpu: [x64] os: [win32] requiresBuild: true dev: true optional: true - /turbo-windows-arm64/1.7.0: - resolution: {integrity: sha512-LzjOUzveWkvTD0jP8DBMYiAnYemmydsvqxdSmsUapHHJkl6wKZIOQNSO7pxsy+9XM/1/+0f9Y9F9ZNl5lePTEA==} + /turbo-windows-arm64/1.7.2: + resolution: {integrity: sha512-mdTUJk23acRv5qxA/yEstYhM1VFenVE3FDrssxGRFq7S80smtCGK1xUd4BEDDzDlVXOqBohmM5jRh9516rcjPQ==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /turbo/1.7.0: - resolution: {integrity: sha512-cwympNwQNnQZ/TffBd8yT0i0O10Cf/hlxccCYgUcwhcGEb9rDjE5thDbHoHw1hlJQUF/5ua7ERJe7Zr0lNE/ww==} + /turbo/1.7.2: + resolution: {integrity: sha512-YR/x3GZEx0C1RV6Yvuw/HB1Ixx3upM6ZTTa4WqKz9WtLWN8u2g+u2h5KpG5YtjCS3wl/8zVXgHf2WiMK6KIghg==} hasBin: true requiresBuild: true optionalDependencies: - turbo-darwin-64: 1.7.0 - turbo-darwin-arm64: 1.7.0 - turbo-linux-64: 1.7.0 - turbo-linux-arm64: 1.7.0 - turbo-windows-64: 1.7.0 - turbo-windows-arm64: 1.7.0 + turbo-darwin-64: 1.7.2 + turbo-darwin-arm64: 1.7.2 + turbo-linux-64: 1.7.2 + turbo-linux-arm64: 1.7.2 + turbo-windows-64: 1.7.2 + turbo-windows-arm64: 1.7.2 dev: true /tweetnacl/0.14.5: From 0217a1d03a540ecdcc2dad04d9b5534fd04dfbd7 Mon Sep 17 00:00:00 2001 From: Dan Kochetov Date: Fri, 3 Feb 2023 16:16:56 +0200 Subject: [PATCH 2/7] Bump orm version to 0.18.0 --- drizzle-orm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drizzle-orm/package.json b/drizzle-orm/package.json index 8979ae92f..d7b2d3687 100644 --- a/drizzle-orm/package.json +++ b/drizzle-orm/package.json @@ -1,6 +1,6 @@ { "name": "drizzle-orm", - "version": "0.17.5", + "version": "0.18.0", "description": "Drizzle ORM package for SQL databases", "scripts": { "build": "tsc && resolve-tspaths && cp ../README.md package.json dist/", From 80a243bfef8bedd1861ff3b9cdfadece128438d8 Mon Sep 17 00:00:00 2001 From: Dan Kochetov Date: Fri, 3 Feb 2023 16:18:50 +0200 Subject: [PATCH 3/7] Fix import paths --- drizzle-orm/src/aws-data-api/pg/session.ts | 2 +- drizzle-orm/src/planetscale-serverless/session.ts | 13 ++++--------- drizzle-orm/src/sqlite-proxy/session.ts | 7 +++++-- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/drizzle-orm/src/aws-data-api/pg/session.ts b/drizzle-orm/src/aws-data-api/pg/session.ts index 5a091db51..3554e38f5 100644 --- a/drizzle-orm/src/aws-data-api/pg/session.ts +++ b/drizzle-orm/src/aws-data-api/pg/session.ts @@ -10,8 +10,8 @@ import { RollbackTransactionCommandInput, } from '@aws-sdk/client-rds-data'; import { Logger } from '~/logger'; -import { SelectFieldsOrdered } from '~/operations'; import { PgDialect, PgSession, PreparedQuery, PreparedQueryConfig, QueryResultHKT } from '~/pg-core'; +import { SelectFieldsOrdered } from '~/pg-core/query-builders/select.types'; import { fillPlaceholders, Query, QueryTypingsValue, SQL } from '~/sql'; import { mapResultRow } from '~/utils'; import { getValueFromDataApi, toValueParam } from '../common'; diff --git a/drizzle-orm/src/planetscale-serverless/session.ts b/drizzle-orm/src/planetscale-serverless/session.ts index 0dfe64101..8f92032f1 100644 --- a/drizzle-orm/src/planetscale-serverless/session.ts +++ b/drizzle-orm/src/planetscale-serverless/session.ts @@ -1,13 +1,8 @@ import { connect, Connection, ExecutedQuery } from '@planetscale/database'; import { Logger, NoopLogger } from '~/logger'; import { MySqlDialect } from '~/mysql-core/dialect'; -import { SelectFieldsOrdered } from '~/mysql-core/operations'; -import { - MySqlSession, - PreparedQuery, - PreparedQueryConfig, - QueryResultHKT, -} from '~/mysql-core/session'; +import { SelectFieldsOrdered } from '~/mysql-core/query-builders/select.types'; +import { MySqlSession, PreparedQuery, PreparedQueryConfig, QueryResultHKT } from '~/mysql-core/session'; import { fillPlaceholders, Query } from '~/sql'; import { mapResultRow } from '~/utils'; @@ -88,9 +83,9 @@ export class PlanetscaleSession extends MySqlSession { async transaction(queries: { sql: string; params?: any[] }[]) { await this.client.transaction(async (tx) => { for (const query of queries) { - await tx.execute(query.sql, query.params) + await tx.execute(query.sql, query.params); } - }) + }); } async queryObjects( diff --git a/drizzle-orm/src/sqlite-proxy/session.ts b/drizzle-orm/src/sqlite-proxy/session.ts index 35164b50c..faf45803e 100644 --- a/drizzle-orm/src/sqlite-proxy/session.ts +++ b/drizzle-orm/src/sqlite-proxy/session.ts @@ -1,7 +1,7 @@ import { Logger, NoopLogger } from '~/logger'; import { fillPlaceholders, Query } from '~/sql'; import { SQLiteAsyncDialect } from '~/sqlite-core/dialect'; -import { SelectFieldsOrdered } from '~/sqlite-core/operations'; +import { SelectFieldsOrdered } from '~/sqlite-core/query-builders/select.types'; import { PreparedQuery as PreparedQueryBase, PreparedQueryConfig as PreparedQueryConfigBase, @@ -34,7 +34,10 @@ export class SQLiteRemoteSession extends SQLiteSession<'async', SqliteRemoteResu // return this.client(this.queryString, params).then(({ rows }) => rows!) } - prepareQuery>(query: Query, fields?: SelectFieldsOrdered): PreparedQuery { + prepareQuery>( + query: Query, + fields?: SelectFieldsOrdered, + ): PreparedQuery { return new PreparedQuery(this.client, query.sql, query.params, this.logger, fields); } } From fed46480d8ddf8b6745c9274b1fe2239531483fc Mon Sep 17 00:00:00 2001 From: Dan Kochetov Date: Sat, 4 Feb 2023 16:39:39 +0200 Subject: [PATCH 4/7] Propagate join changes to other dialects --- drizzle-orm/src/aws-data-api/pg/session.ts | 2 +- drizzle-orm/src/better-sqlite3/session.ts | 6 +- drizzle-orm/src/bun-sqlite/session.ts | 4 +- drizzle-orm/src/d1/session.ts | 4 +- drizzle-orm/src/mysql-core/dialect.ts | 18 ++- .../src/mysql-core/query-builders/insert.ts | 2 +- .../src/mysql-core/query-builders/select.ts | 10 +- .../mysql-core/query-builders/select.types.ts | 93 ++++------------ .../src/mysql-core/query-builders/update.ts | 11 +- drizzle-orm/src/mysql-core/session.ts | 3 + drizzle-orm/src/mysql-core/utils.ts | 15 --- drizzle-orm/src/mysql2/session.ts | 11 +- drizzle-orm/src/neon-serverless/session.ts | 4 +- drizzle-orm/src/node-postgres/session.ts | 8 +- drizzle-orm/src/pg-core/dialect.ts | 7 +- .../src/pg-core/query-builders/select.ts | 11 +- .../pg-core/query-builders/select.types.ts | 10 -- drizzle-orm/src/pg-core/session.ts | 6 +- .../src/planetscale-serverless/session.ts | 4 +- drizzle-orm/src/postgres-js/session.ts | 4 +- drizzle-orm/src/sql-js/session.ts | 6 +- drizzle-orm/src/sqlite-core/dialect.ts | 23 ++-- .../src/sqlite-core/query-builders/delete.ts | 10 +- .../src/sqlite-core/query-builders/insert.ts | 10 +- .../src/sqlite-core/query-builders/select.ts | 14 ++- .../query-builders/select.types.ts | 103 +++++------------- .../src/sqlite-core/query-builders/update.ts | 18 ++- drizzle-orm/src/sqlite-core/session.ts | 15 ++- drizzle-orm/src/sqlite-proxy/session.ts | 4 +- drizzle-orm/src/utils.ts | 2 +- drizzle-orm/tests/mysql/dan/select.ts | 60 ++-------- drizzle-orm/tests/sqlite/dan/select.ts | 41 ++----- integration-tests/package.json | 1 + integration-tests/tests/better-sqlite.test.ts | 31 ++++-- integration-tests/tests/mysql.test.ts | 18 ++- integration-tests/tests/postgres.js.test.ts | 13 +++ integration-tests/tests/sql.js.test.ts | 29 +++-- 37 files changed, 266 insertions(+), 365 deletions(-) diff --git a/drizzle-orm/src/aws-data-api/pg/session.ts b/drizzle-orm/src/aws-data-api/pg/session.ts index 3554e38f5..414fee901 100644 --- a/drizzle-orm/src/aws-data-api/pg/session.ts +++ b/drizzle-orm/src/aws-data-api/pg/session.ts @@ -61,7 +61,7 @@ export class AwsDataApiPreparedQuery extends Prep return result.records?.map((result) => { const mappedResult = result.map((res) => getValueFromDataApi(res)); - return mapResultRow(fields, mappedResult); + return mapResultRow(fields, mappedResult, this.joinsNotNullableMap); }); } diff --git a/drizzle-orm/src/better-sqlite3/session.ts b/drizzle-orm/src/better-sqlite3/session.ts index dbcb7ae40..5d5482677 100644 --- a/drizzle-orm/src/better-sqlite3/session.ts +++ b/drizzle-orm/src/better-sqlite3/session.ts @@ -34,7 +34,7 @@ export class BetterSQLiteSession extends SQLiteSession<'sync', RunResult> { prepareQuery>( query: Query, - fields?: SelectFieldsOrdered, + fields: SelectFieldsOrdered | undefined, ): PreparedQuery { const stmt = this.client.prepare(query.sql); return new PreparedQuery(stmt, query.sql, query.params, this.logger, fields); @@ -63,7 +63,7 @@ export class PreparedQuery all(placeholderValues?: Record): T['all'] { const { fields } = this; if (fields) { - return this.values(placeholderValues).map((row) => mapResultRow(fields, row)); + return this.values(placeholderValues).map((row) => mapResultRow(fields, row, this.joinsNotNullableMap)); } const params = fillPlaceholders(this.params, placeholderValues ?? {}); @@ -81,7 +81,7 @@ export class PreparedQuery return value; } - return mapResultRow(fields, value); + return mapResultRow(fields, value, this.joinsNotNullableMap); } values(placeholderValues?: Record): T['values'] { diff --git a/drizzle-orm/src/bun-sqlite/session.ts b/drizzle-orm/src/bun-sqlite/session.ts index 8f2f4d860..317f40e4e 100644 --- a/drizzle-orm/src/bun-sqlite/session.ts +++ b/drizzle-orm/src/bun-sqlite/session.ts @@ -66,7 +66,7 @@ export class PreparedQuery all(placeholderValues?: Record): T['all'] { const { fields } = this; if (fields) { - return this.values(placeholderValues).map((row) => mapResultRow(fields, row)); + return this.values(placeholderValues).map((row) => mapResultRow(fields, row, this.joinsNotNullableMap)); } const params = fillPlaceholders(this.params, placeholderValues ?? {}); @@ -84,7 +84,7 @@ export class PreparedQuery return value; } - return mapResultRow(fields, value); + return mapResultRow(fields, value, this.joinsNotNullableMap); } values(placeholderValues?: Record): T['values'] { diff --git a/drizzle-orm/src/d1/session.ts b/drizzle-orm/src/d1/session.ts index f9240613d..70ad37a02 100644 --- a/drizzle-orm/src/d1/session.ts +++ b/drizzle-orm/src/d1/session.ts @@ -62,7 +62,9 @@ export class PreparedQuery all(placeholderValues?: Record): Promise { const { fields } = this; if (fields) { - return this.values(placeholderValues).then((values) => values.map((row) => mapResultRow(fields, row))); + return this.values(placeholderValues).then((values) => + values.map((row) => mapResultRow(fields, row, this.joinsNotNullableMap)) + ); } const params = fillPlaceholders(this.params, placeholderValues ?? {}); diff --git a/drizzle-orm/src/mysql-core/dialect.ts b/drizzle-orm/src/mysql-core/dialect.ts index 60e99d6c3..6b1a622d1 100644 --- a/drizzle-orm/src/mysql-core/dialect.ts +++ b/drizzle-orm/src/mysql-core/dialect.ts @@ -1,13 +1,14 @@ import { AnyColumn, Column } from '~/column'; import { MigrationMeta } from '~/migrator'; import { Name, Query, SQL, sql, SQLResponse, SQLSourceParam } from '~/sql'; -import { Table } from '~/table'; +import { getTableName, Table } from '~/table'; +import { UpdateSet } from '~/utils'; import { AnyMySqlColumn, MySqlColumn } from './columns/common'; import { MySqlDatabase } from './db'; import { MySqlDeleteConfig } from './query-builders/delete'; import { MySqlInsertConfig } from './query-builders/insert'; import { MySqlSelectConfig, SelectFieldsOrdered } from './query-builders/select.types'; -import { MySqlUpdateConfig, MySqlUpdateSet } from './query-builders/update'; +import { MySqlUpdateConfig } from './query-builders/update'; import { MySqlSession } from './session'; import { AnyMySqlTable, MySqlTable } from './table'; @@ -73,7 +74,7 @@ export class MySqlDialect { return sql`delete from ${table}${whereSql}${returningSql}`; } - buildUpdateSet(table: AnyMySqlTable, set: MySqlUpdateSet): SQL { + buildUpdateSet(table: AnyMySqlTable, set: UpdateSet): SQL { const setEntries = Object.entries(set); const setSize = setEntries.length; @@ -161,6 +162,17 @@ export class MySqlDialect { } buildSelectQuery({ fields, where, table, joins, orderBy, groupBy, limit, offset }: MySqlSelectConfig): SQL { + fields.forEach((f) => { + let tableName: string; + if ( + f.field instanceof Column && f.field.table !== table && !((tableName = getTableName(f.field.table)) in joins) + ) { + throw new Error( + `Column "${f.path.join('.')}" was selected, but its table "${tableName}" was not joined`, + ); + } + }); + const joinKeys = Object.keys(joins); const selection = this.buildSelection(fields, { isSingleTable: joinKeys.length === 0 }); diff --git a/drizzle-orm/src/mysql-core/query-builders/insert.ts b/drizzle-orm/src/mysql-core/query-builders/insert.ts index 909a0dacf..f8f9f3260 100644 --- a/drizzle-orm/src/mysql-core/query-builders/insert.ts +++ b/drizzle-orm/src/mysql-core/query-builders/insert.ts @@ -7,10 +7,10 @@ import { QueryResultKind, } from '~/mysql-core/session'; import { AnyMySqlTable, InferModel } from '~/mysql-core/table'; -import { mapUpdateSet } from '~/mysql-core/utils'; import { QueryPromise } from '~/query-promise'; import { Param, Placeholder, Query, SQL, sql, SQLWrapper } from '~/sql'; import { Table } from '~/table'; +import { mapUpdateSet } from '~/utils'; import { SelectFieldsOrdered } from './select.types'; import { MySqlUpdateSetSource } from './update'; export interface MySqlInsertConfig { diff --git a/drizzle-orm/src/mysql-core/query-builders/select.ts b/drizzle-orm/src/mysql-core/query-builders/select.ts index 6db8e3e7f..7b5c55e7c 100644 --- a/drizzle-orm/src/mysql-core/query-builders/select.ts +++ b/drizzle-orm/src/mysql-core/query-builders/select.ts @@ -148,8 +148,8 @@ export class MySqlSelect< } toSQL(): Omit { - const { typings, ...rest} = this.dialect.sqlToQuery(this.getSQL()); - return rest + const { typings, ...rest } = this.dialect.sqlToQuery(this.getSQL()); + return rest; } private _prepare(name?: string): PreparedQuery< @@ -157,7 +157,11 @@ export class MySqlSelect< execute: SelectResult[]; } > { - return this.session.prepareQuery(this.dialect.sqlToQuery(this.getSQL()), this.config.fields, name); + const query = this.session.prepareQuery< + PreparedQueryConfig & { execute: SelectResult[] } + >(this.dialect.sqlToQuery(this.getSQL()), this.config.fields, name); + query.joinsNotNullableMap = this.joinsNotNullable; + return query; } prepare(name: string): PreparedQuery< diff --git a/drizzle-orm/src/mysql-core/query-builders/select.types.ts b/drizzle-orm/src/mysql-core/query-builders/select.types.ts index 18bc90ec5..16e9b4e65 100644 --- a/drizzle-orm/src/mysql-core/query-builders/select.types.ts +++ b/drizzle-orm/src/mysql-core/query-builders/select.types.ts @@ -31,11 +31,6 @@ export type ApplyNullability = TNullabi : TNullability extends 'null' ? null : T; -export type ApplyNullabilityNested = T extends Record ? { - [Key in keyof T]: ApplyNullabilityNested; - } - : ApplyNullability; - export type ApplyNotNullMapToJoins> = { [TTableName in keyof TResult & keyof TNullabilityMap & string]: ApplyNullability< TResult[TTableName], @@ -47,40 +42,36 @@ export type SelectResult< TResult, TSelectMode extends SelectMode, TJoinsNotNullable extends Record, -> = TSelectMode extends 'partial' - ? RemoveDuplicates>> +> = TSelectMode extends 'partial' ? SelectPartialResult : TSelectMode extends 'single' ? TResult - : RemoveDuplicates>>; + : Simplify>; type IsUnion = (T extends any ? (U extends T ? false : true) : never) extends false ? false : true; type Not = T extends true ? false : true; -type SelectPartialResult< - TFields, - TNullability extends Record, - TIsSimpleFields extends boolean, -> = TNullability extends TNullability ? { +type SelectPartialResult> = TNullability extends + TNullability ? { [Key in keyof TFields]: TFields[Key] extends infer TField - ? TField extends AnyMySqlTable - ? TIsSimpleFields extends true ? GetTableConfig extends keyof TNullability ? ApplyNullability< - SelectResultFields>, - TNullability[GetTableConfig] - > - : never - : SelectPartialResult, TNullability, TIsSimpleFields> + ? TField extends AnyMySqlTable ? GetTableConfig extends keyof TNullability ? ApplyNullability< + SelectResultFields>, + TNullability[GetTableConfig] + > + : never : TField extends AnyMySqlColumn ? GetColumnConfig extends infer TTableName extends keyof TNullability ? ApplyNullability, TNullability[TTableName]> : never : TField extends SQL | SQLResponse ? SelectResultField : TField extends Record - ? [TIsSimpleFields, TField[keyof TField]] extends - [true, AnyMySqlColumn<{ tableName: infer TTableName extends string }>] - ? ApplyNullability, TNullability[TTableName]> - : SelectPartialResult - : SelectResultField + ? TField[keyof TField] extends + AnyMySqlColumn<{ tableName: infer TTableName extends string }> | SQL | SQLResponse + ? Not> extends true + ? ApplyNullability, TNullability[TTableName]> + : SelectPartialResult + : never + : never : never; } : never; @@ -115,55 +106,16 @@ type SetJoinsNullability [Key in keyof TNullabilityMap]: TValue; }; -// https://stackoverflow.com/a/70061272/9929789 -type UnionToParm = U extends any ? (k: U) => void : never; -type UnionToSect = UnionToParm extends ((k: infer I) => void) ? I : never; -type ExtractParm = F extends { (a: infer A): void } ? A : never; -type SpliceOne = Exclude>; -type ExtractOne = ExtractParm>>; -type ToTupleRec = SpliceOne extends never ? [ExtractOne, ...Result] - : ToTupleRec, [ExtractOne, ...Result]>; -export type RemoveDuplicates = ToTupleRec extends any[] ? ToTupleRec[number] : never; - export type AppendToJoinsNotNull< TJoinsNotNull extends Record, TJoinedName extends string, TJoinType extends JoinType, - TIsSimpleFields extends boolean, -> = 'left' extends TJoinType ? TIsSimpleFields extends true ? TJoinsNotNull & { [name in TJoinedName]: 'nullable' } - : TJoinsNotNull & { [name in TJoinedName]: 'not-null' } | TJoinsNotNull & { [name in TJoinedName]: 'null' } - : 'right' extends TJoinType - ? [TIsSimpleFields, Not>] extends [true, true] - ? SetJoinsNullability & { [name in TJoinedName]: 'not-null' } - : - | TJoinsNotNull & { [name in TJoinedName]: 'not-null' } - | SetJoinsNullability & { [name in TJoinedName]: 'not-null' } +> = 'left' extends TJoinType ? TJoinsNotNull & { [name in TJoinedName]: 'nullable' } + : 'right' extends TJoinType ? SetJoinsNullability & { [name in TJoinedName]: 'not-null' } : 'inner' extends TJoinType ? TJoinsNotNull & { [name in TJoinedName]: 'not-null' } - : 'full' extends TJoinType ? - | (TJoinsNotNull & { [name in TJoinedName]: 'not-null' }) - | (TJoinsNotNull & { [name in TJoinedName]: 'null' }) - | (SetJoinsNullability & { [name in TJoinedName]: 'not-null' }) + : 'full' extends TJoinType ? SetJoinsNullability & { [name in TJoinedName]: 'nullable' } : never; -// Field selection is considered "simple" if it's either a flat object of columns from the same table (select w/o joins), or nested objects, where each object only has columns from the same table. -// If we are dealing with a simple field selection, the resulting type will be much easier to understand, and you'll be able to use more joins in a single statement, -// because in that case we can just mark the whole nested object as nullable instead of creating unions, where all fields of a certain table are either null or not null. -export type IsSimpleObject = T[keyof T] extends - AnyMySqlColumn<{ tableName: infer TTableName extends string }> | SQL | SQLResponse - ? Not> extends true ? true : false - : false; - -export type IsSimpleFields = IsSimpleObject extends true ? true : Equal< - true, - { - [Key in keyof TFields]: TFields[Key] extends AnyMySqlTable ? true - : TFields[Key] extends - Record | SQL | SQLResponse> - ? Not> - : false; - }[keyof TFields] ->; - export interface MySqlSelectConfig { fields: SelectFieldsOrdered; where?: SQL | undefined; @@ -188,12 +140,7 @@ export type JoinFn< TTable, AppendToResult, TSelectMode>, TSelectMode extends 'partial' ? TSelectMode : 'multiple', - AppendToJoinsNotNull< - TJoinsNotNullable, - TJoinedName, - TJoinType, - TSelectMode extends 'partial' ? IsSimpleFields : true - > + AppendToJoinsNotNull >; export type SelectFields = SelectFieldsBase; diff --git a/drizzle-orm/src/mysql-core/query-builders/update.ts b/drizzle-orm/src/mysql-core/query-builders/update.ts index 94dee8734..411a779ee 100644 --- a/drizzle-orm/src/mysql-core/query-builders/update.ts +++ b/drizzle-orm/src/mysql-core/query-builders/update.ts @@ -8,15 +8,14 @@ import { QueryResultKind, } from '~/mysql-core/session'; import { AnyMySqlTable, GetTableConfig } from '~/mysql-core/table'; -import { mapUpdateSet } from '~/mysql-core/utils'; import { QueryPromise } from '~/query-promise'; -import { Param, Query, SQL, SQLWrapper } from '~/sql'; -import { Simplify } from '~/utils'; +import { Query, SQL, SQLWrapper } from '~/sql'; +import { mapUpdateSet, Simplify, UpdateSet } from '~/utils'; import { SelectFieldsOrdered } from './select.types'; export interface MySqlUpdateConfig { where?: SQL | undefined; - set: MySqlUpdateSet; + set: UpdateSet; table: AnyMySqlTable; returning?: SelectFieldsOrdered; } @@ -29,8 +28,6 @@ export type MySqlUpdateSetSource = Simplify< } >; -export type MySqlUpdateSet = Record; - export class MySqlUpdateBuilder { declare protected $table: TTable; @@ -67,7 +64,7 @@ export class MySqlUpdate< constructor( table: TTable, - set: MySqlUpdateSet, + set: UpdateSet, private session: MySqlSession, private dialect: MySqlDialect, ) { diff --git a/drizzle-orm/src/mysql-core/session.ts b/drizzle-orm/src/mysql-core/session.ts index 73c82b2d9..bacaee709 100644 --- a/drizzle-orm/src/mysql-core/session.ts +++ b/drizzle-orm/src/mysql-core/session.ts @@ -20,6 +20,9 @@ export interface PreparedQueryConfig { } export abstract class PreparedQuery { + /** @internal */ + joinsNotNullableMap?: Record; + abstract execute(placeholderValues?: Record): Promise; /** @internal */ diff --git a/drizzle-orm/src/mysql-core/utils.ts b/drizzle-orm/src/mysql-core/utils.ts index 8f9d55c31..8d36079aa 100644 --- a/drizzle-orm/src/mysql-core/utils.ts +++ b/drizzle-orm/src/mysql-core/utils.ts @@ -1,9 +1,7 @@ -import { Param, SQL } from '~/sql'; import { Table } from '~/table'; import { Check, CheckBuilder } from './checks'; import { ForeignKey, ForeignKeyBuilder } from './foreign-keys'; import { Index, IndexBuilder } from './indexes'; -import { MySqlUpdateSet } from './query-builders/update'; import { AnyMySqlTable, MySqlTable } from './table'; /** @internal */ @@ -80,16 +78,3 @@ export function getTableChecks(table: TTable) { const keys = Reflect.ownKeys(checks); return keys.map((key) => checks[key]!); } - -/** @internal */ -export function mapUpdateSet(table: AnyMySqlTable, values: Record): MySqlUpdateSet { - return Object.fromEntries( - Object.entries(values).map(([key, value]) => { - if (value instanceof SQL || value === null || value === undefined) { - return [key, value]; - } else { - return [key, new Param(value, table[MySqlTable.Symbol.Columns][key])]; - } - }), - ); -} diff --git a/drizzle-orm/src/mysql2/session.ts b/drizzle-orm/src/mysql2/session.ts index 07ea3b922..f4a6c0f8d 100644 --- a/drizzle-orm/src/mysql2/session.ts +++ b/drizzle-orm/src/mysql2/session.ts @@ -2,12 +2,7 @@ import { Connection, FieldPacket, OkPacket, Pool, QueryOptions, ResultSetHeader, import { Logger, NoopLogger } from '~/logger'; import { MySqlDialect } from '~/mysql-core/dialect'; import { SelectFieldsOrdered } from '~/mysql-core/query-builders/select.types'; -import { - MySqlSession, - PreparedQuery, - PreparedQueryConfig, - QueryResultHKT, -} from '~/mysql-core/session'; +import { MySqlSession, PreparedQuery, PreparedQueryConfig, QueryResultHKT } from '~/mysql-core/session'; import { fillPlaceholders, Query } from '~/sql'; import { mapResultRow } from '~/utils'; @@ -66,7 +61,9 @@ export class MySql2PreparedQuery extends Prepared const result = this.client.query(this.query, params); - return result.then((result) => result[0].map((row) => mapResultRow(fields, row))); + return result.then((result) => + result[0].map((row) => mapResultRow(fields, row, this.joinsNotNullableMap)) + ); } async all(placeholderValues: Record | undefined = {}): Promise { diff --git a/drizzle-orm/src/neon-serverless/session.ts b/drizzle-orm/src/neon-serverless/session.ts index 1d5aef2c2..3ea97dbf9 100644 --- a/drizzle-orm/src/neon-serverless/session.ts +++ b/drizzle-orm/src/neon-serverless/session.ts @@ -53,7 +53,9 @@ export class NeonPreparedQuery extends PreparedQu const result = this.client.query(this.query, params); - return result.then((result) => result.rows.map((row) => mapResultRow(fields, row))); + return result.then((result) => + result.rows.map((row) => mapResultRow(fields, row, this.joinsNotNullableMap)) + ); } all(placeholderValues: Record | undefined = {}): Promise { diff --git a/drizzle-orm/src/node-postgres/session.ts b/drizzle-orm/src/node-postgres/session.ts index c48c18b83..ad6387a72 100644 --- a/drizzle-orm/src/node-postgres/session.ts +++ b/drizzle-orm/src/node-postgres/session.ts @@ -20,9 +20,8 @@ export class NodePgPreparedQuery extends Prepared private logger: Logger, private fields: SelectFieldsOrdered | undefined, name: string | undefined, - joinsNotNullable?: Record, ) { - super(joinsNotNullable); + super(); this.rawQuery = { name, text: queryString, @@ -47,7 +46,7 @@ export class NodePgPreparedQuery extends Prepared const result = this.client.query(this.query, params); return result.then((result) => - result.rows.map((row) => mapResultRow(fields, row, this.joinsNotNullable)) + result.rows.map((row) => mapResultRow(fields, row, this.joinsNotNullableMap)) ); } @@ -84,9 +83,8 @@ export class NodePgSession extends PgSession { query: Query, fields: SelectFieldsOrdered | undefined, name: string | undefined, - joinsNotNullable?: Record, ): PreparedQuery { - return new NodePgPreparedQuery(this.client, query.sql, query.params, this.logger, fields, name, joinsNotNullable); + return new NodePgPreparedQuery(this.client, query.sql, query.params, this.logger, fields, name); } async query(query: string, params: unknown[]): Promise { diff --git a/drizzle-orm/src/pg-core/dialect.ts b/drizzle-orm/src/pg-core/dialect.ts index 357f2b8bc..c64ce9f99 100644 --- a/drizzle-orm/src/pg-core/dialect.ts +++ b/drizzle-orm/src/pg-core/dialect.ts @@ -153,9 +153,12 @@ export class PgDialect { buildSelectQuery({ fields, where, table, joins, orderBy, groupBy, limit, offset }: PgSelectConfig): SQL { fields.forEach((f) => { - if (f.field instanceof Column && f.field.table !== table && !(getTableName(f.field.table) in joins)) { + let tableName: string; + if ( + f.field instanceof Column && f.field.table !== table && !((tableName = getTableName(f.field.table)) in joins) + ) { throw new Error( - `Column "${f.path.join('.')}" was selected, but its table "${getTableName(f.field.table)}" was not joined`, + `Column "${f.path.join('.')}" was selected, but its table "${tableName}" was not joined`, ); } }); diff --git a/drizzle-orm/src/pg-core/query-builders/select.ts b/drizzle-orm/src/pg-core/query-builders/select.ts index 2f8530fe3..bf8e3fa06 100644 --- a/drizzle-orm/src/pg-core/query-builders/select.ts +++ b/drizzle-orm/src/pg-core/query-builders/select.ts @@ -158,12 +158,11 @@ export class PgSelect< execute: SelectResult[]; } > { - return this.session.prepareQuery( - this.dialect.sqlToQuery(this.getSQL()), - this.config.fields, - name, - this.joinsNotNullable, - ); + const query = this.session.prepareQuery< + PreparedQueryConfig & { execute: SelectResult[] } + >(this.dialect.sqlToQuery(this.getSQL()), this.config.fields, name); + query.joinsNotNullableMap = this.joinsNotNullable; + return query; } prepare(name: string): PreparedQuery< diff --git a/drizzle-orm/src/pg-core/query-builders/select.types.ts b/drizzle-orm/src/pg-core/query-builders/select.types.ts index eecd3dda2..d17b4f084 100644 --- a/drizzle-orm/src/pg-core/query-builders/select.types.ts +++ b/drizzle-orm/src/pg-core/query-builders/select.types.ts @@ -99,16 +99,6 @@ type SetJoinsNullability [Key in keyof TNullabilityMap]: TValue; }; -// https://stackoverflow.com/a/70061272/9929789 -type UnionToParm = U extends any ? (k: U) => void : never; -type UnionToSect = UnionToParm extends ((k: infer I) => void) ? I : never; -type ExtractParm = F extends { (a: infer A): void } ? A : never; -type SpliceOne = Exclude>; -type ExtractOne = ExtractParm>>; -type ToTupleRec = SpliceOne extends never ? [ExtractOne, ...Result] - : ToTupleRec, [ExtractOne, ...Result]>; -export type RemoveDuplicates = ToTupleRec extends any[] ? ToTupleRec[number] : never; - export type AppendToJoinsNotNull< TJoinsNotNull extends Record, TJoinedName extends string, diff --git a/drizzle-orm/src/pg-core/session.ts b/drizzle-orm/src/pg-core/session.ts index 037a8b2ea..e6d2895d7 100644 --- a/drizzle-orm/src/pg-core/session.ts +++ b/drizzle-orm/src/pg-core/session.ts @@ -9,9 +9,8 @@ export interface PreparedQueryConfig { } export abstract class PreparedQuery { - constructor( - protected joinsNotNullable?: Record, - ) {} + /** @internal */ + joinsNotNullableMap?: Record; abstract execute(placeholderValues?: Record): Promise; @@ -29,7 +28,6 @@ export abstract class PgSession { query: Query, fields: SelectFieldsOrdered | undefined, name: string | undefined, - joinsNotNullable?: Record, ): PreparedQuery; execute(query: SQL): Promise { diff --git a/drizzle-orm/src/planetscale-serverless/session.ts b/drizzle-orm/src/planetscale-serverless/session.ts index 8f92032f1..edef01cd2 100644 --- a/drizzle-orm/src/planetscale-serverless/session.ts +++ b/drizzle-orm/src/planetscale-serverless/session.ts @@ -40,7 +40,9 @@ export class PlanetScalePreparedQuery extends Pre const result = this.client.execute(this.queryString, params, this.query); - return result.then((eQuery) => eQuery.rows.map((row) => mapResultRow(fields, row as unknown[]))); + return result.then((eQuery) => + eQuery.rows.map((row) => mapResultRow(fields, row as unknown[], this.joinsNotNullableMap)) + ); } async all(placeholderValues: Record | undefined = {}): Promise { diff --git a/drizzle-orm/src/postgres-js/session.ts b/drizzle-orm/src/postgres-js/session.ts index 3386fa2d2..cf3c4484c 100644 --- a/drizzle-orm/src/postgres-js/session.ts +++ b/drizzle-orm/src/postgres-js/session.ts @@ -34,7 +34,9 @@ export class PostgresJsPreparedQuery extends Prep const result = this.client.unsafe(this.query, params as any[]).values(); - return result.then((result) => result.map((row) => mapResultRow(fields, row))); + return result.then((result) => + result.map((row) => mapResultRow(fields, row, this.joinsNotNullableMap)) + ); } all(placeholderValues: Record | undefined = {}): Promise { diff --git a/drizzle-orm/src/sql-js/session.ts b/drizzle-orm/src/sql-js/session.ts index 0ff790e6c..99bf7bef5 100644 --- a/drizzle-orm/src/sql-js/session.ts +++ b/drizzle-orm/src/sql-js/session.ts @@ -78,7 +78,9 @@ export class PreparedQuery all(placeholderValues?: Record): T['all'] { const { fields } = this; if (fields) { - return this.values(placeholderValues).map((row) => mapResultRow(fields, row.map(normalizeFieldValue))); + return this.values(placeholderValues).map((row) => + mapResultRow(fields, row.map(normalizeFieldValue), this.joinsNotNullableMap) + ); } const params = fillPlaceholders(this.params, placeholderValues ?? {}); @@ -117,7 +119,7 @@ export class PreparedQuery this.free(); } - return mapResultRow(fields, row.map(normalizeFieldValue)); + return mapResultRow(fields, row.map(normalizeFieldValue), this.joinsNotNullableMap); } values(placeholderValues?: Record): T['values'] { diff --git a/drizzle-orm/src/sqlite-core/dialect.ts b/drizzle-orm/src/sqlite-core/dialect.ts index 548c14370..c575590bb 100644 --- a/drizzle-orm/src/sqlite-core/dialect.ts +++ b/drizzle-orm/src/sqlite-core/dialect.ts @@ -2,14 +2,10 @@ import { AnyColumn, Column } from '~/column'; import { MigrationMeta } from '~/migrator'; import { Name, param, Query, SQL, sql, SQLResponse, SQLSourceParam } from '~/sql'; import { AnySQLiteColumn, SQLiteColumn } from '~/sqlite-core/columns'; -import { - SQLiteDeleteConfig, - SQLiteInsertConfig, - SQLiteUpdateConfig, - SQLiteUpdateSet, -} from '~/sqlite-core/query-builders'; +import { SQLiteDeleteConfig, SQLiteInsertConfig, SQLiteUpdateConfig } from '~/sqlite-core/query-builders'; import { AnySQLiteTable, SQLiteTable } from '~/sqlite-core/table'; -import { Table } from '~/table'; +import { getTableName, Table } from '~/table'; +import { UpdateSet } from '~/utils'; import { SelectFieldsOrdered, SQLiteSelectConfig } from './query-builders/select.types'; import { SQLiteSession } from './session'; @@ -32,7 +28,7 @@ export abstract class SQLiteDialect { return sql`delete from ${table}${whereSql}${returningSql}`; } - buildUpdateSet(table: AnySQLiteTable, set: SQLiteUpdateSet): SQL { + buildUpdateSet(table: AnySQLiteTable, set: UpdateSet): SQL { const setEntries = Object.entries(set); const setSize = setEntries.length; @@ -120,6 +116,17 @@ export abstract class SQLiteDialect { } buildSelectQuery({ fields, where, table, joins, orderBy, groupBy, limit, offset }: SQLiteSelectConfig): SQL { + fields.forEach((f) => { + let tableName: string; + if ( + f.field instanceof Column && f.field.table !== table && !((tableName = getTableName(f.field.table)) in joins) + ) { + throw new Error( + `Column "${f.path.join('.')}" was selected, but its table "${tableName}" was not joined`, + ); + } + }); + const joinKeys = Object.keys(joins); const selection = this.buildSelection(fields, { isSingleTable: joinKeys.length === 0 }); diff --git a/drizzle-orm/src/sqlite-core/query-builders/delete.ts b/drizzle-orm/src/sqlite-core/query-builders/delete.ts index a5207c648..fa75bf862 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/delete.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/delete.ts @@ -4,7 +4,7 @@ import { SQLiteDialect } from '~/sqlite-core/dialect'; import { PreparedQuery, SQLiteSession } from '~/sqlite-core/session'; import { AnySQLiteTable, InferModel, SQLiteTable } from '~/sqlite-core/table'; import { orderSelectedFields } from '~/utils'; -import { SelectFieldsOrdered, SelectResultFields, SQLiteSelectFields } from './select.types'; +import { SelectFields, SelectFieldsOrdered, SelectResultFields } from './select.types'; export interface SQLiteDeleteConfig { where?: SQL | undefined; @@ -41,11 +41,11 @@ export class SQLiteDelete< } returning(): Omit>, 'where' | 'returning'>; - returning( + returning( fields: TSelectedFields, ): Omit>, 'where' | 'returning'>; returning( - fields: SQLiteSelectFields = this.table[SQLiteTable.Symbol.Columns], + fields: SelectFields = this.table[SQLiteTable.Symbol.Columns], ): SQLiteDelete { this.config.returning = orderSelectedFields(fields); return this; @@ -57,8 +57,8 @@ export class SQLiteDelete< } toSQL(): Omit { - const { typings, ...rest} = this.dialect.sqlToQuery(this.getSQL()); - return rest + const { typings, ...rest } = this.dialect.sqlToQuery(this.getSQL()); + return rest; } prepare(): PreparedQuery<{ diff --git a/drizzle-orm/src/sqlite-core/query-builders/insert.ts b/drizzle-orm/src/sqlite-core/query-builders/insert.ts index f5a1837bb..9673b689f 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/insert.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/insert.ts @@ -6,7 +6,7 @@ import { SQLiteDialect } from '~/sqlite-core/dialect'; import { IndexColumn } from '~/sqlite-core/indexes'; import { PreparedQuery, SQLiteSession } from '~/sqlite-core/session'; import { AnySQLiteTable, InferModel, SQLiteTable } from '~/sqlite-core/table'; -import { SelectFieldsOrdered, SelectResultFields, SQLiteSelectFields } from './select.types'; +import { SelectFields, SelectFieldsOrdered, SelectResultFields } from './select.types'; import { SQLiteUpdateSetSource } from './update'; export interface SQLiteInsertConfig { @@ -82,14 +82,14 @@ export class SQLiteInsert< SQLiteInsert>, 'returning' | `onConflict${string}` >; - returning( + returning( fields: TSelectedFields, ): Omit< SQLiteInsert>, 'returning' | `onConflict${string}` >; returning( - fields: SQLiteSelectFields = this.config.table[SQLiteTable.Symbol.Columns], + fields: SelectFields = this.config.table[SQLiteTable.Symbol.Columns], ): SQLiteInsert { this.config.returning = orderSelectedFields(fields); return this; @@ -122,8 +122,8 @@ export class SQLiteInsert< } toSQL(): Omit { - const { typings, ...rest} = this.dialect.sqlToQuery(this.getSQL()); - return rest + const { typings, ...rest } = this.dialect.sqlToQuery(this.getSQL()); + return rest; } prepare(): PreparedQuery< diff --git a/drizzle-orm/src/sqlite-core/query-builders/select.ts b/drizzle-orm/src/sqlite-core/query-builders/select.ts index 7ae7e3fe5..9fe9ca676 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/select.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/select.ts @@ -12,10 +12,10 @@ import { JoinFn, JoinNullability, JoinType, + SelectFields, SelectMode, SelectResult, SQLiteSelectConfig, - SQLiteSelectFields, } from './select.types'; export interface SQLiteSelect< @@ -62,7 +62,7 @@ export class SQLiteSelect< private createJoin( joinType: TJoinType, - ): JoinFn { + ): JoinFn { return (table: AnySQLiteTable, on: SQL): AnySQLiteSelect => { const tableName = table[Table.Symbol.Name]; @@ -112,7 +112,7 @@ export class SQLiteSelect< fullJoin = this.createJoin('full'); - fields( + fields( fields: TSelect, ): Omit< SQLiteSelect, @@ -154,8 +154,8 @@ export class SQLiteSelect< } toSQL(): Omit { - const { typings, ...rest} = this.dialect.sqlToQuery(this.getSQL()); - return rest + const { typings, ...rest } = this.dialect.sqlToQuery(this.getSQL()); + return rest; } prepare(): PreparedQuery< @@ -167,7 +167,9 @@ export class SQLiteSelect< values: any[][]; } > { - return this.session.prepareQuery(this.dialect.sqlToQuery(this.getSQL()), this.config.fields); + const query = this.session.prepareQuery(this.dialect.sqlToQuery(this.getSQL()), this.config.fields); + query.joinsNotNullableMap = this.joinsNotNullable; + return query; } run: ReturnType['run'] = (placeholderValues) => { diff --git a/drizzle-orm/src/sqlite-core/query-builders/select.types.ts b/drizzle-orm/src/sqlite-core/query-builders/select.types.ts index 928945406..c409ac761 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/select.types.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/select.types.ts @@ -12,7 +12,7 @@ import { UpdateTableConfig, } from '~/sqlite-core/table'; -import { SelectFields, SelectFieldsOrdered as SelectFieldsOrderedBase } from '~/operations'; +import { SelectFields as SelectFieldsBase, SelectFieldsOrdered as SelectFieldsOrderedBase } from '~/operations'; import { SQLiteSelect } from './select'; export type JoinType = 'inner' | 'left' | 'right' | 'full'; @@ -31,11 +31,6 @@ export type ApplyNullability = TNullabi : TNullability extends 'null' ? null : T; -export type ApplyNullabilityNested = T extends Record ? { - [Key in keyof T]: ApplyNullabilityNested; - } - : ApplyNullability; - export type ApplyNotNullMapToJoins> = { [TTableName in keyof TResult & keyof TNullabilityMap & string]: ApplyNullability< TResult[TTableName], @@ -47,40 +42,36 @@ export type SelectResult< TResult, TSelectMode extends SelectMode, TJoinsNotNullable extends Record, -> = TSelectMode extends 'partial' - ? RemoveDuplicates>> +> = TSelectMode extends 'partial' ? SelectPartialResult : TSelectMode extends 'single' ? TResult - : RemoveDuplicates>>; + : Simplify>; type IsUnion = (T extends any ? (U extends T ? false : true) : never) extends false ? false : true; type Not = T extends true ? false : true; -type SelectPartialResult< - TFields, - TNullability extends Record, - TIsSimpleFields extends boolean, -> = TNullability extends TNullability ? { +type SelectPartialResult> = TNullability extends + TNullability ? { [Key in keyof TFields]: TFields[Key] extends infer TField - ? TField extends AnySQLiteTable - ? TIsSimpleFields extends true ? GetTableConfig extends keyof TNullability ? ApplyNullability< - SelectResultFields>, - TNullability[GetTableConfig] - > - : never - : SelectPartialResult, TNullability, TIsSimpleFields> + ? TField extends AnySQLiteTable ? GetTableConfig extends keyof TNullability ? ApplyNullability< + SelectResultFields>, + TNullability[GetTableConfig] + > + : never : TField extends AnySQLiteColumn ? GetColumnConfig extends infer TTableName extends keyof TNullability ? ApplyNullability, TNullability[TTableName]> : never : TField extends SQL | SQLResponse ? SelectResultField : TField extends Record - ? [TIsSimpleFields, TField[keyof TField]] extends - [true, AnySQLiteColumn<{ tableName: infer TTableName extends string }>] - ? ApplyNullability, TNullability[TTableName]> - : SelectPartialResult - : SelectResultField + ? TField[keyof TField] extends + AnySQLiteColumn<{ tableName: infer TTableName extends string }> | SQL | SQLResponse + ? Not> extends true + ? ApplyNullability, TNullability[TTableName]> + : SelectPartialResult + : never + : never : never; } : never; @@ -104,7 +95,7 @@ export type AppendToResult< TTableName extends AnySQLiteTable, TResult, TJoinedName extends string, - TSelectedFields extends SQLiteSelectFields, + TSelectedFields extends SelectFields, TOldSelectMode extends SelectMode, > = TOldSelectMode extends 'partial' ? TResult : TOldSelectMode extends 'single' @@ -115,55 +106,16 @@ type SetJoinsNullability [Key in keyof TNullabilityMap]: TValue; }; -// https://stackoverflow.com/a/70061272/9929789 -type UnionToParm = U extends any ? (k: U) => void : never; -type UnionToSect = UnionToParm extends ((k: infer I) => void) ? I : never; -type ExtractParm = F extends { (a: infer A): void } ? A : never; -type SpliceOne = Exclude>; -type ExtractOne = ExtractParm>>; -type ToTupleRec = SpliceOne extends never ? [ExtractOne, ...Result] - : ToTupleRec, [ExtractOne, ...Result]>; -export type RemoveDuplicates = ToTupleRec extends any[] ? ToTupleRec[number] : never; - export type AppendToJoinsNotNull< TJoinsNotNull extends Record, TJoinedName extends string, TJoinType extends JoinType, - TIsSimpleFields extends boolean, -> = 'left' extends TJoinType ? TIsSimpleFields extends true ? TJoinsNotNull & { [name in TJoinedName]: 'nullable' } - : TJoinsNotNull & { [name in TJoinedName]: 'not-null' } | TJoinsNotNull & { [name in TJoinedName]: 'null' } - : 'right' extends TJoinType - ? [TIsSimpleFields, Not>] extends [true, true] - ? SetJoinsNullability & { [name in TJoinedName]: 'not-null' } - : - | TJoinsNotNull & { [name in TJoinedName]: 'not-null' } - | SetJoinsNullability & { [name in TJoinedName]: 'not-null' } +> = 'left' extends TJoinType ? TJoinsNotNull & { [name in TJoinedName]: 'nullable' } + : 'right' extends TJoinType ? SetJoinsNullability & { [name in TJoinedName]: 'not-null' } : 'inner' extends TJoinType ? TJoinsNotNull & { [name in TJoinedName]: 'not-null' } - : 'full' extends TJoinType ? - | (TJoinsNotNull & { [name in TJoinedName]: 'not-null' }) - | (TJoinsNotNull & { [name in TJoinedName]: 'null' }) - | (SetJoinsNullability & { [name in TJoinedName]: 'not-null' }) + : 'full' extends TJoinType ? SetJoinsNullability & { [name in TJoinedName]: 'nullable' } : never; -// Field selection is considered "simple" if it's either a flat object of columns from the same table (select w/o joins), or nested objects, where each object only has columns from the same table. -// If we are dealing with a simple field selection, the resulting type will be much easier to understand, and you'll be able to use more joins in a single statement, -// because in that case we can just mark the whole nested object as nullable instead of creating unions, where all fields of a certain table are either null or not null. -export type IsSimpleObject = T[keyof T] extends - AnySQLiteColumn<{ tableName: infer TTableName extends string }> | SQL | SQLResponse - ? Not> extends true ? true : false - : false; - -export type IsSimpleFields = IsSimpleObject extends true ? true : Equal< - true, - { - [Key in keyof TFields]: TFields[Key] extends AnySQLiteTable ? true - : TFields[Key] extends - Record | SQL | SQLResponse> - ? Not> - : false; - }[keyof TFields] ->; - export interface SQLiteSelectConfig { fields: SelectFieldsOrdered; where?: SQL | undefined; @@ -177,8 +129,8 @@ export interface SQLiteSelectConfig { export type JoinFn< TTable extends AnySQLiteTable, - TRunResult, TResultType extends 'sync' | 'async', + TRunResult, TSelectMode extends SelectMode, TJoinType extends JoinType, TResult, @@ -192,15 +144,10 @@ export type JoinFn< TRunResult, AppendToResult, TSelectMode>, TSelectMode extends 'partial' ? TSelectMode : 'multiple', - AppendToJoinsNotNull< - TJoinsNotNullable, - TJoinedName, - TJoinType, - TSelectMode extends 'partial' ? IsSimpleFields : true - > + AppendToJoinsNotNull >; -export type SQLiteSelectFields = SelectFields; +export type SelectFields = SelectFieldsBase; export type SelectFieldsOrdered = SelectFieldsOrderedBase; @@ -211,7 +158,7 @@ export type SelectResultField = T extends AnySQLiteTable ? SelectResultField< : T extends Record ? { [Key in keyof T]: SelectResultField } : never; -export type SelectResultFields = Simplify< +export type SelectResultFields = Simplify< { [Key in keyof TSelectedFields & string]: SelectResultField; } diff --git a/drizzle-orm/src/sqlite-core/query-builders/update.ts b/drizzle-orm/src/sqlite-core/query-builders/update.ts index a71c64120..6acbf898c 100644 --- a/drizzle-orm/src/sqlite-core/query-builders/update.ts +++ b/drizzle-orm/src/sqlite-core/query-builders/update.ts @@ -3,12 +3,12 @@ import { Param, Query, SQL, SQLWrapper } from '~/sql'; import { SQLiteDialect } from '~/sqlite-core/dialect'; import { PreparedQuery, SQLiteSession } from '~/sqlite-core/session'; import { AnySQLiteTable, GetTableConfig, InferModel, SQLiteTable } from '~/sqlite-core/table'; -import { mapUpdateSet, orderSelectedFields, Simplify } from '~/utils'; -import { SelectFieldsOrdered, SelectResultFields, SQLiteSelectFields } from './select.types'; +import { mapUpdateSet, orderSelectedFields, Simplify, UpdateSet } from '~/utils'; +import { SelectFields, SelectFieldsOrdered, SelectResultFields } from './select.types'; export interface SQLiteUpdateConfig { where?: SQL | undefined; - set: SQLiteUpdateSet; + set: UpdateSet; table: AnySQLiteTable; returning?: SelectFieldsOrdered; } @@ -21,8 +21,6 @@ export type SQLiteUpdateSetSource = Simplify< } >; -export type SQLiteUpdateSet = Record; - export class SQLiteUpdateBuilder< TTable extends AnySQLiteTable, TResultType extends 'sync' | 'async', @@ -60,7 +58,7 @@ export class SQLiteUpdate< constructor( table: TTable, - set: SQLiteUpdateSet, + set: UpdateSet, private session: SQLiteSession, private dialect: SQLiteDialect, ) { @@ -76,14 +74,14 @@ export class SQLiteUpdate< SQLiteUpdate>, 'where' | 'returning' >; - returning( + returning( fields: TSelectedFields, ): Omit< SQLiteUpdate>, 'where' | 'returning' >; returning( - fields: SQLiteSelectFields = this.config.table[SQLiteTable.Symbol.Columns], + fields: SelectFields = this.config.table[SQLiteTable.Symbol.Columns], ): Omit, 'where' | 'returning'> { this.config.returning = orderSelectedFields(fields); return this; @@ -95,8 +93,8 @@ export class SQLiteUpdate< } toSQL(): Omit { - const { typings, ...rest} = this.dialect.sqlToQuery(this.getSQL()); - return rest + const { typings, ...rest } = this.dialect.sqlToQuery(this.getSQL()); + return rest; } prepare(): PreparedQuery< diff --git a/drizzle-orm/src/sqlite-core/session.ts b/drizzle-orm/src/sqlite-core/session.ts index d127d0c87..a312fc846 100644 --- a/drizzle-orm/src/sqlite-core/session.ts +++ b/drizzle-orm/src/sqlite-core/session.ts @@ -11,6 +11,9 @@ export interface PreparedQueryConfig { } export abstract class PreparedQuery { + /** @internal */ + joinsNotNullableMap?: Record; + abstract run(placeholderValues?: Record): ResultKind; abstract all(placeholderValues?: Record): ResultKind; @@ -26,12 +29,12 @@ export abstract class SQLiteSession { const { fields } = this; if (fields) { - return this.values(placeholderValues).then((values) => values.map((row) => mapResultRow(fields, row))); + return this.values(placeholderValues).then((values) => + values.map((row) => mapResultRow(fields, row, this.joinsNotNullableMap)) + ); } const params = fillPlaceholders(this.params, placeholderValues ?? {}); diff --git a/drizzle-orm/src/utils.ts b/drizzle-orm/src/utils.ts index 3e5fa9e21..83b31b80c 100644 --- a/drizzle-orm/src/utils.ts +++ b/drizzle-orm/src/utils.ts @@ -17,7 +17,7 @@ export const npmVersion = '0.17.0'; export function mapResultRow( columns: SelectFieldsOrdered, row: unknown[], - joinsNotNullableMap?: Record, + joinsNotNullableMap: Record | undefined, ): TResult { // Key -> nested object key, value -> table name if all fields in the nested object are from the same table, false otherwise const nullifyMap: Record = {}; diff --git a/drizzle-orm/tests/mysql/dan/select.ts b/drizzle-orm/tests/mysql/dan/select.ts index 4cb8abe4a..cd051351e 100644 --- a/drizzle-orm/tests/mysql/dan/select.ts +++ b/drizzle-orm/tests/mysql/dan/select.ts @@ -46,7 +46,7 @@ const join = await db.select(users) Expect< Equal< - ({ + { users: { id: number; text: string | null; @@ -59,7 +59,7 @@ Expect< age1: number; createdAt: Date; enumCol: 'a' | 'b' | 'c'; - }; + } | null; cities: { id: number; name: string; @@ -69,29 +69,11 @@ Expect< id: number; name: string; population: number | null; - }; - city1: { - id: number; - }; - } | { - users: null; - cities: null; - city: { - id: number; - name: string; - population: number | null; - }; - city1: { - id: number; - }; - } | { - users: null; - cities: null; - city: null; + } | null; city1: { id: number; }; - })[], + }[], typeof join > >; @@ -105,16 +87,10 @@ const join2 = await db.select(users) Expect< Equal< - ({ - userId: number; - cityId: number; - } | { - userId: number; - cityId: null; - } | { - userId: null; - cityId: number; - })[], + { + userId: number | null; + cityId: number | null; + }[], typeof join2 > >; @@ -130,23 +106,11 @@ const join3 = await db.select(users) Expect< Equal< - ({ - userId: number; - cityId: number; - classId: number; - } | { - userId: number; - cityId: null; - classId: number; - } | { - userId: null; - cityId: number; - classId: number; - } | { - userId: null; - cityId: null; + { + userId: number | null; + cityId: number | null; classId: number; - })[], + }[], typeof join3 > >; diff --git a/drizzle-orm/tests/sqlite/dan/select.ts b/drizzle-orm/tests/sqlite/dan/select.ts index be913a399..e764edc93 100644 --- a/drizzle-orm/tests/sqlite/dan/select.ts +++ b/drizzle-orm/tests/sqlite/dan/select.ts @@ -84,30 +84,17 @@ const joinPartial = db.select(users) Expect< Equal< - ({ - user: { - id: number; - name: string | null; - age: number; - }; - city: { - id: number; - name: string; - }; - } | { + { user: { id: number; name: string | null; age: number; - }; - city: null; - } | { - user: null; + } | null; city: { id: number; name: string; - }; - })[], + } | null; + }[], typeof joinPartial > >; @@ -122,23 +109,11 @@ const join3 = db.select(users) .rightJoin(classes, eq(users.id, classes.id)).all(); Expect< - Equal<({ - userId: number; - cityId: number; - classId: number; - } | { - userId: null; - cityId: number; - classId: number; - } | { - userId: number; - cityId: null; - classId: number; - } | { - userId: null; - cityId: null; + Equal<{ + userId: number | null; + cityId: number | null; classId: number; - })[], typeof join3> + }[], typeof join3> >; db.select(users) diff --git a/integration-tests/package.json b/integration-tests/package.json index 2d9a94acb..bd5ee0ad8 100644 --- a/integration-tests/package.json +++ b/integration-tests/package.json @@ -11,6 +11,7 @@ "tests/**/*.ts", "!tests/awsdatapi.alltypes.test.ts", "!tests/awsdatapi.test.ts", + "!tests/planetscale-serverless/**/*.ts", "!tests/bun/**/*" ], "extensions": { diff --git a/integration-tests/tests/better-sqlite.test.ts b/integration-tests/tests/better-sqlite.test.ts index 17e7fc8a0..4ca4db490 100644 --- a/integration-tests/tests/better-sqlite.test.ts +++ b/integration-tests/tests/better-sqlite.test.ts @@ -1,10 +1,10 @@ import anyTest, { TestFn } from 'ava'; import Database from 'better-sqlite3'; -import { sql } from 'drizzle-orm'; +import { DefaultLogger, sql } from 'drizzle-orm'; import { BetterSQLite3Database, drizzle } from 'drizzle-orm/better-sqlite3'; import { migrate } from 'drizzle-orm/better-sqlite3/migrator'; import { asc, eq } from 'drizzle-orm/expressions'; -import { name, placeholder } from 'drizzle-orm/sql'; +import { Name, name, placeholder } from 'drizzle-orm/sql'; import { alias, blob, InferModel, integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core'; const usersTable = sqliteTable('users', { @@ -72,6 +72,19 @@ test.beforeEach((t) => { json blob, created_at integer not null default (cast((julianday('now') - 2440587.5)*86400000 as integer)) )`); + ctx.db.run(sql`drop table if exists ${users2Table}`); + ctx.db.run(sql` + create table ${users2Table} ( + id integer primary key, + name text not null, + city_id integer references ${citiesTable}(${new Name(citiesTable.id.name)}) + )`); + ctx.db.run(sql`drop table if exists ${citiesTable}`); + ctx.db.run(sql` + create table ${citiesTable} ( + id integer primary key, + name text not null + )`); }); test.serial('select all fields', (t) => { @@ -578,7 +591,7 @@ test.serial('left join (flat object fields)', (t) => { .values({ name: 'Paris' }, { name: 'London' }) .returning({ id: citiesTable.id }).all()[0]!; - db.insert(users2Table).values({ name: 'John', cityId }, { name: 'Jane' }); + db.insert(users2Table).values({ name: 'John', cityId }, { name: 'Jane' }).run(); const res = db.select(users2Table) .fields({ @@ -587,7 +600,8 @@ test.serial('left join (flat object fields)', (t) => { cityId: citiesTable.id, cityName: citiesTable.name, }) - .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)); + .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)) + .all(); t.deepEqual(res, [ { userId: 1, userName: 'John', cityId, cityName: 'Paris' }, @@ -602,7 +616,7 @@ test.serial('left join (grouped fields)', (t) => { .values({ name: 'Paris' }, { name: 'London' }) .returning({ id: citiesTable.id }).all()[0]!; - db.insert(users2Table).values({ name: 'John', cityId }, { name: 'Jane' }); + db.insert(users2Table).values({ name: 'John', cityId }, { name: 'Jane' }).run(); const res = db.select(users2Table) .fields({ @@ -617,7 +631,8 @@ test.serial('left join (grouped fields)', (t) => { nameUpper: sql`upper(${citiesTable.name})`.as(), }, }) - .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)); + .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)) + .all(); t.deepEqual(res, [ { @@ -640,10 +655,10 @@ test.serial('left join (all fields)', (t) => { .values({ name: 'Paris' }, { name: 'London' }) .returning({ id: citiesTable.id }).all()[0]!; - db.insert(users2Table).values({ name: 'John', cityId }, { name: 'Jane' }); + db.insert(users2Table).values({ name: 'John', cityId }, { name: 'Jane' }).run(); const res = db.select(users2Table) - .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)); + .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)).all(); t.deepEqual(res, [ { diff --git a/integration-tests/tests/mysql.test.ts b/integration-tests/tests/mysql.test.ts index 7dfa4aca1..830119a0a 100644 --- a/integration-tests/tests/mysql.test.ts +++ b/integration-tests/tests/mysql.test.ts @@ -9,7 +9,6 @@ import { datetime, int, json, - MySqlDatabase, mysqlEnum, mysqlTable, serial, @@ -131,6 +130,8 @@ 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 \`datestable\``); + 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 schema public`); await ctx.db.execute( sql`create table \`userstest\` ( @@ -152,6 +153,21 @@ test.beforeEach(async (t) => { \`year\` year )`, ); + + 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 + )`, + ); }); test.serial('select all fields', async (t) => { diff --git a/integration-tests/tests/postgres.js.test.ts b/integration-tests/tests/postgres.js.test.ts index 466b6c6fc..4f53a325f 100644 --- a/integration-tests/tests/postgres.js.test.ts +++ b/integration-tests/tests/postgres.js.test.ts @@ -114,6 +114,19 @@ test.beforeEach(async (t) => { created_at timestamptz not null default now() )`, ); + await ctx.db.execute( + sql`create table cities ( + id serial primary key, + name text not null + )`, + ); + await ctx.db.execute( + sql`create table users2 ( + id serial primary key, + name text not null, + city_id integer references cities(id) + )`, + ); }); test.serial('select all fields', async (t) => { diff --git a/integration-tests/tests/sql.js.test.ts b/integration-tests/tests/sql.js.test.ts index 8596d3542..8e3c9ba5e 100644 --- a/integration-tests/tests/sql.js.test.ts +++ b/integration-tests/tests/sql.js.test.ts @@ -1,7 +1,7 @@ import anyTest, { TestFn } from 'ava'; import { sql } from 'drizzle-orm'; import { asc, eq } from 'drizzle-orm/expressions'; -import { name, placeholder } from 'drizzle-orm/sql'; +import { Name, name, placeholder } from 'drizzle-orm/sql'; import { drizzle, SQLJsDatabase } from 'drizzle-orm/sql-js'; import { migrate } from 'drizzle-orm/sql-js/migrator'; import { alias, blob, InferModel, integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core'; @@ -72,6 +72,19 @@ test.beforeEach((t) => { json blob, created_at integer not null default (cast((julianday('now') - 2440587.5)*86400000 as integer)) )`); + ctx.db.run(sql`drop table if exists ${users2Table}`); + ctx.db.run(sql` + create table ${users2Table} ( + id integer primary key, + name text not null, + city_id integer references ${citiesTable}(${new Name(citiesTable.id.name)}) + )`); + ctx.db.run(sql`drop table if exists ${citiesTable}`); + ctx.db.run(sql` + create table ${citiesTable} ( + id integer primary key, + name text not null + )`); }); test.serial('select all fields', (t) => { @@ -578,7 +591,7 @@ test.serial('left join (flat object fields)', (t) => { .values({ name: 'Paris' }, { name: 'London' }) .returning({ id: citiesTable.id }).all()[0]!; - db.insert(users2Table).values({ name: 'John', cityId }, { name: 'Jane' }); + db.insert(users2Table).values({ name: 'John', cityId }, { name: 'Jane' }).run(); const res = db.select(users2Table) .fields({ @@ -587,7 +600,8 @@ test.serial('left join (flat object fields)', (t) => { cityId: citiesTable.id, cityName: citiesTable.name, }) - .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)); + .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)) + .all(); t.deepEqual(res, [ { userId: 1, userName: 'John', cityId, cityName: 'Paris' }, @@ -602,7 +616,7 @@ test.serial('left join (grouped fields)', (t) => { .values({ name: 'Paris' }, { name: 'London' }) .returning({ id: citiesTable.id }).all()[0]!; - db.insert(users2Table).values({ name: 'John', cityId }, { name: 'Jane' }); + db.insert(users2Table).values({ name: 'John', cityId }, { name: 'Jane' }).run(); const res = db.select(users2Table) .fields({ @@ -617,7 +631,8 @@ test.serial('left join (grouped fields)', (t) => { nameUpper: sql`upper(${citiesTable.name})`.as(), }, }) - .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)); + .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)) + .all(); t.deepEqual(res, [ { @@ -640,10 +655,10 @@ test.serial('left join (all fields)', (t) => { .values({ name: 'Paris' }, { name: 'London' }) .returning({ id: citiesTable.id }).all()[0]!; - db.insert(users2Table).values({ name: 'John', cityId }, { name: 'Jane' }); + db.insert(users2Table).values({ name: 'John', cityId }, { name: 'Jane' }).run(); const res = db.select(users2Table) - .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)); + .leftJoin(citiesTable, eq(users2Table.cityId, citiesTable.id)).all(); t.deepEqual(res, [ { From b663be81d73477389260bab06843e146398ac966 Mon Sep 17 00:00:00 2001 From: Dan Kochetov Date: Mon, 6 Feb 2023 13:13:43 +0200 Subject: [PATCH 5/7] Add joins docs --- README.md | 11 +- docs/joins.md | 247 ++++++++++++++++++++++++++ drizzle-orm/src/mysql-core/README.md | 2 + drizzle-orm/src/pg-core/README.md | 3 +- drizzle-orm/src/sqlite-core/README.md | 2 + 5 files changed, 259 insertions(+), 6 deletions(-) create mode 100644 docs/joins.md diff --git a/README.md b/README.md index ee64c03cb..812306026 100644 --- a/README.md +++ b/README.md @@ -115,12 +115,12 @@ const upperCaseNames /* : { id: number; name: string }[] */ = await db.select(us // You wouldn't BELIEVE how SMART the result type is! 😱 const allUsersWithCities = await db.select(users) .fields({ - user: { - id: users.id, - name: users.fullName, + id: users.id, + name: users.fullName, + city: { + id: cities.id, + name: cities.name, }, - cityId: cities.id, - cityName: cities.name, }) .leftJoin(cities, eq(users.cityId, cities.id)); @@ -131,6 +131,7 @@ const deletedNames /* : { name: string }[] */ = await db.delete(users) ``` **See full docs for further reference:** + - [PostgreSQL](./drizzle-orm/src/pg-core/README.md) - [MySQL](./drizzle-orm/src/mysql-core/README.md) - [SQLite](./drizzle-orm/src/sqlite-core/README.md) diff --git a/docs/joins.md b/docs/joins.md new file mode 100644 index 000000000..15c3f4d9a --- /dev/null +++ b/docs/joins.md @@ -0,0 +1,247 @@ +# Drizzle ORM - Joins + +As with other parts of Drizzle ORM, the joins syntax is a balance between the SQL-likeness and type safety. +Here's an example of how a common `1-to-many` relationship can be modelled. + +```typescript +const users = pgTable('users', { + id: serial('id').primaryKey(), + firstName: text('first_name').notNull(), + lastName: text('last_name'), + cityId: int('city_id').references(() => cities.id), +}); + +const cities = pgTable('cities', { + id: serial('id').primaryKey(), + name: text('name').notNull(), +}); +``` + +Now, let's select all cities with all users that live in that city. + +This is how you'd write it in raw SQL: + +```sql +select + cities.id as city_id, + cities.name as city_name, + users.id as user_id, + users.first_name, + users.last_name +from cities +left join users on users.city_id = cities.id +``` + +And here's how to do the same with Drizzle ORM: + +```typescript +const rows = await db.select(cities) + .fields({ + cityId: cities.id, + cityName: cities.name, + userId: users.id, + firstName: users.firstName, + lastName: users.lastName, + }) + .leftJoin(users, eq(users.cityId, cities.id)); +``` + +`rows` will have the following type: + +```typescript +{ + cityId: number; + cityName: string; + userId: number | null; + firstName: string | null; + lastName: string | null; +}[] +``` + +As you can see, all the joined columns have been nullified. This might do the trick if you're using joins to form a single row of results, but in our case we have two separate entities in our row - a city and a user. +It might not be very convenient to check every field for nullability separately (or, even worse, just add an `!` after every field to "make compiler happy"). It would be much more useful if you could somehow run a single check +to verify that the user was joined and all of its fields are available. + +**To achieve that, you can group the fields of a certain table in a nested object inside the `.fields()`:** + +```typescript +const rows = await db.select(cities) + .fields({ + cityId: cities.id, + cityName: cities.name, + user: { + id: users.id, + firstName: users.firstName, + lastName: users.lastName, + }, + }) + .leftJoin(users, eq(users.cityId, cities.id)); +``` + +In that case, the ORM will use dark TypeScript magic (as if it wasn't already) and figure out that you have a nested object where all the fields belong to the same table. So, the `rows` type will now look like this: + +```typescript +{ + cityId: number; + cityName: string; + user: { + id: number; + firstName: string; + lastName: string | null; + } | null; +} +``` + +This is much more convenient! Now, you can just do a single check for `row.user !== null`, and all the user fields will become available. + +
+ +Note that you can group any fields in a nested object however you like, but the single check optimization will only be applied to a certain nested object if all its fields belong to the same table. +So, for example, you can group the city fields, too: + +```typescript +.fields({ + city: { + id: cities.id, + name: cities.name, + }, + user: { + id: users.id, + firstName: users.firstName, + lastName: users.lastName, + }, +}) +``` + +And the result type will look like this: + +```typescript +{ + city: { + id: number; + name: string; + }; + user: { + id: number; + firstName: string; + lastName: string | null; + } | null; +} +``` + +
+ +If you just need all the fields from all the tables you're selecting and joining, you can skip the `.fields()` method altogether: + +```typescript +const rows = await db.select(cities).leftJoin(users, eq(users.cityId, cities.id)); +``` + +> **Note**: in this case, the DB table names will be used as the keys in the result object. + +```typescript +{ + cities: { + id: number; + name: string; + }; + users: { + id: number; + firstName: string; + lastName: string | null; + cityId: number | null; + } | null; +}[] +``` + +
+ +There are cases where you'd want to select all the fields from one table, but pick fields from others. In that case, instead of listing all the table fields, you can just pass a table: + +```typescript +.fields({ + cities, // shorthand for "cities: cities", the key can be anything + user: { + firstName: users.firstName, + }, +}) +``` + +```typescript +{ + cities: { + id: number; + name: string; + }; + user: { + firstName: string; + } | null; +} +``` + +
+ +But what happens if you group columns from multiple tables in the same nested object? Nothing, really - they will still be all individually nullable, just grouped under the same object (as you might expect!): + +```typescript +.fields({ + id: cities.id, + cityAndUser: { + cityName: cities.name, + userId: users.id, + firstName: users.firstName, + lastName: users.lastName, + } +}) +``` + +```typescript +{ + id: number; + cityAndUser: { + cityName: string; + userId: number | null; + firstName: string | null; + lastName: string | null; + }; +} +``` + +## Aggregating results + +OK, so you have obtained all the cities and the users for every city. But what you **really** wanted is a **list** of users for every city, and what you currently have is a `city-user?` combinations. So, how do you transform it? +That's the neat part - you can do that however you'd like! No hand-holding here. + +For example, one of the ways to do that would be `Array.reduce()`: + +```typescript +import { InferModel } from 'drizzle-orm'; + +type User = InferModel; +type City = InferModel; + +const rows = await db.select(cities) + .fields({ + city: cities, + user: users, + }) + .leftJoin(users, eq(users.cityId, cities.id)); + +const result = rows.reduce>( + (acc, row) => { + const city = row.city; + const user = row.user; + + if (!acc[city.id]) { + acc[city.id] = { city, users: [] }; + } + + if (user) { + acc[cityId].users.push(user); + } + + return acc; + }, + {}, +); +``` diff --git a/drizzle-orm/src/mysql-core/README.md b/drizzle-orm/src/mysql-core/README.md index 500ffa5cc..1eb0167b2 100644 --- a/drizzle-orm/src/mysql-core/README.md +++ b/drizzle-orm/src/mysql-core/README.md @@ -481,6 +481,8 @@ await db.delete(users) Last but not least. Probably the most powerful feature in the library🚀 +> **Note**: for in-depth partial select joins documentation, refer to [this page](/docs/joins.md). + #### Many-to-one ```typescript diff --git a/drizzle-orm/src/pg-core/README.md b/drizzle-orm/src/pg-core/README.md index 9aa6f6945..1ed9dd4fa 100644 --- a/drizzle-orm/src/pg-core/README.md +++ b/drizzle-orm/src/pg-core/README.md @@ -537,6 +537,8 @@ const deletedUserId: { deletedId: number }[] = await db.delete(users) Last but not least. Probably the most powerful feature in the library🚀 +> **Note**: for in-depth partial select joins documentation, refer to [this page](/docs/joins.md). + #### Many-to-one ```typescript @@ -610,7 +612,6 @@ const result1 = await db.select(cities).fields({ // Select all fields from users and only id and name from cities const result2 = await db.select(cities).fields({ - // Supports any level of nesting! user: users, city: { id: cities.id, diff --git a/drizzle-orm/src/sqlite-core/README.md b/drizzle-orm/src/sqlite-core/README.md index 13eb4c72f..4f2158c95 100644 --- a/drizzle-orm/src/sqlite-core/README.md +++ b/drizzle-orm/src/sqlite-core/README.md @@ -407,6 +407,8 @@ db.select(orders).fields({ Last but not least. Probably the most powerful feature in the library🚀 +> **Note**: for in-depth partial select joins documentation, refer to [this page](/docs/joins.md). + ### Many-to-one ```typescript From c8c74edbfaaf0b6c136bac191d93f0ceb7de4f77 Mon Sep 17 00:00:00 2001 From: Dan Kochetov Date: Mon, 6 Feb 2023 13:23:32 +0200 Subject: [PATCH 6/7] Add changelog for 0.18.0 --- changelogs/drizzle-orm/0.18.0.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/drizzle-orm/0.18.0.md diff --git a/changelogs/drizzle-orm/0.18.0.md b/changelogs/drizzle-orm/0.18.0.md new file mode 100644 index 000000000..72fc644c9 --- /dev/null +++ b/changelogs/drizzle-orm/0.18.0.md @@ -0,0 +1,4 @@ +- Improved join result types for partial selects (refer to the [docs](/docs/joins.md) page for more information) +- Renamed import paths for Postgres.js and SQL.js drivers to avoid bundling errors: + - `drizzle-orm/postgres.js` -> `drizzle-orm/postgres-js` + - `drizzle-orm/sql.js` -> `drizzle-orm/sql-js` From 8a420a137f8da1f07482b0951e72cf49e701bf92 Mon Sep 17 00:00:00 2001 From: Dan Kochetov Date: Mon, 6 Feb 2023 14:09:07 +0200 Subject: [PATCH 7/7] Fix docs typo --- docs/joins.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/joins.md b/docs/joins.md index 15c3f4d9a..13663efcf 100644 --- a/docs/joins.md +++ b/docs/joins.md @@ -209,7 +209,7 @@ But what happens if you group columns from multiple tables in the same nested ob ## Aggregating results -OK, so you have obtained all the cities and the users for every city. But what you **really** wanted is a **list** of users for every city, and what you currently have is a `city-user?` combinations. So, how do you transform it? +OK, so you have obtained all the cities and the users for every city. But what you **really** wanted is a **list** of users for every city, and what you currently have is an array of `city-user?` combinations. So, how do you transform it? That's the neat part - you can do that however you'd like! No hand-holding here. For example, one of the ways to do that would be `Array.reduce()`: