Skip to content

protean upgrade-check

Reports the changes that need attention when upgrading a domain to a newer Protean, with concrete remediation. The checks accumulate across releases rather than targeting one, so the table below spans 0.16 and 0.17; each row says which release the change came from. It is read-only: schema changes are generated as SQL for you to review and run, never applied automatically.

protean upgrade-check --domain=my_app
protean upgrade-check --domain=my_app --format=json

Options

Option Default Description
--domain / -d . Path to the domain module (e.g. my_app.domain)
--format / -f rich Output format: rich or json
--opportunities off Report shipped capability the domain still hand-rolls, instead of the readiness checks (see Opportunities)
--pinned-version installed Protean The version the domain is pinned to, for --opportunities

The domain is initialized so the schema check can introspect the configured databases. --opportunities skips that init: those detectors only read your source, so the mode runs with the databases and brokers down. Exit code is 0 when only advisory (info) findings are present and 2 when any warnings need review.

Checks

Code Level Surface What it reports
POOL_DEFAULTS_CHANGED warning Config A SQL database with pool_size unset. 0.16 raised the SQLAlchemy pool defaults to pool_size=5, max_overflow=10.
HEALTH_PORT_BIND info Config protean server runs a health-check server on port 8080; 0.17 changed its default bind host to loopback (127.0.0.1), so probes are no longer reachable off-host unless you set host = "0.0.0.0".
ELASTICSEARCH_SERVER_V8 warning Infra An Elasticsearch provider; installs now default to the v8 client, which requires an Elasticsearch 8.x server.
OUTBOX_NEEDS_ALTER warning Schema A live outbox table with unbounded string columns; emits the exact backend ALTER to apply the new VARCHAR(N) bounds.
NESTED_UNIT_OF_WORK warning Source A UnitOfWork opened inside another. After ADR-0027 it joins the outer transaction with no savepoints, so a nested rollback dooms the whole thing.
UNIT_OF_WORK_NESTING_REVIEW info Source No lexical nesting found, but nesting through a call cannot be seen statically. Reports how many blocks are worth walking.
IO_INSIDE_UNIT_OF_WORK warning Source An HTTP call, broker publish or email send inside a UnitOfWork, which now holds database locks for the length of the call.
OUTBOX_TARGET_BROKER_NULLABLE warning Schema A live outbox table whose target_broker still allows NULL. Emits the backfill plus the SET NOT NULL for your dialect. Shipped in 0.16.2; reaches main in 0.17.
OUTBOX_UNIQUE_INDEX_LEGACY warning Schema A live outbox table still carrying the message_id-only unique index. Emits the swap to the composite (message_id, target_broker) index that the dual-write idempotency guard depends on.
CHECK_FAILED warning — A check could not complete (e.g. the database was unreachable); the report may be incomplete for that area.

Generated SQL

For OUTBOX_NEEDS_ALTER, the command introspects the live table and emits the ALTER tailored to the connected database, for example on PostgreSQL:

ALTER TABLE outbox
  ALTER COLUMN message_id TYPE varchar(255),
  ALTER COLUMN status TYPE varchar(32),
  ...

Review the output, confirm no existing value exceeds the new bounds, then run it. Protean never applies the migration for you: schema changes are an adapter/operator concern (see ADR-0004).

See the v0.16 migration guide for the full upgrade notes.

Opportunities: what you hand-roll that the framework now ships

The checks above report what needs attention when you move up a release. --opportunities reports the other direction: capability the framework has already shipped that your domain still appears to hand-roll. A codebase written against an older Protean keeps its hand-rolled queue table, its custom middleware, its raw SQL, long after the framework caught up, because nobody goes back to re-check. This mode names those spots.

protean upgrade-check --opportunities --domain=my_app
protean upgrade-check --opportunities --domain=my_app --pinned-version=0.15
protean upgrade-check --opportunities --domain=my_app --format=json

Every detector is deterministic: it reads your source and matches on structure, its imports, call shapes, and the literal choices in a field, so it gives the same findings every run. The raw-SQL detector is import-gated: text is matched only when it comes from sqlalchemy. The middleware detector matches a BaseHTTPMiddleware subclass or a dispatch method, and an add_middleware call that names something outside the framework's own middlewares and the standard Starlette and FastAPI ones. The queue-status detector reads a status or state field and needs at least two queue-vocabulary choices before it fires. Detector findings are advisory (info) and do not change the exit code. The one exception is CHECK_FAILED: if a detector cannot complete, that is a warning, and any warning exits 2 under the shared exit-code rule.

The pinned version

Each detector declares the release its capability arrived in. A finding is reported only when that release is at or below the pinned version, i.e. you already have the capability installed but still hand-roll it. The pinned version defaults to the installed Protean. --pinned-version overrides it, so you can ask "what has arrived since the release this code was written against". Pinning below a capability's release suppresses that capability's finding, because you do not own it yet.

--pinned-version takes MAJOR.MINOR with an optional patch and an optional release tag: 0.16, 0.16.3, 0.16.0rc1, 0.16.0.dev1. Anything else exits 1, so a typo does not silently read as some other version.

Detectors

Code Level Capability (release) What it finds
OPPORTUNITY_QUERY_API info Query API (0.16.0) Raw sqlalchemy.text(...) SQL sites. The query API (Q(field__isnull=), F(), QuerySet.count(), .only(), .all(with_total=False), dispatched through @domain.query_handler and domain.dispatch()) covers most of what raw SQL is reached for. The text name is import-gated to sqlalchemy, so an unrelated text( call is not flagged.
OPPORTUNITY_DOMAIN_CONTEXT_MIDDLEWARE info DomainContextMiddleware (0.15.0) A custom ASGI middleware: a BaseHTTPMiddleware subclass, a class with async def dispatch(self, request, call_next), or an app.add_middleware(...) of a class the framework does not ship. DomainContextMiddleware wires domain context plus correlation-id propagation, and registering it (or a standard Starlette/FastAPI middleware like CORSMiddleware or GZipMiddleware) is not flagged.
OPPORTUNITY_OUTBOX info Outbox (0.14.0) A status/state field whose choices cycle through queue states (pending / processing / done / failed). That is usually a hand-rolled work queue the outbox (retry, backoff, DLQ) now covers. The match needs a queue-like choice set, so a plain status with business choices does not fire.
SANITIZE_DEFAULT_CHANGED info Sanitize default flip (0.18.0) A String/Text field that leaves sanitize unset, either by not passing it or by passing None, and therefore relied on the pre-0.18 default of sanitize=True. Every site is listed by module:line so the finding works as a migration checklist. Not flagged: a field that declares sanitize by keyword or positionally (Text(True)), a field with a real choices= value (never sanitized), a container content spec (List(String(...)), never sanitized), a String/Text that is not Protean's, and a field whose arguments hide behind a splat (String(**opts)). The whole finding is suppressed when the domain sets [field_defaults] sanitize = true, which opts every unset field back into the old behaviour. See the migration guide.
CHECK_FAILED warning n/a A detector could not complete; the report may be incomplete for that area.

Where the line is

This mode ships only deterministic detectors, so the report gives the same verdict every run. Most of them report a capability the domain hand-rolls that the framework now ships. SANITIZE_DEFAULT_CHANGED is the exception: it reports a behaviour change, the 0.18.0 sanitize-default flip, whose effect is silent at runtime. It works the same way, a deterministic source scan gated on the release. Judgment-heavy advice ("this orchestration is really a process manager") stays out of OSS; that is the commercial Domain Assessment surface, on the non-deterministic side of the open-core boundary.

Exit code 2 is easier to hit in 0.17

The mapping is unchanged (any warning finding exits 2), but NESTED_UNIT_OF_WORK and IO_INSIDE_UNIT_OF_WORK are both warnings, and both read your source rather than your config. A domain that exited 0 on 0.16 can exit 2 on 0.17 with nothing changed. See the migration guide for what the two findings mean.