Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ DerivedData
Package.resolved
.swiftpm
Tests/LinuxMain.swift
.derivedData
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was an accident as I was unable to use DocC to break a cache. I will remove it.

35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,41 @@ try await client.query("""

While this looks at first glance like a classic case of [SQL injection](https://en.wikipedia.org/wiki/SQL_injection) 😱, PostgresNIO's API ensures that this usage is safe. The first parameter of the [`query(_:logger:)`] method is not a plain `String`, but a [`PostgresQuery`], which implements Swift's `ExpressibleByStringInterpolation` protocol. PostgresNIO uses the literal parts of the provided string as the SQL query and replaces each interpolated value with a parameter binding. Only values which implement the [`PostgresEncodable`] protocol may be interpolated in this way. As with [`PostgresDecodable`], PostgresNIO provides default implementations for most common types.

#### Manual query construction with PostgresBindings

For more complex scenarios where you need to build queries dynamically, you can use `PostgresBindings` together with `PostgresQuery`:

```swift
func buildSearchQuery(filters: [String: Any]) -> PostgresQuery {
var bindings = PostgresBindings()
var sql = "SELECT * FROM products WHERE 1=1"

if let name = filters["name"] as? String {
bindings.append(name)
sql += " AND name = $\(bindings.count)"
}

if let minPrice = filters["minPrice"] as? Double {
bindings.append(minPrice)
sql += " AND price >= $\(bindings.count)"
}

if let category = filters["category"] as? String {
bindings.append(category)
sql += " AND category = $\(bindings.count)"
}

return PostgresQuery(unsafeSQL: sql, binds: bindings)
}

// Usage
let filters = ["name": "Widget", "minPrice": 9.99]
let query = buildSearchQuery(filters: filters)
let rows = try await client.query(query, logger: logger)
```

This approach is particularly useful when you need to conditionally add filters or build complex queries programmatically.

Some queries do not receive any rows from the server (most often `INSERT`, `UPDATE`, and `DELETE` queries with no `RETURNING` clause, not to mention most DDL queries). To support this, the [`query(_:logger:)`] method is marked `@discardableResult`, so that the compiler does not issue a warning if the return value is not used.

## Security
Expand Down
4 changes: 2 additions & 2 deletions Sources/PostgresNIO/Data/PostgresRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ extension PostgresRandomAccessRow {
/// - type: The type to decode the data into
/// - Throws: The error of the decoding implementation. See also `PSQLDecodable` protocol for this.
/// - Returns: The decoded value of Type T.
func decode<T: PostgresDecodable, JSONDecoder: PostgresJSONDecoder>(
public func decode<T: PostgresDecodable, JSONDecoder: PostgresJSONDecoder>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

column: String,
as type: T.Type,
context: PostgresDecodingContext<JSONDecoder>,
Expand All @@ -233,7 +233,7 @@ extension PostgresRandomAccessRow {
/// - type: The type to decode the data into
/// - Throws: The error of the decoding implementation. See also `PSQLDecodable` protocol for this.
/// - Returns: The decoded value of Type T.
func decode<T: PostgresDecodable, JSONDecoder: PostgresJSONDecoder>(
public func decode<T: PostgresDecodable, JSONDecoder: PostgresJSONDecoder>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

column index: Int,
as type: T.Type,
context: PostgresDecodingContext<JSONDecoder>,
Expand Down
196 changes: 193 additions & 3 deletions Sources/PostgresNIO/Docs.docc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,198 @@ applications.
task cancellation. The query interface makes use of backpressure to ensure that memory can not grow
unbounded for queries that return thousands of rows.

``PostgresNIO`` runs efficiently on Linux and Apple platforms. On Apple platforms developers can
configure ``PostgresConnection`` to use `Network.framework` as the underlying transport framework.

``PostgresNIO`` runs efficiently on Linux and Apple platforms. On Apple platforms developers can
configure ``PostgresConnection`` to use `Network.framework` as the underlying transport framework.

## Quick Start

### 1. Create and Run a PostgresClient

First, create a ``PostgresClient/Configuration`` and initialize your client:

```swift
import PostgresNIO

// Configure the client with individual parameters
let config = PostgresClient.Configuration(
host: "localhost",
port: 5432,
username: "my_username",
password: "my_password",
database: "my_database",
tls: .disable
)

// Or parse from a PostgreSQL URL string
let urlString = "postgresql://username:password@localhost:5432/my_database"
let url = URL(string: urlString)!
let config = PostgresClient.Configuration(
host: url.host!,
port: url.port ?? 5432,
username: url.user!,
password: url.password,
database: url.path.trimmingPrefix("/"),
tls: .disable
)

// Create the client
let client = PostgresClient(configuration: config)

// Run the client (required)
await withTaskGroup(of: Void.self) { taskGroup in
taskGroup.addTask {
await client.run()
}

// Your application code using the client goes here

// Shutdown when done
taskGroup.cancelAll()
}
```

### 2. Running Queries with PostgresQuery

Use string interpolation to safely execute queries with parameters:

```swift
// Simple SELECT query
let minAge = 21
let rows = try await client.query(
"SELECT * FROM users WHERE age > \(minAge)",
logger: logger
)

for try await row in rows {
let randomAccessRow = row.makeRandomAccess()
let id: Int = try randomAccessRow.decode(column: "id", as: Int.self, context: .default)
let name: String = try randomAccessRow.decode(column: "name", as: String.self, context: .default)
print("User: \(name) (ID: \(id))")
}

// INSERT query
let name = "Alice"
let email = "alice@example.com"
try await client.execute(
"INSERT INTO users (name, email) VALUES (\(name), \(email))",
logger: logger
)
```

### 3. Building Dynamic Queries with PostgresBindings

For complex or dynamic queries, manually construct bindings:

```swift
func buildSearchQuery(filters: [String: Any]) -> PostgresQuery {
var bindings = PostgresBindings()
var sql = "SELECT * FROM products WHERE 1=1"

if let name = filters["name"] as? String {
bindings.append(name)
sql += " AND name = $\(bindings.count)"
}

if let minPrice = filters["minPrice"] as? Double {
bindings.append(minPrice)
sql += " AND price >= $\(bindings.count)"
}

return PostgresQuery(unsafeSQL: sql, binds: bindings)
}

// Execute the dynamic query
let filters = ["name": "Widget", "minPrice": 9.99]
let query = buildSearchQuery(filters: filters)
let rows = try await client.query(query, logger: logger)
```

### 4. Using Transactions with withTransaction

Execute multiple queries atomically:

```swift
try await client.withTransaction { connection in
// All queries execute within a transaction

// Debit from account
try await connection.execute(
"UPDATE accounts SET balance = balance - \(amount) WHERE id = \(fromAccount)",
logger: logger
)

// Credit to account
try await connection.execute(
"UPDATE accounts SET balance = balance + \(amount) WHERE id = \(toAccount)",
logger: logger
)

// If any query fails, the entire transaction rolls back
// If this closure completes successfully, the transaction commits
}
```

### 5. Using withConnection for Multiple Queries

Execute multiple queries on the same connection for better performance:

```swift
try await client.withConnection { connection in
let userRows = try await connection.query(
"SELECT * FROM users WHERE id = \(userID)",
logger: logger
)

let orderRows = try await connection.query(
"SELECT * FROM orders WHERE user_id = \(userID)",
logger: logger
)

// Process results...
}
```

For more details, see <doc:running-queries>.

### 6. Using Custom Types with PostgresCodable

Many Swift types already work out of the box. For custom types, implement ``PostgresEncodable`` and ``PostgresDecodable``:

```swift
// Store complex data as JSONB
struct UserProfile: Codable {
let displayName: String
let bio: String
let interests: [String]
}

// Use directly in queries (encodes as JSONB automatically via Codable)
let profile = UserProfile(
displayName: "Alice",
bio: "Swift developer",
interests: ["coding", "hiking"]
)

try await client.execute(
"UPDATE users SET profile = \(profile) WHERE id = \(userID)",
logger: logger
)

// Decode from results
let rows = try await client.query(
"SELECT profile FROM users WHERE id = \(userID)",
logger: logger
)

for try await row in rows {
let randomAccessRow = row.makeRandomAccess()
let profile = try randomAccessRow.decode(column: "profile", as: UserProfile.self, context: .default)
print("Display name: \(profile.displayName)")
}
```

For advanced usage including custom PostgreSQL types, binary encoding, and RawRepresentable enums, see <doc:postgres-codable>.

## Topics

### Essentials
Expand All @@ -40,6 +229,7 @@ configure ``PostgresConnection`` to use `Network.framework` as the underlying tr

### Advanced

- <doc:postgres-codable>
- <doc:coding>
- <doc:prepared-statement>
- <doc:listen>
Expand Down
Loading
Loading