Skip to content

Commit 7c2cbfe

Browse files
authored
Merge pull request #1759 from groue/dev/expression-builder
Build request with closures
2 parents 9064717 + 034b241 commit 7c2cbfe

File tree

78 files changed

+7683
-1931
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+7683
-1931
lines changed

Documentation/AssociationsBasics.md

Lines changed: 121 additions & 104 deletions
Large diffs are not rendered by default.

Documentation/Combine.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ let hallOfFamePublisher = ValueObservation
332332

333333
// 2nd request
334334
let bestPlayers = try Player
335-
.order(Column("score").desc)
335+
.order(\.score.desc)
336336
.limit(10)
337337
.fetchAll(db)
338338

@@ -358,7 +358,7 @@ let totalPlayerCountPublisher = ValueObservation
358358
// OK
359359
let bestPlayerPublisher = ValueObservation
360360
.tracking(Player
361-
.order(Column("score").desc)
361+
.order(\.score.desc)
362362
.limit(10)
363363
.fetchAll)
364364
.publisher(in: dbQueue)

Documentation/CommonTableExpressions.md

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ The CTE request can be provided as a [query interface request]:
3131
// WITH playerName AS (SELECT name FROM player) ...
3232
let playerNameCTE = CommonTableExpression(
3333
named: "playerName",
34-
request: Player.select(Column("name")))
34+
request: Player.select(\.name))
3535
```
3636

3737
You can feed a CTE with raw SQL as well (second and third examples use [SQL Interpolation]):
@@ -123,7 +123,7 @@ And we can then filter the `player` table with a subquery:
123123
// WHERE name = (SELECT * FROM playerName)
124124
let request = Player
125125
.with(playerNameCTE)
126-
.filter(Column("name") == playerNameCTE.all())
126+
.filter { $0.name == playerNameCTE.all() }
127127
```
128128

129129
> **Note**: the `with(_:)` method can be called as many times as a there are common table expressions in your request.
@@ -167,13 +167,13 @@ Common table expressions can also be used as subqueries, when you update or dele
167167
// UPDATE player SET name = (SELECT * FROM playerName)
168168
try Player
169169
.with(playerNameCTE)
170-
.updateAll(db, Column("name").set(to: playerNameCTE.all()))
170+
.updateAll(db) { $0.name.set(to: playerNameCTE.all()) }
171171

172172
// WITH playerName AS (SELECT 'O''Brien')
173173
// DELETE FROM player WHERE name = (SELECT * FROM playerName)
174174
try Player
175175
.with(playerNameCTE)
176-
.filter(Column("name") == playerNameCTE.all())
176+
.filter { $0.name == playerNameCTE.all() }
177177
.deleteAll(db)
178178
```
179179

@@ -281,9 +281,15 @@ struct Chat: Codable, FetchableRecord, PersistableRecord {
281281
}
282282

283283
struct Message: Codable, FetchableRecord, PersistableRecord {
284-
var chatID: Int64
284+
var chatId: Int64
285285
var date: Date
286286
...
287+
288+
enum Columns {
289+
static let chatId = Column("chatId")
290+
static let date = Column("date")
291+
...
292+
}
287293
}
288294
```
289295

@@ -315,16 +321,16 @@ We start by defining the CTE request, which loads the latest messages of all cha
315321
```swift
316322
// SELECT *, MAX(date) FROM message GROUP BY chatID
317323
let latestMessageRequest = Message
318-
.annotated(with: max(Column("date")))
319-
.group(Column("chatID"))
324+
.annotated { max($0.date) }
325+
.group(\.chatId)
320326
```
321327

322328
We can now define the CTE for the latest messages:
323329

324330
```swift
325331
// WITH latestMessage AS
326332
// (SELECT *, MAX(date) FROM message GROUP BY chatID)
327-
let latestMessageCTE = CommonTableExpression(
333+
let latestMessageCTE = CommonTableExpression<Message>(
328334
named: "latestMessage",
329335
request: latestMessageRequest)
330336
```
@@ -337,9 +343,9 @@ The association from a chat to its latest message follows:
337343
let latestMessage = Chat.association(
338344
to: latestMessageCTE,
339345
on: { chat, latestMessage in
340-
chat[Column("id")] == latestMessage[Column("chatID")]
346+
chat.id == latestMessage.chatId
341347
})
342-
.order(Column("date").desc)
348+
.order(\.date.desc)
343349
```
344350

345351
The final request can now be defined:

Documentation/DemoApps/GRDBDemo/GRDBDemo/Database/Models/Player.swift

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -64,45 +64,3 @@ extension Player: Codable, FetchableRecord, MutablePersistableRecord {
6464
id = inserted.rowID
6565
}
6666
}
67-
68-
// Convenience access to player columns in this file
69-
private typealias Columns = Player.Columns
70-
71-
// MARK: - Player Database Requests
72-
73-
/// Define some player requests used by the application.
74-
///
75-
/// See <https://swiftpackageindex.com/groue/GRDB.swift/documentation/grdb/recordrecommendedpractices>
76-
extension DerivableRequest<Player> {
77-
/// A request of players ordered by name.
78-
///
79-
/// For example:
80-
///
81-
/// let players: [Player] = try dbWriter.read { db in
82-
/// try Player.all().orderedByName().fetchAll(db)
83-
/// }
84-
func orderedByName() -> Self {
85-
// Sort by name in a localized case insensitive fashion
86-
// See https://github.com/groue/GRDB.swift/blob/master/README.md#string-comparison
87-
order(Columns.name.collating(.localizedCaseInsensitiveCompare))
88-
}
89-
90-
/// A request of players ordered by score.
91-
///
92-
/// For example:
93-
///
94-
/// let players: [Player] = try dbWriter.read { db in
95-
/// try Player.all().orderedByScore().fetchAll(db)
96-
/// }
97-
/// let bestPlayer: Player? = try dbWriter.read { db in
98-
/// try Player.all().orderedByScore().fetchOne(db)
99-
/// }
100-
func orderedByScore() -> Self {
101-
// Sort by descending score, and then by name, in a
102-
// localized case insensitive fashion
103-
// See https://github.com/groue/GRDB.swift/blob/master/README.md#string-comparison
104-
order(
105-
Columns.score.desc,
106-
Columns.name.collating(.localizedCaseInsensitiveCompare))
107-
}
108-
}

Documentation/DemoApps/GRDBDemo/GRDBDemo/Views/PlayerListModel.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,16 @@ import GRDB
4141
let observation = ValueObservation.tracking { [ordering] db in
4242
switch ordering {
4343
case .byName:
44-
try Player.all().orderedByName().fetchAll(db)
44+
try Player
45+
.order { $0.name.collating(.localizedCaseInsensitiveCompare) }
46+
.fetchAll(db)
4547
case .byScore:
46-
try Player.all().orderedByScore().fetchAll(db)
48+
try Player
49+
.order { [
50+
$0.score.desc,
51+
$0.name.collating(.localizedCaseInsensitiveCompare),
52+
] }
53+
.fetchAll(db)
4754
}
4855
}
4956

Documentation/FullTextSearch.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ Use them in the [query interface](../README.md#the-query-interface):
316316
let documents = try Document.matching(pattern).fetchAll(db)
317317

318318
// Search in a specific column:
319-
let documents = try Document.filter(Column("content").match(pattern)).fetchAll(db)
319+
let documents = try Document.filter { $0.content.match(pattern) }.fetchAll(db)
320320
```
321321

322322

@@ -565,7 +565,7 @@ Use them in the [query interface](../README.md#the-query-interface):
565565
let documents = try Document.matching(pattern).fetchAll(db)
566566

567567
// Search in a specific column:
568-
let documents = try Document.filter(Column("content").match(pattern)).fetchAll(db)
568+
let documents = try Document.filter { $0.content.match(pattern) }.fetchAll(db)
569569
```
570570

571571

Documentation/Playgrounds/Tour.playground/Contents.swift

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,14 @@ try dbQueue.inDatabase { db in
3232
CREATE TABLE place (
3333
id INTEGER PRIMARY KEY,
3434
title TEXT,
35-
favorite BOOLEAN NOT NULL,
35+
isFavorite BOOLEAN NOT NULL,
3636
latitude DOUBLE NOT NULL,
3737
longitude DOUBLE NOT NULL
3838
)
3939
""")
4040

4141
try db.execute(sql: """
42-
INSERT INTO place (title, favorite, latitude, longitude)
42+
INSERT INTO place (title, isFavorite, latitude, longitude)
4343
VALUES (?, ?, ?, ?)
4444
""", arguments: ["Paris", true, 48.85341, 2.3488])
4545
let parisId = db.lastInsertedRowID
@@ -52,11 +52,11 @@ try! dbQueue.inDatabase { db in
5252
let rows = try Row.fetchCursor(db, sql: "SELECT * FROM place")
5353
while let row = try rows.next() {
5454
let title: String = row["title"]
55-
let favorite: Bool = row["favorite"]
55+
let isFavorite: Bool = row["isFavorite"]
5656
let coordinate = CLLocationCoordinate2D(
5757
latitude: row["latitude"],
5858
longitude: row["longitude"])
59-
print("Fetched", title, favorite, coordinate)
59+
print("Fetched", title, isFavorite, coordinate)
6060
}
6161

6262
let placeCount = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM place")! // Int
@@ -69,35 +69,43 @@ try! dbQueue.inDatabase { db in
6969
struct Place {
7070
var id: Int64?
7171
var title: String?
72-
var favorite: Bool
72+
var isFavorite: Bool
7373
var coordinate: CLLocationCoordinate2D
7474
}
7575

7676
// Adopt FetchableRecord
7777
extension Place : FetchableRecord {
7878
init(row: Row) {
79-
id = row["id"]
80-
title = row["title"]
81-
favorite = row["favorite"]
79+
id = row[Columns.id]
80+
title = row[Columns.title]
81+
isFavorite = row[Columns.isFavorite]
8282
coordinate = CLLocationCoordinate2DMake(
83-
row["latitude"],
84-
row["longitude"])
83+
row[Columns.latitude],
84+
row[Columns.longitude])
8585
}
8686
}
8787

8888
// Adopt TableRecord
8989
extension Place : TableRecord {
9090
static let databaseTableName = "place"
91+
92+
enum Columns {
93+
static let id = Column("id")
94+
static let title = Column("title")
95+
static let isFavorite = Column("isFavorite")
96+
static let latitude = Column("latitude")
97+
static let longitude = Column("longitude")
98+
}
9199
}
92100

93101
// Adopt MutablePersistableRecord
94102
extension Place : MutablePersistableRecord {
95103
func encode(to container: inout PersistenceContainer) throws {
96-
container["id"] = id
97-
container["title"] = title
98-
container["favorite"] = favorite
99-
container["latitude"] = coordinate.latitude
100-
container["longitude"] = coordinate.longitude
104+
container[Columns.id] = id
105+
container[Columns.title] = title
106+
container[Columns.isFavorite] = isFavorite
107+
container[Columns.latitude] = coordinate.latitude
108+
container[Columns.longitude] = coordinate.longitude
101109
}
102110

103111
mutating func didInsert(_ inserted: InsertionSuccess) {
@@ -109,13 +117,13 @@ try dbQueue.inDatabase { db in
109117
var berlin = Place(
110118
id: nil,
111119
title: "Berlin",
112-
favorite: false,
120+
isFavorite: false,
113121
coordinate: CLLocationCoordinate2DMake(52.52437, 13.41053))
114122

115123
try berlin.insert(db)
116124
berlin.id // some value
117125

118-
berlin.favorite = true
126+
berlin.isFavorite = true
119127
try berlin.update(db)
120128

121129
// Fetch from SQL
@@ -124,13 +132,10 @@ try dbQueue.inDatabase { db in
124132

125133
//: Avoid SQL with the query interface:
126134

127-
let title = Column("title")
128-
let favorite = Column("favorite")
129-
130-
berlin = try Place.filter(title == "Berlin").fetchOne(db)! // Place
135+
berlin = try Place.filter { $0.title == "Berlin" }.fetchOne(db)! // Place
131136
let paris = try Place.fetchOne(db, key: 1) // Place?
132137
let favoritePlaces = try Place // [Place]
133-
.filter(favorite == true)
134-
.order(title)
138+
.filter { $0.isFavorite == true }
139+
.order { $0.title }
135140
.fetchAll(db)
136141
}

Documentation/QueryInterfaceOrganization.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ Columns are special expressions that allow some optimizations and niceties:
9393
ValueObservation.tracking { db in
9494
try Player.fetchOne(db, id: 1)
9595
// or
96-
try Player.filter(Column("id") == 1).fetchOne(db)
96+
try Player.filter { $0.id == 1 }.fetchOne(db)
9797
}
9898

9999
// Non-optimized observations
@@ -110,7 +110,7 @@ Columns are special expressions that allow some optimizations and niceties:
110110
// Nicer SQL
111111
// SELECT * FROM player WHERE id = 1
112112
try Player.fetchOne(db, id: 1)
113-
try Player.filter(Column("id") == 1).fetchOne(db)
113+
try Player.filter { $0.id == 1 }.fetchOne(db)
114114

115115
// Less nice SQL
116116
// SELECT * FROM player WHERE id = 1 LIMIT 1
@@ -261,7 +261,7 @@ The basic value types conform to [DatabaseValueConvertible] so that they can fee
261261
```swift
262262
// SELECT * FROM player WHERE name = 'O''Brien'
263263
// ~~~~~~~~~~
264-
Player.filter(Column("name") == "O'Brien")
264+
Player.filter { $0.name == "O'Brien" }
265265
```
266266

267267
### QueryInterfaceRequest
@@ -346,8 +346,9 @@ Functions and methods that build an SQL expression should return an SQLExpressio
346346

347347
```swift
348348
// SELECT * FROM player WHERE LENGTH(name) > 0
349-
let expression = length(Column("name")) > 0 // SQLExpression
350-
Player.filter(expression)
349+
Player.filter {
350+
length($0.name) > 0 // SQLExpression
351+
}
351352
```
352353

353354
When it looks like GRDB APIs are unable to build a particular expression, use [SQL]:
@@ -358,7 +359,7 @@ func date(_ value: SQLSpecificExpressible) -> SQLExpression {
358359
}
359360

360361
// SELECT * FROM player WHERE DATE(createdAt) = '2020-01-23'
361-
let request = Player.filter(date(Column("createdAt")) == "2020-01-23")
362+
let request = Player.filter { date($0.createdAt) == "2020-01-23" }
362363
```
363364

364365
This technique, based on [SQL Interpolation], is composable and works well even when several tables are involved. See how the `createdAt` column below is correctly attributed to the `player` table:
@@ -368,7 +369,7 @@ This technique, based on [SQL Interpolation], is composable and works well even
368369
// JOIN team ON team.id = player.teamId
369370
// WHERE DATE(player.createdAt) = '2020-01-23'
370371
let request = Player
371-
.filter(date(Column("createdAt")) == "2020-01-23")
372+
.filter { date($0.createdAt) == "2020-01-23" }
372373
.including(required: Player.team)
373374
```
374375

@@ -583,7 +584,7 @@ SQLSubqueryable provides the GRDB support for subqueries. Its [SQLSpecificExpres
583584
// SELECT * FROM player
584585
// WHERE score >= (SELECT AVG(score) FROM player)
585586
let averageScore = Player.select(average(Column("score")))
586-
Player.filter(Column("score") >= averageScore)
587+
Player.filter { $0.score >= averageScore }
587588
```
588589

589590
SQLSubqueryable has the `contains(_:)` and `exists()` methods that support the `value IN (subquery)` and `EXISTS (subquery)` expressions.

Documentation/SQLInterpolation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ func date(_ expression: SQLExpressible) -> SQLExpression {
175175
SQL("DATE(\(expression))").sqlExpression
176176
}
177177

178-
let request = Player.filter(date(Column("createdAt")) == "2020-01-23")
178+
let request = Player.filter { date($0.createdAt) == "2020-01-23" }
179179
```
180180

181181

Documentation/WhyAdoptGRDB.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,13 +207,13 @@ try player.updateChanges {
207207

208208
// SELECT * FROM player ORDER BY score DESC LIMIT 10
209209
let bestPlayers: [Player] = try Player
210-
.order(scoreColumn.desc)
210+
.order(\.score.desc)
211211
.limit(10)
212212
.fetchAll(db)
213213

214214
// SELECT MAX(score) FROM player
215215
let maximumScore: Int? = try Player
216-
.select(max(scoreColumn))
216+
.select { max($0.score) }
217217
.asRequestOf(Int.self)
218218
.fetchOne(db)
219219

0 commit comments

Comments
 (0)