Skip to content

Commit 466f158

Browse files
committed
refactor: improve API ergonomics with @_disfavoredOverload
Prevents ambiguous method resolution between query methods and stdlib: - Added @_disfavoredOverload to QueryExpression.min()/.max() - Added @_disfavoredOverload to stdlib-conflicting QueryExpression methods - Improved Optional handling in queries - Enhanced documentation for schema definition
1 parent ae5defe commit 466f158

File tree

3 files changed

+174
-62
lines changed

3 files changed

+174
-62
lines changed

Sources/StructuredQueriesCore/Documentation.docc/Articles/DefiningYourSchema.md

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -264,20 +264,10 @@ With that you can insert reminders with notes like so:
264264
#### Tagged identifiers
265265

266266
The [Tagged](https://github.com/pointfreeco/swift-tagged) library provides lightweight syntax for
267-
introducing type-safe identifiers (and more) to your models. StructuredQueries ships support for
268-
Tagged with a `StructuredQueriesTagged` package trait, which is available starting from Swift 6.1.
267+
introducing type-safe identifiers (and more) to your models. StructuredQueriesPostgres ships with
268+
support for Tagged **enabled by default** (available starting from Swift 6.1).
269269

270-
To enable the trait, specify it in the Package.swift file that depends on StructuredQueries:
271-
272-
```diff
273-
.package(
274-
url: "https://github.com/pointfreeco/swift-structured-queries",
275-
from: "0.22.0",
276-
+ traits: ["StructuredQueriesTagged"]
277-
),
278-
```
279-
280-
This will allow you to introduce distinct `Tagged` identifiers throughout your schema:
270+
This allows you to introduce distinct `Tagged` identifiers throughout your schema:
281271

282272
```diff
283273
@Table
@@ -307,18 +297,44 @@ RemindersList.leftJoin(Reminder.all) {
307297
}
308298
```
309299

310-
Tagged works with any query-representable value. For example, if you want a Tagged UUID to use the
311-
`UUID` type (PostgreSQL has native UUID support):
300+
##### PostgreSQL UUID with Tagged
301+
302+
Tagged works with any query-representable value. PostgreSQL has native UUID support, which works
303+
seamlessly with Tagged:
312304

313305
```swift
314306
@Table
315-
struct RemindersList: Identifiable {
307+
struct User: Identifiable {
316308
typealias ID = Tagged<Self, UUID>
317-
318-
@Column(as: Tagged<Self, UUID.BytesRepresentation>.self)
319309
let id: ID
320-
// ...
310+
var name: String
311+
}
312+
313+
// PostgreSQL will use native UUID type:
314+
// CREATE TABLE "users" (
315+
// "id" UUID PRIMARY KEY,
316+
// "name" TEXT NOT NULL
317+
// )
318+
```
319+
320+
You can generate new tagged UUIDs easily:
321+
322+
```swift
323+
let userId = User.ID() // Generates a new UUID
324+
let user = User(id: userId, name: "Alice")
325+
326+
User.insert { user }
327+
```
328+
329+
And query by tagged UUIDs with full type safety:
330+
331+
```swift
332+
func fetchUser(id: User.ID) -> Statement<User?> {
333+
User.where { $0.id == id }
321334
}
335+
336+
// Compile-time safety: can't accidentally pass wrong ID type
337+
let user = try await fetchUser(id: userId).fetchOne(db)
322338
```
323339

324340
### Primary-keyed tables

Sources/StructuredQueriesCore/Optional.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ extension QueryExpression where QueryValue: _OptionalProtocol {
270270
///
271271
/// - Parameter transform: A closure that takes an unwrapped version of this expression.
272272
/// - Returns: The result of the transform function, optionalized.
273+
@_disfavoredOverload
273274
public func map<T>(
274275
_ transform: (SQLQueryExpression<QueryValue.Wrapped>) -> some QueryExpression<T>
275276
) -> some QueryExpression<T?> {
@@ -289,6 +290,7 @@ extension QueryExpression where QueryValue: _OptionalProtocol {
289290
///
290291
/// - Parameter transform: A closure that takes an unwrapped version of this expression.
291292
/// - Returns: The result of the transform function.
293+
@_disfavoredOverload
292294
public func flatMap<T>(
293295
_ transform: (SQLQueryExpression<QueryValue.Wrapped>) -> some QueryExpression<T?>
294296
) -> some QueryExpression<T?> {
Lines changed: 137 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,144 @@
1-
#if StructuredQueriesTagged
2-
import _StructuredQueriesSQLite
3-
import Dependencies
4-
import Foundation
5-
import InlineSnapshotTesting
6-
import StructuredQueriesPostgres
7-
import Tagged
8-
import Testing
9-
10-
extension SnapshotTests {
11-
@Suite struct TaggedTests {
12-
@Test func basics() {
13-
assertQuery(
14-
Reminder
15-
.insert {
16-
Reminder(
17-
id: 11 as Reminder.ID,
18-
remindersListID: 1 as Tagged<RemindersList, Int>)
19-
}
20-
.returning(\.self)
21-
) {
22-
"""
23-
INSERT INTO "reminders"
24-
("id", "remindersListID")
25-
VALUES
26-
(11, 1)
27-
RETURNING "id", "remindersListID"
28-
"""
29-
} results: {
30-
"""
31-
┌────────────────────────────────────────┐
32-
│ SnapshotTests.TaggedTests.Reminder( │
33-
│ id: Tagged(rawValue: 11), │
34-
│ remindersListID: Tagged(rawValue: 1) │
35-
│ ) │
36-
└────────────────────────────────────────┘
37-
"""
1+
import Foundation
2+
import InlineSnapshotTesting
3+
import StructuredQueriesPostgres
4+
import StructuredQueriesPostgresTestSupport
5+
import Tagged
6+
import Testing
7+
8+
extension SnapshotTests {
9+
@Suite struct TaggedTests {
10+
@Test func basicTaggedInt() async {
11+
await assertSQL(
12+
of: Reminder.insert {
13+
Reminder(
14+
id: 11 as Reminder.ID,
15+
remindersListID: 1
16+
)
17+
}
18+
) {
19+
"""
20+
INSERT INTO "reminders"
21+
("id", "remindersListID")
22+
VALUES
23+
(11, 1)
24+
"""
25+
}
26+
}
27+
28+
@Test func taggedUUID() async {
29+
let userId = User.ID(uuidString: "550e8400-e29b-41d4-a716-446655440000")!
30+
await assertSQL(
31+
of: User.insert {
32+
User(id: userId, name: "Alice")
3833
}
34+
) {
35+
"""
36+
INSERT INTO "users"
37+
("id", "name")
38+
VALUES
39+
('550e8400-e29b-41d4-a716-446655440000', 'Alice')
40+
"""
41+
}
42+
}
43+
44+
@Test func taggedUUIDWhereClause() async {
45+
let userId = User.ID(uuidString: "550e8400-e29b-41d4-a716-446655440000")!
46+
await assertSQL(of: User.where { $0.id == userId }) {
47+
"""
48+
SELECT "users"."id", "users"."name"
49+
FROM "users"
50+
WHERE ("users"."id") = ('550e8400-e29b-41d4-a716-446655440000')
51+
"""
52+
}
53+
}
54+
55+
@Test func taggedInClause() async {
56+
let reminderIds: [Reminder.ID] = [
57+
Reminder.ID(rawValue: 1),
58+
Reminder.ID(rawValue: 2),
59+
Reminder.ID(rawValue: 3)
60+
]
61+
62+
await assertSQL(of: Reminder.where { reminderIds.contains($0.id) }) {
63+
"""
64+
SELECT "reminders"."id", "reminders"."remindersListID"
65+
FROM "reminders"
66+
WHERE ("reminders"."id") IN (1, 2, 3)
67+
"""
68+
}
69+
}
70+
71+
@Test func taggedUpdate() async {
72+
let userId = User.ID(uuidString: "550e8400-e29b-41d4-a716-446655440000")!
73+
await assertSQL(
74+
of: User.where { $0.id == userId }.update { $0.name = "Bob" }
75+
) {
76+
"""
77+
UPDATE "users"
78+
SET "name" = 'Bob'
79+
WHERE ("users"."id") = ('550e8400-e29b-41d4-a716-446655440000')
80+
"""
3981
}
82+
}
4083

41-
@Table
42-
fileprivate struct Reminder {
43-
typealias ID = Tagged<Self, Int>
84+
@Test func taggedDelete() async {
85+
let reminderIds: [Reminder.ID] = [
86+
Reminder.ID(rawValue: 1),
87+
Reminder.ID(rawValue: 2)
88+
]
89+
await assertSQL(of: Reminder.where { reminderIds.contains($0.id) }.delete()) {
90+
"""
91+
DELETE FROM "reminders"
92+
WHERE ("reminders"."id") IN (1, 2)
93+
"""
94+
}
95+
}
4496

45-
let id: ID
46-
let remindersListID: Tagged<RemindersList, Int>
97+
@Test func taggedJoin() async {
98+
await assertSQL(
99+
of: Reminder.join(RemindersList.all) { $0.remindersListID == $1.id }
100+
.select { ($0.id, $1.name) }
101+
) {
102+
"""
103+
SELECT "reminders"."id", "remindersLists"."name"
104+
FROM "reminders"
105+
JOIN "remindersLists" ON ("reminders"."remindersListID") = ("remindersLists"."id")
106+
"""
47107
}
48108
}
109+
110+
@Test func taggedSelectPreservesType() {
111+
// Compile-time test: verify that selecting Tagged columns preserves the Tagged wrapper
112+
let _: Select<Reminder.ID, Reminder, ()> = Reminder.select { $0.id }
113+
let _: Select<User.ID, User, ()> = User.select { $0.id }
114+
115+
// Verify tuple selections preserve Tagged types
116+
let _: Select<(Reminder.ID, Int), Reminder, ()> = Reminder.select { ($0.id, $0.remindersListID) }
117+
let _: Select<(User.ID, String), User, ()> = User.select { ($0.id, $0.name) }
118+
}
119+
120+
@Table
121+
fileprivate struct Reminder {
122+
typealias ID = Tagged<Self, Int>
123+
124+
let id: ID
125+
let remindersListID: Int
126+
}
127+
128+
@Table
129+
fileprivate struct User {
130+
typealias ID = Tagged<Self, UUID>
131+
132+
let id: ID
133+
let name: String
134+
}
135+
136+
@Table
137+
fileprivate struct RemindersList {
138+
typealias ID = Int
139+
140+
let id: ID
141+
let name: String
142+
}
49143
}
50-
#endif
144+
}

0 commit comments

Comments
 (0)