From fa68b62be10b857bff008e05c538b0d07d5048e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 11 Apr 2018 13:39:44 +0200 Subject: [PATCH 1/2] Improve Row.description & Row.debugDescription --- GRDB/Core/DatabaseValue.swift | 2 +- GRDB/Core/Row.swift | 65 ++++++++++++++++--- GRDB/Core/RowAdapter.swift | 8 ++- Tests/GRDBTests/DatabaseValueTests.swift | 2 +- Tests/GRDBTests/RowAdapterTests.swift | 38 +++++++++++ .../RowCopiedFromStatementTests.swift | 9 +++ .../RowFromDictionaryLiteralTests.swift | 6 ++ Tests/GRDBTests/RowFromDictionaryTests.swift | 8 +++ Tests/GRDBTests/RowFromStatementTests.swift | 14 ++++ 9 files changed, 139 insertions(+), 13 deletions(-) diff --git a/GRDB/Core/DatabaseValue.swift b/GRDB/Core/DatabaseValue.swift index b19c2b9fc6..7f4603cf4e 100644 --- a/GRDB/Core/DatabaseValue.swift +++ b/GRDB/Core/DatabaseValue.swift @@ -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))" } } } diff --git a/GRDB/Core/Row.swift b/GRDB/Core/Row.swift index 4c96781654..5ef3922de3 100644 --- a/GRDB/Core/Row.swift +++ b/GRDB/Core/Row.swift @@ -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 @@ -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: @@ -874,15 +890,41 @@ extension Row { } } -// CustomStringConvertible +// CustomStringConvertible & CustomDebugStringConvertible extension Row { /// :nodoc: public var description: String { - return "" + 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 } } @@ -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(atUncheckedIndex index: Int) -> Value func fastValue(atUncheckedIndex index: Int) -> Value? @@ -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 } diff --git a/GRDB/Core/RowAdapter.swift b/GRDB/Core/RowAdapter.swift index bfbc5be629..68f9bedfe3 100644 --- a/GRDB/Core/RowAdapter.swift +++ b/GRDB/Core/RowAdapter.swift @@ -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 @@ -464,7 +464,7 @@ extension Row { } } -struct AdapterRowImpl : RowImpl { +struct AdaptedRowImpl : RowImpl { let base: Row let adapter: LayoutedRowAdapter let mapping: LayoutedColumnMapping @@ -475,6 +475,10 @@ struct AdapterRowImpl : RowImpl { self.mapping = adapter.mapping } + var unadaptedRow: Row { + return base.unadapted + } + var count: Int { return mapping.layoutColumns.count } diff --git a/Tests/GRDBTests/DatabaseValueTests.swift b/Tests/GRDBTests/DatabaseValueTests.swift index ecce041009..2db3b009a3 100644 --- a/Tests/GRDBTests/DatabaseValueTests.swift +++ b/Tests/GRDBTests/DatabaseValueTests.swift @@ -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 } } diff --git a/Tests/GRDBTests/RowAdapterTests.swift b/Tests/GRDBTests/RowAdapterTests.swift index ac079ff3ad..5d86741ef5 100644 --- a/Tests/GRDBTests/RowAdapterTests.swift +++ b/Tests/GRDBTests/RowAdapterTests.swift @@ -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"] + """) + } + } } diff --git a/Tests/GRDBTests/RowCopiedFromStatementTests.swift b/Tests/GRDBTests/RowCopiedFromStatementTests.swift index 10dff17942..8767a074bb 100644 --- a/Tests/GRDBTests/RowCopiedFromStatementTests.swift +++ b/Tests/GRDBTests/RowCopiedFromStatementTests.swift @@ -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)]") + } + } } diff --git a/Tests/GRDBTests/RowFromDictionaryLiteralTests.swift b/Tests/GRDBTests/RowFromDictionaryLiteralTests.swift index a542768e8d..36f72601c1 100644 --- a/Tests/GRDBTests/RowFromDictionaryLiteralTests.swift +++ b/Tests/GRDBTests/RowFromDictionaryLiteralTests.swift @@ -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]") + } } diff --git a/Tests/GRDBTests/RowFromDictionaryTests.swift b/Tests/GRDBTests/RowFromDictionaryTests.swift index 79e4578528..bfdc2bb767 100644 --- a/Tests/GRDBTests/RowFromDictionaryTests.swift +++ b/Tests/GRDBTests/RowFromDictionaryTests.swift @@ -217,4 +217,12 @@ class RowFromDictionaryTests : RowTestCase { let copiedRow = row.copy() XCTAssertEqual(row, copiedRow) } + + func testDescription() throws { + let row = Row(["a": 0, "b": "foo"]) + let variants: Set = ["[a:0 b:\"foo\"]", "[b:\"foo\" a:0]"] + XCTAssert(variants.contains(row.description)) + let debugVariants: Set = ["[a:0 b:\"foo\"]", "[b:\"foo\" a:0]"] + XCTAssert(debugVariants.contains(row.debugDescription)) + } } diff --git a/Tests/GRDBTests/RowFromStatementTests.swift b/Tests/GRDBTests/RowFromStatementTests.swift index 69ca661e5a..5561514288 100644 --- a/Tests/GRDBTests/RowFromStatementTests.swift +++ b/Tests/GRDBTests/RowFromStatementTests.swift @@ -367,4 +367,18 @@ class RowFromStatementTests : RowTestCase { XCTAssertTrue(try values.next() == nil) } } + + func testDescription() throws { + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + let rows = try Row.fetchCursor(db, "SELECT NULL AS \"null\", 1 AS \"int\", 1.1 AS \"double\", 'foo' AS \"string\", x'53514C697465' AS \"data\"") + var rowFetched = false + while let row = try rows.next() { + rowFetched = true + 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)]") + } + XCTAssertTrue(rowFetched) + } + } } From 26912e746b35943254461a654e0107500fbf657c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 11 Apr 2018 15:49:16 +0200 Subject: [PATCH 2/2] Update documentation for new Row description format --- GRDB/Core/Database+Schema.swift | 2 +- GRDB/Core/Row.swift | 2 +- GRDB/Core/RowAdapter.swift | 12 ++++++------ GRDB/Core/Statement.swift | 2 +- README.md | 32 ++++++++++++++++---------------- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/GRDB/Core/Database+Schema.swift b/GRDB/Core/Database+Schema.swift index 4c053acb5d..7ac80ece11 100644 --- a/GRDB/Core/Database+Schema.swift +++ b/GRDB/Core/Database+Schema.swift @@ -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:"..."] let id: Int = row[0] let seq: Int = row[1] let table: String = row[2] diff --git a/GRDB/Core/Row.swift b/GRDB/Core/Row.swift index 5ef3922de3..77abae6bc4 100644 --- a/GRDB/Core/Row.swift +++ b/GRDB/Core/Row.swift @@ -804,7 +804,7 @@ extension Row { /// /// let row: Row = ["foo": 1, "foo": "bar", "baz": nil] /// print(row) - /// // Prints + /// // 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) })) } diff --git a/GRDB/Core/RowAdapter.swift b/GRDB/Core/RowAdapter.swift index 68f9bedfe3..014d97f88c 100644 --- a/GRDB/Core/RowAdapter.swift +++ b/GRDB/Core/RowAdapter.swift @@ -77,7 +77,7 @@ public struct LayoutedColumnMapping { /// } /// } /// - /// // + /// // [foo:"foo" bar: "bar"] /// try Row.fetchOne(db, "SELECT NULL, 'foo', 'bar'", adapter: FooBarAdapter()) public init(layoutColumns: S) where S.Iterator.Element == (Int, String) { self.layoutColumns = Array(layoutColumns) @@ -200,7 +200,7 @@ extension SelectStatement : RowLayout { /// let adapter = SuffixRowAdapter(fromIndex: 2) /// let sql = "SELECT 1 AS foo, 2 AS bar, 3 AS baz" /// -/// // +/// // [baz:3] /// try Row.fetchOne(db, sql, adapter: adapter) public protocol RowAdapter { @@ -223,7 +223,7 @@ public protocol RowAdapter { /// } /// } /// - /// // + /// // [foo:1] /// try Row.fetchOne(db, "SELECT 1, 2, 3", adapter: FirstColumnAdapter()) func layoutedAdapter(from layout: RowLayout) throws -> LayoutedRowAdapter } @@ -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" /// -/// // +/// // [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. @@ -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" /// -/// // +/// // [baz:3] /// try Row.fetchOne(db, sql, adapter: adapter) public struct SuffixRowAdapter : RowAdapter { /// The suffix index @@ -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" /// -/// // +/// // [bar:2 baz: 3] /// try Row.fetchOne(db, sql, adapter: adapter) public struct RangeRowAdapter : RowAdapter { /// The range diff --git a/GRDB/Core/Statement.swift b/GRDB/Core/Statement.swift index b5bfa0dc19..c84ac6e1ea 100644 --- a/GRDB/Core/Statement.swift +++ b/GRDB/Core/Statement.swift @@ -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 +/// // 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] = [:] diff --git a/README.md b/README.md index f4541e5c98..c7db629b25 100644 --- a/README.md +++ b/README.md @@ -1634,14 +1634,14 @@ try db.create(table: "players") { t in t.column("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) } -// -// +// [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) } @@ -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 -// +// [newName:"Hello"] let adapter = ColumnMapping(["newName": "oldName"]) let row = try Row.fetchOne(db, "SELECT 'Hello' AS oldName", adapter: adapter)! ``` @@ -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 -// +// [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)! ``` @@ -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 -// +// [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)! ``` @@ -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.scoped(on: "left") // -row.scoped(on: "right") // +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 ``` @@ -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") // -leftRow.scoped(on: "right") // +leftRow.scoped(on: "left") // [a:0] +leftRow.scoped(on: "right") // [b:1] let rightRow = row.scoped(on: "right")! -rightRow.scoped(on: "left") // -rightRow.scoped(on: "right") // +rightRow.scoped(on: "left") // [c:2] +rightRow.scoped(on: "right") // [d:3] ``` Any adapter can be extended with scopes: @@ -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.scoped(on: "remainder") // +row // [a:0 b:1] +row.scoped(on: "remainder") // [c:2 d:3] ```