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

Improve Row descriptions #331

Merged
merged 2 commits into from
Apr 11, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/Database+Schema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ extension Database {
var rawForeignKeys: [(destinationTable: String, mapping: [(origin: String, destination: String?, seq: Int)])] = []
var previousId: Int? = nil
for row in try Row.fetchAll(self, "PRAGMA foreign_key_list(\(tableName.quotedDatabaseIdentifier))") {
// row = <Row id:0 seq:0 table:"parents" from:"parentId" to:"id" on_update:"..." on_delete:"..." match:"...">
// row = [id:0 seq:0 table:"parents" from:"parentId" to:"id" on_update:"..." on_delete:"..." match:"..."]
let id: Int = row[0]
let seq: Int = row[1]
let table: String = row[2]
Expand Down
2 changes: 1 addition & 1 deletion GRDB/Core/DatabaseValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ extension DatabaseValue {
case .string(let string):
return String(reflecting: string)
case .blob(let data):
return data.description
return "Data(\(data.description))"
}
}
}
67 changes: 57 additions & 10 deletions GRDB/Core/Row.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Foundation
#endif

/// A database row.
public final class Row : Equatable, Hashable, RandomAccessCollection, ExpressibleByDictionaryLiteral, CustomStringConvertible {
public final class Row : Equatable, Hashable, RandomAccessCollection, ExpressibleByDictionaryLiteral, CustomStringConvertible, CustomDebugStringConvertible {
let impl: RowImpl

/// Unless we are producing a row array, we use a single row when iterating
Expand Down Expand Up @@ -541,11 +541,27 @@ extension Row {
return impl.scoped(on: name)
}

/// Returns a copy of the row, without any scoped row (if the row was fetched
/// with a row adapter that defines scopes).
/// Returns a copy of the row, without any scopes.
///
/// This property can turn out useful when you want to test the content of
/// adapted rows, such as rows fetched from joined requests.
///
/// let row = ...
/// // Failure because row equality tests for row scopes:
/// XCTAssertEqual(row, ["id": 1, "name": "foo"])
/// // Success:
/// XCTAssertEqual(row.unscoped, ["id": 1, "name": "foo"])
public var unscoped: Row {
return Row(impl: ArrayRowImpl(columns: map { ($0, $1) }))
}

/// Return the raw row fetched from the database.
///
/// This property can turn out useful when you debug the consumption of
/// adapted rows, such as rows fetched from joined requests.
public var unadapted: Row {
return impl.unadaptedRow
}
}

/// A cursor of database rows. For example:
Expand Down Expand Up @@ -788,7 +804,7 @@ extension Row {
///
/// let row: Row = ["foo": 1, "foo": "bar", "baz": nil]
/// print(row)
/// // Prints <Row foo:1 foo:"bar" baz:NULL>
/// // Prints [foo:1 foo:"bar" baz:NULL]
public convenience init(dictionaryLiteral elements: (String, DatabaseValueConvertible?)...) {
self.init(impl: ArrayRowImpl(columns: elements.map { ($0, $1?.databaseValue ?? .null) }))
}
Expand Down Expand Up @@ -874,15 +890,41 @@ extension Row {
}
}

// CustomStringConvertible
// CustomStringConvertible & CustomDebugStringConvertible
extension Row {
/// :nodoc:
public var description: String {
return "<Row"
+ map { (column, dbValue) in
" \(column):\(dbValue)"
}.joined(separator: "")
+ ">"
return "["
+ map { (column, dbValue) in "\(column):\(dbValue)" }.joined(separator: " ")
+ "]"
}

/// :nodoc:
public var debugDescription: String {
return debugDescription(level: 0)
}

private func debugDescription(level: Int) -> String {
if level == 0 && self == self.unadapted {
return description
}
let prefix = repeatElement(" ", count: level + 1).joined(separator: "")
var str = ""
if level == 0 {
str = "▿ " + description
let unadapted = self.unadapted
if self != unadapted {
str += "\n" + prefix + "unadapted: " + unadapted.description
}
} else {
str = description
}
for scope in scopeNames.sorted() {
let scopedRow = scoped(on: scope)!
str += "\n" + prefix + "- " + scope + ": " + scopedRow.debugDescription(level: level + 1)
}

return str
}
}

Expand Down Expand Up @@ -928,6 +970,7 @@ extension RowIndex {
protocol RowImpl {
var count: Int { get }
var isFetched: Bool { get }
var unadaptedRow: Row { get }
func databaseValue(atUncheckedIndex index: Int) -> DatabaseValue
func fastValue<Value: DatabaseValueConvertible & StatementColumnConvertible>(atUncheckedIndex index: Int) -> Value
func fastValue<Value: DatabaseValueConvertible & StatementColumnConvertible>(atUncheckedIndex index: Int) -> Value?
Expand All @@ -947,6 +990,10 @@ protocol RowImpl {
}

extension RowImpl {
var unadaptedRow: Row {
return Row(impl: self)
}

func hasNull(atUncheckedIndex index:Int) -> Bool {
return databaseValue(atUncheckedIndex: index).isNull
}
Expand Down
20 changes: 12 additions & 8 deletions GRDB/Core/RowAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public struct LayoutedColumnMapping {
/// }
/// }
///
/// // <Row foo:"foo" bar: "bar">
/// // [foo:"foo" bar: "bar"]
/// try Row.fetchOne(db, "SELECT NULL, 'foo', 'bar'", adapter: FooBarAdapter())
public init<S: Sequence>(layoutColumns: S) where S.Iterator.Element == (Int, String) {
self.layoutColumns = Array(layoutColumns)
Expand Down Expand Up @@ -200,7 +200,7 @@ extension SelectStatement : RowLayout {
/// let adapter = SuffixRowAdapter(fromIndex: 2)
/// let sql = "SELECT 1 AS foo, 2 AS bar, 3 AS baz"
///
/// // <Row baz:3>
/// // [baz:3]
/// try Row.fetchOne(db, sql, adapter: adapter)
public protocol RowAdapter {

Expand All @@ -223,7 +223,7 @@ public protocol RowAdapter {
/// }
/// }
///
/// // <Row foo:1>
/// // [foo:1]
/// try Row.fetchOne(db, "SELECT 1, 2, 3", adapter: FirstColumnAdapter())
func layoutedAdapter(from layout: RowLayout) throws -> LayoutedRowAdapter
}
Expand Down Expand Up @@ -264,7 +264,7 @@ public struct EmptyRowAdapter: RowAdapter {
/// let adapter = ColumnMapping(["foo": "bar"])
/// let sql = "SELECT 'foo' AS foo, 'bar' AS bar, 'baz' AS baz"
///
/// // <Row foo:"bar">
/// // [foo:"bar"]
/// try Row.fetchOne(db, sql, adapter: adapter)
public struct ColumnMapping : RowAdapter {
/// A dictionary from mapped column names to column names in a base row.
Expand Down Expand Up @@ -298,7 +298,7 @@ public struct ColumnMapping : RowAdapter {
/// let adapter = SuffixRowAdapter(fromIndex: 2)
/// let sql = "SELECT 1 AS foo, 2 AS bar, 3 AS baz"
///
/// // <Row baz:3>
/// // [baz:3]
/// try Row.fetchOne(db, sql, adapter: adapter)
public struct SuffixRowAdapter : RowAdapter {
/// The suffix index
Expand All @@ -325,7 +325,7 @@ public struct SuffixRowAdapter : RowAdapter {
/// let adapter = RangeRowAdapter(1..<3)
/// let sql = "SELECT 1 AS foo, 2 AS bar, 3 AS baz, 4 as qux"
///
/// // <Row bar:2 baz: 3>
/// // [bar:2 baz: 3]
/// try Row.fetchOne(db, sql, adapter: adapter)
public struct RangeRowAdapter : RowAdapter {
/// The range
Expand Down Expand Up @@ -452,7 +452,7 @@ struct ChainedAdapter : RowAdapter {
extension Row {
/// Creates a row from a base row and a statement adapter
convenience init(base: Row, adapter: LayoutedRowAdapter) {
self.init(impl: AdapterRowImpl(base: base, adapter: adapter))
self.init(impl: AdaptedRowImpl(base: base, adapter: adapter))
}

/// Returns self if adapter is nil
Expand All @@ -464,7 +464,7 @@ extension Row {
}
}

struct AdapterRowImpl : RowImpl {
struct AdaptedRowImpl : RowImpl {
let base: Row
let adapter: LayoutedRowAdapter
let mapping: LayoutedColumnMapping
Expand All @@ -475,6 +475,10 @@ struct AdapterRowImpl : RowImpl {
self.mapping = adapter.mapping
}

var unadaptedRow: Row {
return base.unadapted
}

var count: Int {
return mapping.layoutColumns.count
}
Expand Down
2 changes: 1 addition & 1 deletion GRDB/Core/Statement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ extension UpdateStatement: AuthorizedStatement { }
/// let sql = "SELECT ?2 AS two, :foo AS foo, ?1 AS one, :foo AS foo2, :bar AS bar"
/// let row = try Row.fetchOne(db, sql, arguments: [1, 2, "bar"] + ["foo": "foo"])!
/// print(row)
/// // Prints <Row two:2 foo:"foo" one:1 foo2:"foo" bar:"bar">
/// // Prints [two:2 foo:"foo" one:1 foo2:"foo" bar:"bar"]
public struct StatementArguments: CustomStringConvertible, Equatable, ExpressibleByArrayLiteral, ExpressibleByDictionaryLiteral {
var values: [DatabaseValue] = []
var namedValues: [String: DatabaseValue] = [:]
Expand Down
32 changes: 16 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1634,14 +1634,14 @@ try db.create(table: "players") { t in
t.column("name", .text)
}

// <Row type:"table" name:"players" tbl_name:"players" rootpage:2
// sql:"CREATE TABLE players(id INTEGER PRIMARY KEY, name TEXT)">
// [type:"table" name:"players" tbl_name:"players" rootpage:2
// sql:"CREATE TABLE players(id INTEGER PRIMARY KEY, name TEXT)"]
for row in try Row.fetchAll(db, "SELECT * FROM sqlite_master") {
print(row)
}

// <Row cid:0 name:"id" type:"INTEGER" notnull:0 dflt_value:NULL pk:1>
// <Row cid:1 name:"name" type:"TEXT" notnull:0 dflt_value:NULL pk:0>
// [cid:0 name:"id" type:"INTEGER" notnull:0 dflt_value:NULL pk:1]
// [cid:1 name:"name" type:"TEXT" notnull:0 dflt_value:NULL pk:0]
for row in try Row.fetchAll(db, "PRAGMA table_info('players')") {
print(row)
}
Expand Down Expand Up @@ -1687,7 +1687,7 @@ To see how row adapters can be used, see [Joined Queries Support](#joined-querie
ColumnMapping renames columns. Build one with a dictionary whose keys are adapted column names, and values the column names in the raw row:

```swift
// <Row newName:"Hello">
// [newName:"Hello"]
let adapter = ColumnMapping(["newName": "oldName"])
let row = try Row.fetchOne(db, "SELECT 'Hello' AS oldName", adapter: adapter)!
```
Expand All @@ -1697,7 +1697,7 @@ let row = try Row.fetchOne(db, "SELECT 'Hello' AS oldName", adapter: adapter)!
`SuffixRowAdapter` hides the first columns in a row:

```swift
// <Row b:1 c:2>
// [b:1 c:2]
let adapter = SuffixRowAdapter(fromIndex: 1)
let row = try Row.fetchOne(db, "SELECT 0 AS a, 1 AS b, 2 AS c", adapter: adapter)!
```
Expand All @@ -1707,7 +1707,7 @@ let row = try Row.fetchOne(db, "SELECT 0 AS a, 1 AS b, 2 AS c", adapter: adapter
`RangeRowAdapter` only exposes a range of columns.

```swift
// <Row b:1>
// [b:1]
let adapter = RangeRowAdapter(1..<2)
let row = try Row.fetchOne(db, "SELECT 0 AS a, 1 AS b, 2 AS c", adapter: adapter)!
```
Expand Down Expand Up @@ -1739,9 +1739,9 @@ let row = try Row.fetchOne(db, "SELECT 0 AS a, 1 AS b, 2 AS c, 3 AS d", adapter:
ScopeAdapter does not change the columns and values of the fetched row. Instead, it defines *scopes*, which you access with the `Row.scoped(on:)` method. This method returns an optional Row, which is nil if the scope is missing.

```swift
row // <Row a:0 b:1 c:2 d:3>
row.scoped(on: "left") // <Row a:0 b:1>
row.scoped(on: "right") // <Row c:2 d:3>
row // [a:0 b:1 c:2 d:3]
row.scoped(on: "left") // [a:0 b:1]
row.scoped(on: "right") // [c:2 d:3]
row.scoped(on: "missing") // nil
```

Expand All @@ -1759,12 +1759,12 @@ let adapter = ScopeAdapter([
let row = try Row.fetchOne(db, "SELECT 0 AS a, 1 AS b, 2 AS c, 3 AS d", adapter: adapter)!

let leftRow = row.scoped(on: "left")!
leftRow.scoped(on: "left") // <Row a:0>
leftRow.scoped(on: "right") // <Row b:1>
leftRow.scoped(on: "left") // [a:0]
leftRow.scoped(on: "right") // [b:1]

let rightRow = row.scoped(on: "right")!
rightRow.scoped(on: "left") // <Row c:2>
rightRow.scoped(on: "right") // <Row d:3>
rightRow.scoped(on: "left") // [c:2]
rightRow.scoped(on: "right") // [d:3]
```

Any adapter can be extended with scopes:
Expand All @@ -1775,8 +1775,8 @@ let adapter = ScopeAdapter(base: baseAdapter, scopes: [
"remainder": SuffixRowAdapter(fromIndex: 2)])
let row = try Row.fetchOne(db, "SELECT 0 AS a, 1 AS b, 2 AS c, 3 AS d", adapter: adapter)!

row // <Row a:0 b:1>
row.scoped(on: "remainder") // <Row c:2 d:3>
row // [a:0 b:1]
row.scoped(on: "remainder") // [c:2 d:3]
```


Expand Down
2 changes: 1 addition & 1 deletion Tests/GRDBTests/DatabaseValueTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,6 @@ class DatabaseValueTests: GRDBTestCase {
XCTAssertEqual(databaseValue_Int64.description, "1")
XCTAssertEqual(databaseValue_Double.description, "100000.1")
XCTAssertEqual(databaseValue_String.description, "\"foo\\n\\t\\r\"")
XCTAssertEqual(databaseValue_Data.description, "3 bytes") // may be fragile
XCTAssertEqual(databaseValue_Data.description, "Data(3 bytes)") // may be fragile
}
}
38 changes: 38 additions & 0 deletions Tests/GRDBTests/RowAdapterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -712,4 +712,42 @@ class AdapterRowTests : RowTestCase {
}
}
}

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, "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.scoped(on: "a")!.description, "[id:1 val:\"foo1\"]")
XCTAssertEqual(row.scoped(on: "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"]
""")
}
}
}
9 changes: 9 additions & 0 deletions Tests/GRDBTests/RowCopiedFromStatementTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -279,4 +279,13 @@ class RowCopiedFromStatementTests: RowTestCase {
XCTAssertEqual(row, copiedRow)
}
}

func testDescription() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
let row = try Row.fetchOne(db, "SELECT NULL AS \"null\", 1 AS \"int\", 1.1 AS \"double\", 'foo' AS \"string\", x'53514C697465' AS \"data\"")!
XCTAssertEqual(row.description, "[null:NULL int:1 double:1.1 string:\"foo\" data:Data(6 bytes)]")
XCTAssertEqual(row.debugDescription, "[null:NULL int:1 double:1.1 string:\"foo\" data:Data(6 bytes)]")
}
}
}
6 changes: 6 additions & 0 deletions Tests/GRDBTests/RowFromDictionaryLiteralTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -227,4 +227,10 @@ class RowFromDictionaryLiteralTests : RowTestCase {
let copiedRow = row.copy()
XCTAssertEqual(row, copiedRow)
}

func testDescription() throws {
let row: Row = ["a": 0, "b": 1, "c": 2]
XCTAssertEqual(row.description, "[a:0 b:1 c:2]")
XCTAssertEqual(row.debugDescription, "[a:0 b:1 c:2]")
}
}
Loading