Skip to content
This repository was archived by the owner on Mar 1, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 3 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
3 changes: 2 additions & 1 deletion packages/cli/test/ts-schema-gen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ model Post {
{
name: 'onDelete',
value: {
kind: 'literal',
kind: 'enum',
type: 'ReferentialAction',
value: 'Cascade',
},
},
Expand Down
30 changes: 25 additions & 5 deletions packages/orm/src/client/crud/dialects/base-dialect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -498,17 +498,17 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
}

case 'has': {
clauses.push(this.eb(fieldRef, '@>', this.eb.val([value])));
clauses.push(this.buildArrayContains(fieldRef, this.eb.val(value)));
break;
}

case 'hasEvery': {
clauses.push(this.eb(fieldRef, '@>', this.eb.val(value)));
clauses.push(this.buildArrayHasEvery(fieldRef, this.eb.val(value)));
break;
}

case 'hasSome': {
clauses.push(this.eb(fieldRef, '&&', this.eb.val(value)));
clauses.push(this.buildArrayHasSome(fieldRef, this.eb.val(value)));
break;
}

Expand Down Expand Up @@ -1420,9 +1420,24 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
abstract buildArrayLength(array: Expression<unknown>): AliasableExpression<number>;

/**
* Builds an array literal SQL string for the given values.
* Builds an array value expression.
*/
abstract buildArrayLiteralSQL(values: unknown[]): AliasableExpression<unknown>;
abstract buildArrayValue(values: Expression<unknown>[]): AliasableExpression<unknown>;

/**
* Builds an expression that checks if an array contains a single value.
*/
abstract buildArrayContains(field: Expression<unknown>, value: Expression<unknown>): AliasableExpression<SqlBool>;

/**
* Builds an expression that checks if an array contains all values from another array.
*/
abstract buildArrayHasEvery(field: Expression<unknown>, values: Expression<unknown>): AliasableExpression<SqlBool>;

/**
* Builds an expression that checks if an array overlaps with another array.
*/
abstract buildArrayHasSome(field: Expression<unknown>, values: Expression<unknown>): AliasableExpression<SqlBool>;

/**
* Casts the given expression to an integer type.
Expand All @@ -1434,6 +1449,11 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
*/
abstract castText<T extends Expression<any>>(expression: T): T;

/**
* Casts the given expression to an enum type.
*/
abstract castEnum<T extends Expression<any>>(expression: T, enumType: string): T;

/**
* Trims double quotes from the start and end of a text expression.
*/
Expand Down
30 changes: 28 additions & 2 deletions packages/orm/src/client/crud/dialects/mysql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,11 @@ export class MySqlCrudDialect<Schema extends SchemaDef> extends LateralJoinDiale
return sql`CAST(${expression} AS CHAR CHARACTER SET utf8mb4)` as unknown as T;
}

override castEnum<T extends Expression<any>>(expression: T, _enumType: string): T {
// mysql doesn't need special enum casting
return expression;
}

override trimTextQuotes<T extends Expression<string>>(expression: T): T {
return sql`TRIM(BOTH ${sql.lit('"')} FROM ${expression})` as unknown as T;
}
Expand All @@ -223,8 +228,29 @@ export class MySqlCrudDialect<Schema extends SchemaDef> extends LateralJoinDiale
return this.eb.fn('JSON_LENGTH', [array]);
}

override buildArrayLiteralSQL(_values: unknown[]): AliasableExpression<number> {
throw new Error('MySQL does not support array literals');
override buildArrayValue(_values: Expression<unknown>[]): AliasableExpression<number> {
throw createNotSupportedError('MySQL does not support array value');
}

override buildArrayContains(
_field: Expression<unknown>,
_value: Expression<unknown>,
): AliasableExpression<SqlBool> {
throw createNotSupportedError('MySQL does not support native array operations');
}

override buildArrayHasEvery(
_field: Expression<unknown>,
_values: Expression<unknown>,
): AliasableExpression<SqlBool> {
throw createNotSupportedError('MySQL does not support native array operations');
}

override buildArrayHasSome(
_field: Expression<unknown>,
_values: Expression<unknown>,
): AliasableExpression<SqlBool> {
throw createNotSupportedError('MySQL does not support native array operations');
}

protected override buildJsonEqualityFilter(
Expand Down
65 changes: 22 additions & 43 deletions packages/orm/src/client/crud/dialects/postgresql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import type { BuiltinType, FieldDef, SchemaDef } from '../../../schema';
import type { SortOrder } from '../../crud-types';
import { createInternalError, createInvalidInputError } from '../../errors';
import type { ClientOptions } from '../../options';
import { getEnum, isEnum, isTypeDef } from '../../query-utils';
import { isEnum, isTypeDef } from '../../query-utils';
import { LateralJoinDialectBase } from './lateral-join-dialect-base';

export class PostgresCrudDialect<Schema extends SchemaDef> extends LateralJoinDialectBase<Schema> {
Expand Down Expand Up @@ -95,18 +95,7 @@ export class PostgresCrudDialect<Schema extends SchemaDef> extends LateralJoinDi
if (type === 'Json' && !forArrayField) {
// scalar `Json` fields need their input stringified
return JSON.stringify(value);
}
if (isEnum(this.schema, type)) {
// cast to enum array `CAST(ARRAY[...] AS "enum_type"[])`
return this.eb.cast(
sql`ARRAY[${sql.join(
value.map((v) => this.transformInput(v, type, false)),
sql.raw(','),
)}]`,
this.createSchemaQualifiedEnumType(type, true),
);
} else {
// `Json[]` fields need their input as array (not stringified)
return value.map((v) => this.transformInput(v, type, false));
}
} else {
Expand Down Expand Up @@ -136,32 +125,6 @@ export class PostgresCrudDialect<Schema extends SchemaDef> extends LateralJoinDi
}
}

private createSchemaQualifiedEnumType(type: string, array: boolean) {
// determines the postgres schema name for the enum type, and returns the
// qualified name

let qualified = type;

const enumDef = getEnum(this.schema, type);
if (enumDef) {
// check if the enum has a custom "@@schema" attribute
const schemaAttr = enumDef.attributes?.find((attr) => attr.name === '@@schema');
if (schemaAttr) {
const mapArg = schemaAttr.args?.find((arg) => arg.name === 'map');
if (mapArg && mapArg.value.kind === 'literal') {
const schemaName = mapArg.value.value as string;
qualified = `"${schemaName}"."${type}"`;
}
} else {
// no custom schema, use default from datasource or 'public'
const defaultSchema = this.schema.provider.defaultSchema ?? 'public';
qualified = `"${defaultSchema}"."${type}"`;
}
}

return array ? sql.raw(`${qualified}[]`) : sql.raw(qualified);
}

override transformOutput(value: unknown, type: BuiltinType, array: boolean) {
if (value === null || value === undefined) {
return value;
Expand Down Expand Up @@ -282,6 +245,10 @@ export class PostgresCrudDialect<Schema extends SchemaDef> extends LateralJoinDi
return this.eb.cast(expression, 'text') as unknown as T;
}

override castEnum<T extends Expression<any>>(expression: T, enumType: string): T {
return this.eb.cast(expression, sql.ref(enumType)) as unknown as T;
}

override trimTextQuotes<T extends Expression<string>>(expression: T): T {
return this.eb.fn('trim', [expression, sql.lit('"')]) as unknown as T;
}
Expand All @@ -290,17 +257,29 @@ export class PostgresCrudDialect<Schema extends SchemaDef> extends LateralJoinDi
return this.eb.fn('array_length', [array]);
}

override buildArrayLiteralSQL(values: unknown[]): AliasableExpression<unknown> {
override buildArrayValue(values: Expression<unknown>[]): AliasableExpression<unknown> {
if (values.length === 0) {
return sql`{}`;
} else {
return sql`ARRAY[${sql.join(
values.map((v) => sql.val(v)),
sql.raw(','),
)}]`;
return sql`ARRAY[${sql.join(values, sql.raw(','))}]`;
}
}

override buildArrayContains(field: Expression<unknown>, value: Expression<unknown>): AliasableExpression<SqlBool> {
// PostgreSQL @> operator expects array on both sides, so wrap single value in array
return this.eb(field, '@>', sql`ARRAY[${value}]`);
}

override buildArrayHasEvery(field: Expression<unknown>, values: Expression<unknown>): AliasableExpression<SqlBool> {
// PostgreSQL @> operator: field contains all elements in values
return this.eb(field, '@>', values);
}

override buildArrayHasSome(field: Expression<unknown>, values: Expression<unknown>): AliasableExpression<SqlBool> {
// PostgreSQL && operator: arrays have any elements in common
return this.eb(field, '&&', values);
}

protected override buildJsonPathSelection(receiver: Expression<any>, path: string | undefined) {
if (path) {
return this.eb.fn('jsonb_path_query_first', [receiver, this.eb.val(path)]);
Expand Down
30 changes: 28 additions & 2 deletions packages/orm/src/client/crud/dialects/sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,8 +447,29 @@ export class SqliteCrudDialect<Schema extends SchemaDef> extends BaseCrudDialect
return this.eb.fn('json_array_length', [array]);
}

override buildArrayLiteralSQL(_values: unknown[]): AliasableExpression<unknown> {
throw new Error('SQLite does not support array literals');
override buildArrayValue(_values: Expression<unknown>[]): AliasableExpression<unknown> {
throw new Error('SQLite does not support array values');
}
Comment thread
ymc9 marked this conversation as resolved.
Outdated

override buildArrayContains(
_field: Expression<unknown>,
_value: Expression<unknown>,
): AliasableExpression<SqlBool> {
throw createNotSupportedError('SQLite does not support native array operations');
}

override buildArrayHasEvery(
_field: Expression<unknown>,
_values: Expression<unknown>,
): AliasableExpression<SqlBool> {
throw createNotSupportedError('SQLite does not support native array operations');
}

override buildArrayHasSome(
_field: Expression<unknown>,
_values: Expression<unknown>,
): AliasableExpression<SqlBool> {
throw createNotSupportedError('SQLite does not support native array operations');
}

override castInt<T extends Expression<any>>(expression: T): T {
Expand All @@ -459,6 +480,11 @@ export class SqliteCrudDialect<Schema extends SchemaDef> extends BaseCrudDialect
return this.eb.cast(expression, 'text') as unknown as T;
}

override castEnum<T extends Expression<any>>(expression: T, _enumType: string): T {
// sqlite doesn't need special enum casting
return expression;
}

override trimTextQuotes<T extends Expression<string>>(expression: T): T {
return this.eb.fn('trim', [expression, sql.lit('"')]) as unknown as T;
}
Expand Down
1 change: 1 addition & 0 deletions packages/orm/src/client/crud/validator/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ function applyValidation(
function evalExpression(data: any, expr: Expression): unknown {
return match(expr)
.with({ kind: 'literal' }, (e) => e.value)
.with({ kind: 'enum' }, (e) => e.value)
.with({ kind: 'array' }, (e) => e.items.map((item) => evalExpression(data, item)))
.with({ kind: 'field' }, (e) => evalField(data, e))
.with({ kind: 'member' }, (e) => evalMember(data, e))
Expand Down
Loading
Loading