Schema & Constraint Violation Exceptions
Typed failures for "the engine refuses to proceed": incompatible schema, failed migration, version downgrade, duplicate key.
Status: โ Implemented ยท Visibility: Public ยท Level: ๐ฃ Advanced ยท Category: Errors
๐ฏ What it solves
Some failures aren't transient contention โ they mean the data model itself is in a state the engine cannot safely reconcile: a struct changed in a way that can't be auto-migrated, a user-supplied migration function failed partway through, the database was written by a newer binary than the one opening it, or an insert would create a duplicate key in a column declared unique. Silently proceeding in any of these cases risks misreading bytes or corrupting an index. Typhon refuses instead, and reports exactly which case it hit through a dedicated, typed exception rather than a generic error.
โ๏ธ How it works (in brief)
Four exception types sit directly under TyphonException, each non-transient (IsTransient == false โ retrying without changing the schema or the input never helps): SchemaValidationException (breaking schema mismatch on reopen, no migration registered โ carries the full SchemaDiff), SchemaMigrationException (a registered migration function threw for one or more entities โ carries per-entity MigrationFailure records; old segments are left untouched), SchemaDowngradeException (persisted schema revision is newer than the runtime struct's โ opening with an older binary is refused outright), and UniqueConstraintViolationException (an insert/update would duplicate a key already present in a unique index). SchemaValidationException and SchemaDowngradeException share the SchemaValidation error code โ both are "this component cannot be opened" from an operator's point of view; the exception type tells you which.
๐ป Usage
try
{
dbe.RegisterComponentFromAccessor<Player>();
}
catch (SchemaDowngradeException ex)
{
log.LogCritical("Refusing to open '{Component}': persisted rev {Persisted} > runtime rev {Runtime}",
ex.ComponentName, ex.PersistedRevision, ex.RuntimeRevision);
throw;
}
catch (SchemaValidationException ex)
{
foreach (var change in ex.Diff.FieldChanges)
{
log.LogError("{Field}: {Kind} ({Old} -> {New})", change.FieldName, change.Kind, change.OldType, change.NewType);
}
throw;
}
catch (SchemaMigrationException ex)
{
log.LogError("Migration for '{Component}' failed on {Count} entities", ex.ComponentName, ex.FailedEntityCount);
foreach (var failure in ex.Failures)
{
log.LogError(" ChunkId={ChunkId}: {Exception}", failure.ChunkId, failure.Exception);
}
throw; // old segments untouched โ fix the migration function and restart
}
Duplicate keys surface at commit time, not registration:
try
{
using var tx = dbe.CreateQuickTransaction();
tx.Spawn<PlayerArch>(PlayerArch.PlayerId.Set(existingId)); // PlayerId has [Index] (unique)
tx.Commit();
}
catch (UniqueConstraintViolationException)
{
// Not transient โ same key will conflict again until the caller picks a different one.
}
โ ๏ธ Guarantees & limits
- Never partially applied. A
SchemaValidationExceptionorSchemaDowngradeExceptionblocks registration before any data is touched; aSchemaMigrationExceptionleaves old segments fully intact โ fix the cause and re-run, nothing is lost either way. IsTransient == falseon all four โ retrying without changing the struct, the migration function, the binary version, or the duplicate key never succeeds.SchemaValidationException.Diffis the fullSchemaDiff(everyFieldChange/IndexChange, with aCompatibilityLevelper change) โ enough to log or render without re-deriving it.SchemaMigrationException.Failurescaps detailed formatting in the exception message at 10 entries but theIReadOnlyList<MigrationFailure>itself carries every failure (ChunkId, hex dump of the old bytes, the thrown exception).SchemaDowngradeExceptionis unconditional โ there is noSchemaValidationModethat allows opening data written by a newer revision.UniqueConstraintViolationExceptioncarries no typed context (no key value, no index name) โ catch it for control flow, log the surrounding operation for diagnostics.- All four share the catch-by-type model used across the hierarchy โ
catch (TyphonException)still catches them, but there's no common intermediate type to catch only "schema/constraint" failures as a group; catch each concretely or useTyphonException.
๐งช Tests
- MigrationFunctionTests โ a throwing user migration function surfaces as
SchemaMigrationException(per-entityMigrationFailure); a breaking change with no registered migration surfaces asSchemaValidationException. - SchemaValidationIntegrationTests โ breaking field/index changes on reopen throw
SchemaValidationException(Diff); a persisted revision newer than the runtime struct throwsSchemaDowngradeException.
๐ Related
- Related feature: Schema Validation on Reopen, User-Defined Migration Functions โ the mechanisms that throw
SchemaValidationException/SchemaMigrationException - Source:
src/Typhon.Engine/Errors/public/SchemaValidationException.cs,SchemaMigrationException.cs,SchemaDowngradeException.cs,UniqueConstraintViolationException.cs - Related catalog entries: TyphonException Hierarchy & Catalog