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

Add "LIMIT 1" to fetchOne requests #515

Merged
merged 22 commits into from
Apr 18, 2019
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f9868bd
Update tests to expect "LIMIT 1" where appropriate
alextrob Apr 11, 2019
af1fd6b
More tests for presence of LIMIT 1
alextrob Apr 11, 2019
072fa25
Add hints for use in request preparation
alextrob Apr 11, 2019
dd139b6
Fix testing lastSQLQuery when a cursor hasn't retrieved a record
alextrob Apr 11, 2019
207b90f
Update FetchRequest protocol in README
alextrob Apr 11, 2019
088f6c9
Update inline docs
alextrob Apr 11, 2019
20aecd5
Add failing test for fetchOne with a non-zero offset
alextrob Apr 16, 2019
7070ded
Replace FetchRequestHint with a boolean for single results
alextrob Apr 16, 2019
235a612
Remove LIMIT 1 from expected SQL when filtering by key
alextrob Apr 16, 2019
e2211ac
Exclude LIMIT 1 in more cases
alextrob Apr 16, 2019
1b3f0e8
Update FetchRequest protocol in README
alextrob Apr 16, 2019
46791c8
Remove QueryInterfaceRequest.init(query:)
groue Apr 16, 2019
4716f91
Bake the single result expectation right into `filter(key:)` methods
groue Apr 16, 2019
693e6da
Do not test that fetchOne does not add LIMIT 1 to raw SQL requests
groue Apr 17, 2019
e3a0935
Add SQL tests for all record testFetchOne…() methods
groue Apr 17, 2019
65fe2bb
Tests for the singleResult hint and fetching methods
groue Apr 17, 2019
21c0175
Tests for the singleResult hint and fetchCount
groue Apr 17, 2019
a128300
Tests for the singleResult hint and databaseRegion
groue Apr 17, 2019
3f8047f
Tests for the singleResult hint and SQLRequest.init(_:request:)
groue Apr 17, 2019
a172473
CHANGELOG
groue Apr 17, 2019
2711b9b
Merge branch 'GRDB-4.0' into feature/fetchOne-limit-1
groue Apr 18, 2019
5af1686
Thank you notice
groue Apr 18, 2019
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
2 changes: 1 addition & 1 deletion GRDB/Core/DatabaseValueConvertible.swift
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ extension DatabaseValueConvertible {
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchOne<R: FetchRequest>(_ db: Database, _ request: R) throws -> Self? {
let (statement, adapter) = try request.prepare(db)
let (statement, adapter) = try request.prepare(db, hint: .limitOne)
return try fetchOne(statement, adapter: adapter)
}
}
Expand Down
45 changes: 35 additions & 10 deletions GRDB/Core/FetchRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,20 @@ public protocol FetchRequest: DatabaseRegionConvertible {
/// Returns a tuple that contains a prepared statement that is ready to be
/// executed, and an eventual row adapter.
///
/// Default implementation uses `prepare(db, hint: nil)`.
Copy link
Owner

@groue groue Apr 11, 2019

Choose a reason for hiding this comment

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

Second question: what is the purpose of keeping prepare(_:) in the protocol? Is there any reason for a type to implement both methods? Isn't prepare(_:hint:) sufficient?

Copy link
Collaborator Author

@alextrob alextrob Apr 11, 2019

Choose a reason for hiding this comment

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

Because the protocol's public I had this idea of not wanting to break compatibility with anything already calling prepare(_:). Also thought hint could be confusing when you don't need it.

I'd have preferred to declare it as:

func prepare(_ db: Database, hint: FetchRequestHint? = nil) throws -> (SelectStatement, RowAdapter?)

… but you can't have a default argument in a protocol.

Anyway, I'm not too attached to it. Happy to take it out if you'd prefer.

Copy link
Owner

@groue groue Apr 12, 2019

Choose a reason for hiding this comment

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

All right, I understand. The good news is that we can break things, because we are targeting the next major version. GRDB 4 will break several things (see #479 for the list of changes). So we can keep a single method.

I agree the hint adds a layer of complexity to the API, but I don't expect many users to write their own concrete type which conforms to FetchRequest. This is an advanced technique. If they do, I guess they'll be able to handle it. And we don't want a default value for the hint, because being explicit is better here.

But we have to document clearly that this hint is only an opportunity for optimization, and can safely be ignored.

Towards this goal of clarity, I think we'll also have to look for alternative names for hint and FetchRequestHint, and wonder if we really want an optional. Those currently sound too much like an implementation detail, and not enough like a public API.

I don't know... The radical solution is:

func prepare(_ db: Database, forSingleResult singleResult: Bool) -> ... {
    if singleResult {
        // Add LIMIT 1
        return ...
    } else {
        // Don't add LIMIT 1
        return ...
    }
}

But a boolean has little room for further extensions (even if I can't foresee any right now). I agree with your idea of an enum:

func prepare(_ db: Database, forCardinality cardinality: FetchCardinality) -> ... {
    switch cardinality {
    case .single:
        // Add LIMIT 1
        return ...
    case .multiple:
        // Don't add LIMIT 1
        return ...
    }
}

But "cardinality" can't really be extended either. Not really an improvement over a boolean. Let's go all general:

func prepare(_ db: Database, forPurpose purpose: FetchPurpose) -> ... {
    switch purpose {
    case .singleResult:
        // Add LIMIT 1
        return ...
    case .multipleResults:
        // Don't add LIMIT 1
        return ...
    }
}

I wish we could have merged FetchRequest.fetchCount here:

func prepare(_ db: Database, forPurpose purpose: FetchPurpose) -> ... {
    switch purpose {
    case .singleResult:
        // Add LIMIT 1
        return ...
    case .multipleResults:
        // Don't add LIMIT 1
        return ...
    case .count:
        // Build the counting statement
        return ...
    }
}

But we can't choose this last route, because fetchCount is currently an optional method with a non-trivial default implementation: users would now have real difficulties adopting the FetchRequest protocol.

Well.. As you see, we have much room in our way to the best design. I'm curious about your ideas as well. Our goal: turn our implementation strategy into a public API that can make sense for people who don't have a clue about our implementation strategy. I mean that most users are utterly indifferent about how the library works, and only care about their needs. Good public APIs acknowledge this hard fact. It may look difficult, but this can also be a funny game :-)

A technique that has constantly helped me finding an answer to this kind of question has been the README. We currently write, below the introduction to the FetchRequest protocol:

The prepare method returns a prepared statement and an optional row adapter. The prepared statement tells which SQL query should be executed. The row adapter helps presenting the fetched rows in the way expected by the row decoders (see row adapter).

We can try to update this paragraph so that it explains how to use the hint. Experience shows that when the documentation looks like it fills its teaching goal, the API is good as well.

Copy link
Owner

@groue groue Apr 12, 2019

Choose a reason for hiding this comment

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

Applying my own advice makes the plain and simple boolean approach look not so bad, after all:

The prepare(_:forSingleResult:) method accepts a database connection, a singleResult hint, and returns a prepared statement and an optional row adapter. Conforming types can use the singleResult hint as an optimization opportunity, and return a prepared statement that fetches at most one row, with a LIMIT SQL clause, when possible. The optional row adapter helps presenting the fetched rows in the way expected by the row decoders (see row adapters).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm coming around to the idea of a plain old boolean. Complicating the API for unknown future cases probably isn't worth it. If other scenarios do come up in the future, it can be changed in a way that really makes sense.

Copy link
Owner

@groue groue Apr 15, 2019

Choose a reason for hiding this comment

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

Complicating the API for unknown future cases probably isn't worth it.

Indeed, if we really can't find any extension for the "hint" today, then let's not try to outsmart ourselves. As you say, we'll always be able to revise our opinion in the future, if needed!

We have done our exploration job: if the boolean approach is OK with you too, then I think we can just relax :-)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Cool. I've switched it to a boolean and updated the README.

///
/// - parameter db: A database connection.
/// - returns: A prepared statement and an eventual row adapter.
func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?)

/// Returns a tuple that contains a prepared statement that is ready to be
/// executed, and an eventual row adapter.
///
/// - parameter db: A database connection.
/// - parameter hint: A hint as to how the request should be prepared.
/// - returns: A prepared statement and an eventual row adapter.
func prepare(_ db: Database, hint: FetchRequestHint?) throws -> (SelectStatement, RowAdapter?)

/// Returns the number of rows fetched by the request.
///
/// The default implementation builds a naive SQL query based on the
Expand All @@ -33,6 +43,10 @@ public protocol FetchRequest: DatabaseRegionConvertible {
}

extension FetchRequest {
public func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) {
return try self.prepare(db, hint: nil)
}

/// Returns an adapted request.
public func adapted(_ adapter: @escaping (Database) throws -> RowAdapter) -> AdaptedFetchRequest<Self> {
return AdaptedFetchRequest(self, adapter)
Expand Down Expand Up @@ -79,8 +93,8 @@ public struct AdaptedFetchRequest<Base: FetchRequest> : FetchRequest {
}

/// :nodoc:
public func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) {
let (statement, baseAdapter) = try base.prepare(db)
public func prepare(_ db: Database, hint: FetchRequestHint?) throws -> (SelectStatement, RowAdapter?) {
let (statement, baseAdapter) = try base.prepare(db, hint: hint)
if let baseAdapter = baseAdapter {
return try (statement, ChainedAdapter(first: baseAdapter, second: adapter(db)))
} else {
Expand Down Expand Up @@ -108,7 +122,7 @@ public struct AdaptedFetchRequest<Base: FetchRequest> : FetchRequest {
public struct AnyFetchRequest<T> : FetchRequest {
public typealias RowDecoder = T

private let _prepare: (Database) throws -> (SelectStatement, RowAdapter?)
private let _prepare: (Database, FetchRequestHint?) throws -> (SelectStatement, RowAdapter?)
private let _fetchCount: (Database) throws -> Int
private let _databaseRegion: (Database) throws -> DatabaseRegion

Expand All @@ -121,26 +135,26 @@ public struct AnyFetchRequest<T> : FetchRequest {

/// Creates a request whose `prepare()` method wraps and forwards
/// operations the argument closure.
public init(_ prepare: @escaping (Database) throws -> (SelectStatement, RowAdapter?)) {
_prepare = { db in
try prepare(db)
public init(_ prepare: @escaping (Database, FetchRequestHint?) throws -> (SelectStatement, RowAdapter?)) {
_prepare = { db, hint in
try prepare(db, hint)
}

_fetchCount = { db in
let (statement, _) = try prepare(db)
let (statement, _) = try prepare(db, nil)
let sql = "SELECT COUNT(*) FROM (\(statement.sql))"
return try Int.fetchOne(db, sql: sql, arguments: statement.arguments)!
}

_databaseRegion = { db in
let (statement, _) = try prepare(db)
let (statement, _) = try prepare(db, nil)
return statement.databaseRegion
}
}

/// :nodoc:
public func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) {
return try _prepare(db)
public func prepare(_ db: Database, hint: FetchRequestHint?) throws -> (SelectStatement, RowAdapter?) {
return try _prepare(db, hint)
}

/// :nodoc:
Expand All @@ -153,3 +167,14 @@ public struct AnyFetchRequest<T> : FetchRequest {
return try _databaseRegion(db)
}
}

/// A hint as to how the fetch request should be prepared.
///
/// FetchRequest implementations (such as SQLRequest) may ignore these hints.
///
/// - limitOne: Only 1 record should be fetched.
/// - primaryKeyOrUnique: The query filters on primary keys or a unique index and is expected to return 1 record.
public enum FetchRequestHint {
case limitOne
case primaryKeyOrUnique
}
2 changes: 1 addition & 1 deletion GRDB/Core/Row.swift
Original file line number Diff line number Diff line change
Expand Up @@ -871,7 +871,7 @@ extension Row {
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchOne<R: FetchRequest>(_ db: Database, _ request: R) throws -> Row? {
let (statement, adapter) = try request.prepare(db)
let (statement, adapter) = try request.prepare(db, hint: .limitOne)
return try fetchOne(statement, adapter: adapter)
}
}
Expand Down
3 changes: 2 additions & 1 deletion GRDB/Core/SQLRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,10 @@ public struct SQLRequest<T> : FetchRequest {
/// executed, and an eventual row adapter.
///
/// - parameter db: A database connection.
/// - parameter hint: SQLRequest disregards this value.
///
/// :nodoc:
public func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) {
public func prepare(_ db: Database, hint: FetchRequestHint?) throws -> (SelectStatement, RowAdapter?) {
let statement: SelectStatement
switch cache {
case .none:
Expand Down
2 changes: 1 addition & 1 deletion GRDB/Core/StatementColumnConvertible.swift
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible {
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchOne<R: FetchRequest>(_ db: Database, _ request: R) throws -> Self? {
let (statement, adapter) = try request.prepare(db)
let (statement, adapter) = try request.prepare(db, hint: .limitOne)
return try fetchOne(statement, adapter: adapter)
}
}
Expand Down
16 changes: 15 additions & 1 deletion GRDB/QueryInterface/QueryInterfaceRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
/// See https://github.com/groue/GRDB.swift#the-query-interface
public struct QueryInterfaceRequest<T> {
var query: SQLSelectQuery

init(query: SQLSelectQuery) {
self.query = query
}
}

extension QueryInterfaceRequest : FetchRequest {
Expand All @@ -39,9 +43,19 @@ extension QueryInterfaceRequest : FetchRequest {
/// executed, and an eventual row adapter.
///
/// - parameter db: A database connection.
/// - parameter hint: A hint about how the query should be prepared.
/// - returns: A prepared statement and an eventual row adapter.
/// :nodoc:
public func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) {
public func prepare(_ db: Database, hint: FetchRequestHint?) throws -> (SelectStatement, RowAdapter?) {
let query: SQLSelectQuery

switch hint {
case .limitOne?:
query = self.query.limit(1)
Copy link
Owner

Choose a reason for hiding this comment

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

Here we break the following code:

let player = try Player.limit(1, offset: 10).fetchOne(db)

We have to honor the eventual offset, if present.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Whoops! Should be fixed in the latest commits (and added a test to catch it).

case nil, .primaryKeyOrUnique?:
query = self.query
}

return try SQLSelectQueryGenerator(query).prepare(db)
}

Expand Down
4 changes: 2 additions & 2 deletions GRDB/Record/FetchableRecord+TableRecord.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ extension FetchableRecord where Self: TableRecord {
// Avoid hitting the database
return nil
}
return try filter(key: key).fetchOne(db)
return try filter(key: key).fetchOne(db, hint: .primaryKeyOrUnique)
}
}

Expand Down Expand Up @@ -186,6 +186,6 @@ extension FetchableRecord where Self: TableRecord {
// Avoid hitting the database
return nil
}
return try filter(key: key).fetchOne(db)
return try filter(key: key).fetchOne(db, hint: .primaryKeyOrUnique)
}
}
14 changes: 12 additions & 2 deletions GRDB/Record/FetchableRecord.swift
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,12 @@ extension FetchableRecord {
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public static func fetchOne<R: FetchRequest>(_ db: Database, _ request: R) throws -> Self? {
let (statement, adapter) = try request.prepare(db)
return try Self.fetchOne(db, request, hint: .limitOne)
}

@inlinable
static func fetchOne<R: FetchRequest>(_ db: Database, _ request: R, hint: FetchRequestHint?) throws -> Self? {
let (statement, adapter) = try request.prepare(db, hint: hint)
return try fetchOne(statement, adapter: adapter)
}
}
Expand Down Expand Up @@ -368,7 +373,12 @@ extension FetchRequest where RowDecoder: FetchableRecord {
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
@inlinable
public func fetchOne(_ db: Database) throws -> RowDecoder? {
return try RowDecoder.fetchOne(db, self)
return try fetchOne(db, hint: .limitOne)
}

@inlinable
func fetchOne(_ db: Database, hint: FetchRequestHint?) throws -> RowDecoder? {
return try RowDecoder.fetchOne(db, self, hint: hint)
}
}

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4612,7 +4612,7 @@ protocol FetchRequest: DatabaseRegionConvertible {
associatedtype RowDecoder

/// A tuple that contains a prepared statement, and an eventual row adapter.
func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?)
func prepare(_ db: Database, hint: FetchRequestHint?) throws -> (SelectStatement, RowAdapter?)

/// The number of rows fetched by the request.
func fetchCount(_ db: Database) throws -> Int
Expand Down
2 changes: 1 addition & 1 deletion Tests/GRDBTests/CompilationProtocolTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ private class UserPersistableRecord2 : PersistableRecord {
private struct UserRowRequest : FetchRequest {
struct CustomType { }
typealias RowDecoder = CustomType
func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) { preconditionFailure() }
func prepare(_ db: Database, hint: FetchRequestHint?) throws -> (SelectStatement, RowAdapter?) { preconditionFailure() }
}

// MARK: - FetchableRecord
Expand Down
10 changes: 5 additions & 5 deletions Tests/GRDBTests/FetchRequestTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class FetchRequestTests: GRDBTestCase {
func testRequestFetchRows() throws {
struct CustomRequest : FetchRequest {
typealias RowDecoder = Row
func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) {
func prepare(_ db: Database, hint: FetchRequestHint?) throws -> (SelectStatement, RowAdapter?) {
return try (db.makeSelectStatement(sql: "SELECT * FROM table1"), nil)
}
}
Expand All @@ -37,7 +37,7 @@ class FetchRequestTests: GRDBTestCase {
func testRequestFetchValues() throws {
struct CustomRequest : FetchRequest {
typealias RowDecoder = Int
func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) {
func prepare(_ db: Database, hint: FetchRequestHint?) throws -> (SelectStatement, RowAdapter?) {
return try (db.makeSelectStatement(sql: "SELECT id FROM table1"), nil)
}
}
Expand Down Expand Up @@ -65,7 +65,7 @@ class FetchRequestTests: GRDBTestCase {
}
struct CustomRequest : FetchRequest {
typealias RowDecoder = CustomRecord
func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) {
func prepare(_ db: Database, hint: FetchRequestHint?) throws -> (SelectStatement, RowAdapter?) {
return try (db.makeSelectStatement(sql: "SELECT id FROM table1"), nil)
}
}
Expand All @@ -90,7 +90,7 @@ class FetchRequestTests: GRDBTestCase {
func testRequestFetchCount() throws {
struct CustomRequest : FetchRequest {
typealias RowDecoder = Row
func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) {
func prepare(_ db: Database, hint: FetchRequestHint?) throws -> (SelectStatement, RowAdapter?) {
return try (db.makeSelectStatement(sql: "SELECT * FROM table1"), nil)
}
}
Expand All @@ -113,7 +113,7 @@ class FetchRequestTests: GRDBTestCase {
func testRequestCustomizedFetchCount() throws {
struct CustomRequest : FetchRequest {
typealias RowDecoder = Row
func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?) {
func prepare(_ db: Database, hint: FetchRequestHint?) throws -> (SelectStatement, RowAdapter?) {
return try (db.makeSelectStatement(sql: "INVALID"), nil)
}

Expand Down
18 changes: 12 additions & 6 deletions Tests/GRDBTests/FetchableRecord+QueryInterfaceRequestTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,18 +92,20 @@ class FetchableRecordQueryInterfaceRequestTests: GRDBTestCase {

do {
let reader = try request.fetchOne(db)!
XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\"")
XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\" LIMIT 1")
XCTAssertEqual(reader.id!, arthur.id!)
XCTAssertEqual(reader.name, arthur.name)
XCTAssertEqual(reader.age, arthur.age)
}

do {
let names = try request.fetchCursor(db).map { $0.name }
XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\"")
XCTAssertEqual(try names.next()!, arthur.name)
XCTAssertEqual(try names.next()!, barbara.name)
XCTAssertTrue(try names.next() == nil)

// validate query *after* cursor has retrieved a record
XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\"")
}
}
}
Expand All @@ -130,7 +132,7 @@ class FetchableRecordQueryInterfaceRequestTests: GRDBTestCase {

do {
let reader = try Reader.fetchOne(db)!
XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\"")
XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\" LIMIT 1")
XCTAssertEqual(reader.id!, arthur.id!)
XCTAssertEqual(reader.name, arthur.name)
XCTAssertEqual(reader.age, arthur.age)
Expand All @@ -139,10 +141,12 @@ class FetchableRecordQueryInterfaceRequestTests: GRDBTestCase {
do {
let cursor = try Reader.fetchCursor(db)
let names = cursor.map { $0.name }
XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\"")
XCTAssertEqual(try names.next()!, arthur.name)
XCTAssertEqual(try names.next()!, barbara.name)
XCTAssertTrue(try names.next() == nil)

// validate query *after* cursor has retrieved a record
XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\"")
}
}
}
Expand Down Expand Up @@ -171,18 +175,20 @@ class FetchableRecordQueryInterfaceRequestTests: GRDBTestCase {

do {
let reader = try AltReader.fetchOne(db, request)!
XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\"")
XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\" LIMIT 1")
XCTAssertEqual(reader.id!, arthur.id!)
XCTAssertEqual(reader.name, arthur.name)
XCTAssertEqual(reader.age, arthur.age)
}

do {
let names = try AltReader.fetchCursor(db, request).map { $0.name }
XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\"")
XCTAssertEqual(try names.next()!, arthur.name)
XCTAssertEqual(try names.next()!, barbara.name)
XCTAssertTrue(try names.next() == nil)

// validate query *after* cursor has retrieved a record
XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"readers\"")
}
}
}
Expand Down
Loading