Skip to content

Commit 91059c0

Browse files
committed
refactor: improve Array function APIs with Swifty naming and security fixes
- Rename array functions to use participial forms and Swift conventions: - .append() → .appending() - .arrayPrepend() → .prepending() - .arrayCat() → .concatenating() - .arrayRemove() → .removing() - .arrayReplace() → .replacing(_:with:) - .arrayToString() → .joined(separator:) - .arrayDims() → .dimensions (property) - .arrayToJson() → .toJSON() - .stringToArray() → .split(separator:) - arrayFill() → fill(value:count:) with cleaner overloads - arrayFrom() → array() - emptyArray(_:) → emptyArray(of:) - Fix SQL injection vulnerabilities: - Replace string interpolation with proper \(bind:) for array elements - Secure array construction functions - Type system improvements: - emptyArray(of:) now requires PostgreSQLType conformance - Remove runtime type switching with postgresTypeName() - Resolve .joined() naming conflict: - Add @_disfavoredOverload to SQL .joined() methods - Compiler now prefers Swift stdlib for regular arrays - SQL version still available for QueryExpression types All 817 tests passing.
1 parent a36fd25 commit 91059c0

File tree

3 files changed

+268
-263
lines changed

3 files changed

+268
-263
lines changed

Sources/StructuredQueriesPostgres/Functions/Array/ArrayConstruction.swift

Lines changed: 111 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,98 @@ import StructuredQueriesCore
88
//
99
// Array construction and concatenation functions for building and modifying arrays.
1010

11+
// MARK: - Extension Methods (Swifty API)
12+
13+
extension QueryExpression where QueryValue: Collection, QueryValue.Element: QueryBindable {
14+
/// Appends an element to the end of an array
15+
///
16+
/// PostgreSQL's `array_append(anyarray, anyelement)` function.
17+
///
18+
/// ```swift
19+
/// Post.select { $0.tags.appending("swift") }
20+
/// // SELECT array_append("posts"."tags", 'swift') FROM "posts"
21+
/// ```
22+
///
23+
/// - Parameter element: The element to append to the array
24+
/// - Returns: A new array with the element appended
25+
public func appending(_ element: QueryValue.Element) -> some QueryExpression<QueryValue> {
26+
SQLQueryExpression(
27+
"array_append(\(self.queryFragment), \(bind: element))",
28+
as: QueryValue.self
29+
)
30+
}
31+
32+
/// Prepends an element to the beginning of an array
33+
///
34+
/// PostgreSQL's `array_prepend(anyelement, anyarray)` function.
35+
///
36+
/// ```swift
37+
/// Post.select { $0.tags.prepending("featured") }
38+
/// // SELECT array_prepend('featured', "posts"."tags") FROM "posts"
39+
/// ```
40+
///
41+
/// - Parameter element: The element to prepend to the array
42+
/// - Returns: A new array with the element prepended
43+
public func prepending(_ element: QueryValue.Element) -> some QueryExpression<QueryValue> {
44+
SQLQueryExpression(
45+
"array_prepend(\(bind: element), \(self.queryFragment))",
46+
as: QueryValue.self
47+
)
48+
}
49+
50+
/// Concatenates another array to this array
51+
///
52+
/// PostgreSQL's `array_cat(anyarray, anyarray)` function.
53+
///
54+
/// ```swift
55+
/// Post.select { $0.tags.concatenating($0.categories) }
56+
/// // SELECT array_cat("posts"."tags", "posts"."categories") FROM "posts"
57+
/// ```
58+
///
59+
/// - Parameter other: The array to concatenate
60+
/// - Returns: A new array containing elements from both arrays
61+
public func concatenating(_ other: some QueryExpression<QueryValue>) -> some QueryExpression<QueryValue> {
62+
SQLQueryExpression(
63+
"array_cat(\(self.queryFragment), \(other.queryFragment))",
64+
as: QueryValue.self
65+
)
66+
}
67+
68+
/// Concatenates a Swift array literal to this array
69+
///
70+
/// PostgreSQL's `array_cat(anyarray, anyarray)` function with literal array.
71+
///
72+
/// ```swift
73+
/// Post.select { $0.tags.concatenating(["swift", "postgres"]) }
74+
/// // SELECT array_cat("posts"."tags", ARRAY['swift', 'postgres']) FROM "posts"
75+
/// ```
76+
///
77+
/// - Parameter elements: The elements to concatenate
78+
/// - Returns: A new array containing elements from both arrays
79+
public func concatenating(_ elements: [QueryValue.Element]) -> some QueryExpression<QueryValue> {
80+
// Build array using proper binding for each element
81+
var fragments: [QueryFragment] = []
82+
for element in elements {
83+
fragments.append("\(bind: element)")
84+
}
85+
let arrayLiteral = "ARRAY[\(fragments.joined(separator: ", "))]"
86+
return SQLQueryExpression(
87+
"array_cat(\(self.queryFragment), \(raw: arrayLiteral))",
88+
as: QueryValue.self
89+
)
90+
}
91+
}
92+
93+
// MARK: - Free Functions (For composing without receiver)
94+
1195
/// Appends an element to the end of an array
1296
///
1397
/// PostgreSQL's `array_append(anyarray, anyelement)` function.
1498
///
1599
/// ```swift
16-
/// Post.select { $0.tags.append("swift") }
17-
/// // SELECT array_append("posts"."tags", 'swift') FROM "posts"
100+
/// User.select { append($0.tags, "swift") }
101+
/// // SELECT array_append("users"."tags", 'swift') FROM "users"
18102
/// ```
19-
///
20-
/// - Parameter element: The element to append to the array
21-
/// - Returns: A new array with the element appended
22103
public func append<Element>(
23104
_ array: some QueryExpression<[Element]>,
24105
_ element: Element
@@ -29,71 +110,33 @@ public func append<Element>(
29110
)
30111
}
31112

32-
/// Appends an element to the end of an array (method syntax)
33-
extension QueryExpression where QueryValue: Collection, QueryValue.Element: QueryBindable {
34-
/// Appends an element to the end of an array
35-
///
36-
/// ```swift
37-
/// Post.select { $0.tags.append("swift") }
38-
/// // SELECT array_append("posts"."tags", 'swift') FROM "posts"
39-
/// ```
40-
public func append(_ element: QueryValue.Element) -> some QueryExpression<QueryValue> {
41-
SQLQueryExpression(
42-
"array_append(\(self.queryFragment), \(bind: element))",
43-
as: QueryValue.self
44-
)
45-
}
46-
}
47-
48113
/// Prepends an element to the beginning of an array
49114
///
50115
/// PostgreSQL's `array_prepend(anyelement, anyarray)` function.
51116
///
52117
/// ```swift
53-
/// Post.select { $0.tags.arrayPrepend("featured") }
54-
/// // SELECT array_prepend('featured', "posts"."tags") FROM "posts"
118+
/// User.select { prepend("featured", to: $0.tags) }
119+
/// // SELECT array_prepend('featured', "users"."tags") FROM "users"
55120
/// ```
56-
///
57-
/// - Parameter element: The element to prepend to the array
58-
/// - Returns: A new array with the element prepended
59-
public func arrayPrepend<Element>(
121+
public func prepend<Element>(
60122
_ element: Element,
61-
_ array: some QueryExpression<[Element]>
123+
to array: some QueryExpression<[Element]>
62124
) -> some QueryExpression<[Element]> where Element: QueryBindable {
63125
SQLQueryExpression(
64126
"array_prepend(\(bind: element), \(array.queryFragment))",
65127
as: [Element].self
66128
)
67129
}
68130

69-
/// Prepends an element to the beginning of an array (method syntax)
70-
extension QueryExpression where QueryValue: Collection, QueryValue.Element: QueryBindable {
71-
/// Prepends an element to the beginning of an array
72-
///
73-
/// ```swift
74-
/// Post.select { $0.tags.arrayPrepend("featured") }
75-
/// // SELECT array_prepend('featured', "posts"."tags") FROM "posts"
76-
/// ```
77-
public func arrayPrepend(_ element: QueryValue.Element) -> some QueryExpression<QueryValue> {
78-
SQLQueryExpression(
79-
"array_prepend(\(bind: element), \(self.queryFragment))",
80-
as: QueryValue.self
81-
)
82-
}
83-
}
84-
85131
/// Concatenates two arrays
86132
///
87133
/// PostgreSQL's `array_cat(anyarray, anyarray)` function.
88134
///
89135
/// ```swift
90-
/// Post.select { $0.tags.arrayCat(["swift", "postgres"]) }
91-
/// // SELECT array_cat("posts"."tags", ARRAY['swift', 'postgres']) FROM "posts"
136+
/// User.select { concatenate($0.tags, $0.categories) }
137+
/// // SELECT array_cat("users"."tags", "users"."categories") FROM "users"
92138
/// ```
93-
///
94-
/// - Parameter other: The array to concatenate
95-
/// - Returns: A new array containing elements from both arrays
96-
public func arrayCat<Element>(
139+
public func concatenate<Element>(
97140
_ array1: some QueryExpression<[Element]>,
98141
_ array2: some QueryExpression<[Element]>
99142
) -> some QueryExpression<[Element]> where Element: QueryBindable {
@@ -103,55 +146,29 @@ public func arrayCat<Element>(
103146
)
104147
}
105148

106-
/// Concatenates two arrays (method syntax)
107-
extension QueryExpression where QueryValue: Collection, QueryValue.Element: QueryBindable {
108-
/// Concatenates another array to this array
109-
///
110-
/// ```swift
111-
/// Post.select { $0.tags.arrayCat(["swift", "postgres"]) }
112-
/// // SELECT array_cat("posts"."tags", ARRAY['swift', 'postgres']) FROM "posts"
113-
/// ```
114-
public func arrayCat(_ other: [QueryValue.Element]) -> some QueryExpression<QueryValue> {
115-
let arrayLiteral = "ARRAY[\(other.map { "'\($0)'" }.joined(separator: ", "))]"
116-
return SQLQueryExpression(
117-
"array_cat(\(self.queryFragment), \(raw: arrayLiteral))",
118-
as: QueryValue.self
119-
)
120-
}
121-
122-
/// Concatenates another array expression to this array
123-
///
124-
/// ```swift
125-
/// Post.select { $0.tags.arrayCat($0.categories) }
126-
/// // SELECT array_cat("posts"."tags", "posts"."categories") FROM "posts"
127-
/// ```
128-
public func arrayCat(_ other: some QueryExpression<QueryValue>) -> some QueryExpression<
129-
QueryValue
130-
> {
131-
SQLQueryExpression(
132-
"array_cat(\(self.queryFragment), \(other.queryFragment))",
133-
as: QueryValue.self
134-
)
135-
}
136-
}
149+
// MARK: - Array Constructors
137150

138151
/// Creates an array from the given elements
139152
///
140153
/// PostgreSQL's ARRAY constructor syntax.
141154
///
142155
/// ```swift
143-
/// let tags = arrayFrom(["swift", "postgres", "server"])
156+
/// let tags = array(["swift", "postgres", "server"])
144157
/// Post.insert { Post.Draft(title: "Hello", tags: tags) }
145158
/// // INSERT INTO "posts" ("title", "tags") VALUES ('Hello', ARRAY['swift', 'postgres', 'server'])
146159
/// ```
147160
///
148161
/// - Parameter elements: The elements to create an array from
149162
/// - Returns: An array expression
150-
public func arrayFrom<Element>(
163+
public func array<Element>(
151164
_ elements: [Element]
152165
) -> some QueryExpression<[Element]> where Element: QueryBindable {
153-
let arrayLiteral = QueryFragment(
154-
"ARRAY[\(raw: elements.map { "'\($0)'" }.joined(separator: ", "))]")
166+
// Build array using proper binding for each element
167+
var fragments: [QueryFragment] = []
168+
for element in elements {
169+
fragments.append("\(bind: element)")
170+
}
171+
let arrayLiteral = QueryFragment("ARRAY[\(fragments.joined(separator: ", "))]")
155172
return SQLQueryExpression(arrayLiteral, as: [Element].self)
156173
}
157174

@@ -160,42 +177,18 @@ public func arrayFrom<Element>(
160177
/// PostgreSQL's empty ARRAY constructor.
161178
///
162179
/// ```swift
163-
/// Post.insert { Post.Draft(title: "Hello", tags: emptyArray(String.self)) }
180+
/// Post.insert { Post.Draft(title: "Hello", tags: emptyArray(of: String.self)) }
164181
/// // INSERT INTO "posts" ("title", "tags") VALUES ('Hello', ARRAY[]::text[])
165182
/// ```
166183
///
167184
/// - Parameter elementType: The type of elements in the array
168185
/// - Returns: An empty array expression
169186
public func emptyArray<Element>(
170-
_ elementType: Element.Type
171-
) -> some QueryExpression<[Element]> where Element: QueryBindable {
172-
// PostgreSQL requires type cast for empty arrays
173-
let pgType = postgresTypeName(for: elementType)
174-
return SQLQueryExpression("ARRAY[]::\(raw: pgType)[]", as: [Element].self)
175-
}
176-
177-
// Helper to map Swift types to PostgreSQL type names
178-
private func postgresTypeName<T>(for type: T.Type) -> String {
179-
switch type {
180-
case is String.Type, is String?.Type:
181-
return "text"
182-
case is Int.Type, is Int?.Type:
183-
return "integer"
184-
case is Int64.Type, is Int64?.Type:
185-
return "bigint"
186-
case is Double.Type, is Double?.Type:
187-
return "double precision"
188-
case is Float.Type, is Float?.Type:
189-
return "real"
190-
case is Bool.Type, is Bool?.Type:
191-
return "boolean"
192-
case is UUID.Type, is UUID?.Type:
193-
return "uuid"
194-
case is Date.Type, is Date?.Type:
195-
return "timestamp"
196-
case is Data.Type, is Data?.Type:
197-
return "bytea"
198-
default:
199-
return "text" // Fallback to text
200-
}
201-
}
187+
of elementType: Element.Type
188+
) -> some QueryExpression<[Element]> where Element: PostgreSQLType {
189+
// Use PostgreSQLType protocol for type-safe type name resolution
190+
return SQLQueryExpression(
191+
"ARRAY[]::\(raw: Element.typeName)[]",
192+
as: [Element].self
193+
)
194+
}

0 commit comments

Comments
 (0)