Files
ccn/Packages/Persistence/README.md
T

5.3 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

The package provides, grouped by role:

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
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. 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.
  • 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.
  • 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.
  • Method structs. Service, PrepareDB, and Probe hold their lifetime-fixed configuration in init and take only per-call inputs in callAsFunction.

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

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 fake PostgreSQL server, 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 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:

# in-memory only
swift test                                
# or
# with the local PostgreSQL up (make db-mount):
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.

Every suite carries a tag naming the kind of API it exercises — .enumeration or .method, declared in Tests/Utils/Extensions/Tag+Constants.swift — so test plans and result summaries can slice the run by kind. A new suite must adopt the tag matching its subject (or add a tag there if none fits).

Requirements

  • 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).
  • 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.