Migrated the documentations and test cases in the Persistence package to use/name PostgreSQL instead.

This commit is contained in:
2026-08-04 17:46:04 +02:00
parent 8ca31e415c
commit 3d27a27cb1
4 changed files with 15 additions and 15 deletions
+9 -9
View File
@@ -1,11 +1,11 @@
# Persistence # Persistence
The [Fluent](https://github.com/hummingbird-project/hummingbird-fluent)-based data layer the **Loud** services build on: runtime selection between a MySQL/MariaDB backend and an ephemeral in-memory SQLite one, single-place migration registration, and a database readiness probe. The [Fluent](https://github.com/hummingbird-project/hummingbird-fluent)-based data layer the **Loud** services build on: runtime selection between a PostgreSQL backend and an ephemeral in-memory SQLite one, single-place migration registration, and a database readiness probe.
## Overview ## Overview
The package provides, grouped by role: The package provides, grouped by role:
| Role | Types | | Role | Types |
| --- | --- | | --- | --- |
| Backend selection | `Driver` (`mysql` or `inMemory`), `Configuration` (the MySQL/MariaDB connection parameters), `TLS` (the connection's TLS posture) | | Backend selection | `Driver` (`postgres` or `inMemory`), `Configuration` (the PostgreSQL connection parameters), `TLS` (the connection's TLS posture) |
| Service | `Service`, which builds the `Fluent` service configured for the chosen driver | | Service | `Service`, which builds the `Fluent` service configured for the chosen driver |
| Migrations | `PrepareDB`, the single registrar declaring every migration, in order | | Migrations | `PrepareDB`, the single registrar declaring every migration, in order |
| Readiness | `Probe`, which reports whether the default database answers a `SELECT 1` | | Readiness | `Probe`, which reports whether the default database answers a `SELECT 1` |
@@ -14,13 +14,13 @@ The package provides, grouped by role:
## Design rules ## Design rules
- **The package reads no configuration.** The executable maps its `database.*` keys onto a `Driver` and hands it over; connection values arrive as plain data. See the Website service's `ConfigReader+Properties` for the mapping. - **The package reads no configuration.** The executable maps its `database.*` keys onto a `Driver` and hands it over; connection values arrive as plain data. See the Website service's `ConfigReader+Properties` for the mapping.
- **One default database.** `Service` registers the selected backend as the *default* database, so repositories resolve it with a plain `fluent.db()` and stay agnostic of which driver is in use. - **One default database.** `Service` registers the selected backend as the *default* database, so repositories resolve it with a plain `fluent.db()` and stay agnostic of which driver is in use.
- **Migrations are declared once, and append-only.** `PrepareDB` is the single place migrations are registered, in the order they must run; alter the schema by adding a new migration, never by editing one that has already run. Registering does not apply them — the in-memory backend is migrated on startup, while a shared MySQL/MariaDB database is migrated out of band (the executable's migrate-and-exit mode), so multiple booting instances never race. - **Migrations are declared once, and append-only.** `PrepareDB` is the single place migrations are registered, in the order they must run; alter the schema by adding a new migration, never by editing one that has already run. Registering does not apply them — the in-memory backend is migrated on startup, while a shared PostgreSQL database is migrated out of band (the executable's migrate-and-exit mode), so multiple booting instances never race.
- **Models never cross a concurrency boundary.** FluentKit models are mutable reference types; repositories map them to `Sendable` value-type snapshots (e.g. `Example`) before returning, and the models themselves stay internal to the package. - **Models never cross a concurrency boundary.** FluentKit models are mutable reference types; repositories map them to `Sendable` value-type snapshots (e.g. `Example`) before returning, and the models themselves stay internal to the package.
- **Readiness never throws.** `Probe` runs a `SELECT 1` — the cheapest statement both backends understand, independent of any schema — and maps every failure to `false`, so callers translate it straight into a readiness response. - **Readiness never throws.** `Probe` runs a `SELECT 1` — the cheapest statement both backends understand, independent of any schema — and maps every failure to `false`, so callers translate it straight into a readiness response.
- **A single connection for the in-memory store.** The SQLite backend is capped at one connection per event loop so every query reaches the same in-memory database, rather than each pooled connection getting its own private one. - **A single connection for the in-memory store.** The SQLite backend is capped at one connection per event loop so every query reaches the same in-memory database, rather than each pooled connection getting its own private one.
- **Method structs.** `Service`, `PrepareDB`, and `Probe` hold their lifetime-fixed configuration in `init` and take only per-call inputs in `callAsFunction`. - **Method structs.** `Service`, `PrepareDB`, and `Probe` hold their lifetime-fixed configuration in `init` and take only per-call inputs in `callAsFunction`.
> **Note:** the `prefer` TLS posture is enforced by the driver itself: a supplied TLS configuration upgrades the connection only when the server advertises TLS, and continues in plaintext otherwise (pinned by a test against a fake server that offers no TLS). `require` currently maps to the same configuration and therefore behaves like `prefer` — the refusal when the server offers no TLS is not yet enforced. > **Note:** both TLS postures are enforced by the driver itself: `prefer` upgrades the connection only when the server advertises TLS and continues in plaintext otherwise, while `require` refuses the connection when the server offers none. Both behaviors are pinned by tests against a fake server that offers no TLS.
## Layout ## Layout
Sources are split by visibility, then by kind, one type per file: Sources are split by visibility, then by kind, one type per file:
@@ -37,17 +37,17 @@ Sources/
Tests/ Tests/
├── Cases/ the test suites, mirroring the Sources/ layout ├── Cases/ the test suites, mirroring the Sources/ layout
└── Utils/ the NotSQL* fakes backing the probe's non-SQL-database case, the └── Utils/ the NotSQL* fakes backing the probe's non-SQL-database case, the
plaintext-only fake MySQL server, and the suite Tag constants plaintext-only fake PostgreSQL server, and the suite Tag constants
``` ```
## Testing ## Testing
The suite runs against the in-memory backend by default, so `swift test` needs no database. The MySQL/MariaDB integration test is skipped unless a database is pointed at via `MYSQL_TEST_HOST` (with optional `MYSQL_TEST_PORT`, `MYSQL_TEST_NAME`, `MYSQL_TEST_USERNAME`, and `MYSQL_TEST_PASSWORD`); it reverts its migrations afterwards, so the shared database is left as it was found: The suite runs against the in-memory backend by default, so `swift test` needs no database. The PostgreSQL integration test is skipped unless a database is pointed at via `POSTGRES_TEST_HOST` (with optional `POSTGRES_TEST_PORT`, `POSTGRES_TEST_NAME`, `POSTGRES_TEST_USERNAME`, and `POSTGRES_TEST_PASSWORD`); it reverts its migrations afterwards, so the shared database is left as it was found:
```sh ```sh
# in-memory only # in-memory only
swift test swift test
# or # or
# with the local MariaDB up (make db-mount): # with the local PostgreSQL up (make db-mount):
MYSQL_TEST_HOST=127.0.0.1 swift test POSTGRES_TEST_HOST=127.0.0.1 swift test
``` ```
Outside the application's service group, a built `Fluent` service must be shut down explicitly — even on failure — or its connection pool asserts on `deinit`; the suites' `do`/`catch` pattern around `fluent.shutdown()` is the shape to follow. Outside the application's service group, a built `Fluent` service must be shut down explicitly — even on failure — or its connection pool asserts on `deinit`; the suites' `do`/`catch` pattern around `fluent.shutdown()` is the shape to follow.
@@ -57,4 +57,4 @@ Every suite carries a tag naming the kind of API it exercises — `.enumeration`
## Requirements ## Requirements
- Swift 6.3 toolchain (`swift-tools-version:6.3`). - Swift 6.3 toolchain (`swift-tools-version:6.3`).
- macOS 15, matching the sibling `Infrastructure` and `Localization` packages (the services deploy to Linux containers; the packages carry no UI platforms). - macOS 15, matching the sibling `Infrastructure` and `Localization` packages (the services deploy to Linux containers; the packages carry no UI platforms).
- Package dependencies: `hummingbird-fluent`, `fluent-mysql-driver`, `fluent-sqlite-driver`, and `sql-kit`; the test target additionally depends on `mysql-nio` and `swift-nio` for the TLS fallback test. - Package dependencies: `hummingbird-fluent`, `fluent-postgres-driver`, `fluent-sqlite-driver`, and `sql-kit`; the test target additionally depends on `postgres-nio`, `swift-nio`, and `swift-nio-ssl` for the TLS fallback tests.
@@ -26,7 +26,7 @@ public struct Probe: Sendable {
/// Reports whether the database behind the `Fluent` service is reachable. /// Reports whether the database behind the `Fluent` service is reachable.
/// ///
/// Runs a trivial `SELECT 1` against the default database the cheapest statement both the MySQL/MariaDB and SQLite backends understand so /// Runs a trivial `SELECT 1` against the default database the cheapest statement both the PostgreSQL and SQLite backends understand so
/// a readiness check does not depend on any particular schema or model. Any failure (connection refused, authentication error, pool exhausted) is /// a readiness check does not depend on any particular schema or model. Any failure (connection refused, authentication error, pool exhausted) is
/// reported as not reachable rather than thrown, so callers can map it straight onto a readiness response. A default database that is not an SQL /// reported as not reachable rather than thrown, so callers can map it straight onto a readiness response. A default database that is not an SQL
/// database is likewise reported as not reachable. /// database is likewise reported as not reachable.
@@ -1,4 +1,4 @@
/// The connection parameters for the MySQL/MariaDB backend. /// The connection parameters for the PostgreSQL backend.
/// ///
/// The executable builds this from its `database.*` configuration; the package itself reads no configuration, so these values arrive as plain data. /// The executable builds this from its `database.*` configuration; the package itself reads no configuration, so these values arrive as plain data.
public struct Configuration: Sendable { public struct Configuration: Sendable {
@@ -28,7 +28,7 @@ public struct Configuration: Sendable {
// MARK: Initializers // MARK: Initializers
/// Creates a set of MySQL/MariaDB connection parameters. /// Creates a set of PostgreSQL connection parameters.
/// - Parameters: /// - Parameters:
/// - host: the host the database server is reached at. /// - host: the host the database server is reached at.
/// - port: the port the database server listens on. /// - port: the port the database server listens on.
@@ -19,7 +19,7 @@ struct ProbeTests {
driver: .inMemory, driver: .inMemory,
logger: Logger(label: "test") logger: Logger(label: "test")
) )
let fluent = service() let fluent = try service()
let probe = Probe(fluent: fluent) let probe = Probe(fluent: fluent)
let isReachable = await probe() let isReachable = await probe()
@@ -34,7 +34,7 @@ struct ProbeTests {
// Port 1 on the loopback interface has nothing listening, so the connection is refused // Port 1 on the loopback interface has nothing listening, so the connection is refused
// immediately instead of timing out. // immediately instead of timing out.
let service = Service( let service = Service(
driver: .mysql( driver: .postgres(
.init( .init(
host: "127.0.0.1", host: "127.0.0.1",
port: 1, port: 1,
@@ -47,7 +47,7 @@ struct ProbeTests {
), ),
logger: Logger(label: "test") logger: Logger(label: "test")
) )
let fluent = service() let fluent = try service()
let probe = Probe(fluent: fluent) let probe = Probe(fluent: fluent)
let isReachable = await probe() let isReachable = await probe()