Skip to content

Commit fd43e1b

Browse files
committed
refactor: reorganize directory structure for PostgreSQL Chapter 9 alignment
Cleaned up directory structure to eliminate empty placeholder directories and align aggregate functions with PostgreSQL documentation organization. Changes: - Moved StandardAggregates.swift to Functions/Aggregate/ directory - Removed empty Aggregates/ directory - Deleted 7 empty Statements/ subdirectories (Common, Delete, Insert, Select, Update, Views, CommonTableExpressions) - Deleted 5 empty Types/ subdirectories (Boolean, DateTime, Numeric, String, UUID) - Deleted empty Functions/UUID/ directory Benefits: - Clear PostgreSQL Chapter 9 structure - all aggregates now in Functions/Aggregate/ - No ambiguity about where code belongs - Directory structure matches official PostgreSQL documentation - Zero empty directories creating confusion Build status: ✅ All builds pass Test status: ✅ All 280+ tests pass WIP WIP fix .desc syntax for windows
1 parent 895cd15 commit fd43e1b

File tree

21 files changed

+178
-663
lines changed

21 files changed

+178
-663
lines changed

Architecture.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1424,7 +1424,7 @@ Conducted comprehensive audit against [PostgreSQL SELECT Documentation](https://
14241424
.window("dept_salary") {
14251425
WindowSpec()
14261426
.partition(by: $0.department)
1427-
.order(by: $0.salary, .desc)
1427+
.order(by: $0.salary.desc())
14281428
}
14291429
.select {
14301430
(
@@ -1448,10 +1448,10 @@ Conducted comprehensive audit against [PostgreSQL SELECT Documentation](https://
14481448
.window("list_order") {
14491449
WindowSpec()
14501450
.partition(by: $0.remindersListID)
1451-
.order(by: $0.title, .desc)
1451+
.order(by: $0.title.desc())
14521452
}
14531453
.window("overall_order") {
1454-
WindowSpec().order(by: $0.title, .desc)
1454+
WindowSpec().order(by: $0.title.desc())
14551455
}
14561456
.select {
14571457
(
@@ -1473,7 +1473,7 @@ Conducted comprehensive audit against [PostgreSQL SELECT Documentation](https://
14731473
.window("list_window") {
14741474
WindowSpec()
14751475
.partition(by: $0.remindersListID)
1476-
.order(by: $0.title, .desc)
1476+
.order(by: $0.title.desc())
14771477
}
14781478
.select {
14791479
let id = $0.id

Package.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,13 @@ let package = Package(
4949
description:
5050
"Enable SQL syntax validation against PostgreSQL using postgres-nio. Heavy dependency - only enable for validation testing."
5151
),
52-
.default(enabledTraits: [
53-
"StructuredQueriesPostgresCasePaths", "StructuredQueriesPostgresSQLValidation",
54-
]),
52+
.default(
53+
enabledTraits: [
54+
"StructuredQueriesPostgresCasePaths",
55+
// "StructuredQueriesPostgresTagged",
56+
"StructuredQueriesPostgresSQLValidation",
57+
]
58+
),
5559
],
5660
dependencies: [
5761
.package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.0.0"),

README.md

Lines changed: 36 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,12 @@ struct User {
2727
let id: Int
2828
var name: String
2929
var email: String
30+
var isActive: Bool = true
3031
}
3132

3233
// Build query (this package)
3334
let statement = User
34-
.where { $0.email.hasSuffix("@example.com") }
35+
.where { $0.isActive && $0.email.hasSuffix("@example.com") }
3536
.order(by: \.name)
3637
.limit(10)
3738

@@ -89,7 +90,11 @@ User
8990
.where { $0.isActive }
9091
.order(by: \.name)
9192
.limit(10)
92-
// SQL: SELECT * FROM "users" WHERE "users"."isActive" ORDER BY "users"."name" LIMIT 10
93+
// SQL: SELECT "users"."id", "users"."name", "users"."email", "users"."isActive"
94+
// FROM "users"
95+
// WHERE "users"."isActive"
96+
// ORDER BY "users"."name"
97+
// LIMIT 10
9398
```
9499

95100
**INSERT with Draft types** (PostgreSQL NULL PRIMARY KEY handling):
@@ -99,14 +104,14 @@ User
99104
User.insert {
100105
User.Draft(name: "Alice", email: "alice@example.com")
101106
}
102-
// SQL: INSERT INTO "users" ("name", "email") VALUES ('Alice', 'alice@example.com')
107+
// SQL: INSERT INTO "users" ("id", "name", "email", "isActive") VALUES (DEFAULT, 'Alice', 'alice@example.com', true)
103108

104109
// Mixed records - uses DEFAULT for NULL PKs
105110
User.insert {
106111
User(id: 1, name: "Alice", email: "alice@example.com")
107112
User.Draft(name: "Bob", email: "bob@example.com")
108113
}
109-
// SQL: INSERT INTO "users" ("id", "name", "email") VALUES (1, 'Alice', 'alice@example.com'), (DEFAULT, 'Bob', 'bob@example.com')
114+
// SQL: INSERT INTO "users" ("id", "name", "email", "isActive") VALUES (1, 'Alice', 'alice@example.com', true), (DEFAULT, 'Bob', 'bob@example.com', true)
110115
```
111116

112117
**UPDATE with RETURNING:**
@@ -131,37 +136,35 @@ User
131136

132137
### PostgreSQL JSONB
133138

134-
**Containment operators (@>, <@):**
139+
**Containment operator (@>):**
135140

136141
```swift
137-
struct UserSettings: Codable {
138-
var theme: String
139-
var notifications: Bool
140-
}
141-
142142
@Table
143143
struct User {
144144
let id: Int
145-
var settings: UserSettings
145+
var name: String
146+
147+
@Column(as: Data.self)
148+
var settings: Data
146149
}
147150

148151
// Find users with dark theme
149-
User.where { $0.settings.contains(UserSettings(theme: "dark", notifications: true)) }
150-
// SQL: WHERE "users"."settings" @> '{"theme":"dark","notifications":true}'
152+
User.where { $0.settings.contains(["theme": "dark"]) }
153+
// SQL: WHERE "users"."settings" @> '{"theme":"dark"}'::jsonb
151154
```
152155

153-
**JSON path operators (->, ->>):**
156+
**JSON path operator (->>):**
154157

155158
```swift
156-
User.where { $0.settings.getText("theme") == "dark" }
159+
User.where { $0.settings.fieldAsText("theme") == "dark" }
157160
// SQL: WHERE ("users"."settings" ->> 'theme') = 'dark'
158161
```
159162

160-
**GIN indexing for fast JSONB queries:**
163+
**Key existence operator (?):**
161164

162165
```swift
163-
User.createGINIndex(on: \.settings, operatorClass: .jsonb_path_ops)
164-
// SQL: CREATE INDEX "users_settings_gin_idx" ON "users" USING GIN ("settings" jsonb_path_ops)
166+
User.where { $0.settings.hasKey("notifications") }
167+
// SQL: WHERE "users"."settings" ? 'notifications'
165168
```
166169

167170
### Window Functions
@@ -172,32 +175,35 @@ User.createGINIndex(on: \.settings, operatorClass: .jsonb_path_ops)
172175
@Table
173176
struct Employee {
174177
let id: Int
178+
var name: String
175179
var department: String
176180
var salary: Double
177181
}
178182

179183
Employee
180184
.select {
181-
(
185+
let department = $0.department
186+
let salary = $0.salary
187+
return (
182188
$0.name,
183-
$0.salary,
189+
salary,
184190
rank().over {
185-
$0.partition(by: $1.department)
186-
.order(by: $1.salary, .desc)
191+
$0.partition(by: department)
192+
.order(by: salary.desc())
187193
}
188194
)
189195
}
190-
// SQL: SELECT "name", "salary", RANK() OVER (PARTITION BY "department" ORDER BY "salary" DESC)
196+
// SQL: SELECT "employees"."name", "employees"."salary", RANK() OVER (PARTITION BY "employees"."department" ORDER BY "employees"."salary" DESC)
197+
// FROM "employees"
191198
```
192199

193200
**Named windows (WINDOW clause):**
194201

195202
```swift
196203
Employee
197204
.window("dept_salary") {
198-
WindowSpec()
199-
.partition(by: $0.department)
200-
.order(by: $0.salary, .desc)
205+
$0.partition(by: $1.department)
206+
.order(by: $1.salary.desc())
201207
}
202208
.select {
203209
(
@@ -207,23 +213,9 @@ Employee
207213
rowNumber().over("dept_salary")
208214
)
209215
}
210-
// SQL: SELECT "name", RANK() OVER dept_salary, DENSE_RANK() OVER dept_salary, ROW_NUMBER() OVER dept_salary
216+
// SQL: SELECT "employees"."name", RANK() OVER dept_salary, DENSE_RANK() OVER dept_salary, ROW_NUMBER() OVER dept_salary
211217
// FROM "employees"
212-
// WINDOW dept_salary AS (PARTITION BY "department" ORDER BY "salary" DESC)
213-
```
214-
215-
**Running totals with SUM() window function:**
216-
217-
```swift
218-
Sale
219-
.select {
220-
(
221-
$0.date,
222-
$0.amount,
223-
sum($0.amount).over { $0.order(by: $1.date) }
224-
)
225-
}
226-
// SQL: SELECT "date", "amount", SUM("amount") OVER (ORDER BY "date")
218+
// WINDOW dept_salary AS (PARTITION BY "employees"."department" ORDER BY "employees"."salary" DESC)
227219
```
228220

229221
### Triggers
@@ -235,6 +227,8 @@ Sale
235227
struct Product {
236228
let id: Int
237229
var name: String
230+
var price: Double
231+
var stock: Int
238232
var updatedAt: Date?
239233
}
240234

Sources/StructuredQueriesCore/FrameClause.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ extension WindowSpec {
209209
/// ```swift
210210
/// Leaderboard.select {
211211
/// ($0.player, $0.score, $0.score.count().over {
212-
/// $0.order(by: $0.score, .desc)
212+
/// $0.order(by: $0.score.desc())
213213
/// .groups(between: .currentRow, and: .unboundedFollowing)
214214
/// })
215215
/// }

Sources/StructuredQueriesCore/Statements/Select/Select+Window.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ extension Select {
1111
/// ```swift
1212
/// Employee
1313
/// .window("dept_window") { spec, cols in
14-
/// spec.partition(by: cols.department).order(by: cols.salary, .desc)
14+
/// spec.partition(by: cols.department).order(by: cols.salary.desc())
1515
/// }
1616
/// .select {
1717
/// ($0.name, rank().over("dept_window"))
@@ -23,7 +23,7 @@ extension Select {
2323
/// ```swift
2424
/// Employee
2525
/// .window("dept_window") {
26-
/// $0.partition(by: $1.department).order(by: $1.salary, .desc)
26+
/// $0.partition(by: $1.department).order(by: $1.salary.desc())
2727
/// }
2828
/// .select {
2929
/// ($0.name, rank().over("dept_window"))

Sources/StructuredQueriesCore/Statements/Update/Update+Returning.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ extension Update {
1616
) -> Update<From, (repeat each QueryValue)> {
1717
var returning: [QueryFragment] = []
1818
for resultColumn in repeat each selection(From.columns) {
19-
returning.append("\(quote: resultColumn.name)")
19+
returning.append(resultColumn.queryFragment)
2020
}
2121
return Update<From, (repeat each QueryValue)>(
2222
isEmpty: false,
@@ -63,7 +63,7 @@ extension Update {
6363
) -> Update<From, From> {
6464
var returning: [QueryFragment] = []
6565
for resultColumn in From.TableColumns.allColumns {
66-
returning.append("\(quote: resultColumn.name)")
66+
returning.append(resultColumn.queryFragment)
6767
}
6868
return Update<From, From>(
6969
isEmpty: isEmpty,

Sources/StructuredQueriesCore/WindowSpec.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
/// ```swift
99
/// WindowSpec()
1010
/// .partition(by: columns.category)
11-
/// .order(by: columns.price, .desc)
11+
/// .order(by: columns.price.desc())
1212
/// ```
1313
public struct WindowSpec: Sendable {
1414
public var partitions: [QueryFragment] = []
@@ -43,17 +43,17 @@ public struct WindowSpec: Sendable {
4343
return copy
4444
}
4545

46-
/// Add an ordering expression with direction
46+
/// Add an ordering expression
47+
///
48+
/// Use `.asc()` or `.desc()` on the expression to specify direction:
49+
/// ```swift
50+
/// WindowSpec().order(by: columns.price.desc())
51+
/// ```
4752
public func order(
48-
by expression: some QueryExpression,
49-
_ direction: OrderDirection = .asc
53+
by expression: some QueryExpression
5054
) -> WindowSpec {
5155
var copy = self
52-
var fragment: QueryFragment = expression.queryFragment
53-
if direction == .desc {
54-
fragment.append(" DESC")
55-
}
56-
copy.orderings.append(fragment)
56+
copy.orderings.append(expression.queryFragment)
5757
return copy
5858
}
5959

Sources/StructuredQueriesPostgres/Functions/TextSearch/TextSearch+Ranking.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ extension TableDefinition where QueryValue: FullTextSearchable {
1111
/// ```swift
1212
/// Article.where { $0.match("swift") }
1313
/// .select { ($0, $0.rank(by: "swift")) }
14-
/// .order(by: \.1, .desc)
14+
/// .order(by: \.1.desc())
1515
/// // SELECT *, ts_rank("searchVector", to_tsquery('english', 'swift'))
1616
/// // FROM "articles"
1717
/// // WHERE "articles"."searchVector" @@ to_tsquery('english', 'swift')
@@ -56,7 +56,7 @@ extension TableDefinition where QueryValue: FullTextSearchable {
5656
/// ```swift
5757
/// Article.where { $0.match("quick <-> brown <-> fox") }
5858
/// .select { ($0, $0.rank(byCoverage: "quick <-> brown <-> fox")) }
59-
/// .order(by: \.1, .desc)
59+
/// .order(by: \.1.desc())
6060
/// ```
6161
///
6262
/// - Parameters:
@@ -89,7 +89,7 @@ extension TableDefinition where QueryValue: FullTextSearchable {
8989
/// // Weight A positions 10x more than D positions
9090
/// Article.where { $0.match("swift") }
9191
/// .select { ($0, $0.rank(by: "swift", weights: [0.1, 0.2, 0.4, 1.0])) }
92-
/// .order(by: \.1, .desc)
92+
/// .order(by: \.1.desc())
9393
/// // SELECT *, ts_rank('{0.1, 0.2, 0.4, 1.0}', "searchVector", to_tsquery('swift'))
9494
/// ```
9595
///
@@ -198,7 +198,7 @@ extension TableColumnExpression where Value == TextSearch.Vector {
198198
/// ```swift
199199
/// Article.where { $0.searchVector.match("swift") }
200200
/// .select { ($0, $0.searchVector.rank(by: "swift")) }
201-
/// .order(by: \.1, .desc)
201+
/// .order(by: \.1.desc())
202202
/// ```
203203
///
204204
/// - Parameters:

0 commit comments

Comments
 (0)