Skip to content

Commit

Permalink
add case statements/operators. (#404)
Browse files Browse the repository at this point in the history
* add case and when nodes.

* add case builder.

* add `.case` to expression builder.

* add `.case` to Kysely.

* add case & when node compilation.

* add case test suite.

* fix single argument filter parsing for when and beyond.

* add when/then/else overloads for common use cases @ case builder.

* add more complex case tests.

* freeze lists @ case node.

Co-authored-by: Sami Koskimäki <[email protected]>

* simplify case example.

* add case typings tests.

---------

Co-authored-by: Sami Koskimäki <[email protected]>
Co-authored-by: Sami Koskimäki <[email protected]>
  • Loading branch information
3 people authored May 13, 2023
1 parent bf5613f commit 16a589d
Show file tree
Hide file tree
Showing 19 changed files with 940 additions and 3 deletions.
76 changes: 76 additions & 0 deletions src/expression/expression-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ import {
} from '../query-builder/function-module.js'
import {
ExtractTypeFromReferenceExpression,
parseReferenceExpression,
parseStringReference,
ReferenceExpression,
SimpleReferenceExpression,
StringReference,
} from '../parser/reference-parser.js'
import { QueryExecutor } from '../query-executor/query-executor.js'
Expand Down Expand Up @@ -47,6 +49,9 @@ import {
} from '../parser/value-parser.js'
import { NOOP_QUERY_EXECUTOR } from '../query-executor/noop-query-executor.js'
import { ValueNode } from '../operation-node/value-node.js'
import { CaseBuilder } from '../query-builder/case-builder.js'
import { CaseNode } from '../operation-node/case-node.js'
import { isUndefined } from '../util/object-utils.js'

export interface ExpressionBuilder<DB, TB extends keyof DB> {
/**
Expand Down Expand Up @@ -150,6 +155,65 @@ export interface ExpressionBuilder<DB, TB extends keyof DB> {
from: TE
): SelectQueryBuilder<From<DB, TE>, FromTables<DB, TB, TE>, {}>

/**
* Creates a `case` statement/operator.
*
* ### Examples
*
* Kitchen sink example with 2 flavors of `case` operator:
*
* ```ts
* import { sql } from 'kysely'
*
* const { title, name } = await db
* .selectFrom('person')
* .where('id', '=', '123')
* .select((eb) => [
* eb.fn.coalesce('last_name', 'first_name').as('name'),
* eb
* .case()
* .when('gender', '=', 'male')
* .then('Mr.')
* .when('gender', '=', 'female')
* .then(
* eb
* .case('martialStatus')
* .when('single')
* .then('Ms.')
* .else('Mrs.')
* .end()
* )
* .end()
* .as('title'),
* ])
* .executeTakeFirstOrThrow()
* ```
*
* The generated SQL (PostgreSQL):
*
* ```sql
* select
* coalesce("last_name", "first_name") as "name",
* case
* when "gender" = $1 then $2
* when "gender" = $3 then
* case "martialStatus"
* when $4 then $5
* else $6
* end
* end as "title"
* from "person"
* where "id" = $7
* ```
*/
case(): CaseBuilder<DB, TB>

case<C extends SimpleReferenceExpression<DB, TB>>(
column: C
): CaseBuilder<DB, TB, ExtractTypeFromReferenceExpression<DB, TB, C>>

case<O>(expression: Expression<O>): CaseBuilder<DB, TB, O>

/**
* This can be used to reference columns.
*
Expand Down Expand Up @@ -505,6 +569,18 @@ export function createExpressionBuilder<DB, TB extends keyof DB>(
})
},

case<RE extends ReferenceExpression<DB, TB>>(
reference?: RE
): CaseBuilder<DB, TB, ExtractTypeFromReferenceExpression<DB, TB, RE>> {
return new CaseBuilder({
node: CaseNode.create(
isUndefined(reference)
? undefined
: parseReferenceExpression(reference)
),
})
},

ref<RE extends StringReference<DB, TB>>(
reference: RE
): ExpressionWrapper<ExtractTypeFromReferenceExpression<DB, TB, RE>> {
Expand Down
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export {
ExpressionBuilder,
expressionBuilder,
} from './expression/expression-builder.js'
export * from './expression/expression-wrapper.js'

export * from './query-builder/where-interface.js'
export * from './query-builder/returning-interface.js'
Expand All @@ -22,6 +23,7 @@ export * from './query-builder/delete-result.js'
export * from './query-builder/update-result.js'
export * from './query-builder/on-conflict-builder.js'
export * from './query-builder/aggregate-function-builder.js'
export * from './query-builder/case-builder.js'

export * from './raw-builder/raw-builder.js'
export * from './raw-builder/sql.js'
Expand Down Expand Up @@ -102,6 +104,7 @@ export * from './operation-node/alias-node.js'
export * from './operation-node/alter-column-node.js'
export * from './operation-node/alter-table-node.js'
export * from './operation-node/and-node.js'
export * from './operation-node/case-node.js'
export * from './operation-node/check-constraint-node.js'
export * from './operation-node/column-definition-node.js'
export * from './operation-node/column-node.js'
Expand Down Expand Up @@ -167,6 +170,7 @@ export * from './operation-node/update-query-node.js'
export * from './operation-node/value-list-node.js'
export * from './operation-node/value-node.js'
export * from './operation-node/values-node.js'
export * from './operation-node/when-node.js'
export * from './operation-node/where-node.js'
export * from './operation-node/with-node.js'
export * from './operation-node/explain-node.js'
Expand Down
23 changes: 22 additions & 1 deletion src/kysely.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { QueryCreator, QueryCreatorProps } from './query-creator.js'
import { KyselyPlugin } from './plugin/kysely-plugin.js'
import { DefaultQueryExecutor } from './query-executor/default-query-executor.js'
import { DatabaseIntrospector } from './dialect/database-introspector.js'
import { freeze, isObject } from './util/object-utils.js'
import { freeze, isObject, isUndefined } from './util/object-utils.js'
import { RuntimeDriver } from './driver/runtime-driver.js'
import { SingleConnectionProvider } from './driver/single-connection-provider.js'
import {
Expand All @@ -27,6 +27,10 @@ import { QueryResult } from './driver/database-connection.js'
import { CompiledQuery } from './query-compiler/compiled-query.js'
import { createQueryId, QueryId } from './util/query-id.js'
import { Compilable, isCompilable } from './util/compilable.js'
import { CaseBuilder } from './query-builder/case-builder.js'
import { CaseNode } from './operation-node/case-node.js'
import { parseExpression } from './parser/expression-parser.js'
import { Expression } from './expression/expression.js'
import { WithSchemaPlugin } from './plugin/with-schema/with-schema-plugin.js'

/**
Expand Down Expand Up @@ -142,6 +146,23 @@ export class Kysely<DB>
return this.#props.dialect.createIntrospector(this.withoutPlugins())
}

/**
* Creates a `case` statement/operator.
*
* See {@link ExpressionBuilder.case} for more information.
*/
case(): CaseBuilder<DB, keyof DB>

case<V>(value: Expression<V>): CaseBuilder<DB, keyof DB, V>

case<V>(value?: Expression<V>): any {
return new CaseBuilder({
node: CaseNode.create(
isUndefined(value) ? undefined : parseExpression(value)
),
})
}

/**
* Returns a {@link FunctionModule} that can be used to write type safe function
* calls.
Expand Down
59 changes: 59 additions & 0 deletions src/operation-node/case-node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { freeze } from '../util/object-utils.js'
import { OperationNode } from './operation-node.js'
import { WhenNode } from './when-node.js'

export interface CaseNode extends OperationNode {
readonly kind: 'CaseNode'
readonly value?: OperationNode
readonly when?: ReadonlyArray<WhenNode>
readonly else?: OperationNode
readonly isStatement?: boolean
}

/**
* @internal
*/
export const CaseNode = freeze({
is(node: OperationNode): node is CaseNode {
return node.kind === 'CaseNode'
},

create(value?: OperationNode): CaseNode {
return freeze({
kind: 'CaseNode',
value,
})
},

cloneWithWhen(caseNode: CaseNode, when: WhenNode): CaseNode {
return freeze({
...caseNode,
when: freeze(caseNode.when ? [...caseNode.when, when] : [when]),
})
},

cloneWithThen(caseNode: CaseNode, then: OperationNode): CaseNode {
return freeze({
...caseNode,
when: caseNode.when
? freeze([
...caseNode.when.slice(0, -1),
WhenNode.cloneWithResult(
caseNode.when[caseNode.when.length - 1],
then
),
])
: undefined,
})
},

cloneWith(
caseNode: CaseNode,
props: Partial<Pick<CaseNode, 'else' | 'isStatement'>>
): CaseNode {
return freeze({
...caseNode,
...props,
})
},
})
22 changes: 22 additions & 0 deletions src/operation-node/operation-node-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ import { BinaryOperationNode } from './binary-operation-node.js'
import { UnaryOperationNode } from './unary-operation-node.js'
import { UsingNode } from './using-node.js'
import { FunctionNode } from './function-node.js'
import { CaseNode } from './case-node.js'
import { WhenNode } from './when-node.js'

/**
* Transforms an operation node tree into another one.
Expand Down Expand Up @@ -194,6 +196,8 @@ export class OperationNodeTransformer {
UnaryOperationNode: this.transformUnaryOperation.bind(this),
UsingNode: this.transformUsing.bind(this),
FunctionNode: this.transformFunction.bind(this),
CaseNode: this.transformCase.bind(this),
WhenNode: this.transformWhen.bind(this),
})

transformNode<T extends OperationNode | undefined>(node: T): T {
Expand Down Expand Up @@ -898,6 +902,24 @@ export class OperationNodeTransformer {
})
}

protected transformCase(node: CaseNode): CaseNode {
return requireAllProps<CaseNode>({
kind: 'CaseNode',
value: this.transformNode(node.value),
when: this.transformNodeList(node.when),
else: this.transformNode(node.else),
isStatement: node.isStatement,
})
}

protected transformWhen(node: WhenNode): WhenNode {
return requireAllProps<WhenNode>({
kind: 'WhenNode',
condition: this.transformNode(node.condition),
result: this.transformNode(node.result),
})
}

protected transformDataType(node: DataTypeNode): DataTypeNode {
// An Object.freezed leaf node. No need to clone.
return node
Expand Down
6 changes: 6 additions & 0 deletions src/operation-node/operation-node-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ import { BinaryOperationNode } from './binary-operation-node.js'
import { UnaryOperationNode } from './unary-operation-node.js'
import { UsingNode } from './using-node.js'
import { FunctionNode } from './function-node.js'
import { WhenNode } from './when-node.js'
import { CaseNode } from './case-node.js'

export abstract class OperationNodeVisitor {
protected readonly nodeStack: OperationNode[] = []
Expand Down Expand Up @@ -171,6 +173,8 @@ export abstract class OperationNodeVisitor {
UnaryOperationNode: this.visitUnaryOperation.bind(this),
UsingNode: this.visitUsing.bind(this),
FunctionNode: this.visitFunction.bind(this),
CaseNode: this.visitCase.bind(this),
WhenNode: this.visitWhen.bind(this),
})

protected readonly visitNode = (node: OperationNode): void => {
Expand Down Expand Up @@ -268,4 +272,6 @@ export abstract class OperationNodeVisitor {
protected abstract visitUnaryOperation(node: UnaryOperationNode): void
protected abstract visitUsing(node: UsingNode): void
protected abstract visitFunction(node: FunctionNode): void
protected abstract visitCase(node: CaseNode): void
protected abstract visitWhen(node: WhenNode): void
}
2 changes: 2 additions & 0 deletions src/operation-node/operation-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ export type OperationNodeKind =
| 'UnaryOperationNode'
| 'UsingNode'
| 'FunctionNode'
| 'CaseNode'
| 'WhenNode'

export interface OperationNode {
readonly kind: OperationNodeKind
Expand Down
31 changes: 31 additions & 0 deletions src/operation-node/when-node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { freeze } from '../util/object-utils.js'
import { OperationNode } from './operation-node.js'

export interface WhenNode extends OperationNode {
readonly kind: 'WhenNode'
readonly condition: OperationNode
readonly result?: OperationNode
}

/**
* @internal
*/
export const WhenNode = freeze({
is(node: OperationNode): node is WhenNode {
return node.kind === 'WhenNode'
},

create(condition: OperationNode): WhenNode {
return freeze({
kind: 'WhenNode',
condition,
})
},

cloneWithResult(whenNode: WhenNode, result: OperationNode): WhenNode {
return freeze({
...whenNode,
result,
})
},
})
Loading

1 comment on commit 16a589d

@vercel
Copy link

@vercel vercel bot commented on 16a589d May 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

kysely – ./

kysely-kysely-team.vercel.app
kysely.dev
kysely-git-master-kysely-team.vercel.app
www.kysely.dev

Please sign in to comment.