Skip to content

Commit 0d25fbb

Browse files
feat: allow expressions in unique constraint (#1518)
Co-authored-by: Eric So <[email protected]> Co-authored-by: Igal Klebanov <[email protected]>
1 parent 3821fb5 commit 0d25fbb

File tree

4 files changed

+137
-16
lines changed

4 files changed

+137
-16
lines changed
Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { freeze } from '../util/object-utils.js'
1+
import { logOnce } from '../util/log-once.js'
2+
import { freeze, isString } from '../util/object-utils.js'
23
import { ColumnNode } from './column-node.js'
34
import { IdentifierNode } from './identifier-node.js'
45
import { OperationNode } from './operation-node.js'
56

67
export interface UniqueConstraintNode extends OperationNode {
78
readonly kind: 'UniqueConstraintNode'
8-
readonly columns: ReadonlyArray<ColumnNode>
9+
readonly columns: ReadonlyArray<OperationNode>
910
readonly name?: IdentifierNode
1011
readonly nullsNotDistinct?: boolean
1112
readonly deferrable?: boolean
@@ -18,21 +19,56 @@ export type UniqueConstraintNodeProps = Omit<
1819
>
1920

2021
/**
22+
* TODO: remove this interface once support for `string[]` is removed.
23+
*
2124
* @internal
2225
*/
23-
export const UniqueConstraintNode = freeze({
26+
interface UniqueConstraintNodeFactory {
27+
is(node: OperationNode): node is UniqueConstraintNode
28+
create(
29+
columns: OperationNode[],
30+
constraintName?: string,
31+
nullsNotDistinct?: boolean,
32+
): UniqueConstraintNode
33+
/**
34+
* @deprecated pass `ColumnNode[]` instead of strings.
35+
*/
36+
create(
37+
columns: string[],
38+
constraintName?: string,
39+
nullsNotDistinct?: boolean,
40+
): UniqueConstraintNode
41+
cloneWith(
42+
node: UniqueConstraintNode,
43+
props: UniqueConstraintNodeProps,
44+
): UniqueConstraintNode
45+
}
46+
47+
/**
48+
* @internal
49+
*/
50+
export const UniqueConstraintNode: UniqueConstraintNodeFactory = freeze({
2451
is(node: OperationNode): node is UniqueConstraintNode {
2552
return node.kind === 'UniqueConstraintNode'
2653
},
2754

2855
create(
29-
columns: string[],
56+
columns: string[] | OperationNode[],
3057
constraintName?: string,
3158
nullsNotDistinct?: boolean,
3259
): UniqueConstraintNode {
60+
// TODO: remove this block when support for `string[]` is removed.
61+
if (isString(columns.at(0))) {
62+
logOnce(
63+
'`UniqueConstraintNode.create(columns: string[], ...)` is deprecated - pass `ColumnNode[]` instead.',
64+
)
65+
66+
columns = (columns as string[]).map(ColumnNode.create)
67+
}
68+
3369
return freeze({
3470
kind: 'UniqueConstraintNode',
35-
columns: freeze(columns.map(ColumnNode.create)),
71+
columns: freeze(columns) as OperationNode[],
3672
name: constraintName ? IdentifierNode.create(constraintName) : undefined,
3773
nullsNotDistinct,
3874
})
@@ -42,9 +78,6 @@ export const UniqueConstraintNode = freeze({
4278
node: UniqueConstraintNode,
4379
props: UniqueConstraintNodeProps,
4480
): UniqueConstraintNode {
45-
return freeze({
46-
...node,
47-
...props,
48-
})
81+
return freeze({ ...node, ...props })
4982
},
5083
})

src/schema/alter-table-builder.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { OperationNodeSource } from '../operation-node/operation-node-source.js'
77
import { RenameColumnNode } from '../operation-node/rename-column-node.js'
88
import { CompiledQuery } from '../query-compiler/compiled-query.js'
99
import { Compilable } from '../util/compilable.js'
10-
import { freeze, noop } from '../util/object-utils.js'
10+
import { freeze, isString, noop } from '../util/object-utils.js'
1111
import {
1212
ColumnDefinitionBuilder,
1313
ColumnDefinitionBuilderCallback,
@@ -55,6 +55,10 @@ import {
5555
CheckConstraintBuilderCallback,
5656
} from './check-constraint-builder.js'
5757
import { RenameConstraintNode } from '../operation-node/rename-constraint-node.js'
58+
import {
59+
ExpressionOrFactory,
60+
parseExpression,
61+
} from '../parser/expression-parser.js'
5862

5963
/**
6064
* This builder can be used to create a `alter table` query.
@@ -173,12 +177,19 @@ export class AlterTableBuilder implements ColumnAlteringInterface {
173177
*/
174178
addUniqueConstraint(
175179
constraintName: string,
176-
columns: string[],
180+
columns: (string | ExpressionOrFactory<any, any, any>)[],
177181
build: UniqueConstraintNodeBuilderCallback = noop,
178182
): AlterTableExecutor {
179183
const uniqueConstraintBuilder = build(
180184
new UniqueConstraintNodeBuilder(
181-
UniqueConstraintNode.create(columns, constraintName),
185+
UniqueConstraintNode.create(
186+
columns.map((column) =>
187+
isString(column)
188+
? ColumnNode.create(column)
189+
: parseExpression(column),
190+
),
191+
constraintName,
192+
),
182193
),
183194
)
184195

src/schema/create-table-builder.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Compilable } from '../util/compilable.js'
99
import { QueryExecutor } from '../query-executor/query-executor.js'
1010
import { ColumnDefinitionBuilder } from './column-definition-builder.js'
1111
import { QueryId } from '../util/query-id.js'
12-
import { freeze, noop } from '../util/object-utils.js'
12+
import { freeze, isString, noop } from '../util/object-utils.js'
1313
import { ForeignKeyConstraintNode } from '../operation-node/foreign-key-constraint-node.js'
1414
import { ColumnNode } from '../operation-node/column-node.js'
1515
import {
@@ -30,7 +30,10 @@ import {
3030
UniqueConstraintNodeBuilder,
3131
UniqueConstraintNodeBuilderCallback,
3232
} from './unique-constraint-builder.js'
33-
import { parseExpression } from '../parser/expression-parser.js'
33+
import {
34+
ExpressionOrFactory,
35+
parseExpression,
36+
} from '../parser/expression-parser.js'
3437
import {
3538
PrimaryKeyConstraintBuilder,
3639
PrimaryKeyConstraintBuilderCallback,
@@ -243,15 +246,39 @@ export class CreateTableBuilder<TB extends string, C extends string = never>
243246
* )
244247
* .execute()
245248
* ```
249+
*
250+
* In dialects such as MySQL you create unique constraints on expressions as follows:
251+
*
252+
* ```ts
253+
*
254+
* import { sql } from 'kysely'
255+
*
256+
* await db.schema
257+
* .createTable('person')
258+
* .addColumn('first_name', 'varchar(64)')
259+
* .addColumn('last_name', 'varchar(64)')
260+
* .addUniqueConstraint(
261+
* 'first_name_last_name_unique',
262+
* [sql`(lower('first_name'))`, 'last_name']
263+
* )
264+
* .execute()
265+
* ```
246266
*/
247267
addUniqueConstraint(
248268
constraintName: string,
249-
columns: C[],
269+
columns: (C | ExpressionOrFactory<any, any, any>)[],
250270
build: UniqueConstraintNodeBuilderCallback = noop,
251271
): CreateTableBuilder<TB, C> {
252272
const uniqueConstraintBuilder = build(
253273
new UniqueConstraintNodeBuilder(
254-
UniqueConstraintNode.create(columns, constraintName),
274+
UniqueConstraintNode.create(
275+
columns.map((column) =>
276+
isString(column)
277+
? ColumnNode.create(column)
278+
: parseExpression(column),
279+
),
280+
constraintName,
281+
),
255282
),
256283
)
257284

test/node/src/schema.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,33 @@ for (const dialect of DIALECTS) {
517517
await builder.execute()
518518
})
519519

520+
if (sqlSpec === 'mysql') {
521+
it('should create a table with a unique constraints using expressions', async () => {
522+
const builder = ctx.db.schema
523+
.createTable('test')
524+
.addColumn('a', 'varchar(255)')
525+
.addColumn('b', 'varchar(255)')
526+
.addColumn('c', 'varchar(255)')
527+
.addUniqueConstraint('a_b_unique', [
528+
sql`(lower(a))`,
529+
sql`(lower(b))`,
530+
])
531+
.addUniqueConstraint('a_c_unique', [sql`(lower(a))`, 'c'])
532+
533+
testSql(builder, dialect, {
534+
postgres: NOT_SUPPORTED,
535+
mysql: {
536+
sql: 'create table `test` (`a` varchar(255), `b` varchar(255), `c` varchar(255), constraint `a_b_unique` unique ((lower(a)), (lower(b))), constraint `a_c_unique` unique ((lower(a)), `c`))',
537+
parameters: [],
538+
},
539+
mssql: NOT_SUPPORTED,
540+
sqlite: NOT_SUPPORTED,
541+
})
542+
543+
await builder.execute()
544+
})
545+
}
546+
520547
if (sqlSpec === 'postgres') {
521548
it('should create a table with a unique constraint and "nulls not distinct" option', async () => {
522549
const builder = ctx.db.schema
@@ -3367,6 +3394,29 @@ for (const dialect of DIALECTS) {
33673394
})
33683395
}
33693396

3397+
if (sqlSpec === 'mysql') {
3398+
it('should add a unique constraint using expressions', async () => {
3399+
const builder = ctx.db.schema
3400+
.alterTable('test')
3401+
.addUniqueConstraint('unique_constraint', [
3402+
sql`(lower(varchar_col))`,
3403+
'integer_col',
3404+
])
3405+
3406+
testSql(builder, dialect, {
3407+
postgres: NOT_SUPPORTED,
3408+
mysql: {
3409+
sql: 'alter table `test` add constraint `unique_constraint` unique ((lower(varchar_col)), `integer_col`)',
3410+
parameters: [],
3411+
},
3412+
mssql: NOT_SUPPORTED,
3413+
sqlite: NOT_SUPPORTED,
3414+
})
3415+
3416+
await builder.execute()
3417+
})
3418+
}
3419+
33703420
if (
33713421
sqlSpec === 'postgres' ||
33723422
sqlSpec === 'mysql' ||

0 commit comments

Comments
 (0)