Skip to content

Commit

Permalink
revisiting orderBy. (kysely-org#1326)
Browse files Browse the repository at this point in the history
  • Loading branch information
igalklebanov committed Feb 9, 2025
1 parent 22895c1 commit 25fe2d0
Show file tree
Hide file tree
Showing 27 changed files with 1,146 additions and 292 deletions.
17 changes: 17 additions & 0 deletions src/dialect/mssql/mssql-query-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { DropColumnNode } from '../../operation-node/drop-column-node.js'
import { OffsetNode } from '../../operation-node/offset-node.js'
import { MergeQueryNode } from '../../operation-node/merge-query-node.js'
import { DefaultQueryCompiler } from '../../query-compiler/default-query-compiler.js'
import { CollateNode } from '../../operation-node/collate-node.js'

const COLLATION_CHAR_REGEX = /^[a-z0-9_]$/i

export class MssqlQueryCompiler extends DefaultQueryCompiler {
protected override getCurrentParameterPlaceholder(): string {
Expand Down Expand Up @@ -85,6 +88,20 @@ export class MssqlQueryCompiler extends DefaultQueryCompiler {
this.append(';')
}

protected override visitCollate(node: CollateNode): void {
this.append('collate ')

const { name } = node.collation

for (const char of name) {
if (!COLLATION_CHAR_REGEX.test(char)) {
throw new Error(`Invalid collation: ${name}`)
}
}

this.append(name)
}

protected override announcesNewColumnDataType(): boolean {
return false
}
Expand Down
7 changes: 7 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export * from './query-builder/where-interface.js'
export * from './query-builder/returning-interface.js'
export * from './query-builder/output-interface.js'
export * from './query-builder/having-interface.js'
export * from './query-builder/order-by-interface.js'
export * from './query-builder/select-query-builder.js'
export * from './query-builder/insert-query-builder.js'
export * from './query-builder/update-query-builder.js'
Expand All @@ -28,6 +29,7 @@ export * from './query-builder/case-builder.js'
export * from './query-builder/json-path-builder.js'
export * from './query-builder/merge-query-builder.js'
export * from './query-builder/merge-result.js'
export * from './query-builder/order-by-item-builder.js'

export * from './raw-builder/raw-builder.js'
export * from './raw-builder/sql.js'
Expand Down Expand Up @@ -125,6 +127,7 @@ export * from './operation-node/binary-operation-node.js'
export * from './operation-node/case-node.js'
export * from './operation-node/cast-node.js'
export * from './operation-node/check-constraint-node.js'
export * from './operation-node/collate-node.js'
export * from './operation-node/column-definition-node.js'
export * from './operation-node/column-node.js'
export * from './operation-node/column-update-node.js'
Expand Down Expand Up @@ -269,6 +272,9 @@ export { UpdateObject } from './parser/update-set-parser.js'
export {
OrderByExpression,
OrderByDirectionExpression,
OrderByModifiers,
OrderByDirection,
OrderByModifiersCallbackExpression,
} from './parser/order-by-parser.js'
export {
ComparisonOperatorExpression,
Expand All @@ -281,3 +287,4 @@ export {
OperandExpression,
ExpressionOrFactory,
} from './parser/expression-parser.js'
export { Collation } from './parser/collate-parser.js'
24 changes: 24 additions & 0 deletions src/operation-node/collate-node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { freeze } from '../util/object-utils.js'
import { IdentifierNode } from './identifier-node.js'
import { OperationNode } from './operation-node.js'

export interface CollateNode extends OperationNode {
readonly kind: 'CollateNode'
readonly collation: IdentifierNode
}

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

create(collation: string): CollateNode {
return freeze({
kind: 'CollateNode',
collation: IdentifierNode.create(collation),
})
},
}
30 changes: 14 additions & 16 deletions src/operation-node/delete-query-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ExplainNode } from './explain-node.js'
import { UsingNode } from './using-node.js'
import { TopNode } from './top-node.js'
import { OutputNode } from './output-node.js'
import { QueryNode } from './query-node.js'

export interface DeleteQueryNode extends OperationNode {
readonly kind: 'DeleteQueryNode'
Expand Down Expand Up @@ -45,24 +46,21 @@ export const DeleteQueryNode = freeze({
})
},

cloneWithOrderByItems(
deleteNode: DeleteQueryNode,
// TODO: remove in v0.29
/**
* @deprecated Use `QueryNode.cloneWithoutOrderBy` instead.
*/
cloneWithOrderByItems: (
node: DeleteQueryNode,
items: ReadonlyArray<OrderByItemNode>,
): DeleteQueryNode {
return freeze({
...deleteNode,
orderBy: deleteNode.orderBy
? OrderByNode.cloneWithItems(deleteNode.orderBy, items)
: OrderByNode.create(items),
})
},
) => QueryNode.cloneWithOrderByItems(node, items),

cloneWithoutOrderBy(deleteNode: DeleteQueryNode): DeleteQueryNode {
return freeze({
...deleteNode,
orderBy: undefined,
})
},
// TODO: remove in v0.29
/**
* @deprecated Use `QueryNode.cloneWithoutOrderBy` instead.
*/
cloneWithoutOrderBy: (node: DeleteQueryNode) =>
QueryNode.cloneWithoutOrderBy(node),

cloneWithLimit(
deleteNode: DeleteQueryNode,
Expand Down
10 changes: 10 additions & 0 deletions src/operation-node/operation-node-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ import { TopNode } from './top-node.js'
import { OutputNode } from './output-node.js'
import { RefreshMaterializedViewNode } from './refresh-materialized-view-node.js'
import { OrActionNode } from './or-action-node.js'
import { CollateNode } from './collate-node.js'

/**
* Transforms an operation node tree into another one.
Expand Down Expand Up @@ -233,6 +234,7 @@ export class OperationNodeTransformer {
TopNode: this.transformTop.bind(this),
OutputNode: this.transformOutput.bind(this),
OrActionNode: this.transformOrAction.bind(this),
CollateNode: this.transformCollate.bind(this),
})

transformNode<T extends OperationNode | undefined>(node: T): T {
Expand Down Expand Up @@ -502,6 +504,8 @@ export class OperationNodeTransformer {
kind: 'OrderByItemNode',
orderBy: this.transformNode(node.orderBy),
direction: this.transformNode(node.direction),
collation: this.transformNode(node.collation),
nulls: node.nulls,
})
}

Expand Down Expand Up @@ -534,6 +538,7 @@ export class OperationNodeTransformer {
limit: this.transformNode(node.limit),
top: this.transformNode(node.top),
output: this.transformNode(node.output),
orderBy: this.transformNode(node.orderBy),
})
}

Expand Down Expand Up @@ -1140,4 +1145,9 @@ export class OperationNodeTransformer {
// An Object.freezed leaf node. No need to clone.
return node
}

protected transformCollate(node: CollateNode): CollateNode {
// An Object.freezed leaf node. No need to clone.
return node
}
}
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 @@ -98,6 +98,7 @@ import { TopNode } from './top-node.js'
import { OutputNode } from './output-node.js'
import { RefreshMaterializedViewNode } from './refresh-materialized-view-node.js'
import { OrActionNode } from './or-action-node.js'
import { CollateNode } from './collate-node.js'

export abstract class OperationNodeVisitor {
protected readonly nodeStack: OperationNode[] = []
Expand Down Expand Up @@ -203,6 +204,7 @@ export abstract class OperationNodeVisitor {
TopNode: this.visitTop.bind(this),
OutputNode: this.visitOutput.bind(this),
OrActionNode: this.visitOrAction.bind(this),
CollateNode: this.visitCollate.bind(this),
})

protected readonly visitNode = (node: OperationNode): void => {
Expand Down Expand Up @@ -318,4 +320,5 @@ export abstract class OperationNodeVisitor {
protected abstract visitTop(node: TopNode): void
protected abstract visitOutput(node: OutputNode): void
protected abstract visitOrAction(node: OrActionNode): void
protected abstract visitCollate(node: CollateNode): 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 @@ -94,6 +94,7 @@ export type OperationNodeKind =
| 'TopNode'
| 'OutputNode'
| 'OrActionNode'
| 'CollateNode'

export interface OperationNode {
readonly kind: OperationNodeKind
Expand Down
16 changes: 15 additions & 1 deletion src/operation-node/order-by-item-node.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { freeze } from '../util/object-utils.js'
import { CollateNode } from './collate-node.js'
import { OperationNode } from './operation-node.js'

export type OrderByItemNodeProps = Omit<OrderByItemNode, 'kind' | 'orderBy'>

export interface OrderByItemNode extends OperationNode {
readonly kind: 'OrderByItemNode'
readonly orderBy: OperationNode
// TODO(samiko): Do we need an OrderByDirectionNode for consistency?
readonly direction?: OperationNode
readonly nulls?: 'first' | 'last'
readonly collation?: CollateNode
}

/**
Expand All @@ -23,4 +27,14 @@ export const OrderByItemNode = freeze({
direction,
})
},

cloneWith(
node: OrderByItemNode,
props: OrderByItemNodeProps,
): OrderByItemNode {
return freeze({
...node,
...props,
})
},
})
22 changes: 22 additions & 0 deletions src/operation-node/query-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { Expression } from '../expression/expression.js'
import { MergeQueryNode } from './merge-query-node.js'
import { TopNode } from './top-node.js'
import { OutputNode } from './output-node.js'
import { OrderByNode } from './order-by-node.js'
import { OrderByItemNode } from './order-by-item-node.js'

export type QueryNode =
| SelectQueryNode
Expand All @@ -29,6 +31,7 @@ type HasExplain = { explain?: ExplainNode }
type HasTop = { top?: TopNode }
type HasOutput = { output?: OutputNode }
type HasEndModifiers = { endModifiers?: ReadonlyArray<OperationNode> }
type HasOrderBy = { orderBy?: OrderByNode }

/**
* @internal
Expand Down Expand Up @@ -127,4 +130,23 @@ export const QueryNode = freeze({
: OutputNode.create(selections),
})
},

cloneWithOrderByItems<T extends HasOrderBy>(
node: T,
items: ReadonlyArray<OrderByItemNode>,
): T {
return freeze({
...node,
orderBy: node.orderBy
? OrderByNode.cloneWithItems(node.orderBy, items)
: OrderByNode.create(items),
})
},

cloneWithoutOrderBy<T extends HasOrderBy>(node: T): T {
return freeze({
...node,
orderBy: undefined,
})
},
})
30 changes: 14 additions & 16 deletions src/operation-node/select-query-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { ExplainNode } from './explain-node.js'
import { SetOperationNode } from './set-operation-node.js'
import { FetchNode } from './fetch-node.js'
import { TopNode } from './top-node.js'
import { QueryNode } from './query-node.js'

export interface SelectQueryNode extends OperationNode {
readonly kind: 'SelectQueryNode'
Expand Down Expand Up @@ -101,17 +102,14 @@ export const SelectQueryNode = freeze({
})
},

cloneWithOrderByItems(
selectNode: SelectQueryNode,
// TODO: remove in v0.29
/**
* @deprecated Use `QueryNode.cloneWithoutOrderBy` instead.
*/
cloneWithOrderByItems: (
node: SelectQueryNode,
items: ReadonlyArray<OrderByItemNode>,
): SelectQueryNode {
return freeze({
...selectNode,
orderBy: selectNode.orderBy
? OrderByNode.cloneWithItems(selectNode.orderBy, items)
: OrderByNode.create(items),
})
},
) => QueryNode.cloneWithOrderByItems(node, items),

cloneWithGroupByItems(
selectNode: SelectQueryNode,
Expand Down Expand Up @@ -200,12 +198,12 @@ export const SelectQueryNode = freeze({
})
},

cloneWithoutOrderBy(select: SelectQueryNode): SelectQueryNode {
return freeze({
...select,
orderBy: undefined,
})
},
// TODO: remove in v0.29
/**
* @deprecated Use `QueryNode.cloneWithoutOrderBy` instead.
*/
cloneWithoutOrderBy: (node: SelectQueryNode) =>
QueryNode.cloneWithoutOrderBy(node),

cloneWithoutGroupBy(select: SelectQueryNode): SelectQueryNode {
return freeze({
Expand Down
2 changes: 2 additions & 0 deletions src/operation-node/update-query-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { LimitNode } from './limit-node.js'
import { TopNode } from './top-node.js'
import { OutputNode } from './output-node.js'
import { ListNode } from './list-node.js'
import { OrderByNode } from './order-by-node.js'

export type UpdateValuesNode = ValueListNode | PrimitiveValueListNode

Expand All @@ -30,6 +31,7 @@ export interface UpdateQueryNode extends OperationNode {
readonly limit?: LimitNode
readonly top?: TopNode
readonly output?: OutputNode
readonly orderBy?: OrderByNode
}

/**
Expand Down
8 changes: 8 additions & 0 deletions src/parser/collate-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type Collation =
// anything super common or simple should be added here.
// https://sqlite.org/datatype3.html#collating_sequences
| 'nocase'
| 'binary'
| 'rtrim'
// otherwise, allow any string, while still providing autocompletion.
| (string & {})
Loading

0 comments on commit 25fe2d0

Please sign in to comment.