Skip to content

Commit 9c433b9

Browse files
committed
test: add comprehensive Array manipulation tests with .joined() disambiguation
- Add ArrayManipulationTests with 18 tests covering all Array functions - Demonstrate @_disfavoredOverload working correctly: - Swift stdlib .joined() for regular arrays (line 22-23) - SQL .joined() for QueryExpression types (line 26-32) - Test coverage for all refactored function names: - .removing(), .replacing(), .joined(), .dimensions, .toJSON() - .split(), .appending(), .prepending(), .concatenating() - Real-world use cases and SQL injection prevention tests - All 18 tests passing
1 parent 91059c0 commit 9c433b9

File tree

1 file changed

+260
-0
lines changed

1 file changed

+260
-0
lines changed
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
import Foundation
2+
import InlineSnapshotTesting
3+
import StructuredQueriesPostgres
4+
import StructuredQueriesPostgresTestSupport
5+
import Testing
6+
7+
extension SnapshotTests.PostgresArrayOps {
8+
@Suite("Array Manipulation") struct ArrayManipulationTests {
9+
10+
// MARK: - .joined() Disambiguation Test
11+
12+
@Test("Swift stdlib .joined() vs SQL .joined() - disambiguation")
13+
func joinedDisambiguation() async {
14+
// This test demonstrates that both .joined() methods coexist peacefully:
15+
// 1. Swift's stdlib .joined() for regular Swift arrays (preferred via @_disfavoredOverload)
16+
// 2. SQL's .joined() for QueryExpression types (PostgreSQL's array_to_string)
17+
18+
// Swift stdlib .joined() - used for building SQL strings
19+
let tagNames = ["swift", "postgres", "server"]
20+
let swiftJoined = tagNames.joined(separator: ", ") // Calls Swift.Sequence.joined()
21+
#expect(swiftJoined == "swift, postgres, server")
22+
23+
// SQL .joined() - generates PostgreSQL's array_to_string() in query
24+
await assertSQL(
25+
of: Post.select { $0.tags.joined(separator: ", ") } // Calls QueryExpression.joined()
26+
) {
27+
"""
28+
SELECT array_to_string("posts"."tags", ', ')
29+
FROM "posts"
30+
"""
31+
}
32+
}
33+
34+
// MARK: - Array Manipulation Functions
35+
36+
@Test func removing() async {
37+
await assertSQL(
38+
of: Post.select { $0.tags.removing("deprecated") }
39+
) {
40+
"""
41+
SELECT array_remove("posts"."tags", 'deprecated')
42+
FROM "posts"
43+
"""
44+
}
45+
}
46+
47+
@Test func replacing() async {
48+
await assertSQL(
49+
of: Post.select { $0.tags.replacing("old-tag", with: "new-tag") }
50+
) {
51+
"""
52+
SELECT array_replace("posts"."tags", 'old-tag', 'new-tag')
53+
FROM "posts"
54+
"""
55+
}
56+
}
57+
58+
@Test func joinedWithNullReplacement() async {
59+
await assertSQL(
60+
of: Post.select { $0.tags.joined(separator: ", ", nullReplacement: "[none]") }
61+
) {
62+
"""
63+
SELECT array_to_string("posts"."tags", ', ', '[none]')
64+
FROM "posts"
65+
"""
66+
}
67+
}
68+
69+
@Test func dimensions() async {
70+
await assertSQL(
71+
of: Post.select { $0.tags.dimensions }
72+
) {
73+
"""
74+
SELECT array_dims("posts"."tags")
75+
FROM "posts"
76+
"""
77+
}
78+
}
79+
80+
@Test func toJSON() async {
81+
await assertSQL(
82+
of: Post.select { $0.tags.toJSON() }
83+
) {
84+
"""
85+
SELECT array_to_json("posts"."tags")
86+
FROM "posts"
87+
"""
88+
}
89+
}
90+
91+
@Test func toJSONPrettyPrint() async {
92+
await assertSQL(
93+
of: Post.select { $0.tags.toJSON(prettyPrint: true) }
94+
) {
95+
"""
96+
SELECT array_to_json("posts"."tags", true)
97+
FROM "posts"
98+
"""
99+
}
100+
}
101+
102+
// MARK: - String to Array Conversion
103+
104+
@Test func split() async {
105+
await assertSQL(
106+
of: Account.select { $0.commaSeparatedTags.split(separator: ",") }
107+
) {
108+
"""
109+
SELECT string_to_array("accounts"."commaSeparatedTags", ',')
110+
FROM "accounts"
111+
"""
112+
}
113+
}
114+
115+
@Test func splitWithNullString() async {
116+
await assertSQL(
117+
of: Account.select { $0.commaSeparatedTags.split(separator: ",", nullString: "NULL") }
118+
) {
119+
"""
120+
SELECT string_to_array("accounts"."commaSeparatedTags", ',', 'NULL')
121+
FROM "accounts"
122+
"""
123+
}
124+
}
125+
126+
// MARK: - Array Construction
127+
128+
@Test func appending() async {
129+
await assertSQL(
130+
of: Post.select { $0.tags.appending("swift") }
131+
) {
132+
"""
133+
SELECT array_append("posts"."tags", 'swift')
134+
FROM "posts"
135+
"""
136+
}
137+
}
138+
139+
@Test func prepending() async {
140+
await assertSQL(
141+
of: Post.select { $0.tags.prepending("featured") }
142+
) {
143+
"""
144+
SELECT array_prepend('featured', "posts"."tags")
145+
FROM "posts"
146+
"""
147+
}
148+
}
149+
150+
@Test func concatenatingArray() async {
151+
await assertSQL(
152+
of: Post.select { $0.tags.concatenating(["archived", "reviewed"]) }
153+
) {
154+
"""
155+
SELECT array_cat("posts"."tags", ARRAY['archived', 'reviewed'])
156+
FROM "posts"
157+
"""
158+
}
159+
}
160+
161+
// MARK: - Special Characters & SQL Injection Prevention
162+
163+
@Test("Properly escapes special characters in array manipulation")
164+
func specialCharactersEscaped() async {
165+
await assertSQL(
166+
of: Post.select { $0.tags.appending("it's \"quoted\"") }
167+
) {
168+
"""
169+
SELECT array_append("posts"."tags", 'it''s "quoted"')
170+
FROM "posts"
171+
"""
172+
}
173+
}
174+
175+
// MARK: - Real-World Use Cases
176+
177+
@Test("Display tags as comma-separated string")
178+
func displayTagsAsString() async {
179+
// Real-world: Convert array column to user-friendly display string
180+
await assertSQL(
181+
of: Post.select { ($0.title, $0.tags.joined(separator: ", ")) }
182+
) {
183+
"""
184+
SELECT "posts"."title", array_to_string("posts"."tags", ', ')
185+
FROM "posts"
186+
"""
187+
}
188+
}
189+
190+
@Test("Remove deprecated tag from all posts")
191+
func removeDeprecatedTag() async {
192+
// Real-world: Cleanup operation - remove a specific tag
193+
await assertSQL(
194+
of: Post.update { $0.tags = $0.tags.removing("deprecated") }
195+
) {
196+
"""
197+
UPDATE "posts"
198+
SET "tags" = array_remove("posts"."tags", 'deprecated')
199+
"""
200+
}
201+
}
202+
203+
@Test("Rename tag across all posts")
204+
func renameTag() async {
205+
// Real-world: Tag migration - rename old tag to new tag
206+
await assertSQL(
207+
of: Post.update { $0.tags = $0.tags.replacing("old-name", with: "new-name") }
208+
) {
209+
"""
210+
UPDATE "posts"
211+
SET "tags" = array_replace("posts"."tags", 'old-name', 'new-name')
212+
"""
213+
}
214+
}
215+
216+
@Test("Parse CSV string into array column")
217+
func parseCSV() async {
218+
// Real-world: Import CSV data into array column
219+
await assertSQL(
220+
of: Account.update { $0.tags = $0.commaSeparatedTags.split(separator: ",") }
221+
) {
222+
"""
223+
UPDATE "accounts"
224+
SET "tags" = string_to_array("accounts"."commaSeparatedTags", ',')
225+
"""
226+
}
227+
}
228+
229+
@Test("Export array as JSON for API response")
230+
func exportAsJSON() async {
231+
// Real-world: API endpoint returning tags as JSON array
232+
await assertSQL(
233+
of: Post.select { ($0.id, $0.tags.toJSON()) }
234+
) {
235+
"""
236+
SELECT "posts"."id", array_to_json("posts"."tags")
237+
FROM "posts"
238+
"""
239+
}
240+
}
241+
}
242+
}
243+
244+
// MARK: - Test Models
245+
246+
@Table
247+
private struct Post {
248+
let id: Int
249+
let title: String
250+
@Column(as: [String].self)
251+
let tags: [String]
252+
}
253+
254+
@Table("accounts")
255+
private struct Account {
256+
let id: Int
257+
let commaSeparatedTags: String
258+
@Column(as: [String].self)
259+
let tags: [String]
260+
}

0 commit comments

Comments
 (0)