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

Support tuples #611

Merged
merged 4 commits into from
Jul 25, 2023
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
203 changes: 203 additions & 0 deletions src/expression/expression-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@ import {
Selection,
parseSelectArg,
} from '../parser/select-parser.js'
import {
RefTuple2,
RefTuple3,
RefTuple4,
RefTuple5,
ValTuple2,
ValTuple3,
ValTuple4,
ValTuple5,
} from '../parser/tuple-parser.js'
import { TupleNode } from '../operation-node/tuple-node.js'

export interface ExpressionBuilder<DB, TB extends keyof DB> {
/**
Expand Down Expand Up @@ -474,6 +485,184 @@ export interface ExpressionBuilder<DB, TB extends keyof DB> {
value: VE
): ExpressionWrapper<DB, TB, ExtractTypeFromValueExpression<VE>>

/**
* Creates a tuple expression.
*
* This creates a tuple using column references by default. See {@link tuple}
* if you need to create value tuples.
*
* ### Examples
*
* ```ts
* db.selectFrom('person')
* .selectAll('person')
* .where(({ eb, refTuple, tuple }) => eb(
* refTuple('first_name', 'last_name'),
* 'in',
* [
* tuple('Jennifer', 'Aniston'),
* tuple('Sylvester', 'Stallone')
* ]
* ))
* ```
*
* The generated SQL (PostgreSQL):
*
* ```sql
* select
* "person".*
* from
* "person"
* where
* ("first_name", "last_name")
* in
* (
* ($1, $2),
* ($3, $4)
* )
* ```
*
* In the next example a reference tuple is compared to a subquery. Note that
* in this case you need to use the {@link @SelectQueryBuilder.$asTuple | $asTuple}
* function:
*
* ```ts
* db.selectFrom('person')
* .selectAll('person')
* .where(({ eb, refTuple, selectFrom }) => eb(
* refTuple('first_name', 'last_name'),
* 'in',
* selectFrom('pet')
* .select(['name', 'species'])
* .where('species', '!=', 'cat')
* .$asTuple('name', 'species')
* ))
* ```
*
* The generated SQL (PostgreSQL):
*
* ```sql
* select
* "person".*
* from
* "person"
* where
* ("first_name", "last_name")
* in
* (
* select "name", "species"
* from "pet"
* where "species" != $1
* )
* ```
*/
refTuple<
R1 extends ReferenceExpression<DB, TB>,
R2 extends ReferenceExpression<DB, TB>
>(
value1: R1,
value2: R2
): ExpressionWrapper<DB, TB, RefTuple2<DB, TB, R1, R2>>

refTuple<
R1 extends ReferenceExpression<DB, TB>,
R2 extends ReferenceExpression<DB, TB>,
R3 extends ReferenceExpression<DB, TB>
>(
value1: R1,
value2: R2,
value3: R3
): ExpressionWrapper<DB, TB, RefTuple3<DB, TB, R1, R2, R3>>

refTuple<
R1 extends ReferenceExpression<DB, TB>,
R2 extends ReferenceExpression<DB, TB>,
R3 extends ReferenceExpression<DB, TB>,
R4 extends ReferenceExpression<DB, TB>
>(
value1: R1,
value2: R2,
value3: R3,
value4: R4
): ExpressionWrapper<DB, TB, RefTuple4<DB, TB, R1, R2, R3, R4>>

refTuple<
R1 extends ReferenceExpression<DB, TB>,
R2 extends ReferenceExpression<DB, TB>,
R3 extends ReferenceExpression<DB, TB>,
R4 extends ReferenceExpression<DB, TB>,
R5 extends ReferenceExpression<DB, TB>
>(
value1: R1,
value2: R2,
value3: R3,
value4: R4,
value5: R5
): ExpressionWrapper<DB, TB, RefTuple5<DB, TB, R1, R2, R3, R4, R5>>

/**
* Creates a value tuple expression.
*
* This creates a tuple using values by default. See {@link refTuple} if you need to create
* tuples using column references.
*
* ### Examples
*
* ```ts
* db.selectFrom('person')
* .selectAll('person')
* .where(({ eb, refTuple, tuple }) => eb(
* refTuple('first_name', 'last_name'),
* 'in',
* [
* tuple('Jennifer', 'Aniston'),
* tuple('Sylvester', 'Stallone')
* ]
* ))
* ```
*
* The generated SQL (PostgreSQL):
*
* ```sql
* select
* "person".*
* from
* "person"
* where
* ("first_name", "last_name")
* in
* (
* ($1, $2),
* ($3, $4)
* )
* ```
*/
tuple<V1, V2>(
value1: V1,
value2: V2
): ExpressionWrapper<DB, TB, ValTuple2<V1, V2>>

tuple<V1, V2, V3>(
value1: V1,
value2: V2,
value3: V3
): ExpressionWrapper<DB, TB, ValTuple3<V1, V2, V3>>

tuple<V1, V2, V3, V4>(
value1: V1,
value2: V2,
value3: V3,
value4: V4
): ExpressionWrapper<DB, TB, ValTuple4<V1, V2, V3, V4>>

tuple<V1, V2, V3, V4, V5>(
value1: V1,
value2: V2,
value3: V3,
value4: V4,
value5: V5
): ExpressionWrapper<DB, TB, ValTuple5<V1, V2, V3, V4, V5>>

/**
* Returns a literal value expression.
*
Expand Down Expand Up @@ -950,6 +1139,20 @@ export function createExpressionBuilder<DB, TB extends keyof DB>(
return new ExpressionWrapper(parseValueExpressionOrList(value))
},

refTuple(
...values: ReadonlyArray<ReferenceExpression<any, any>>
): ExpressionWrapper<DB, TB, any> {
return new ExpressionWrapper(
TupleNode.create(values.map(parseReferenceExpression))
)
},

tuple(...values: ReadonlyArray<unknown>): ExpressionWrapper<DB, TB, any> {
return new ExpressionWrapper(
TupleNode.create(values.map(parseValueExpression))
)
},

lit<VE extends number | boolean | null>(
value: VE
): ExpressionWrapper<DB, TB, VE> {
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ export * from './operation-node/json-reference-node.js'
export * from './operation-node/json-path-leg-node.js'
export * from './operation-node/json-path-node.js'
export * from './operation-node/json-operator-chain-node.js'
export * from './operation-node/tuple-node.js'

export * from './util/column-type.js'
export * from './util/compilable.js'
Expand Down
9 changes: 9 additions & 0 deletions src/operation-node/operation-node-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ import { JSONReferenceNode } from './json-reference-node.js'
import { JSONPathNode } from './json-path-node.js'
import { JSONPathLegNode } from './json-path-leg-node.js'
import { JSONOperatorChainNode } from './json-operator-chain-node.js'
import { TupleNode } from './tuple-node.js'

/**
* Transforms an operation node tree into another one.
Expand Down Expand Up @@ -206,6 +207,7 @@ export class OperationNodeTransformer {
JSONPathNode: this.transformJSONPath.bind(this),
JSONPathLegNode: this.transformJSONPathLeg.bind(this),
JSONOperatorChainNode: this.transformJSONOperatorChain.bind(this),
TupleNode: this.transformTuple.bind(this),
})

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

protected transformTuple(node: TupleNode): TupleNode {
return requireAllProps<TupleNode>({
kind: 'TupleNode',
values: this.transformNodeList(node.values),
})
}

protected transformDataType(node: DataTypeNode): DataTypeNode {
// An Object.freezed leaf node. No need to clone.
return node
Expand Down
3 changes: 3 additions & 0 deletions src/operation-node/operation-node-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import { JSONReferenceNode } from './json-reference-node.js'
import { JSONPathNode } from './json-path-node.js'
import { JSONPathLegNode } from './json-path-leg-node.js'
import { JSONOperatorChainNode } from './json-operator-chain-node.js'
import { TupleNode } from './tuple-node.js'

export abstract class OperationNodeVisitor {
protected readonly nodeStack: OperationNode[] = []
Expand Down Expand Up @@ -183,6 +184,7 @@ export abstract class OperationNodeVisitor {
JSONPathNode: this.visitJSONPath.bind(this),
JSONPathLegNode: this.visitJSONPathLeg.bind(this),
JSONOperatorChainNode: this.visitJSONOperatorChain.bind(this),
TupleNode: this.visitTuple.bind(this),
})

protected readonly visitNode = (node: OperationNode): void => {
Expand Down Expand Up @@ -286,4 +288,5 @@ export abstract class OperationNodeVisitor {
protected abstract visitJSONPath(node: JSONPathNode): void
protected abstract visitJSONPathLeg(node: JSONPathLegNode): void
protected abstract visitJSONOperatorChain(node: JSONOperatorChainNode): void
protected abstract visitTuple(node: TupleNode): void
}
1 change: 1 addition & 0 deletions src/operation-node/operation-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export type OperationNodeKind =
| 'JSONPathNode'
| 'JSONPathLegNode'
| 'JSONOperatorChainNode'
| 'TupleNode'

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

export interface TupleNode extends OperationNode {
readonly kind: 'TupleNode'
readonly values: ReadonlyArray<OperationNode>
}

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

create(values: ReadonlyArray<OperationNode>): TupleNode {
return freeze({
kind: 'TupleNode',
values: freeze(values),
})
},
})
24 changes: 14 additions & 10 deletions src/parser/select-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,13 @@ export type SelectArg<
| ReadonlyArray<SE>
| ((eb: ExpressionBuilder<DB, TB>) => ReadonlyArray<SE>)

export type Selection<DB, TB extends keyof DB, SE> = {
[A in ExtractAliasFromSelectExpression<SE>]: SelectType<
ExtractTypeFromSelectExpression<DB, TB, SE, A>
>
}
export type Selection<DB, TB extends keyof DB, SE> = [SE] extends [unknown]
koskimas marked this conversation as resolved.
Show resolved Hide resolved
? {
[A in ExtractAliasFromSelectExpression<SE>]: SelectType<
ExtractTypeFromSelectExpression<DB, TB, SE, A>
>
}
: never

type ExtractAliasFromSelectExpression<SE> = SE extends string
? ExtractAliasFromStringSelectExpression<SE>
Expand Down Expand Up @@ -151,11 +153,13 @@ type ExtractTypeFromStringSelectExpression<
: never
: never

export type AllSelection<DB, TB extends keyof DB> = Selectable<{
[C in AnyColumn<DB, TB>]: {
[T in TB]: C extends keyof DB[T] ? DB[T][C] : never
}[TB]
}>
export type AllSelection<DB, TB extends keyof DB> = [DB] extends [unknown]
? Selectable<{
[C in AnyColumn<DB, TB>]: {
[T in TB]: C extends keyof DB[T] ? DB[T][C] : never
}[TB]
}>
: never

export function parseSelectArg(
selection: SelectArg<any, any, SelectExpression<any, any>>
Expand Down
Loading