Skip to content

Commit

Permalink
Pagination and Range (#130)
Browse files Browse the repository at this point in the history
* add pagination

* add range tests

* add docs

* run count/all at same time
  • Loading branch information
tanner0101 authored Jan 16, 2020
1 parent d3bb891 commit 7dc374e
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 2 deletions.
40 changes: 40 additions & 0 deletions Sources/FluentBenchmark/FluentBenchmarker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public final class FluentBenchmarker {
try self.testDuplicatedUniquePropertyName()
try self.testEmptyEagerLoadChildren()
try self.testUInt8BackedEnum()
try self.testRange()
}

public func testCreate() throws {
Expand Down Expand Up @@ -1821,6 +1822,45 @@ public final class FluentBenchmarker {
}
}

public func testRange() throws {
try runTest(#function, [
GalaxyMigration(),
PlanetMigration(),
GalaxySeed(),
PlanetSeed()
]) {
do {
let planets = try Planet.query(on: self.database)
.range(2..<5)
.sort(\.$name)
.all().wait()
XCTAssertEqual(planets.count, 3)
XCTAssertEqual(planets[0].name, "Mars")
}
do {
let planets = try Planet.query(on: self.database)
.range(...5)
.sort(\.$name)
.all().wait()
XCTAssertEqual(planets.count, 6)
}
do {
let planets = try Planet.query(on: self.database)
.range(..<5)
.sort(\.$name)
.all().wait()
XCTAssertEqual(planets.count, 5)
}
do {
let planets = try Planet.query(on: self.database)
.range(..<5)
.sort(\.$name)
.all().wait()
XCTAssertEqual(planets.count, 5)
}
}
}

// MARK: Utilities

struct Failure: Error {
Expand Down
99 changes: 99 additions & 0 deletions Sources/FluentKit/Query/QueryBuilder+Paginate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
extension QueryBuilder {
/// Returns a single `Page` out of the complete result set according to the supplied `PageRequest`.
///
/// This method will first `count()` the result set, then request a subset of the results using `range()` and `all()`.
/// - Parameters:
/// - request: Describes which page should be fetched.
/// - Returns: A single `Page` of the result set containing the requested items and page metadata.
public func paginate(
_ request: PageRequest
) -> EventLoopFuture<Page<Model>> {
let count = self.count()
let items = self.copy().range(request.start..<request.end).all()
return items.and(count).map { (models, total) in
Page(
items: models,
metadata: .init(
page: request.page,
per: request.per,
total: total
)
)
}
}
}

/// A single section of a larger, traversable result set.
public struct Page<T>: Codable where T: Codable {
/// The page's items. Usually models.
public let items: [T]

/// Metadata containing information about current page, items per page, and total items.
public let metadata: PageMetadata

/// Creates a new `Page`.
public init(items: [T], metadata: PageMetadata) {
self.items = items
self.metadata = metadata
}

/// Maps a page's items to a different type using the supplied closure.
public func map<U>(_ transform: (T) throws -> (U)) rethrows -> Page<U>
where U: Codable
{
try .init(
items: self.items.map(transform),
metadata: self.metadata
)
}
}

/// Metadata for a given `Page`.
public struct PageMetadata: Codable {
/// Current page number. Starts at `1`.
public let page: Int

/// Max items per page.
public let per: Int

/// Total number of items available.
public let total: Int
}

/// Represents information needed to generate a `Page` from the full result set.
public struct PageRequest: Decodable {
/// Page number to request. Starts at `1`.
public let page: Int

/// Max items per page.
public let per: Int

enum CodingKeys: String, CodingKey {
case page = "page"
case per = "per"
}

/// `Decodable` conformance.
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.page = try container.decodeIfPresent(Int.self, forKey: .page) ?? 1
self.per = try container.decodeIfPresent(Int.self, forKey: .per) ?? 10
}

/// Crates a new `PageRequest`
/// - Parameters:
/// - page: Page number to request. Starts at `1`.
/// - per: Max items per page.
public init(page: Int, per: Int) {
self.page = page
self.per = per
}

var start: Int {
(self.page - 1) * self.per
}

var end: Int {
self.page * self.per
}
}
62 changes: 62 additions & 0 deletions Sources/FluentKit/Query/QueryBuilder+Range.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
extension QueryBuilder {
// MARK: Range

/// Limits the results of this query to the specified range.
///
/// query.range(2..<5) // returns at most 3 results, offset by 2
///
/// - returns: Query builder for chaining.
public func range(_ range: Range<Int>) -> Self {
return self.range(lower: range.lowerBound, upper: range.upperBound - 1)
}

/// Limits the results of this query to the specified range.
///
/// query.range(...5) // returns at most 6 results
///
/// - returns: Query builder for chaining.
public func range(_ range: PartialRangeThrough<Int>) -> Self {
return self.range(upper: range.upperBound)
}

/// Limits the results of this query to the specified range.
///
/// query.range(..<5) // returns at most 5 results
///
/// - returns: Query builder for chaining.
public func range(_ range: PartialRangeUpTo<Int>) -> Self {
return self.range(upper: range.upperBound - 1)
}

/// Limits the results of this query to the specified range.
///
/// query.range(5...) // offsets the result by 5
///
/// - returns: Query builder for chaining.
public func range(_ range: PartialRangeFrom<Int>) -> Self {
return self.range(lower: range.lowerBound)
}

/// Limits the results of this query to the specified range.
///
/// query.range(2..<5) // returns at most 3 results, offset by 2
///
/// - returns: Query builder for chaining.
public func range(_ range: ClosedRange<Int>) -> Self {
return self.range(lower: range.lowerBound, upper: range.upperBound)
}

/// Limits the results of this query to the specified range.
///
/// - parameters:
/// - lower: Amount to offset the query by.
/// - upper: `upper` - `lower` = maximum results.
/// - returns: Query builder for chaining.
public func range(lower: Int = 0, upper: Int? = nil) -> Self {
self.query.offsets.append(.count(lower))
upper.flatMap { upper in
self.query.limits.append(.count((upper - lower) + 1))
}
return self
}
}
29 changes: 27 additions & 2 deletions Sources/FluentKit/Query/QueryBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,30 @@ public final class QueryBuilder<Model>
self.joinedModels = []
}

private init(
query: DatabaseQuery,
database: Database,
eagerLoads: EagerLoads,
includeDeleted: Bool,
joinedModels: [AnyModel]
) {
self.query = query
self.database = database
self.eagerLoads = eagerLoads
self.includeDeleted = includeDeleted
self.joinedModels = joinedModels
}

public func copy() -> QueryBuilder<Model> {
.init(
query: self.query,
database: self.database,
eagerLoads: self.eagerLoads,
includeDeleted: self.includeDeleted,
joinedModels: self.joinedModels
)
}

// MARK: Eager Load

@discardableResult
Expand Down Expand Up @@ -550,7 +574,8 @@ public final class QueryBuilder<Model>
) -> EventLoopFuture<Result>
where Result: Codable
{
self.query.fields = [.aggregate(.fields(
let copy = self.copy()
copy.query.fields = [.aggregate(.fields(
method: method,
fields: [.field(
path: [fieldName],
Expand All @@ -559,7 +584,7 @@ public final class QueryBuilder<Model>
]
))]

return self.first().flatMapThrowing { res in
return copy.first().flatMapThrowing { res in
guard let res = res else {
throw FluentError.noResults
}
Expand Down

0 comments on commit 7dc374e

Please sign in to comment.