Table of Contents

Offline Schema Inspection & Dry-Run Validation

Read a database's persisted schema, or simulate a code upgrade against it, without opening it for real.

Status: โœ… Implemented ยท Visibility: Public ยท Level: ๐ŸŸฃ Advanced ยท Category: Schema

๐ŸŽฏ What it solves

Two questions come up before you ever touch a production database file: "what schema does this database actually have?" and "will my new build's component structs and migrations succeed against it, or fail?". Answering either by opening the database with your application normally means accepting whatever side effects registration and auto-migration trigger โ€” including a hard failure that leaves you debugging a broken deployment. DatabaseSchema.Inspect and DatabaseSchema.ValidateEvolution answer both questions from outside the application, without registering any components for real and without writing a single byte to the file.

โš™๏ธ How it works (in brief)

Both calls spin up a throwaway, isolated engine instance against the target file path, read what they need from the persisted system tables, and dispose it โ€” no shared state with any live DatabaseEngine your app may have open elsewhere. Inspect reads persisted component/field/index metadata and per-component entity counts directly, with no component registration at all. ValidateEvolution takes a configuration callback (the same shape as your real startup registration code) but captures the registrations into a recorder instead of applying them: it computes a SchemaDiff per component against the persisted layout, checks whether breaking changes have a registered migration chain, and reports the outcome โ€” all without allocating storage or migrating any entity.

๐Ÿ’ป Usage

// What's actually in this file?
var report = DatabaseSchema.Inspect("prod/game.typhon");

Console.WriteLine($"{report.DatabaseName} (format R{report.DatabaseFormatRevision})");
foreach (var component in report.Components)
{
    Console.WriteLine($"  {component.Name} R{component.Revision} โ€” {component.EntityCount} entities");
    foreach (var field in component.Fields)
        Console.WriteLine($"    {field.Name} ({field.Type}) FieldId={field.FieldId} Offset={field.Offset}");
}

// Will my next release's schema open cleanly against it?
var result = DatabaseSchema.ValidateEvolution("prod/game.typhon", registrar =>
{
    registrar.RegisterComponent<PlayerV2>();
    registrar.RegisterMigration<PlayerV1, PlayerV2>(MigratePlayer);
});

if (!result.IsValid)
{
    foreach (var error in result.Errors)
        Console.WriteLine($"BLOCKED: {error}");
    // fail the deploy / CI step here
}

โš ๏ธ Guarantees & limits

  • Neither call mutates the database file โ€” Inspect never registers a component, ValidateEvolution never allocates segments, migrates entities, or writes a schema history entry.
  • Safe to run against a database another process has open; each call uses its own short-lived engine and service provider, fully disposed before returning.
  • ValidateEvolution only evaluates the components you register in the callback โ€” it does not flag persisted components your configuration omits.
  • EvolutionValidationResult.IsValid is false only when a component has breaking changes with no registered migration path; non-breaking (Compatible/CompatibleWidening) changes are reported as NeedsMigration but don't fail validation, matching what auto-migration would actually do on real registration.
  • A dry run is a prediction, not a guarantee โ€” it doesn't account for runtime conditions at real-registration time (disk space exhaustion mid-migration, a concurrently-open writer, etc.).

๐Ÿงช Tests

  • OperationalToolingTests โ€” Inspect_ReturnsComponentsAndFields (offline read of persisted components/fields), ValidateEvolution_CompatibleChange_IsValid (dry-run reports NeedsMigration/HasMigrationPath without mutating)