Skip to content

Commit

Permalink
feat: let queries be reused
Browse files Browse the repository at this point in the history
- Query subtypes with mutable object props should override their `cloneProps` method
- Any method that mutates `this.props` should first call `this.cloneIfReused` and use the returned object in place of `this` elsewhere in the method
  • Loading branch information
aleclarson committed Sep 23, 2022
1 parent 110975e commit e4ba1ec
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 26 deletions.
54 changes: 43 additions & 11 deletions src/postgres/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,11 @@ export abstract class Query<Props extends object | null = any> {

constructor(parent: Query | Database) {
if (parent instanceof Query) {
this.db = parent.db

// Reuse the node list if our parent is the last node.
let { nodes } = parent
if (parent.position < nodes.length - 1) {
// Otherwise, convert the parent into a reusable node with a frozen
// node list, so it won't hold onto disposable queries that use it.
nodes = parent.nodes = Object.freeze(
nodes.slice(0, parent.position + 1)
) as any
if (parent.position < parent.nodes.length - 1) {
parent.reuse()
}
this.nodes = Object.isFrozen(nodes) ? [...nodes] : nodes
this.db = parent.db
this.nodes = parent.isReused ? [...parent.nodes] : parent.nodes
} else {
this.db = parent
this.nodes = []
Expand All @@ -48,6 +41,9 @@ export abstract class Query<Props extends object | null = any> {
* without repeating the selection manually.
*/
wrap<Result extends Query>(wrapper: (query: this) => Result) {
if (!this.isReused) {
this.reuse()
}
return wrapper(this)
}

Expand All @@ -74,6 +70,42 @@ export abstract class Query<Props extends object | null = any> {
node.query.nodes.push(node)
return node.query
}

protected get isReused() {
return Object.isFrozen(this.nodes)
}

/**
* To reuse a query, its node list must be sliced (so the query is last)
* and then frozen.
*/
protected reuse(): Node[] {
return (this.nodes = Object.freeze(
this.nodes.slice(0, this.position + 1)
) as any)
}

protected cloneIfReused() {
return this.isReused ? this.clone() : this
}

protected clone() {
const clone = Object.create(this.constructor.prototype)
Object.assign(clone, this)
clone.nodes = this.nodes.slice(0, this.position + 1)
clone.nodes[this.position] = {
...clone.nodes[this.position],
props: this.cloneProps(),
}
return clone
}

/**
* By default, only a shallow clone is made.
*/
protected cloneProps() {
return { ...this.props }
}
}

// Using defineProperty for Query#then lets subclasses easily
Expand Down
22 changes: 16 additions & 6 deletions src/postgres/query/base/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,18 +101,27 @@ export abstract class SelectBase<From extends Selectable[]> //
}
return tokens
}
protected cloneProps() {
const { joins, groupBy } = this.props
return {
...this.props,
joins: joins?.slice(),
groupBy: groupBy?.slice(),
}
}

// This method has to return `any` since we can't override
// the type parameters of a superclass.
innerJoin<Joined extends Selectable>(
from: Joined,
on: Where<[...From, Joined]>
): any {
const self = this.cloneIfReused()
const join = { type: 'inner', from } as JoinProps
this.props.joins ||= []
this.props.joins.push(join)
join.where = buildWhereClause(this.props, on)
return this
self.props.joins ||= []
self.props.joins.push(join)
join.where = buildWhereClause(self.props, on)
return self
}
}

Expand All @@ -124,8 +133,9 @@ export interface SelectBase<From extends Selectable[]> {

Object.defineProperty(SelectBase.prototype, 'where', {
value: function where(this: SelectBase<any>, filter: any) {
this.props.where = buildWhereClause(this.props, filter, this.props.where)
return this
const self = this.cloneIfReused()
self.props.where = buildWhereClause(self.props, filter, self.props.where)
return self
},
})

Expand Down
20 changes: 11 additions & 9 deletions src/postgres/query/base/set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,23 @@ export abstract class SetBase<
* - Multiple calls are not supported.
*/
at(offset: number) {
this.props.single = true
if (offset > 0) {
this.props.offset = offset
}
return this.limit(1)
const self = this.cloneIfReused()
self.props.single = true
self.props.limit = 1
self.props.offset = offset > 0 ? offset : undefined
return self
}

limit(n: number) {
this.props.limit = n
return this
const self = this.cloneIfReused()
self.props.limit = n
return self
}

orderBy(selector: SortSelection<From> | SortSelector<From>) {
this.props.orderBy = orderBy(this.sources, selector)
return this
const self = this.cloneIfReused()
self.props.orderBy = orderBy(self.sources, selector)
return self
}

stream(config?: QueryStreamConfig) {
Expand Down

0 comments on commit e4ba1ec

Please sign in to comment.