Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
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
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