Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add some view utils #3553

Merged
merged 7 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions drizzle-orm/src/mysql-core/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type { ColumnsSelection, SQL } from '~/sql/sql.ts';
import { getTableColumns } from '~/utils.ts';
import type { MySqlColumn, MySqlColumnBuilderBase } from './columns/index.ts';
import { QueryBuilder } from './query-builders/query-builder.ts';
import type { SelectedFields } from './query-builders/select.types.ts';
import { mysqlTable } from './table.ts';
import { MySqlViewBase } from './view-base.ts';
import { MySqlViewConfig } from './view-common.ts';
Expand Down Expand Up @@ -58,7 +57,7 @@ export class ViewBuilderCore<TConfig extends { name: string; columns?: unknown }
export class ViewBuilder<TName extends string = string> extends ViewBuilderCore<{ name: TName }> {
static override readonly [entityKind]: string = 'MySqlViewBuilder';

as<TSelectedFields extends SelectedFields>(
as<TSelectedFields extends ColumnsSelection>(
qb: TypedQueryBuilder<TSelectedFields> | ((qb: QueryBuilder) => TypedQueryBuilder<TSelectedFields>),
): MySqlViewWithSelection<TName, false, AddAliasToSelection<TSelectedFields, TName, 'mysql'>> {
if (typeof qb === 'function') {
Expand Down Expand Up @@ -160,7 +159,7 @@ export class MySqlView<
config: {
name: TName;
schema: string | undefined;
selectedFields: SelectedFields;
selectedFields: ColumnsSelection;
query: SQL | undefined;
};
}) {
Expand Down
9 changes: 4 additions & 5 deletions drizzle-orm/src/pg-core/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { getTableColumns } from '~/utils.ts';
import type { RequireAtLeastOne } from '~/utils.ts';
import type { PgColumn, PgColumnBuilderBase } from './columns/common.ts';
import { QueryBuilder } from './query-builders/query-builder.ts';
import type { SelectedFields } from './query-builders/select.types.ts';
import { pgTable } from './table.ts';
import { PgViewBase } from './view-base.ts';
import { PgViewConfig } from './view-common.ts';
Expand Down Expand Up @@ -45,7 +44,7 @@ export class DefaultViewBuilderCore<TConfig extends { name: string; columns?: un
export class ViewBuilder<TName extends string = string> extends DefaultViewBuilderCore<{ name: TName }> {
static override readonly [entityKind]: string = 'PgViewBuilder';

as<TSelectedFields extends SelectedFields>(
as<TSelectedFields extends ColumnsSelection>(
qb: TypedQueryBuilder<TSelectedFields> | ((qb: QueryBuilder) => TypedQueryBuilder<TSelectedFields>),
): PgViewWithSelection<TName, false, AddAliasToSelection<TSelectedFields, TName, 'pg'>> {
if (typeof qb === 'function') {
Expand Down Expand Up @@ -198,7 +197,7 @@ export class MaterializedViewBuilder<TName extends string = string>
{
static override readonly [entityKind]: string = 'PgMaterializedViewBuilder';

as<TSelectedFields extends SelectedFields>(
as<TSelectedFields extends ColumnsSelection>(
qb: TypedQueryBuilder<TSelectedFields> | ((qb: QueryBuilder) => TypedQueryBuilder<TSelectedFields>),
): PgMaterializedViewWithSelection<TName, false, AddAliasToSelection<TSelectedFields, TName, 'pg'>> {
if (typeof qb === 'function') {
Expand Down Expand Up @@ -317,7 +316,7 @@ export class PgView<
config: {
name: TName;
schema: string | undefined;
selectedFields: SelectedFields;
selectedFields: ColumnsSelection;
query: SQL | undefined;
};
}) {
Expand Down Expand Up @@ -362,7 +361,7 @@ export class PgMaterializedView<
config: {
name: TName;
schema: string | undefined;
selectedFields: SelectedFields;
selectedFields: ColumnsSelection;
query: SQL | undefined;
};
}) {
Expand Down
2 changes: 2 additions & 0 deletions drizzle-orm/src/query-builders/select.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export type AddAliasToSelection<
: {
[Key in keyof TSelection]: TSelection[Key] extends Column
? ChangeColumnTableName<TSelection[Key], TAlias, TDialect>
: TSelection[Key] extends Table ? AddAliasToSelection<TSelection[Key]['_']['columns'], TAlias, TDialect>
: TSelection[Key] extends SQL | SQL.Aliased ? TSelection[Key]
: TSelection[Key] extends ColumnsSelection ? MapColumnsToTableAlias<TSelection[Key], TAlias, TDialect>
: never;
Expand Down Expand Up @@ -120,6 +121,7 @@ export type BuildSubquerySelection<
[Key in keyof TSelection]: TSelection[Key] extends SQL
? DrizzleTypeError<'You cannot reference this field without assigning it an alias first - use `.as(<alias>)`'>
: TSelection[Key] extends SQL.Aliased ? TSelection[Key]
: TSelection[Key] extends Table ? BuildSubquerySelection<TSelection[Key]['_']['columns'], TNullability>
: TSelection[Key] extends Column
? ApplyNullabilityToColumn<TSelection[Key], TNullability[TSelection[Key]['_']['tableName']]>
: TSelection[Key] extends ColumnsSelection ? BuildSubquerySelection<TSelection[Key], TNullability>
Expand Down
26 changes: 23 additions & 3 deletions drizzle-orm/src/sql/sql.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { CasingCache } from '~/casing.ts';
import { entityKind, is } from '~/entity.ts';
import type { SelectedFields } from '~/operations.ts';
import { isPgEnum } from '~/pg-core/columns/enum.ts';
import type { SelectResult } from '~/query-builders/select.types.ts';
import { Subquery } from '~/subquery.ts';
import { tracer } from '~/tracing.ts';
import type { Assume, Equal } from '~/utils.ts';
import { ViewBaseConfig } from '~/view-common.ts';
import type { AnyColumn } from '../column.ts';
import { Column } from '../column.ts';
Expand Down Expand Up @@ -617,6 +618,8 @@ export function fillPlaceholders(params: unknown[], values: Record<string, unkno

export type ColumnsSelection = Record<string, unknown>;

const IsDrizzleView = Symbol.for('drizzle:IsDrizzleView');

export abstract class View<
TName extends string = string,
TExisting extends boolean = boolean,
Expand All @@ -637,17 +640,22 @@ export abstract class View<
name: TName;
originalName: TName;
schema: string | undefined;
selectedFields: SelectedFields<AnyColumn, Table>;
selectedFields: ColumnsSelection;
isExisting: TExisting;
query: TExisting extends true ? undefined : SQL;
isAlias: boolean;
};

/** @internal */
[IsDrizzleView] = true;

declare readonly $inferSelect: InferSelectViewModel<View<Assume<TName, string>, TExisting, TSelection>>;

constructor(
{ name, schema, selectedFields, query }: {
name: TName;
schema: string | undefined;
selectedFields: SelectedFields<AnyColumn, Table>;
selectedFields: ColumnsSelection;
query: SQL | undefined;
},
) {
Expand All @@ -667,6 +675,18 @@ export abstract class View<
}
}

export function isView(view: unknown): view is View {
return typeof view === 'object' && view !== null && IsDrizzleView in view;
}

export type InferSelectViewModel<TView extends View> =
Equal<TView['_']['selectedFields'], { [x: string]: unknown }> extends true ? { [x: string]: unknown }
: SelectResult<
TView['_']['selectedFields'],
'single',
Record<TView['_']['name'], 'not-null'>
>;

// Defined separately from the Column class to resolve circular dependency
Column.prototype.getSQL = function() {
return new SQL([this]);
Expand Down
5 changes: 2 additions & 3 deletions drizzle-orm/src/sqlite-core/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type { ColumnsSelection, SQL } from '~/sql/sql.ts';
import { getTableColumns } from '~/utils.ts';
import type { SQLiteColumn, SQLiteColumnBuilderBase } from './columns/common.ts';
import { QueryBuilder } from './query-builders/query-builder.ts';
import type { SelectedFields } from './query-builders/select.types.ts';
import { sqliteTable } from './table.ts';
import { SQLiteViewBase } from './view-base.ts';

Expand Down Expand Up @@ -38,7 +37,7 @@ export class ViewBuilderCore<
export class ViewBuilder<TName extends string = string> extends ViewBuilderCore<{ name: TName }> {
static override readonly [entityKind]: string = 'SQLiteViewBuilder';

as<TSelection extends SelectedFields>(
as<TSelection extends ColumnsSelection>(
qb: TypedQueryBuilder<TSelection> | ((qb: QueryBuilder) => TypedQueryBuilder<TSelection>),
): SQLiteViewWithSelection<TName, false, AddAliasToSelection<TSelection, TName, 'sqlite'>> {
if (typeof qb === 'function') {
Expand Down Expand Up @@ -135,7 +134,7 @@ export class SQLiteView<
config: {
name: TName;
schema: string | undefined;
selectedFields: SelectedFields;
selectedFields: ColumnsSelection;
query: SQL | undefined;
};
}) {
Expand Down
4 changes: 4 additions & 0 deletions drizzle-orm/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,10 @@ export function getTableColumns<T extends Table>(table: T): T['_']['columns'] {
return table[Table.Symbol.Columns];
}

export function getViewSelectedFields<T extends View>(view: T): T['_']['selectedFields'] {
return view[ViewBaseConfig].selectedFields;
}

/** @internal */
export function getTableLikeName(table: TableLike): string | undefined {
return is(table, Subquery)
Expand Down
49 changes: 47 additions & 2 deletions drizzle-orm/type-tests/mysql/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,19 @@ import {
or,
} from '~/expressions.ts';
import { alias } from '~/mysql-core/alias.ts';
import { param, sql } from '~/sql/sql.ts';
import { type InferSelectViewModel, param, sql } from '~/sql/sql.ts';

import type { Equal } from 'type-tests/utils.ts';
import { Expect } from 'type-tests/utils.ts';
import { type MySqlSelect, type MySqlSelectQueryBuilder, QueryBuilder } from '~/mysql-core/index.ts';
import {
int,
type MySqlSelect,
type MySqlSelectQueryBuilder,
mysqlTable,
mysqlView,
QueryBuilder,
text,
} from '~/mysql-core/index.ts';
import { db } from './db.ts';
import { cities, classes, newYorkers, users } from './tables.ts';

Expand Down Expand Up @@ -604,3 +612,40 @@ await db
// @ts-expect-error method was already called
.for('update');
}

{
const table1 = mysqlTable('table1', {
id: int().primaryKey(),
name: text().notNull(),
});
const table2 = mysqlTable('table2', {
id: int().primaryKey(),
age: int().notNull(),
});
const table3 = mysqlTable('table3', {
id: int().primaryKey(),
phone: text().notNull(),
});
const view = mysqlView('view').as((qb) =>
qb.select({
table: table1,
column: table2.age,
nested: {
column: table3.phone,
},
}).from(table1).innerJoin(table2, sql``).leftJoin(table3, sql``)
);
const result = await db.select().from(view);

Expect<
Equal<typeof result, {
table: typeof table1.$inferSelect;
column: number;
nested: {
column: string | null;
};
}[]>
>;
Expect<Equal<typeof result, typeof view.$inferSelect[]>>;
Expect<Equal<typeof result, InferSelectViewModel<typeof view>[]>>;
}
78 changes: 77 additions & 1 deletion drizzle-orm/type-tests/pg/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ import { alias } from '~/pg-core/alias.ts';
import {
boolean,
integer,
pgMaterializedView,
type PgSelect,
type PgSelectQueryBuilder,
pgTable,
pgView,
QueryBuilder,
text,
} from '~/pg-core/index.ts';
import { type SQL, sql } from '~/sql/sql.ts';
import { type InferSelectViewModel, type SQL, sql } from '~/sql/sql.ts';

import { db } from './db.ts';
import { cities, classes, newYorkers, newYorkers2, users } from './tables.ts';
Expand Down Expand Up @@ -1156,3 +1158,77 @@ await db
),
);
}

{
const table1 = pgTable('table1', {
id: integer().primaryKey(),
name: text().notNull(),
});
const table2 = pgTable('table2', {
id: integer().primaryKey(),
age: integer().notNull(),
});
const table3 = pgTable('table3', {
id: integer().primaryKey(),
phone: text().notNull(),
});
const view = pgView('view').as((qb) =>
qb.select({
table: table1,
column: table2.age,
nested: {
column: table3.phone,
},
}).from(table1).innerJoin(table2, sql``).leftJoin(table3, sql``)
);
const result = await db.select().from(view);

Expect<
Equal<typeof result, {
table: typeof table1.$inferSelect;
column: number;
nested: {
column: string | null;
};
}[]>
>;
Expect<Equal<typeof result, typeof view.$inferSelect[]>>;
Expect<Equal<typeof result, InferSelectViewModel<typeof view>[]>>;
}

{
const table1 = pgTable('table1', {
id: integer().primaryKey(),
name: text().notNull(),
});
const table2 = pgTable('table2', {
id: integer().primaryKey(),
age: integer().notNull(),
});
const table3 = pgTable('table3', {
id: integer().primaryKey(),
phone: text().notNull(),
});
const view = pgMaterializedView('view').as((qb) =>
qb.select({
table: table1,
column: table2.age,
nested: {
column: table3.phone,
},
}).from(table1).innerJoin(table2, sql``).leftJoin(table3, sql``)
);
const result = await db.select().from(view);

Expect<
Equal<typeof result, {
table: typeof table1.$inferSelect;
column: number;
nested: {
column: string | null;
};
}[]>
>;
Expect<Equal<typeof result, typeof view.$inferSelect[]>>;
Expect<Equal<typeof result, InferSelectViewModel<typeof view>[]>>;
}
Loading