pg-workload emulates PostgreSQL backend activity that produces diverse query plans, logs, and
runtime statistics for diagnostic observation. It installs, validates, runs, and schedules
declarative activity profiles in the PostgreSQL test flow:
pg-stand -> pg-workload -> pg-diag report
| ^
+-- rerun -----+
pg-stand creates a reproducible database stand, pg-workload changes its behavior, and
pg-diag captures the result. A user can edit a workload profile, repeat the workload, and build
another report without recreating the whole experiment.
The distribution name and command are pg-workload; the import package is pg_workload. The
wheel contains immutable profile templates and a public pg_workload/v1 JSON Schema. Runtime
state, logs, credentials, and generated table contents are never packaged.
mkdir pg-workload-project
cd pg-workload-project
python3 -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install pg-workload
pg-workload --version
pg-workload init
pg-workload profiles
pg-workload validatepg-workload init copies editable profiles and the schema into the current project, creates an
empty desired-state file, and creates per-profile log directories. It is idempotent when bundled
assets are unchanged. It refuses to overwrite local profile edits; use init --force only when
the packaged version should replace those edits.
Initialize another directory with:
pg-workload init --directory /srv/pg-workload/labThe CLI can also be invoked as python -m pg_workload.
git clone https://github.com/O2eg/pg_workload.git
cd pg_workload
python3 -m venv .venv
.venv/bin/pip install -e '.[dev]'
.venv/bin/pg-workload init --directory local-workload
.venv/bin/pg-workload validate --root local-workloadPython 3.10 and newer are supported. psql and pgbench from the selected PostgreSQL major
version must be installed on the runner host.
Set credentials outside the command line. pg-workload has no default password:
export WORKLOAD_ADMIN_PASSWORD='admin-secret'
export WORKLOAD_PASSWORD='new-workload-role-secret'Create the workload database and role, install a profile, and run it once:
pg-workload prepare-db \
--target external \
--host 127.0.0.1 \
--database workload_db
pg-workload install \
--target external \
--host 127.0.0.1 \
--database workload_db \
--profile simple_stock \
--scale 1
pg-workload run \
--target external \
--host 127.0.0.1 \
--database workload_db \
--profile simple_stockFor an existing role, prefer a libpq passfile:
chmod 600 ./workload.pgpass
pg-workload run \
--target external \
--host db.example.net \
--database workload_db \
--passfile ./workload.pgpass \
--profile simple_stockThe passfile has standard libpq fields:
hostname:port:database:username:password
WORKLOAD_ADMIN_PASSWORD authenticates administrative operations. WORKLOAD_PASSWORD is needed
when prepare-db creates a role and may also authenticate workload jobs. PGPASSFILE can be used
instead of --passfile; PGPASSWORD is accepted only as an admin-password compatibility input.
- There is no built-in or generated public password.
- Passwords are not accepted as CLI flags, so they do not appear in process listings or shell history.
- Passwords are never placed in
PGOPTIONS, psql variables, log messages, or scheduler child arguments. prepare-dbdoes not change the password of an existing role.- Password rotation requires both
--rotate-workload-passwordandWORKLOAD_PASSWORD. - Promotion of an existing workload role to superuser requires
--workload-superuser. - Existing roles can use passfile, peer, certificate, or another libpq-supported authentication method.
prepare-db --recreate drops only the selected workload database after terminating its sessions;
it does not drop the workload role.
Three connection modes use the same workload contract:
local: defaults to/var/run/postgresqland is suitable for a command running on the DB host.external: defaults to127.0.0.1; pass--hostfor a remote server or proxy.patroni: resolves a member through one or more--patroni-urlREST endpoints and--patroni-role.
Examples:
pg-workload run --target local --profile simple_stock
pg-workload run \
--target patroni \
--patroni-url http://patroni-1:8008 \
--patroni-url http://patroni-2:8008 \
--patroni-role master \
--profile simple_stockUse --bin-dir /path/to/postgresql/bin when client binaries are outside
/usr/lib/postgresql/<major>/bin. The default major version is 18.
| Profile | Purpose | Data preparation |
|---|---|---|
simple_stock |
Write-heavy CRUD over related stock tables | synthetic Python generator |
simple_stock_spec_symbols |
Identifier, Unicode, and parser edge cases | synthetic Python generator |
imdb |
Movie-domain analytical joins and skew | synthetic Python generator |
pagila |
Mixed Pagila OLTP | synthetic Python generator |
many_objects |
Metadata-heavy schemas and partitions | SQL object creation only |
emulate_errors |
Intentional SQL errors | SQL seed rows only |
pss_overflow |
pg_stat_statements churn |
SQL object creation; extension/preload required |
No profile requires a dump, CSV file, or network download. The Pagila schema is redistributed
under its upstream license; see THIRD_PARTY_NOTICES.md. Its rows are generated locally. The
bundled imdb profile is an original, compact movie-domain model and does not contain Join Order
Benchmark SQL or IMDB source data.
Profiles live at data/<profile>/profile.yml. Version 1 is identified by the stable string
pg_workload/v1:
api_version: pg_workload/v1
name: my_workload
schema: my_workload
description: Small example workload.
requires_write: false
prepare:
steps:
- type: sql
path: sql/schema.sql
- type: generator
path: generator.py
- type: sql
path: sql/indexes.sql
jobs:
- name: reads
type: pgbench
interval: 60
transactions: 10
clients: 2
threads: 2
scripts:
- path: sql/01_read.sql
weight: 5
log: reads.log
- name: analyze
type: psql
interval: 1800
command: "ANALYZE my_workload.items;"
log: analyze.logThe public schema is copied to schema/pg_workload-v1.schema.json. Runtime validation also checks
conditions that JSON Schema alone cannot safely enforce:
- unknown fields are errors at every object level;
api_version,name, and a non-emptyjobslist are required;- job names are unique and types are
pgbenchorpsql; - a pgbench job defines exactly one of
durationortransactions; - threads do not exceed clients;
- SQL files, generators, script globs, and logs stay inside their profile boundary even when symlinks are present;
- generator steps reference an existing Python file;
- referenced local SQL and pgbench scripts exist before any database operation begins.
validate checks the complete contract and all local paths without connecting to PostgreSQL.
install executes prepare.steps in declared order. A normal data profile therefore creates its
schema, runs its generator, and only then creates indexes and statistics.
pg-workload validate
pg-workload validate --profile simple_stock--workload remains a temporary compatibility alias for --profile; --dbname is an alias for
--database.
pg-workload prepare-db --target external --host 127.0.0.1
pg-workload install \
--target external --host 127.0.0.1 \
--profile simple_stock \
--profile pss_overflow \
--scale 1
pg-workload run \
--target external --host 127.0.0.1 \
--profile simple_stock \
--job maininstall --prepare-db combines database preparation and profile installation. Use
--recreate-db with it only for a disposable stand.
CLI pgbench overrides are mutually exclusive:
pg-workload run --profile simple_stock --pgbench-duration 30
pg-workload run --profile simple_stock --pgbench-transactions 100--scale is a positive coefficient passed to every Python generator during install. At
--scale 1, the profiles approximate these domain sizes:
| Profile | Base table sizes at scale 1 |
|---|---|
simple_stock |
150,000 product groups; 1,000,000 SKUs; 1,000,000 stock rows |
simple_stock_spec_symbols |
the same cardinalities with hostile and Unicode identifiers/values |
imdb |
10,000 companies; 100,000 people; 100,000 titles; 1,300,000 fact rows |
pagila |
600 customers; 1,000 films; 4,500 inventory; 16,000 rentals; 16,500 payments |
For example, --scale 2 approximately doubles scalable tables. Very small values retain a
profile-specific minimum so foreign-key structure and query selectivity remain meaningful in CI.
Generators use deterministic seeds, preserve foreign-key relationships, and introduce skewed
random distributions for hot entities, popularity, money, ratings, inventory, and customers.
Profiles that model object count or failures (many_objects, pss_overflow, emulate_errors)
have no Python generator and ignore --scale during preparation.
Installing a generated profile recreates its schema before filling it, so changing --scale and
installing again produces a clean dataset rather than appending rows. Scheduler recovery reuses
the same scale that was passed to the scheduler.
A profile generator is a regular Python program invoked as python generator.py --scale VALUE.
It receives the selected connection through standard PGHOST, PGPORT, PGDATABASE, PGUSER,
PGAPPNAME, and libpq credential environment variables; PG_WORKLOAD_PSQL points to the selected
psql binary. Bundled generators use only the Python standard library and stream server-side SQL
to psql, so generated rows never pass through repository files. Because a locally edited
generator can execute arbitrary code with the runner account, only run profiles from a trusted
project directory.
The scheduler runs in the foreground and owns only its child workload processes. It does not install cron jobs or systemd units.
pg-workload enable simple_stock --job main --interval 60
pg-workload enable pss_overflow --job statement_churn --interval 120
pg-workload state
pg-workload scheduler \
--target external \
--host 127.0.0.1 \
--run-immediatelyChanges made by enable, disable, and set-interval are picked up without restarting the
scheduler:
pg-workload disable simple_stock --job main
pg-workload set-interval pss_overflow statement_churn 300Only one scheduler may hold state/scheduler.lock. Scheduler subprocesses use
python -m pg_workload, so installed operation does not depend on a repository checkout.
Before installation and execution, and while scheduled jobs run, the resource guard checks:
- used space on writable mounted filesystems;
- available memory in percent and MiB;
- average CPU utilization.
Thresholds are configurable with --resource-* options. --no-resource-monitor is intended for
controlled testing only.
Job output is written under data/<profile>/log/. Logs rotate at job start and by size. Configure
retention with --log-max-mb, --log-backups, and --no-log-rotate-on-start. Known secrets are
redacted from command and output logs.
After init, the working directory is:
project/
data/<profile>/
profile.yml
generator.py # only when the profile needs table data
sql/
log/ # runtime, ignored
schema/
pg_workload-v1.schema.json
state/
workloads.yml # runtime desired state
scheduler.lock # runtime lock
The repository .gitignore excludes initialized root-level data/, schema/, state/, logs,
locks, dumps, archives, and CSV datasets. Canonical immutable profile sources live only in
src/pg_workload/bundled/ and are included in the wheel; generated table rows live only in the
target PostgreSQL database.
The normal profile and scheduler CLI remains human-oriented. Authors of
pg_play-compatible orchestrators can use the separate
versioned machine contract.
python -m pip install -e '.[dev]'
ruff check .
ruff format --check .
pytest
python -m build
twine check dist/*Docker PostgreSQL 18 integration is opt-in locally:
WORKLOAD_DOCKER_INTEGRATION=1 pytest tests/test_docker_postgres18.pyRelease automation verifies Python 3.10 and 3.12, Ruff, unit tests, the PostgreSQL 18 Docker
integration test, tag/version equality, sdist/wheel metadata, and a clean wheel smoke test covering
--version, init, profiles, and validate. Publishing uses PyPI Trusted Publishing; no PyPI
API token is stored in the repository.