Skip to content

Database providers

A database provider is what creates the copy of production each environment gets. It is the main extension point, and it is meant to be written by people outside this repository.

database:
provider: docker # or neon
version: 17
ProviderWhere the data livesBranch timeNeeds
dockerA container on the machine running afGrows with the databaseA Docker daemon
neonA Neon projectFlat, because branches share storageA Neon project and an API key

docker is the default and needs nothing. It is the right choice for a repository whose database is small enough that copying it is not the slow part.

neon is the right choice when it is. Neon branches are copy on write, so creating one takes about as long for a hundred gigabytes as for a hundred rows.

A provider named in the manifest and not built into this binary is refused at startup rather than substituted. Falling back to docker would hand somebody an empty preview with no reason for it.

These are not documentation. They are a conformance suite that any implementation runs, so that “conformant” is something a test decides rather than something a maintainer judges.

  • A refresh masks, then verifies, and publishes nothing if verification fails.
  • An unverified golden cannot be branched. This is the product’s central promise and it is enforced in the provider, not in a checklist.
  • Branching twice for one environment returns one branch. The engine retries after timeouts, and a retry that creates a second resource is how an orphan is made.
  • Destroying something already destroyed succeeds, because teardown retries.
  • A connection string is a secret: it renders as [redacted] everywhere text is produced.
  • Every resource the provider holds can be enumerated, so the leak detector has something to compare the journal against.
  • A capability a provider does not have is skipped by name in the suite output, never silently.

A provider may offer a pooled endpoint. Where it does, services receive the pooled connection string and migrations receive the direct one, because a transaction pooler does not support the session level features migrations use. Where it does not, both receive the same string.

Nothing has to be configured for this. The engine asks based on what the provider declares.

Implement provider.Database and run the suite:

func TestMyProvider(t *testing.T) {
conformance.RunDatabase(t, factory, conformance.Options{})
}

Declare only the capabilities you actually have. Declaring one you do not makes the suite run a behaviour it should have skipped, which fails, which is the intended outcome: a capability is a promise the suite checks.