Skip to content

Commit ae5defe

Browse files
committed
refactor: organize functions into PostgreSQL namespaces
Major reorganization aligning with PostgreSQL Chapter 9 documentation: - Created PostgreSQL.String namespace (Chapter 9.4) - Created PostgreSQL.Math namespace (Chapter 9.3) - Created PostgreSQL.Array namespace (Chapter 9.19) - Created Window namespace for window functions (Chapter 9.22) - Created Conditional namespace for CASE/COALESCE (Chapter 9.18) - Created Subquery namespace for quantified comparisons (Chapter 9.24) - Created CTE namespace with file-per-type organization - Reorganized DateTime functions by feature (Chapter 9.9) - Moved Convenience/ → Extensions/ for clarity - Improved Array function APIs with Swifty naming - Added comprehensive tests with .joined() disambiguation
1 parent eb7bc1a commit ae5defe

File tree

73 files changed

+5606
-2807
lines changed

Some content is hidden

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

73 files changed

+5606
-2807
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import Foundation
2+
3+
extension CTE {
4+
/// A builder of common table expressions.
5+
///
6+
/// This result builder is used by ``With/init(recursive:_:query:)`` to insert
7+
/// any number of common table expressions into a `WITH` statement.
8+
@resultBuilder
9+
public enum Builder {
10+
public static func buildExpression<CTETable: Table>(
11+
_ expression: some PartialSelectStatement<CTETable>
12+
) -> Clause {
13+
Clause(
14+
tableName: "\(CTETable.self)",
15+
select: expression.query,
16+
materialization: nil
17+
)
18+
}
19+
20+
public static func buildBlock(
21+
_ component: Clause
22+
) -> [Clause] {
23+
[component]
24+
}
25+
26+
public static func buildPartialBlock(
27+
first: Clause
28+
) -> [Clause] {
29+
[first]
30+
}
31+
32+
public static func buildPartialBlock(
33+
accumulated: [Clause],
34+
next: Clause
35+
) -> [Clause] {
36+
accumulated + [next]
37+
}
38+
}
39+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import Foundation
2+
3+
extension CTE {
4+
/// A single common table expression clause.
5+
public struct Clause: QueryExpression, Sendable {
6+
public typealias QueryValue = ()
7+
8+
let tableName: QueryFragment
9+
let select: QueryFragment
10+
let materialization: MaterializationHint?
11+
12+
public init(
13+
tableName: QueryFragment,
14+
select: QueryFragment,
15+
materialization: MaterializationHint? = nil
16+
) {
17+
self.tableName = tableName
18+
self.select = select
19+
self.materialization = materialization
20+
}
21+
22+
public var queryFragment: QueryFragment {
23+
guard !select.isEmpty else { return "" }
24+
25+
var fragment: QueryFragment = tableName
26+
27+
// Add materialization hint (PostgreSQL 12+ feature)
28+
if let materialization {
29+
switch materialization {
30+
case .materialized:
31+
fragment.append(" AS MATERIALIZED")
32+
case .notMaterialized:
33+
fragment.append(" AS NOT MATERIALIZED")
34+
}
35+
} else {
36+
fragment.append(" AS")
37+
}
38+
39+
fragment.append(" (\(.newline)\(select.indented())\(.newline))")
40+
return fragment
41+
}
42+
43+
/// Checks if this CTE is recursive (references itself in the query).
44+
///
45+
/// A CTE is considered recursive if:
46+
/// 1. The query contains UNION or UNION ALL
47+
/// 2. The query references the CTE's own table name (self-reference)
48+
///
49+
/// This follows PostgreSQL's requirement that recursive CTEs must use `WITH RECURSIVE`.
50+
var isRecursive: Bool {
51+
let tableNameString = extractTableName(from: tableName)
52+
let selectSQL = extractSQL(from: select)
53+
54+
// Check for UNION pattern (required for recursion)
55+
let hasUnion = selectSQL.contains("UNION ALL") || selectSQL.contains("UNION")
56+
guard hasUnion else { return false }
57+
58+
// Check for self-reference in FROM clause
59+
// Look for: FROM "tableName" or FROM tableName
60+
let quotedTableName = "\"\(tableNameString)\""
61+
return selectSQL.contains("FROM \(quotedTableName)")
62+
|| selectSQL.contains("FROM \(tableNameString)")
63+
}
64+
65+
/// Extracts the table name string from a QueryFragment.
66+
private func extractTableName(from fragment: QueryFragment) -> String {
67+
// QueryFragment for table name is typically just the string
68+
fragment.segments
69+
.compactMap { segment in
70+
if case .sql(let sql) = segment {
71+
return sql.trimmingCharacters(in: .whitespacesAndNewlines)
72+
}
73+
return nil
74+
}
75+
.joined()
76+
}
77+
78+
/// Extracts SQL string from QueryFragment for pattern matching.
79+
private func extractSQL(from fragment: QueryFragment) -> String {
80+
fragment.segments
81+
.compactMap { segment in
82+
if case .sql(let sql) = segment {
83+
return sql
84+
}
85+
return nil
86+
}
87+
.joined()
88+
}
89+
}
90+
}
91+
92+
extension CTE.Clause {
93+
/// Materialization hint for CTEs (PostgreSQL 12+).
94+
///
95+
/// Controls whether PostgreSQL computes and stores CTE results separately
96+
/// or inlines them into the main query.
97+
public enum MaterializationHint: Sendable {
98+
/// Force materialization: compute CTE once and store results
99+
case materialized
100+
101+
/// Prevent materialization: inline the CTE into the main query
102+
case notMaterialized
103+
}
104+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import Foundation
2+
3+
extension CTE {
4+
/// Creates a common table expression for factoring subqueries or recursive queries.
5+
///
6+
/// Also available as the global ``With`` typealias.
7+
public struct With<Base: Statement>: Statement, Sendable {
8+
public typealias QueryValue = Base.QueryValue
9+
public typealias From = Never
10+
11+
var ctes: [Clause]
12+
var statement: QueryFragment
13+
let recursive: Bool?
14+
15+
@_disfavoredOverload
16+
public init(
17+
recursive: Bool? = nil,
18+
@Builder _ ctes: () -> [Clause],
19+
query statement: () -> Base
20+
) {
21+
self.recursive = recursive
22+
self.ctes = ctes()
23+
self.statement = statement().query
24+
}
25+
26+
public init<S: SelectStatement, each J: Table>(
27+
recursive: Bool? = nil,
28+
@Builder _ ctes: () -> [Clause],
29+
query statement: () -> S
30+
)
31+
where
32+
S.QueryValue == (),
33+
S.Joins == (repeat each J),
34+
Base == Select<(S.From, repeat each J), S.From, (repeat each J)>
35+
{
36+
self.recursive = recursive
37+
self.ctes = ctes()
38+
self.statement = statement().query
39+
}
40+
41+
@_disfavoredOverload
42+
public init<S: SelectStatement>(
43+
recursive: Bool? = nil,
44+
@Builder _ ctes: () -> [Clause],
45+
query statement: () -> S
46+
)
47+
where
48+
S.QueryValue == (),
49+
S.Joins == (),
50+
Base == Select<S.From, S.From, ()>
51+
{
52+
self.recursive = recursive
53+
self.ctes = ctes()
54+
self.statement = statement().query
55+
}
56+
57+
public var query: QueryFragment {
58+
guard !statement.isEmpty else { return "" }
59+
let cteFragments = ctes.compactMap(\.queryFragment.presence)
60+
guard !cteFragments.isEmpty else { return "" }
61+
62+
var query: QueryFragment = "WITH"
63+
64+
// Add RECURSIVE keyword if needed (auto-detect or explicit)
65+
if isRecursive {
66+
query.append(" RECURSIVE")
67+
}
68+
69+
query.append(" \(cteFragments.joined(separator: ", "))\(.newlineOrSpace)\(statement)")
70+
return query
71+
}
72+
73+
/// Determines if this WITH clause should include the RECURSIVE keyword.
74+
///
75+
/// Returns true if:
76+
/// - The `recursive` parameter was explicitly set to `true`, or
77+
/// - Auto-detection finds a recursive CTE (contains UNION/UNION ALL with self-reference)
78+
private var isRecursive: Bool {
79+
// Use explicit value if provided
80+
if let recursive {
81+
return recursive
82+
}
83+
84+
// Auto-detect: check if any CTE references itself
85+
return ctes.contains { cte in
86+
cte.isRecursive
87+
}
88+
}
89+
}
90+
}
91+
92+
extension CTE.With: PartialSelectStatement where Base: PartialSelectStatement {}
93+
94+
extension QueryFragment {
95+
fileprivate var presence: Self? { isEmpty ? nil : self }
96+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import Foundation
2+
3+
/// Namespace for Common Table Expression (CTE) types.
4+
///
5+
/// Common table expressions allow you to factor subqueries or create
6+
/// recursive queries of trees and graphs.
7+
///
8+
/// Use the global ``With`` typealias for cleaner syntax:
9+
///
10+
/// ```swift
11+
/// With {
12+
/// Stats.all
13+
/// } query: {
14+
/// Stats.all
15+
/// }
16+
/// ```
17+
///
18+
/// See <doc:CommonTableExpressions> for more information.
19+
public enum CTE {}
20+
21+
/// A convenient typealias for ``CTE/With``.
22+
///
23+
/// Creates a common table expression for factoring subqueries or recursive queries.
24+
public typealias With = CTE.With

0 commit comments

Comments
 (0)