Files

855 lines
41 KiB
Swift

import XCTest
import GRDB
private enum CustomValue : Int, DatabaseValueConvertible, Equatable {
case a = 0
case b = 1
case c = 2
}
class AdapterRowTests : RowTestCase {
func testRowAsSequence() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let adapter = ColumnMapping(["a": "basea", "b": "baseb", "c": "basec"])
let row = try Row.fetchOne(db, sql: "SELECT 0 AS basea, 'XXX' AS extra, 1 AS baseb, 2 as basec", adapter: adapter)!
var columnNames = [String]()
var ints = [Int]()
var bools = [Bool]()
for (columnName, dbValue) in row {
columnNames.append(columnName)
ints.append(Int.fromDatabaseValue(dbValue)!)
bools.append(Bool.fromDatabaseValue(dbValue)!)
}
XCTAssertEqual(columnNames, ["a", "b", "c"])
XCTAssertEqual(ints, [0, 1, 2])
XCTAssertEqual(bools, [false, true, true])
}
}
func testRowValueAtIndex() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let adapter = ColumnMapping(["a": "basea", "b": "baseb", "c": "basec"])
let row = try Row.fetchOne(db, sql: "SELECT 0 AS basea, 'XXX' AS extra, 1 AS baseb, 2 as basec", adapter: adapter)!
// Raw extraction
assertRowRawValueEqual(row, index: 0, value: 0 as Int64)
assertRowRawValueEqual(row, index: 1, value: 1 as Int64)
assertRowRawValueEqual(row, index: 2, value: 2 as Int64)
// DatabaseValueConvertible & StatementColumnConvertible
assertRowConvertedValueEqual(row, index: 0, value: 0 as Int)
assertRowConvertedValueEqual(row, index: 1, value: 1 as Int)
assertRowConvertedValueEqual(row, index: 2, value: 2 as Int)
// DatabaseValueConvertible
assertRowConvertedValueEqual(row, index: 0, value: CustomValue.a)
assertRowConvertedValueEqual(row, index: 1, value: CustomValue.b)
assertRowConvertedValueEqual(row, index: 2, value: CustomValue.c)
// Expect fatal error:
//
// row[-1]
// row[3]
}
}
func testRowValueNamed() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let adapter = ColumnMapping(["a": "basea", "b": "baseb", "c": "basec"])
let row = try Row.fetchOne(db, sql: "SELECT 0 AS basea, 'XXX' AS extra, 1 AS baseb, 2 as basec", adapter: adapter)!
// Raw extraction
assertRowRawValueEqual(row, name: "a", value: 0 as Int64)
assertRowRawValueEqual(row, name: "b", value: 1 as Int64)
assertRowRawValueEqual(row, name: "c", value: 2 as Int64)
// DatabaseValueConvertible & StatementColumnConvertible
assertRowConvertedValueEqual(row, name: "a", value: 0 as Int)
assertRowConvertedValueEqual(row, name: "b", value: 1 as Int)
assertRowConvertedValueEqual(row, name: "c", value: 2 as Int)
// DatabaseValueConvertible
assertRowConvertedValueEqual(row, name: "a", value: CustomValue.a)
assertRowConvertedValueEqual(row, name: "b", value: CustomValue.b)
assertRowConvertedValueEqual(row, name: "c", value: CustomValue.c)
}
}
func testRowValueFromColumn() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let adapter = ColumnMapping(["a": "basea", "b": "baseb", "c": "basec"])
let row = try Row.fetchOne(db, sql: "SELECT 0 AS basea, 'XXX' AS extra, 1 AS baseb, 2 as basec", adapter: adapter)!
// Raw extraction
assertRowRawValueEqual(row, column: Column("a"), value: 0 as Int64)
assertRowRawValueEqual(row, column: Column("b"), value: 1 as Int64)
assertRowRawValueEqual(row, column: Column("c"), value: 2 as Int64)
// DatabaseValueConvertible & StatementColumnConvertible
assertRowConvertedValueEqual(row, column: Column("a"), value: 0 as Int)
assertRowConvertedValueEqual(row, column: Column("b"), value: 1 as Int)
assertRowConvertedValueEqual(row, column: Column("c"), value: 2 as Int)
// DatabaseValueConvertible
assertRowConvertedValueEqual(row, column: Column("a"), value: CustomValue.a)
assertRowConvertedValueEqual(row, column: Column("b"), value: CustomValue.b)
assertRowConvertedValueEqual(row, column: Column("c"), value: CustomValue.c)
}
}
func testWithUnsafeData() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let data = "foo".data(using: .utf8)!
let emptyData = Data()
let adapter = ColumnMapping(["a": "basea", "b": "baseb", "c": "basec"])
let row = try Row.fetchOne(db, sql: "SELECT ? AS basea, ? AS baseb, ? AS basec", arguments: [data, emptyData, nil], adapter: adapter)!
try row.withUnsafeData(atIndex: 0) { XCTAssertEqual($0, data) }
try row.withUnsafeData(named: "a") { XCTAssertEqual($0, data) }
try row.withUnsafeData(at: Column("a")) { XCTAssertEqual($0, data) }
try row.withUnsafeData(atIndex: 1) { XCTAssertEqual($0, emptyData) }
try row.withUnsafeData(named: "b") { XCTAssertEqual($0, emptyData) }
try row.withUnsafeData(at: Column("b")) { XCTAssertEqual($0, emptyData) }
try row.withUnsafeData(atIndex: 2) { XCTAssertNil($0) }
try row.withUnsafeData(named: "c") { XCTAssertNil($0) }
try row.withUnsafeData(at: Column("c")) { XCTAssertNil($0) }
try row.withUnsafeData(named: "missing") { XCTAssertNil($0) }
try row.withUnsafeData(at: Column("missing")) { XCTAssertNil($0) }
}
}
func testRowDatabaseValueAtIndex() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let adapter = ColumnMapping(["null": "basenull", "int64": "baseint64", "double": "basedouble", "string": "basestring", "blob": "baseblob"])
let row = try Row.fetchOne(db, sql: "SELECT NULL AS basenull, 'XXX' AS extra, 1 AS baseint64, 1.1 AS basedouble, 'foo' AS basestring, x'53514C697465' AS baseblob", adapter: adapter)!
guard case .null = (row[0] as DatabaseValue).storage else { XCTFail(); return }
guard case .int64(let int64) = (row[1] as DatabaseValue).storage, int64 == 1 else { XCTFail(); return }
guard case .double(let double) = (row[2] as DatabaseValue).storage, double == 1.1 else { XCTFail(); return }
guard case .string(let string) = (row[3] as DatabaseValue).storage, string == "foo" else { XCTFail(); return }
guard case .blob(let data) = (row[4] as DatabaseValue).storage, data == "SQLite".data(using: .utf8) else { XCTFail(); return }
}
}
func testRowDatabaseValueNamed() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let adapter = ColumnMapping(["null": "basenull", "int64": "baseint64", "double": "basedouble", "string": "basestring", "blob": "baseblob"])
let row = try Row.fetchOne(db, sql: "SELECT NULL AS basenull, 'XXX' AS extra, 1 AS baseint64, 1.1 AS basedouble, 'foo' AS basestring, x'53514C697465' AS baseblob", adapter: adapter)!
guard case .null = (row["null"] as DatabaseValue).storage else { XCTFail(); return }
guard case .int64(let int64) = (row["int64"] as DatabaseValue).storage, int64 == 1 else { XCTFail(); return }
guard case .double(let double) = (row["double"] as DatabaseValue).storage, double == 1.1 else { XCTFail(); return }
guard case .string(let string) = (row["string"] as DatabaseValue).storage, string == "foo" else { XCTFail(); return }
guard case .blob(let data) = (row["blob"] as DatabaseValue).storage, data == "SQLite".data(using: .utf8) else { XCTFail(); return }
}
}
func testRowCount() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let adapter = ColumnMapping(["a": "basea", "b": "baseb", "c": "basec"])
let row = try Row.fetchOne(db, sql: "SELECT 0 AS basea, 'XXX' AS extra, 1 AS baseb, 2 as basec", adapter: adapter)!
XCTAssertEqual(row.count, 3)
}
}
func testRowColumnNames() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let adapter = ColumnMapping(["a": "basea", "b": "baseb", "c": "basec"])
let row = try Row.fetchOne(db, sql: "SELECT 0 AS basea, 'XXX' AS extra, 1 AS baseb, 2 as basec", adapter: adapter)!
XCTAssertEqual(Array(row.columnNames), ["a", "b", "c"])
}
}
func testRowDatabaseValues() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let adapter = ColumnMapping(["a": "basea", "b": "baseb", "c": "basec"])
let row = try Row.fetchOne(db, sql: "SELECT 0 AS basea, 'XXX' AS extra, 1 AS baseb, 2 as basec", adapter: adapter)!
XCTAssertEqual(Array(row.databaseValues), [0.databaseValue, 1.databaseValue, 2.databaseValue])
}
}
func testRowIsCaseInsensitive() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let adapter = ColumnMapping(["nAmE": "basenAmE"])
let row = try Row.fetchOne(db, sql: "SELECT 'foo' AS basenAmE, 'XXX' AS extra", adapter: adapter)!
XCTAssertEqual(row["name"] as DatabaseValue, "foo".databaseValue)
XCTAssertEqual(row["NAME"] as DatabaseValue, "foo".databaseValue)
XCTAssertEqual(row["NaMe"] as DatabaseValue, "foo".databaseValue)
XCTAssertEqual(row["name"] as String, "foo")
XCTAssertEqual(row["NAME"] as String, "foo")
XCTAssertEqual(row["NaMe"] as String, "foo")
}
}
func testRowIsCaseInsensitiveAndReturnsLeftmostMatchingColumn() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let adapter = ColumnMapping(["name": "basename", "NAME": "baseNAME"])
let row = try Row.fetchOne(db, sql: "SELECT 1 AS basename, 'XXX' AS extra, 2 AS baseNAME", adapter: adapter)!
XCTAssertEqual(row["name"] as DatabaseValue, 1.databaseValue)
XCTAssertEqual(row["NAME"] as DatabaseValue, 1.databaseValue)
XCTAssertEqual(row["NaMe"] as DatabaseValue, 1.databaseValue)
XCTAssertEqual(row["name"] as Int, 1)
XCTAssertEqual(row["NAME"] as Int, 1)
XCTAssertEqual(row["NaMe"] as Int, 1)
}
}
func testRowAdapterIsCaseInsensitiveAndPicksLeftmostBaseColumn() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let adapter = ColumnMapping(["name": "baseNaMe"])
let row = try Row.fetchOne(db, sql: "SELECT 1 AS basename, 2 AS baseNaMe, 3 AS BASENAME", adapter: adapter)!
XCTAssertEqual(row["name"] as DatabaseValue, 1.databaseValue)
}
}
func testMissingColumn() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let adapter = ColumnMapping(["name": "name"])
let row = try Row.fetchOne(db, sql: "SELECT 1 AS name, 'foo' AS missing", adapter: adapter)!
XCTAssertFalse(row.hasColumn("missing"))
XCTAssertFalse(row.hasColumn("missingInBaseRow"))
XCTAssertTrue(row["missing"] as DatabaseValue? == nil)
XCTAssertTrue(row["missingInBaseRow"] as DatabaseValue? == nil)
XCTAssertTrue(row["missing"] == nil)
XCTAssertTrue(row["missingInBaseRow"] == nil)
}
}
func testRowHasColumnIsCaseInsensitive() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let adapter = ColumnMapping(["nAmE": "basenAmE", "foo": "basefoo"])
let row = try Row.fetchOne(db, sql: "SELECT 'foo' AS basenAmE, 'XXX' AS extra, 1 AS basefoo", adapter: adapter)!
XCTAssertTrue(row.hasColumn("name"))
XCTAssertTrue(row.hasColumn("NAME"))
XCTAssertTrue(row.hasColumn("Name"))
XCTAssertTrue(row.hasColumn("NaMe"))
XCTAssertTrue(row.hasColumn("foo"))
XCTAssertTrue(row.hasColumn("Foo"))
XCTAssertTrue(row.hasColumn("FOO"))
}
}
func testScopes() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let adapter = ScopeAdapter([
"sub1": ColumnMapping(["id": "id1", "val": "val1"]),
"sub2": ColumnMapping(["id": "id2", "val": "val2"])])
let row = try Row.fetchOne(db, sql: "SELECT 1 AS id1, 'foo1' AS val1, 2 as id2, 'foo2' AS val2", adapter: adapter)!
XCTAssertEqual(row.count, 4)
XCTAssertEqual(row["id1"] as Int, 1)
XCTAssertEqual(row["val1"] as String, "foo1")
XCTAssertEqual(row["id2"] as Int, 2)
XCTAssertEqual(row["val2"] as String, "foo2")
XCTAssertEqual(Set(row.scopes.names), ["sub1", "sub2"])
XCTAssertEqual(row.scopes.count, 2)
for (name, scopedRow) in row.scopes {
if name == "sub1" {
XCTAssertEqual(scopedRow, ["id": 1, "val": "foo1"])
} else if name == "sub2" {
XCTAssertEqual(scopedRow, ["id": 2, "val": "foo2"])
} else {
XCTFail()
}
}
XCTAssertEqual(row.scopesTree.names, ["sub1", "sub2"])
XCTAssertEqual(row.scopes["sub1"]!, ["id": 1, "val": "foo1"])
XCTAssertTrue(row.scopes["sub1"]!.scopes.isEmpty)
XCTAssertEqual(row.scopesTree["sub1"], row.scopes["sub1"])
XCTAssertEqual(row.scopes["sub2"]!, ["id": 2, "val": "foo2"])
XCTAssertTrue(row.scopes["sub2"]!.scopes.isEmpty)
XCTAssertEqual(row.scopesTree["sub2"], row.scopes["sub2"])
XCTAssertTrue(row.scopes["SUB1"] == nil)
XCTAssertTrue(row.scopesTree["SUB1"] == nil)
XCTAssertTrue(row.scopes["missing"] == nil)
XCTAssertTrue(row.scopesTree["missing"] == nil)
}
}
func testScopesWithMainMapping() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let adapter = ColumnMapping(["id": "id0", "val": "val0"])
.addingScopes([
"sub1": ColumnMapping(["id": "id1", "val": "val1"]),
"sub2": ColumnMapping(["id": "id2", "val": "val2"])])
let row = try Row.fetchOne(db, sql: "SELECT 0 AS id0, 'foo0' AS val0, 1 AS id1, 'foo1' AS val1, 2 as id2, 'foo2' AS val2", adapter: adapter)!
XCTAssertEqual(row.count, 2)
XCTAssertEqual(row["id"] as Int, 0)
XCTAssertEqual(row["val"] as String, "foo0")
XCTAssertEqual(Set(row.scopes.names), ["sub1", "sub2"])
XCTAssertEqual(row.scopes.count, 2)
for (name, scopedRow) in row.scopes {
if name == "sub1" {
XCTAssertEqual(scopedRow, ["id": 1, "val": "foo1"])
} else if name == "sub2" {
XCTAssertEqual(scopedRow, ["id": 2, "val": "foo2"])
} else {
XCTFail()
}
}
XCTAssertEqual(row.scopesTree.names, ["sub1", "sub2"])
XCTAssertEqual(row.scopes["sub1"]!, ["id": 1, "val": "foo1"])
XCTAssertTrue(row.scopes["sub1"]!.scopes.isEmpty)
XCTAssertEqual(row.scopesTree["sub1"], row.scopes["sub1"])
XCTAssertEqual(row.scopes["sub2"], ["id": 2, "val": "foo2"])
XCTAssertTrue(row.scopes["sub2"]!.scopes.isEmpty)
XCTAssertEqual(row.scopesTree["sub2"], row.scopes["sub2"])
XCTAssertTrue(row.scopes["SUB1"] == nil)
XCTAssertTrue(row.scopesTree["SUB1"] == nil)
XCTAssertTrue(row.scopes["missing"] == nil)
XCTAssertTrue(row.scopesTree["missing"] == nil)
}
}
func testMergeScopes() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let adapter0 = ColumnMapping(["id": "id0", "val": "val0"])
let adapter1 = ColumnMapping(["id": "id1", "val": "val1"])
let adapter2 = ColumnMapping(["id": "id2", "val": "val2"])
// - sub0 is defined in the the first scoped adapter
// - sub1 is defined in the the first scoped adapter, and then
// redefined in the second
// - sub2 is defined in the the second scoped adapter
let mainAdapter = ScopeAdapter(["sub0": adapter0, "sub1": adapter2])
let adapter = mainAdapter.addingScopes(["sub1": adapter1, "sub2": adapter2])
let row = try Row.fetchOne(db, sql: "SELECT 0 AS id0, 'foo0' AS val0, 1 AS id1, 'foo1' AS val1, 2 as id2, 'foo2' AS val2", adapter: adapter)!
XCTAssertEqual(Set(row.scopes.names), ["sub0", "sub1", "sub2"])
XCTAssertEqual(row.scopes.count, 3)
for (name, scopedRow) in row.scopes {
if name == "sub0" {
XCTAssertEqual(scopedRow, ["id": 0, "val": "foo0"])
} else if name == "sub1" {
XCTAssertEqual(scopedRow, ["id": 1, "val": "foo1"])
} else if name == "sub2" {
XCTAssertEqual(scopedRow, ["id": 2, "val": "foo2"])
} else {
XCTFail()
}
}
XCTAssertEqual(row.scopesTree.names, ["sub0", "sub1", "sub2"])
XCTAssertEqual(row.scopes["sub0"], ["id": 0, "val": "foo0"])
XCTAssertTrue(row.scopes["sub0"]!.scopes.isEmpty)
XCTAssertEqual(row.scopesTree["sub0"], row.scopes["sub0"])
XCTAssertEqual(row.scopes["sub1"], ["id": 1, "val": "foo1"])
XCTAssertTrue(row.scopes["sub1"]!.scopes.isEmpty)
XCTAssertEqual(row.scopesTree["sub1"], row.scopes["sub1"])
XCTAssertEqual(row.scopes["sub2"], ["id": 2, "val": "foo2"])
XCTAssertTrue(row.scopes["sub2"]!.scopes.isEmpty)
XCTAssertEqual(row.scopesTree["sub2"], row.scopes["sub2"])
}
}
func testThreeLevelScopes() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let adapter = ColumnMapping(["id": "id0", "val": "val0"])
.addingScopes([
"sub1": ColumnMapping(["id": "id1", "val": "val1"])
.addingScopes([
"sub2": ColumnMapping(["id": "id2", "val": "val2"])])])
let row = try Row.fetchOne(db, sql: "SELECT 0 AS id0, 'foo0' AS val0, 1 AS id1, 'foo1' AS val1, 2 as id2, 'foo2' AS val2", adapter: adapter)!
XCTAssertEqual(row.count, 2)
XCTAssertEqual(row["id"] as Int, 0)
XCTAssertEqual(row["val"] as String, "foo0")
XCTAssertEqual(Set(row.scopes.names), ["sub1"])
XCTAssertEqual(row.scopes.count, 1)
for (name, scopedRow) in row.scopes {
if name == "sub1" {
XCTAssertEqual(scopedRow.unscoped, ["id": 1, "val": "foo1"])
} else {
XCTFail()
}
}
let scopedRow = row.scopes["sub1"]!
XCTAssertEqual(scopedRow.unscoped, ["id": 1, "val": "foo1"])
XCTAssertEqual(Set(scopedRow.scopes.names), ["sub2"])
XCTAssertEqual(scopedRow.scopes.count, 1)
for (name, subScopedRow) in scopedRow.scopes {
if name == "sub2" {
XCTAssertEqual(subScopedRow, ["id": 2, "val": "foo2"])
} else {
XCTFail()
}
}
XCTAssertEqual(scopedRow.scopes["sub2"]!, ["id": 2, "val": "foo2"])
XCTAssertTrue(scopedRow.scopes["sub2"]!.scopes.isEmpty)
// sub2 is only defined in sub1
XCTAssertTrue(row.scopes["sub2"] == nil)
XCTAssertEqual(row.scopesTree.names, ["sub1", "sub2"])
XCTAssertEqual(row.scopesTree["sub1"], row.scopes["sub1"])
XCTAssertEqual(row.scopesTree["sub2"], row.scopes["sub1"]!.scopes["sub2"])
}
}
func testSuffixAdapter() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let sql = "SELECT 0 AS a0, 1 AS a1, 2 AS a2, 3 AS a3"
do {
let row = try Row.fetchOne(db, sql: sql, adapter: SuffixRowAdapter(fromIndex:0))!
XCTAssertEqual(Array(row.columnNames), ["a0", "a1", "a2", "a3"])
XCTAssertEqual(Array(row.databaseValues), [0.databaseValue, 1.databaseValue, 2.databaseValue, 3.databaseValue])
}
do {
let row = try Row.fetchOne(db, sql: sql, adapter: SuffixRowAdapter(fromIndex:1))!
XCTAssertEqual(Array(row.columnNames), ["a1", "a2", "a3"])
XCTAssertEqual(Array(row.databaseValues), [1.databaseValue, 2.databaseValue, 3.databaseValue])
}
do {
let row = try Row.fetchOne(db, sql: sql, adapter: SuffixRowAdapter(fromIndex:3))!
XCTAssertEqual(Array(row.columnNames), ["a3"])
XCTAssertEqual(Array(row.databaseValues), [3.databaseValue])
}
do {
let row = try Row.fetchOne(db, sql: sql, adapter: SuffixRowAdapter(fromIndex:4))!
XCTAssertEqual(Array(row.columnNames), [])
XCTAssertEqual(Array(row.databaseValues), [])
}
}
}
func testSuffixAdapterIndexesAreIndependentFromScopes() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let adapter = ScopeAdapter([
"sub1": SuffixRowAdapter(fromIndex: 1)
.addingScopes([
"sub2": SuffixRowAdapter(fromIndex: 1)])])
let row = try Row.fetchOne(db, sql: "SELECT 0 AS a0, 1 AS a1, 2 AS a2, 3 AS a3", adapter: adapter)!
XCTAssertEqual(Set(row.scopes.names), ["sub1"])
XCTAssertEqual(row.scopes.count, 1)
for (name, scopedRow) in row.scopes {
if name == "sub1" {
XCTAssertEqual(scopedRow.unscoped, ["a1": 1, "a2": 2, "a3": 3])
} else {
XCTFail()
}
}
let scopedRow = row.scopes["sub1"]!
XCTAssertEqual(scopedRow.unscoped, ["a1": 1, "a2": 2, "a3": 3])
XCTAssertEqual(Set(scopedRow.scopes.names), ["sub2"])
XCTAssertEqual(scopedRow.scopes.count, 1)
for (name, subScopedRow) in scopedRow.scopes {
if name == "sub2" {
XCTAssertEqual(subScopedRow, ["a1": 1, "a2": 2, "a3": 3])
} else {
XCTFail()
}
}
let subScopedRow = scopedRow.scopes["sub2"]!
XCTAssertEqual(subScopedRow, ["a1": 1, "a2": 2, "a3": 3])
XCTAssertTrue(subScopedRow.scopes.isEmpty)
// sub2 is only defined in sub1
XCTAssertTrue(row.scopes["sub2"] == nil)
XCTAssertEqual(row.scopesTree.names, ["sub1", "sub2"])
XCTAssertEqual(row.scopesTree["sub1"], row.scopes["sub1"])
XCTAssertEqual(row.scopesTree["sub2"], row.scopes["sub1"]!.scopes["sub2"])
}
}
func testRangeAdapterWithCountableRange() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let sql = "SELECT 0 AS a0, 1 AS a1, 2 AS a2, 3 AS a3"
do {
let row = try Row.fetchOne(db, sql: sql, adapter: RangeRowAdapter(0..<0))!
XCTAssertEqual(Array(row.columnNames), [])
XCTAssertEqual(Array(row.databaseValues), [])
}
do {
let row = try Row.fetchOne(db, sql: sql, adapter: RangeRowAdapter(0..<1))!
XCTAssertEqual(Array(row.columnNames), ["a0"])
XCTAssertEqual(Array(row.databaseValues), [0.databaseValue])
}
do {
let row = try Row.fetchOne(db, sql: sql, adapter: RangeRowAdapter(1..<3))!
XCTAssertEqual(Array(row.columnNames), ["a1", "a2"])
XCTAssertEqual(Array(row.databaseValues), [1.databaseValue, 2.databaseValue])
}
do {
let row = try Row.fetchOne(db, sql: sql, adapter: RangeRowAdapter(0..<4))!
XCTAssertEqual(Array(row.columnNames), ["a0", "a1", "a2", "a3"])
XCTAssertEqual(Array(row.databaseValues), [0.databaseValue, 1.databaseValue, 2.databaseValue, 3.databaseValue])
}
do {
let row = try Row.fetchOne(db, sql: sql, adapter: RangeRowAdapter(4..<4))!
XCTAssertEqual(Array(row.columnNames), [])
XCTAssertEqual(Array(row.databaseValues), [])
}
}
}
func testRangeAdapterWithCountableClosedRange() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let sql = "SELECT 0 AS a0, 1 AS a1, 2 AS a2, 3 AS a3"
do {
let row = try Row.fetchOne(db, sql: sql, adapter: RangeRowAdapter(0...0))!
XCTAssertEqual(Array(row.columnNames), ["a0"])
XCTAssertEqual(Array(row.databaseValues), [0.databaseValue])
}
do {
let row = try Row.fetchOne(db, sql: sql, adapter: RangeRowAdapter(0...1))!
XCTAssertEqual(Array(row.columnNames), ["a0", "a1"])
XCTAssertEqual(Array(row.databaseValues), [0.databaseValue, 1.databaseValue])
}
do {
let row = try Row.fetchOne(db, sql: sql, adapter: RangeRowAdapter(1...2))!
XCTAssertEqual(Array(row.columnNames), ["a1", "a2"])
XCTAssertEqual(Array(row.databaseValues), [1.databaseValue, 2.databaseValue])
}
do {
let row = try Row.fetchOne(db, sql: sql, adapter: RangeRowAdapter(0...3))!
XCTAssertEqual(Array(row.columnNames), ["a0", "a1", "a2", "a3"])
XCTAssertEqual(Array(row.databaseValues), [0.databaseValue, 1.databaseValue, 2.databaseValue, 3.databaseValue])
}
do {
let row = try Row.fetchOne(db, sql: sql, adapter: RangeRowAdapter(3...3))!
XCTAssertEqual(Array(row.columnNames), ["a3"])
XCTAssertEqual(Array(row.databaseValues), [3.databaseValue])
}
}
}
func testRangeAdapterIndexesAreIndependentFromScopes() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let adapter = ScopeAdapter([
"sub1": RangeRowAdapter(1..<3)
.addingScopes([
"sub2": RangeRowAdapter(1..<3)])])
let row = try Row.fetchOne(db, sql: "SELECT 0 AS a0, 1 AS a1, 2 AS a2, 3 AS a3", adapter: adapter)!
XCTAssertEqual(Set(row.scopes.names), ["sub1"])
XCTAssertEqual(row.scopes.count, 1)
for (name, scopedRow) in row.scopes {
if name == "sub1" {
XCTAssertEqual(scopedRow.unscoped, ["a1": 1, "a2": 2])
} else {
XCTFail()
}
}
let scopedRow = row.scopes["sub1"]!
XCTAssertEqual(scopedRow.unscoped, ["a1": 1, "a2": 2])
XCTAssertEqual(Set(scopedRow.scopes.names), ["sub2"])
XCTAssertEqual(scopedRow.scopes.count, 1)
for (name, subScopedRow) in scopedRow.scopes {
if name == "sub2" {
XCTAssertEqual(subScopedRow, ["a1": 1, "a2": 2])
} else {
XCTFail()
}
}
let subScopedRow = scopedRow.scopes["sub2"]!
XCTAssertEqual(subScopedRow, ["a1": 1, "a2": 2])
XCTAssertTrue(subScopedRow.scopes.isEmpty)
// sub2 is only defined in sub1
XCTAssertTrue(row.scopes["sub2"] == nil)
XCTAssertEqual(row.scopesTree.names, ["sub1", "sub2"])
XCTAssertEqual(row.scopesTree["sub1"], row.scopes["sub1"])
XCTAssertEqual(row.scopesTree["sub2"], row.scopes["sub1"]!.scopes["sub2"])
}
}
func testCopy() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let adapter = ColumnMapping(["a": "basea", "b": "baseb", "c": "basec"])
.addingScopes(["sub": ColumnMapping(["a": "baseb"])])
var copiedRow: Row? = nil
let baseRows = try Row.fetchCursor(db, sql: "SELECT 0 AS basea, 'XXX' AS extra, 1 AS baseb, 2 as basec", adapter: adapter)
while let baseRow = try baseRows.next() {
copiedRow = baseRow.copy()
}
if let copiedRow {
XCTAssertEqual(copiedRow.count, 3)
XCTAssertEqual(copiedRow["a"] as Int, 0)
XCTAssertEqual(copiedRow["b"] as Int, 1)
XCTAssertEqual(copiedRow["c"] as Int, 2)
XCTAssertEqual(Set(copiedRow.scopes.names), ["sub"])
XCTAssertEqual(copiedRow.scopes.count, 1)
for (name, scopedRow) in copiedRow.scopes {
if name == "sub" {
XCTAssertEqual(scopedRow, ["a": 1])
} else {
XCTFail()
}
}
XCTAssertEqual(Set(copiedRow.scopes.names), ["sub"])
XCTAssertEqual(copiedRow.scopes["sub"]!, ["a": 1])
XCTAssertTrue(copiedRow.scopes["sub"]!.scopes.isEmpty)
XCTAssertEqual(copiedRow.scopesTree.names, ["sub"])
XCTAssertEqual(copiedRow.scopesTree["sub"]!, ["a": 1])
} else {
XCTFail()
}
}
}
func testEqualityWithCopy() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let adapter = ColumnMapping(["a": "basea", "b": "baseb", "c": "basec"])
var row: Row? = nil
let baseRows = try Row.fetchCursor(db, sql: "SELECT 0 AS basea, 'XXX' AS extra, 1 AS baseb, 2 as basec", adapter: adapter)
while let baseRow = try baseRows.next() {
row = baseRow.copy()
XCTAssertEqual(row, baseRow)
}
if let row {
let copiedRow = row.copy()
XCTAssertEqual(row, copiedRow)
} else {
XCTFail()
}
}
}
func testEqualityComparesScopes() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let adapter1 = ColumnMapping(["a": "basea", "b": "baseb", "c": "basec"])
.addingScopes(["sub": ColumnMapping(["b": "baseb"])])
let adapter2 = ColumnMapping(["a": "basea", "b": "baseb2", "c": "basec"])
let adapter3 = ColumnMapping(["a": "basea", "b": "baseb2", "c": "basec"])
.addingScopes(["sub": ColumnMapping(["b": "baseb2"])])
let adapter4 = ColumnMapping(["a": "basea", "b": "baseb", "c": "basec"])
.addingScopes(["sub": ColumnMapping(["b": "baseb"]), "altSub": ColumnMapping(["a": "baseb2"])])
let adapter5 = ColumnMapping(["a": "basea", "b": "baseb", "c": "basec"])
.addingScopes(["sub": ColumnMapping(["b": "baseb", "c": "basec"])])
let row1 = try Row.fetchOne(db, sql: "SELECT 0 AS basea, 'XXX' AS extra, 1 AS baseb, 1 AS baseb2, 2 as basec", adapter: adapter1)!
let row2 = try Row.fetchOne(db, sql: "SELECT 0 AS basea, 'XXX' AS extra, 1 AS baseb, 1 AS baseb2, 2 as basec", adapter: adapter2)!
let row3 = try Row.fetchOne(db, sql: "SELECT 0 AS basea, 'XXX' AS extra, 1 AS baseb, 1 AS baseb2, 2 as basec", adapter: adapter3)!
let row4 = try Row.fetchOne(db, sql: "SELECT 0 AS basea, 'XXX' AS extra, 1 AS baseb, 1 AS baseb2, 2 as basec", adapter: adapter4)!
let row5 = try Row.fetchOne(db, sql: "SELECT 0 AS basea, 'XXX' AS extra, 1 AS baseb, 1 AS baseb2, 2 as basec", adapter: adapter5)!
let tests = [
(row1, row2, false),
(row1, row3, true),
(row1, row4, false),
(row1, row5, false),
(row1.scopes["sub"], row3.scopes["sub"], true),
(row1.scopes["sub"], row4.scopes["sub"], true),
(row1.scopes["sub"], row5.scopes["sub"], false)]
for (lrow, rrow, equal) in tests {
if equal {
XCTAssertEqual(lrow, rrow)
} else {
XCTAssertNotEqual(lrow, rrow)
}
}
}
}
func testEqualityWithNonMappedRow() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let adapter = ColumnMapping(["id": "baseid", "val": "baseval"])
let mappedRow1 = try Row.fetchOne(db, sql: "SELECT 1 AS baseid, 'XXX' AS extra, 'foo' AS baseval", adapter: adapter)!
let mappedRow2 = try Row.fetchOne(db, sql: "SELECT 'foo' AS baseval, 'XXX' AS extra, 1 AS baseid", adapter: adapter)!
let nonMappedRow1 = try Row.fetchOne(db, sql: "SELECT 1 AS id, 'foo' AS val")!
let nonMappedRow2 = try Row.fetchOne(db, sql: "SELECT 'foo' AS val, 1 AS id")!
// All rows contain the same values. But they differ by column ordering.
XCTAssertEqual(Array(mappedRow1.columnNames), ["id", "val"])
XCTAssertEqual(Array(mappedRow2.columnNames), ["val", "id"])
XCTAssertEqual(Array(nonMappedRow1.columnNames), ["id", "val"])
XCTAssertEqual(Array(nonMappedRow2.columnNames), ["val", "id"])
// Row equality takes ordering in account:
XCTAssertNotEqual(mappedRow1, mappedRow2)
XCTAssertEqual(mappedRow1, nonMappedRow1)
XCTAssertNotEqual(mappedRow1, nonMappedRow2)
XCTAssertNotEqual(mappedRow2, nonMappedRow1)
XCTAssertEqual(mappedRow2, nonMappedRow2)
}
}
func testEmptyMapping() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let adapter = ColumnMapping([:])
let row = try Row.fetchOne(db, sql: "SELECT 'foo' AS foo", adapter: adapter)!
XCTAssertTrue(row.isEmpty)
XCTAssertEqual(row.count, 0)
XCTAssertEqual(Array(row.columnNames), [])
XCTAssertEqual(Array(row.databaseValues), [])
XCTAssertFalse(row.hasColumn("foo"))
}
}
func testRequestAdapter() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
do {
let row = try SQLRequest<Row>(sql: "SELECT 0 AS a0, 1 AS a1, 2 AS a2", adapter: SuffixRowAdapter(fromIndex: 1))
.fetchOne(db)!
XCTAssertEqual(Array(row.columnNames), ["a1", "a2"])
XCTAssertEqual(Array(row.databaseValues), [1.databaseValue, 2.databaseValue])
}
do {
let row = try SQLRequest<Row>(sql: "SELECT 0 AS a0, 1 AS a1, 2 AS a2")
.adapted { _ in SuffixRowAdapter(fromIndex: 1) }
.fetchOne(db)!
XCTAssertEqual(Array(row.columnNames), ["a1", "a2"])
XCTAssertEqual(Array(row.databaseValues), [1.databaseValue, 2.databaseValue])
}
do {
let row = try SQLRequest<Row>(sql: "SELECT 0 AS a0, 1 AS a1, 2 AS a2", adapter: SuffixRowAdapter(fromIndex: 1))
.adapted { _ in SuffixRowAdapter(fromIndex: 1) }
.fetchOne(db)!
XCTAssertEqual(Array(row.columnNames), ["a2"])
XCTAssertEqual(Array(row.databaseValues), [2.databaseValue])
}
do {
let row = try SQLRequest<Row>(sql: "SELECT 0 AS a0", adapter: ColumnMapping(["a1": "a0"]))
.adapted { _ in ColumnMapping(["a2": "a1"]) }
.fetchOne(db)!
XCTAssertEqual(Array(row.columnNames), ["a2"])
XCTAssertEqual(Array(row.databaseValues), [0.databaseValue])
}
do {
let row = try SQLRequest<Row>(sql: "SELECT 0 AS a0", adapter: ColumnMapping(["a1": "a0"]))
.adapted { _ in ColumnMapping(["a2": "a1"]) }
.adapted { _ in ColumnMapping(["a3": "a2"]) }
.fetchOne(db)!
XCTAssertEqual(Array(row.columnNames), ["a3"])
XCTAssertEqual(Array(row.databaseValues), [0.databaseValue])
}
}
}
func testDescription() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let adapter = ColumnMapping(["id": "id0", "val": "val0"])
.addingScopes([
"a": ColumnMapping(["id": "id1", "val": "val1"])
.addingScopes([
"b": ColumnMapping(["id": "id2", "val": "val2"])
.addingScopes([
"c": SuffixRowAdapter(fromIndex:4)]),
"a": SuffixRowAdapter(fromIndex:0)]),
"b": ColumnMapping(["id": "id1", "val": "val1"])
.addingScopes([
"ba": ColumnMapping(["id": "id2", "val": "val2"])])])
let row = try Row.fetchOne(db, sql: "SELECT 0 AS id0, 'foo0' AS val0, 1 AS id1, 'foo1' AS val1, 2 as id2, 'foo2' AS val2", adapter: adapter)!
XCTAssertEqual(row.description, "[id:0 val:\"foo0\"]")
XCTAssertEqual(row.debugDescription, """
▿ [id:0 val:"foo0"]
unadapted: [id0:0 val0:"foo0" id1:1 val1:"foo1" id2:2 val2:"foo2"]
- a: [id:1 val:"foo1"]
- a: [id0:0 val0:"foo0" id1:1 val1:"foo1" id2:2 val2:"foo2"]
- b: [id:2 val:"foo2"]
- c: [id2:2 val2:"foo2"]
- b: [id:1 val:"foo1"]
- ba: [id:2 val:"foo2"]
""")
XCTAssertEqual(row.scopes["a"]!.description, "[id:1 val:\"foo1\"]")
XCTAssertEqual(row.scopes["a"]!.debugDescription, """
▿ [id:1 val:"foo1"]
unadapted: [id0:0 val0:"foo0" id1:1 val1:"foo1" id2:2 val2:"foo2"]
- a: [id0:0 val0:"foo0" id1:1 val1:"foo1" id2:2 val2:"foo2"]
- b: [id:2 val:"foo2"]
- c: [id2:2 val2:"foo2"]
""")
}
}
func testRenameColumnAdapter() throws {
// Test RenameColumn with the use case it was introduced for:
// columns that have a `:NNN` suffix.
// See https://github.com/groue/GRDB.swift/issues/810
let request: SQLRequest<Row> = #"SELECT 1 AS "id:1", 'foo' AS name, 2 AS "id:2""#
let adaptedRequest = request
.adapted { _ in RenameColumnAdapter { String($0.prefix(while: { $0 != ":" })) } }
.adapted { _ in
let adapters = splittingRowAdapters(columnCounts: [2, 1])
return ScopeAdapter([
"a": adapters[0],
"b": adapters[1],
])
}
let row = try makeDatabaseQueue().read(adaptedRequest.fetchOne)!
XCTAssertEqual(row.unscoped, ["id": 1, "name": "foo", "id": 2])
XCTAssertEqual(row.unadapted, ["id:1": 1, "name": "foo", "id:2": 2])
XCTAssertEqual(row.scopes["a"], ["id": 1, "name": "foo"])
XCTAssertEqual(row.scopes["b"], ["id": 2])
}
}