Per-decorator migration kit / prereq surface #12
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Each decorator in
oatpp-authkit/repo/carries an implicit prerequisite that the consumer's schema (or runtime environment) has to satisfy. Today these prereqs live only in header doc-comments, which means a consumer who stacks the wrong decorator on the wrong table only finds out at runtime via UNIQUE-violation crashes, missing-table errors, or silently wrong audit data.This issue tracks adding a small, uniform "decorator prereq" surface — both human-readable and machine-checkable — so consumers can wire the stack up safely.
Concrete shortcomings today
oatpp::Stringfields registered viaOATPP_AUTHKIT_REGISTER_TEMPORAL, AND a multi-version-tolerant unique index — i.e.UNIQUE(entity_id) WHERE valid_until = SENTINELrather thanUNIQUE(entity_id). Discovered while wiring the fewo-webapp Person pilot (uwe.admin/fewo-webapp#457): the schema's whole-column unique index makes every save attempt fail because TemporalRepository's "close prior + insert new" emits two rows per entity_id.IAuditSinkimplementation, but says nothing about the storage shape that sink writes to.Scope
Add a
repo/Prereq.hppheader with two parts:Static doc constant per decorator. Each decorator declares a
static constexpr const char* PREREQ_SQL = ...;(or empty string for runtime-only decorators) listing the SQL it expects the consumer's schema to satisfy. Consumers can#includeand emit it in their schema.sql via concatenation, or paste it directly.Runtime probe. Each decorator gains an optional
void verifyPrereq(DbClient&)method that runs at construction time (or via a separateensurePrereq()call) to check the schema actually matches — e.g. introspect SQLite'ssqlite_masterfor the expected partial unique index. Throws a clear, decorator-specific exception if not.Each decorator's exception message names itself, lists the failed expectation, and suggests the SQL to fix it. No more "UNIQUE constraint failed" with no hint of why.
Constraints
Out of scope
Acceptance
Prereq.hppdefines a smallIDecoratorPrereqinterface (or just a free-function convention) and adecoratorPrereqOk(...)helper that runs all prereqs in a stack at startup.TemporalRepository<T>exposesstatic constexpr const char* PREREQ_SQLwith the partial-unique-index template (with a{table}placeholder).AuditLogRepository<T>documents the expectedIAuditSinkstorage shape (text, schema-free — sinks vary too much to enforce SQL).ScopeGuardRepository<T>adds a static_assert (or runtime null-check) that the predicate is non-null.Decision deferred to implementation time
Whether the prereq probe is mandatory at construction time or opt-in via a separate call. Recommend opt-in — keeps construction cheap, lets consumers choose where the schema check happens.
Agent Evaluation
Feasibility: Medium. Two distinct surfaces (static doc constants + optional runtime probes) but each is small per decorator. The runtime probe is the hardest piece — it requires SQLite-specific introspection of
sqlite_master, which couples a header-only authkit to a specific database engine. Mitigation: keep the probe optional and behind a#ifdef OATPP_AUTHKIT_SQLITEguard, or accept astd::function<bool(const std::string& expectedSql)>that the consumer wires up to its own DbClient.Impact: High for adopters. Discovered while wiring fewo-webapp#457 — without a prereq surface, every consumer rediscovers the same gotchas (UNIQUE-violation crashes, silently wrong audit data, missing tables) at runtime. Static
PREREQ_SQLsnippets are a 5-LOC win for documentation that pays out on every adoption. Runtime probes are a stronger guarantee but cost more.Effort: Medium. ~50 LOC per decorator (a constant + a virtual
verifyPrereqmember + tests), plus a small driver inPrereq.hpp(~30 LOC). Total ~250 LOC including tests.Recommendation: Accept, but scope down to the static
PREREQ_SQLconstants only for v1. Defer runtime probes (verifyPrereq) to a follow-up if they prove necessary — they add real complexity (DB-engine coupling, when to run them, what to do on failure) and the static constants alone solve the documentation problem that motivated this issue.Implementation plan (v1, static-only)
include/oatpp-authkit/repo/Prereq.hpp— small structDecoratorPrereqwithname,requiresSql(string, may contain{table}placeholder),requiresRuntime(free-form text).static constexpr DecoratorPrereq PREREQ;— TemporalRepository emits the partial unique index template, others stay empty / runtime-only.instantiatePrereqSql(const DecoratorPrereq&, std::string_view tableName)— replaces{table}and returns ready-to-paste SQL.PREREQ_SQL.Decision needed
Check one (edit this comment):
PREREQ_SQLonly). Defer runtime probes to a follow-up.Evaluated #12 (Medium) — recommend Accept v1 (static PREREQ_SQL only); defer runtime probes.
Re-evaluation
Owner selected Option A — Accept v1 (static
PREREQ_SQLonly); defer runtime probes.That matches the recommendation, so the implementation plan from the original evaluation stands unchanged:
include/oatpp-authkit/repo/Prereq.hpp—DecoratorPrereqstruct withname,requiresSql(with{table}placeholder),requiresRuntimedoc stringstatic constexpr DecoratorPrereq PREREQ;— TemporalRepository carries the partial unique index template; ScopeGuard / Audit stay empty or runtime-onlyinstantiatePrereqSql(const DecoratorPrereq&, std::string_view tableName)substitutes{table}and returns ready-to-paste SQLPREREQ_SQLNo runtime
verifyPrereqmember, no SQLite introspection, no DB-engine coupling. TherequiresRuntimefield onDecoratorPrereqbecomes a free-form string the README can render — purely documentation.Awaiting
acceptedlabel to proceed.Re-evaluated #12 — owner picked Option A (static PREREQ_SQL only); plan unchanged from initial evaluation; awaiting accept.
Re-evaluation — scope extension
Owner asked to extend scope so each decorator owns its idempotent DB migration alongside its prereq surface, keeping aspects (temporal, audit, scope) independent but bundled with their schema. v1 stays small but adds a thin migration runner so consumers don't hand-paste SQL.
Extended scope
Two migration kinds, both decorator-local, both idempotent, both run at startup:
1. Additive prereqs —
PREREQ_SQL(already in v1)For decorators that need new tables/columns/indexes alongside the entity table. Pure
CREATE TABLE IF NOT EXISTS …,CREATE INDEX IF NOT EXISTS …. Re-running is a no-op. AuditLogRepository's audit_log table is the model case. Each decorator exposesstatic constexpr DecoratorPrereq PREREQ;with a{table}placeholder.2. Reshape migrations —
RESHAPE_STEPS(new)For decorators that need to change the entity's own table shape. TemporalRepository is the only current case: composite UNIQUE on
(entity_id, valid_until), valid_from/valid_until columns, FK reshape on every child. Not safely idempotent in the naive sense — you can'tADD COLUMN valid_untiltwice. Solution: each step is a{detect, apply}pair wheredetectis a SQL probe (PRAGMA table_info(...),sqlite_masterquery) that returns true iff the step has already been applied. Runner skips applied steps; re-running is a no-op.3. Migration runner —
applyDecoratorMigrations<Stack>(DbClient&, tableName)Walks the wired decorator stack at startup, executes every
PREREQ_SQL(idempotent by construction) and everyRESHAPE_STEPSentry whosedetectSqlreturns false. Records applied step names in aoatpp_authkit_schema_migrations(decorator, table, step, applied_at)table for observability — but the detect-probe is the source of truth, so even if that table is wiped the runner is still safe.Consumer wiring becomes:
Why this fits the original aim
IF NOT EXISTS; RESHAPE_STEPS via per-step detect probes.Effort
Moves from Medium to Large:
Total ~550–650 LOC.
Interaction with fewo-webapp#459
#459 becomes "wire
applyDecoratorMigrationsinto App.cpp at startup, then remove the now-redundant SQL fromsql/schema.sql" instead of "write a bespoke schema migration script". Cleaner — and the same machinery serves every future temporal table.Revised acceptance
Prereq.hppdefinesDecoratorPrereq(withPREREQ_SQL) andReshapeStepapplyDecoratorMigrations<Stack>(DbClient&, tableName)walks the stack, runs PREREQ_SQL, runs RESHAPE_STEPS gated on detect probes, records applied steps inoatpp_authkit_schema_migrationsDecision needed
Check one (edit this comment):
Re-evaluated #12 — extended scope to bundle idempotent migrations (PREREQ_SQL + ReshapeStep) per decorator; effort revised Medium → Large; awaiting decision.
Re-evaluation
Owner selected Accept extended scope as written — bundles idempotent migrations with each decorator; closes #459 by reuse.
The plan stands as drafted in the previous re-evaluation:
Prereq.hppdefinesDecoratorPrereq(withPREREQ_SQL+{table}placeholder) andReshapeStep({name, detectSql, applySql})applyDecoratorMigrations<Stack>(DbClient&, tableName)walks the stack at startup, runsPREREQ_SQL(idempotent viaIF NOT EXISTS), runsRESHAPE_STEPSgated ondetectSqlprobes, records applied steps inoatpp_authkit_schema_migrations(decorator, table, step, applied_at)for observabilityEffort: Large (~550–650 LOC including tests). #459 reduces to "wire
applyDecoratorMigrationsinto fewo-webapp App.cpp at startup, drop the redundant temporal-schema SQL fromsql/schema.sql" once this lands.Awaiting
acceptedlabel to proceed.Re-evaluated #12 — owner accepted extended scope (PREREQ_SQL + ReshapeStep + applyDecoratorMigrations runner); plan unchanged from prior re-eval; awaiting accept.
Implemented in commit
b5e1ea1—include/oatpp-authkit/repo/Prereq.hppdefinesDecoratorPrereq+ReshapeStep+applyDecoratorMigrations<...>(table, probe, exec, recorder). TemporalRepository ships add_valid_from/add_valid_until/drop_unique_entity_id (consumer-overridable noop)/composite_unique reshape steps. AuditLogRepository ships the audit_log CREATE TABLE as PREREQ. ScopeGuardRepository ships empty PREREQ + zero-length RESHAPE_STEPS. New test test_decorator_migrations.cpp covers fresh apply, idempotent re-apply (no-op), partial-state recovery, variadic stack ordering, and ScopeGuard's clean no-op — all 10 ctest cases pass. Version bumped 0.6.1 → 0.7.0. README expanded with a 'Decorator migrations' section. fewo-webapp#459 reduces to wiringapplyDecoratorMigrationsinto App.cpp + dropping the redundant SQL from sql/schema.sql.Implemented #12 — per-decorator migration kit (Prereq.hpp + applyDecoratorMigrations runner) shipped in commit
b5e1ea1, tagged v0.7.0; all 10 tests pass.