Files
ccn/Packages/Persistence/README.md
T
2026-08-19 23:19:08 +02:00

5.1 KiB

Persistence

The 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

Role Types
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
Migrations PrepareDB, the single registrar declaring every migration, in order
Readiness Probe, which reports whether the default database answers a SELECT 1 within a deadline
Scaffolding (internal) ExampleRecord, CreateExampleRecord, and ExampleRepository — the model → migration → repository pattern, to be replaced by the first real domain model

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. The Website service's ConfigReader+Properties has 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.
  • Migrations are declared once, and append-only. PrepareDB registers every migration in the order it must run; alter the schema by adding a migration, never by editing one that has already run. Registering does not apply them: the in-memory backend migrates on startup, while a shared PostgreSQL database is migrated out of band (the executable's migrate-and-exit mode), so booting instances never race.
  • Models never cross a concurrency boundary. FluentKit models are mutable reference types, so they stay internal to the package and repositories return Sendable value-type snapshots (e.g. Example) instead.
  • Readiness never throws, and never hangs. Probe runs a schema-independent SELECT 1, maps every failure to false, and races the query against a deadline (2 seconds by default) — a hanging database yields a prompt "not ready" instead of a stalled endpoint.
  • 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 instead of each pooled connection getting a private one.
  • Method structs. Service, PrepareDB, and Probe hold their lifetime-fixed configuration in init and take only per-call inputs in callAsFunction.

Note: the driver itself enforces both TLS postures — prefer upgrades only when the server advertises TLS and continues in plaintext otherwise, require refuses a server that offers none. Tests pin both against a fake plaintext-only server.

Layout

Sources are split by visibility, then by kind, one type per file:

Sources/
├── Public/
│   ├── Enumerations/      Driver, TLS
│   ├── Methods/           Service, PrepareDB, Probe
│   └── Types/             Configuration
└── Internal/
    ├── Migrations/        CreateExampleRecord
    ├── Models/            ExampleRecord
    └── Repositories/      ExampleRepository (returning the Example snapshot)
Tests/
├── Cases/                 the test suites, mirroring the Sources/ layout
└── Utils/                 the NotSQL* fakes backing the probe's non-SQL-database case, the
                           plaintext-only and silent fake PostgreSQL servers, and the suite Tag constants

Testing

The suite runs against the in-memory backend by default, so swift test needs no database. The PostgreSQL integration test is skipped unless POSTGRES_TEST_HOST points at one (with optional POSTGRES_TEST_PORT, POSTGRES_TEST_NAME, POSTGRES_TEST_USERNAME, and POSTGRES_TEST_PASSWORD); it reverts its migrations afterwards, leaving a shared database as it was found:

swift test                               # in-memory only
POSTGRES_TEST_HOST=127.0.0.1 swift test  # against the local PostgreSQL (make db-mount)

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 around fluent.shutdown() is the shape to follow.

Every suite carries a tag for the kind of API it exercises — .enumeration or .method, declared in Tests/Utils/Extensions/Tag+Constants.swift — so test plans and summaries can slice a run by kind. A new suite adopts the tag matching its subject, or adds one when none fits.

Requirements

  • Swift 6.3 toolchain (swift-tools-version:6.3).
  • macOS 15, matching the sibling packages (the services deploy to Linux containers; the packages carry no UI platforms).
  • Package dependencies: hummingbird-fluent, fluent-postgres-driver, fluent-sqlite-driver, sql-kit, postgres-nio, and swift-nio-ssl; the test target additionally depends on swift-nio for the TLS fallback tests' fake server.