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

Introduce RenameColumnAdapter #811

Merged
merged 3 commits into from Jul 19, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
27 changes: 26 additions & 1 deletion GRDB/Core/RowAdapter.swift
Expand Up @@ -325,7 +325,7 @@ public struct RangeRowAdapter: RowAdapter {
}
}

/// ScopeAdapter is a row adapter that lets you define scopes on rows.
/// `ScopeAdapter` is a row adapter that lets you define scopes on rows.
///
/// // Two adapters
/// let fooAdapter = ColumnMapping(["value": "foo"])
Expand Down Expand Up @@ -423,6 +423,31 @@ struct ChainedAdapter: RowAdapter {
}
}

/// `RenameColumnAdapter` is a row adapter that renames columns.
///
/// For example:
///
/// let adapter = RenameColumnAdapter { $0 + "rrr" }
/// let sql = "SELECT 'foo' AS foo, 'bar' AS bar, 'baz' AS baz"
///
/// // [foorrr:"foo", barrrr:"bar", bazrrr:"baz"]
/// try Row.fetchOne(db, sql: sql, adapter: adapter)
public struct RenameColumnAdapter: RowAdapter {
let transform: (String) -> String

/// Creates a `RenameColumnAdapter` adapter that renames columns according to the
/// provided transform function.
public init(_ transform: @escaping (String) -> String) {
self.transform = transform
}

/// :nodoc:
public func _layoutedAdapter(from layout: _RowLayout) throws -> _LayoutedRowAdapter {
let layoutColumns = layout._layoutColumns.map { (index, column) in (index, transform(column)) }
return _LayoutedColumnMapping(layoutColumns: layoutColumns)
}
}

extension Row {
/// Creates a row from a base row and a statement adapter
convenience init(base: Row, adapter: _LayoutedRowAdapter) {
Expand Down
14 changes: 14 additions & 0 deletions GRDB/Core/SQLLiteral.swift
Expand Up @@ -20,6 +20,15 @@ public struct SQLLiteral {
case selectable(SQLSelectable)
case orderingTerm(SQLOrderingTerm)

var isEmpty: Bool {
switch self {
case let .sql(sql, _):
return sql.isEmpty
default:
return false
}
}

fileprivate func sql(_ context: SQLGenerationContext) throws -> String {
switch self {
case let .sql(sql, arguments):
Expand Down Expand Up @@ -88,6 +97,11 @@ public struct SQLLiteral {
self.init(elements: [.expression(expression)])
}

/// Returns true if this literal generates an empty SQL string
public var isEmpty: Bool {
elements.allSatisfy(\.isEmpty)
}

/// Turn a SQLLiteral into raw SQL and arguments.
///
/// - parameter db: A database connection.
Expand Down
90 changes: 73 additions & 17 deletions README.md
Expand Up @@ -1956,36 +1956,65 @@ They basically help two incompatible row interfaces to work together. For exampl
In this case, the `ColumnMapping` row adapter comes in handy:

```swift
// Fetch a 'produced' column, and consume a 'consumed' column:
// Turn the 'produced' column into 'consumed':
let adapter = ColumnMapping(["consumed": "produced"])
let row = try Row.fetchOne(db, sql: "SELECT 'Hello' AS produced", adapter: adapter)!
row // [consumed:"Hello"]
row["consumed"] // "Hello"
row["produced"] // nil
```

[Record types](#records) are typical row consumers that expect database rows to have a specific layout so that they can decode them:

```swift
struct MyRecord: Decodable, FetchableRecord {
var consumed: String
}
let record = try MyRecord.fetchOne(db, sql: "SELECT 'Hello' AS produced", adapter: adapter)!
print(record.consumed) // "Hello"
```

There are several situations where row adapters are useful:

- They help disambiguate columns with identical names, which may happen when you select columns from several tables. See [Joined Queries Support](#joined-queries-support) for an example.

- They help when SQLite outputs unexpected column names, which may happen with some subqueries. See [RenameColumnAdapter](#renamecolumnadapter) for an example.

Available row adapters are described below.

- [ColumnMapping](#columnmapping)
- [EmptyRowAdapter](#emptyrowadapter)
- [RangeRowAdapter](#rangerowadapter)
- [RenameColumnAdapter](#renamecolumnadapter)
- [ScopeAdapter](#scopeadapter)
- [SuffixRowAdapter](#suffixrowadapter)


### ColumnMapping

ColumnMapping renames columns. Build one with a dictionary whose keys are adapted column names, and values the column names in the raw row:
`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, sql: "SELECT 'Hello' AS oldName", adapter: adapter)!
// [newA:0, newB:1]
let adapter = ColumnMapping(["newA": "a", "newB": "b"])
let row = try Row.fetchOne(db, sql: "SELECT 0 AS a, 1 AS b, 2 AS c", adapter: adapter)!
```

### SuffixRowAdapter
Note that columns that are not present in the dictionary are not present in the resulting adapted row.

`SuffixRowAdapter` hides the first columns in a row:

### EmptyRowAdapter

`EmptyRowAdapter` hides all columns.

```swift
// [b:1 c:2]
let adapter = SuffixRowAdapter(fromIndex: 1)
let adapter = EmptyRowAdapter()
let row = try Row.fetchOne(db, sql: "SELECT 0 AS a, 1 AS b, 2 AS c", adapter: adapter)!
row.isEmpty // true
```

This limit adapter may turn out useful in some narrow use cases. You'll be happy to find it when you need it.


### RangeRowAdapter

`RangeRowAdapter` only exposes a range of columns.
Expand All @@ -1996,17 +2025,32 @@ let adapter = RangeRowAdapter(1..<2)
let row = try Row.fetchOne(db, sql: "SELECT 0 AS a, 1 AS b, 2 AS c", adapter: adapter)!
```

### EmptyRowAdapter

`EmptyRowAdapter` hides all columns.
### RenameColumnAdapter

`RenameColumnAdapter` lets you transform column names with a function:

```swift
let adapter = EmptyRowAdapter()
// [arrr:0, brrr:1, crrr:2]
let adapter = RenameColumnAdapter { column in column + "rrr" }
let row = try Row.fetchOne(db, sql: "SELECT 0 AS a, 1 AS b, 2 AS c", adapter: adapter)!
row.isEmpty // true
```

This limit adapter may turn out useful in some narrow use cases. You'll be happy to find it when you need it.
This adapter may turn out useful, for example, when subqueries contain duplicated column names:

```swift
let sql = "SELECT * FROM (SELECT 1 AS id, 2 AS id)"

// Prints ["id", "id:1"]
// Note the "id:1" column, generated by SQLite.
let row = try Row.fetchOne(db, sql: sql)!
print(Array(row.columnNames))

// Drop the `:...` suffix, and prints ["id", "id"]
let adapter = RenameColumnAdapter { String($0.prefix(while: { $0 != ":" })) }
let adaptedRow = try Row.fetchOne(db, sql: sql, adapter: adapter)!
print(Array(adaptedRow.columnNames))
```


### ScopeAdapter
Expand Down Expand Up @@ -2066,6 +2110,17 @@ row.scopes["remainder"] // [c:2 d:3]
To see how `ScopeAdapter` can be used, see [Joined Queries Support](#joined-queries-support).


### SuffixRowAdapter

`SuffixRowAdapter` hides the first columns in a row:

```swift
// [b:1 c:2]
let adapter = SuffixRowAdapter(fromIndex: 1)
let row = try Row.fetchOne(db, sql: "SELECT 0 AS a, 1 AS b, 2 AS c", adapter: adapter)!
```


## Raw SQLite Pointers

**If not all SQLite APIs are exposed in GRDB, you can still use the [SQLite C Interface](https://www.sqlite.org/c3ref/intro.html) and call [SQLite C functions](https://www.sqlite.org/c3ref/funclist.html).**
Expand Down Expand Up @@ -4806,7 +4861,7 @@ try Player.customRequest().fetchAll(db) // [Player]

**To build custom requests**, you can use one of the built-in requests or derive requests from other requests.

- [SQLRequest](http://groue.github.io/GRDB.swift/docs/5.0.0-beta.7/Structs/SQLRequest.html) is a fetch request built from raw SQL. For example:
- [SQLRequest] is a fetch request built from raw SQL. For example:

```swift
extension Player {
Expand Down Expand Up @@ -7846,4 +7901,5 @@ This chapter has been superseded by [ValueObservation] and [DatabaseRegionObserv
[Demo Applications]: Documentation/DemoApps/README.md
[Sharing a Database]: Documentation/SharingADatabase.md
[FAQ]: #faq
[Database Observation]: #database-changes-observation
[Database Observation]: #database-changes-observation
[SQLRequest]: http://groue.github.io/GRDB.swift/docs/5.0.0-beta.7/Structs/SQLRequest.html
22 changes: 22 additions & 0 deletions Tests/GRDBTests/RowAdapterTests.swift
Expand Up @@ -826,4 +826,26 @@ class AdapterRowTests : RowTestCase {
""")
}
}

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])
}
}
6 changes: 6 additions & 0 deletions Tests/GRDBTests/SQLLiteralTests.swift
Expand Up @@ -738,4 +738,10 @@ extension SQLLiteralTests {
}
}
}

func testIsEmpty() {
XCTAssertTrue(SQLLiteral(elements: []).isEmpty)
XCTAssertTrue(SQLLiteral(sql: "").isEmpty)
XCTAssertTrue(SQLLiteral("").isEmpty)
}
}