com.worxbend.persistence

Members list

Type members

Classlikes

object Codecs

JDBC codecs for the two Postgres types the domain refuses to know about.

JDBC codecs for the two Postgres types the domain refuses to know about.

The point of both is negative: no org.postgresql.util.PGobject and no java.sql.Array ever escapes this module. A repository returns io.circe.Json, which is the same type modules/kernel already speaks, so the jsonb-ness of the storage stops at the edge of persistence and swapping Magnum out (ADR §12.4 budgets for it) does not ripple into the domain.

Attributes

Supertypes
class Object
trait Matchable
class Any
Self type
Codecs.type

A source of a DataSource.

A source of a DataSource.

Structurally identical to jakarta.inject.Provider[DataSource] — one method, get(), same erasure — so ferrite binds it in a Guice module with a one-line adapter and nothing about this module leaks into the injection container. It is declared here rather than being jakarta.inject.Provider because jakarta.inject-api is not on this module's classpath, and adding a DI annotation library to an infrastructure library to express "a thing you can ask for a DataSource" would invert the dependency this trait exists to keep pointing inward: cobalt has no Guice at all and must be able to use the same pool.

Attributes

Supertypes
class Object
trait Matchable
class Any
Known subtypes
class HikariPool
object Database

Attributes

Companion
class
Supertypes
class Object
trait Matchable
class Any
Self type
Database.type
final class Database extends AutoCloseable

The two pools of ADR §5, opened and closed together.

The two pools of ADR §5, opened and closed together.

They are a pair rather than two independent objects because their failure modes are shared: if the write pool cannot be opened, a service holding only a working read pool is not degraded, it is wrong. Closing is likewise all-or-nothing, and close closes the second pool even when the first throws — a half-closed pair leaks exactly the sessions the explicit shutdown path exists to reclaim.

Attributes

Companion
object
Supertypes
trait AutoCloseable
class Object
trait Matchable
class Any

Attributes

Companion
class
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type
final case class DatabaseConfig(jdbcUrl: String, username: String, password: String, read: PoolConfig, write: PoolConfig)

Everything needed to reach PostgreSQL, plus the two pools of ADR §5.

Everything needed to reach PostgreSQL, plus the two pools of ADR §5.

The credentials live here rather than being baked into the JDBC URL so that a deployment can supply them from a secret store without string-splicing a URL, and so that a password never appears in the pool name, the metric tags or a connection-error message that quotes the URL.

Attributes

Companion
object
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
final class HikariPool extends DataSourceProvider, AutoCloseable

One bounded HikariCP pool, with an explicit shutdown.

One bounded HikariCP pool, with an explicit shutdown.

AutoCloseable and not a finaliser or a shutdown hook: a pool holds server-side sessions, and leaking them is how a restart loop exhausts max_connections and takes down the other services too. The owner — a Guice @Singleton's stop hook in ferrite, CoordinatedShutdown in cobalt — is responsible for calling close, and making that explicit is the point.

Attributes

Companion
object
Supertypes
trait AutoCloseable
class Object
trait Matchable
class Any
object HikariPool

Attributes

Companion
class
Supertypes
class Object
trait Matchable
class Any
Self type
HikariPool.type
final case class MigrationReport(initialVersion: Option[String], targetVersion: Option[String], executed: Int, applied: Vector[String])

What a migration run did.

What a migration run did.

Returned rather than logged so the caller can assert on it: ADR §9.3 makes "the applied-version list matches a committed baseline" a CI gate, which is the mechanism that catches a migration edited in place — the single most common way a schema silently diverges between a developer's database and production.

targetVersion is the version the schema is at when the run finishes, which is not what Flyway's MigrateResult.targetSchemaVersion holds: Flyway leaves that field null whenever nothing was pending, because to Flyway it means "the version this run migrated towards". Reported verbatim it makes the field say None on every boot after the first — so a replica logs "schema at " against a fully migrated database, and any caller that reads it as "is the schema applied" is right exactly once and wrong forever after. The version after the run is what every caller in this repo actually asks for, so that is what this field means and it is never None for a database that has any migration applied.

Attributes

Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
object Migrations

Programmatic Flyway, callable from a service's startup and from a test.

Programmatic Flyway, callable from a service's startup and from a test.

ferrite owns the migrations (ADR §1) — cobalt and wolfram never run them, so there is exactly one writer of the schema and no ordering question at deploy time. The same entry point is reused by the db-migrate one-shot in compose, which runs the ferrite image in migrate mode rather than pulling the 380 MB flyway/flyway image for what is one JDBC call.

Two configuration choices are load-bearing:

  • validateOnMigrate stays on. It is what turns "someone edited V1__events.sql after it shipped" into a failed startup instead of a database whose schema does not match the file that claims to describe it.
  • cleanDisabled is on. Flyway.clean() drops every object in the schema; there is no operational reason this codebase ever needs it, and leaving it available means a mis-typed command can destroy the event store.

The history table deliberately lands in the connection's default schema (public) rather than in events: V1__events.sql is what creates events, and a history table living inside the schema its own migrations create makes the first run a chicken-and-egg problem that Flyway papers over with createSchemas.

Attributes

Supertypes
class Object
trait Matchable
class Any
Self type
Migrations.type
object PoolConfig

Attributes

Companion
class
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type
PoolConfig.type
final case class PoolConfig(poolName: String, maximumPoolSize: Int, minimumIdle: Int, connectionTimeout: FiniteDuration, idleTimeout: FiniteDuration, maxLifetime: FiniteDuration, validationTimeout: FiniteDuration, readOnly: Boolean, connectionInitSql: Option[String])

One connection pool's worth of settings.

One connection pool's worth of settings.

ADR §5 mandates two pools per service — a read-only search pool and an ingest write pool — so that a runaway search cannot starve persistence and a burst of ingest cannot starve the UI. Two pools rather than one large one is a load-shedding decision: a bounded pool is a queue with a visible depth and a timeout, and the whole point of ADR §0's rejection of a virtual-thread executor is that unbounded concurrency moves the queue somewhere you cannot see it or shed from it.

Value parameters

connectionInitSql

run once per physical connection. This is where the search pool sets statement_timeout, which is the only mechanism that reliably kills a runaway query — a client-side queryTimeout leaves the backend running.

maximumPoolSize

also the size of the matching CustomExecutionContext. They must be equal: more threads than connections means threads blocked in getConnection (a timeout you did not choose), fewer means idle connections.

readOnly

sets the session default. Cheap defence in depth: a repository accidentally wired to the search pool fails on the write rather than performing it.

Attributes

Companion
object
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
object Sql

The fragment algebra every query in this module is built from — and the one place SQL text is produced.

The fragment algebra every query in this module is built from — and the one place SQL text is produced.

The security property is structural, not a convention. lit is an inline method that rejects, at compile time, any argument that is not a literal constant: it asks constValueOpt for the argument's value and calls compiletime.error when there is none. A String computed at runtime — which is what user input is — has no constant type, so it cannot become SQL text. The set of characters that can ever reach a statement is therefore fixed when this module compiles, and every value the user supplied travels through bind into a JDBC placeholder.

That is stronger than ADR §6.2's "lit is private to FilterSql.scala", which is a file-scope rule a reviewer has to notice being broken, and it is why the fragment builders live here rather than being duplicated per query: there is exactly one function in the module whose argument becomes SQL, and the compiler guards it.

A String & Singleton bound alone was tried first and is not sufficient: a stable val holding a request parameter has a singleton type and would have been accepted. The bound is still present — it is what stops the literal type being widened away before constValueOpt sees it — but the constant check is what enforces the rule. The only remaining escape hatch is asInstanceOf, which the ADR already bans by review.

++ is associative in the parameter positions as well as in the text, which is what makes arbitrary AND/OR/NOT nesting work. Magnum's FragWriter.write(ps, pos): Int returns the next free position, so composing two writers is function composition on that position — no fragment needs to know its own offset, and a deeply nested filter binds its parameters in exactly the order its text mentions them.

Attributes

Supertypes
class Object
trait Matchable
class Any
Self type
Sql.type