Steps

The fixture step

A fixture step writes reference data no API creates, such as country codes, through a connection declared writes: true, and commits it. It is the one step in a plan that writes to a database on purpose, and every output says so: vero plan prints a writes: line for each fixture step, vero run prints a fixture line in the summary for each one that ran, passed or not, and the event log carries a fixture event with the statement and whether it committed. Seeding an order this way tests the database insert, not the application's order-creation API; a row the application should write belongs in an http step. Plan format has the keys every step shares and the connections block; this page has what a fixture step adds.

Key Type Meaning
connection string a key of connections declared with writes: true
statement string a literal: one INSERT, UPDATE, DELETE, MERGE or REPLACE
args list of strings templated, bound as parameters $1, $2, ..., never interpolated
timeout duration alternative to the step's timeout; not both

A plan

fixture.yaml migrates a SQLite file beside the plan with an exec step, seeds it with a fixture step, and reads it back with a sql step. It needs no fixture server. The same file is declared twice: once read-only for the sql step, once with writes: true for the fixture step, because a sql step may not read through a writing connection.

connections:
  catalog: { driver: sqlite, dsn: catalog.db }
  catalog-admin: { driver: sqlite, dsn: catalog.db, writes: true }
tasks:
  - name: seed-countries
    needs: [migrate]
    steps:
      - fixture:
          connection: catalog-admin
          statement: >
            insert into countries (code, name) values ($1, $2), ($3, $4)
            on conflict (code) do nothing
          args: [PT, Portugal, ES, Spain]
        assert: [rowsAffected <= 2]
        results: { seeded: rowsAffected }

vero plan names the write before anything runs, and a run labels it in the summary:

$ bin/vero plan examples/fixture.yaml
writes: tasks.seed-countries.steps[0] through connection catalog-admin (fixture step, committed)
ok: 3 tasks, 0 finally tasks; the plan sends 0 HTTP requests

$ bin/vero run examples/fixture.yaml
run 01M3ZRZSEA60SAE56SWJT8E0W4  fixture.yaml  --jobs 4  lifecycle isolated

fixture  seed-countries › step 1  passed through connection catalog-admin: wrote 2 row(s), committed
3 passed  0 failed  0 errored  0 timed out  0 skipped  0 blocked  0 not run   (3 tasks, 0 requests, 0.0s)

Write the statement so that a second run is harmless. This one says on conflict (code) do nothing, so the second run commits and writes no rows:

fixture  seed-countries › step 1  passed through connection catalog-admin: wrote 0 row(s), committed
3 passed  0 failed  0 errored  0 timed out  0 skipped  0 blocked  0 not run   (3 tasks, 0 requests, 0.0s)

The assertion rowsAffected <= 2 holds on both runs. One that said rowsAffected == 2 would hold on the first run only.

What the statement may do

The statement is inspected at load. Its first word is INSERT, UPDATE, DELETE, MERGE or REPLACE, and it is one statement. Keywords that change schema, permissions, the transaction or the session are refused anywhere outside a string or a comment: CREATE, DROP, ALTER, TRUNCATE, GRANT, REVOKE, COPY, ATTACH, DETACH, PRAGMA, BEGIN, COMMIT, ROLLBACK, SAVEPOINT, RELEASE, CALL, VACUUM, LOCK, SECURITY. The list is shorter than the sql step's, so on conflict (code) do nothing and a column named comment load. Schema belongs in a migration run by an exec step, which is what the example's migrate task does.

The statement is a literal. Values go in args, which are templated and bound as parameters $1, $2, and so on, never interpolated into the text. A {{ in the statement is a load error.

The statement runs in its own transaction, which commits on success and rolls back on failure. On Postgres that transaction is READ COMMITTED, the isolation reads get; on SQLite the file is opened as the DSN says, with a 5 s wait on another writer's lock unless the DSN sets its own busy_timeout. The connection's role is not checked the way a sql connection's is: writing is what it is for. A fixture step may not repeat. Its deadline is the step's timeout, or fixture.timeout, 30 s when neither is set; setting both is a load error.

A fixture step may also be a lifecycle.reset step. It then needs a timeout, runs before any task, and is held to exitCode == 0 when it has no assertions of its own. The writes: line names it lifecycle.reset[0], and the summary label reads fixture lifecycle.reset › step 1 passed through connection catalog-admin: wrote 1 row(s), committed.

Names in scope

Name Meaning
rowsAffected the exact number of rows the statement changed, as the database reports it
duration.total the statement, commit included
env, tasks, run as in every step

Assertions are evaluated only when the transaction committed. A statement that failed is rolled back and the step is errored, so rowsAffected == 2 is never checked against a write that did not happen. A result such as seeded: rowsAffected publishes the count to later tasks.

What a failure shows

A statement the database refuses is rolled back, and the step ends errored. The failure block prints the statement, every bound argument, and that nothing was written; the summary still carries the fixture line, now saying rolled back, nothing written, so nobody reading the report misses that the plan tried to write:

ERROR  seed-countries › step 1                                    fixture.yaml

  rolled back: constraint failed: UNIQUE constraint failed: countries.code (1555)

  fixture  insert into countries (code, name) values ($1, $2), ($3, $4)
    $1 = "PT"
    $2 = "Portugal"
    $3 = "PT"
    $4 = "again"
    nothing written: rolled back


BLOCKED  countries-are-there  blocked by seed-countries, which errored

fixture  seed-countries › step 1  errored through connection catalog-admin: rolled back, nothing written
1 passed  0 failed  1 errored  0 timed out  0 skipped  1 blocked  0 not run   (3 tasks, 0 requests, 0.0s)

A statement the deadline cut off is timed_out, with the reason the statement's deadline fired, and it was rolled back: .... When the write committed and an assertion did not hold, the write stays committed, the step is failed, and the block says so under the arguments. This is the example's second run with rowsAffected == 5 in place of rowsAffected <= 2:

FAIL  seed-countries › step 1 › assert #1                         fixture.yaml:30

  fixture  insert into countries (code, name) values ($1, $2), ($3, $4) on conflict (code) do nothing
    $1 = "PT"
    $2 = "Portugal"
    $3 = "ES"
    $4 = "Spain"
    0 row(s) affected, 0.7ms, committed

  assert  rowsAffected == 5    failed
          rowsAffected = 0
          fixture.yaml:30

BLOCKED  countries-are-there  blocked by seed-countries, which failed

fixture  seed-countries › step 1  failed through connection catalog-admin: wrote 0 row(s), committed
1 passed  1 failed  0 errored  0 timed out  0 skipped  1 blocked  0 not run   (3 tasks, 0 requests, 0.0s)

The event log carries the same write as a fixture event: connection, statement, arguments, rowsAffected and committed (vero.events/v1).

Load errors

Plan Load error
no connection a fixture step needs connection, the name of an entry in connections with writes: true
a connection the plan does not declare connection catalog-admn is not declared in connections (did you mean catalog-admin?)
a connection without writes: true connection catalog is read-only; a fixture step writes through a connection declared with writes: true (ADR 0008)
no statement a fixture step needs a statement
a {{ in the statement a fixture statement is a literal; put values in args, which are bound as parameters, never interpolated
create table regions (code text) a fixture step writes rows: INSERT, UPDATE, DELETE, MERGE or REPLACE, not CREATE. Reads belong in a sql step, schema in a migration run by an exec step (ADR 0008)
a refused keyword inside the statement the statement contains DROP; a fixture step writes rows only and never changes schema, permissions or transactions (ADR 0008)
two statements a fixture step runs one statement; this one has 2. Put each write in its own fixture step
a writing connection no fixture step uses connection catalog-admin is declared but no fixture step uses it
timeout on the step and inside fixture set the timeout once, on the step or inside fixture, not both

The parenthetical in three of the messages names the decision record that fixed this rule. The rule is the one this page describes.

This page is docs/content/steps/fixture.md in the repository.

verodocs