Private/JournalSchema.ps1

# On-disk schema (data format) version for journals. This is deliberately
# separate from the module (semver) version: it is only bumped when the
# on-disk format changes in a way that requires migrating existing journals.
$script:CurrentSchemaVersion = 2

# Maps the schema version a journal is *currently* at to the migration that
# brings it one step forward. 'Description' is shown in -WhatIf/progress output;
# 'Action' is the name of the internal migration function (see Migrations.ps1).
# A regular hashtable is used (not [ordered]) so integer indexing does a key
# lookup rather than positional access; Update-LedgerJournal applies steps in
# ascending version order regardless of storage order.
$script:LedgerSchemaMigrations = @{
    1 = @{
        Description = 'Store opening balance as ib.txt metadata'
        Action      = 'Invoke-LedgerOpeningBalanceMigration'
    }
}

# Tracks paths a schema warning has already been shown for, so read commands
# warn once per session instead of on every call.
$script:SchemaWarnedPaths = @{}

function Get-LedgerJournalSchemaVersion {
    <#
    .SYNOPSIS
    Returns the on-disk schema version recorded in a journal's journal.txt.

    .DESCRIPTION
    Reads the 'SchemaVersion' field from journal.txt. Journals created before
    schema versioning was introduced have no such field and are treated as
    version 1. Returns $null if the path is not a journal (journal.txt missing).
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$Path
    )

    $journalFile = Join-Path $Path 'journal.txt'
    if (-not (Test-Path $journalFile)) {
        return $null
    }

    foreach ($line in Get-Content $journalFile) {
        if ($line -match '^SchemaVersion:\s*(\d+)\s*$') {
            return [int]$Matches[1]
        }
    }

    # No SchemaVersion field => legacy journal predating schema versioning.
    return 1
}

function Set-LedgerJournalSchemaVersion {
    <#
    .SYNOPSIS
    Writes (or updates) the SchemaVersion field in a journal's journal.txt.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$Path,

        [Parameter(Mandatory)]
        [int]$Version
    )

    $journalFile = Join-Path $Path 'journal.txt'
    if (-not (Test-Path $journalFile)) {
        throw "Invalid journal - journal.txt not found in: $Path"
    }

    $lines = @(Get-Content $journalFile)
    $updated = $false
    for ($i = 0; $i -lt $lines.Count; $i++) {
        if ($lines[$i] -match '^SchemaVersion:\s*\d+\s*$') {
            $lines[$i] = "SchemaVersion: $Version"
            $updated = $true
            break
        }
    }

    if (-not $updated) {
        $lines += "SchemaVersion: $Version"
    }

    Set-LedgerFileContent -Path $journalFile -Value $lines
}

function Get-LedgerSchemaMigrationDescriptions {
    <#
    .SYNOPSIS
    Returns the descriptions of the migration steps needed to bring a journal
    from one schema version up to another.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)][int]$From,
        [Parameter(Mandatory)][int]$To
    )

    $descriptions = @()
    for ($v = $From; $v -lt $To; $v++) {
        if ($script:LedgerSchemaMigrations.Contains($v)) {
            $descriptions += $script:LedgerSchemaMigrations[$v].Description
        }
    }
    return $descriptions
}

function Assert-LedgerJournalSchema {
    <#
    .SYNOPSIS
    Checks a journal's on-disk schema version against the version this module
    supports.

    .DESCRIPTION
    If the journal's schema is older than this module supports, the operation
    cannot safely proceed without migrating the journal first. Writing commands
    (-Write) throw so no data is written to an out-of-date journal; reading
    commands emit a one-time warning per journal so reports still work but the
    user is told to migrate. A journal newer than this module is also refused
    for writes and warned for reads, with a prompt to upgrade the module.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$Path,

        [switch]$Write
    )

    $version = Get-LedgerJournalSchemaVersion -Path $Path
    if ($null -eq $version) {
        # Not a journal (yet) - nothing to check.
        return
    }

    $current = $script:CurrentSchemaVersion
    if ($version -eq $current) {
        return
    }

    if ($version -lt $current) {
        $message = "Journal '$Path' uses schema version $version but this PSLedger supports version $current. Migrate it with: Update-LedgerJournal -JournalPath '$Path'"
    }
    else {
        $message = "Journal '$Path' uses schema version $version which is newer than this PSLedger supports (version $current). Upgrade the PSLedger module to work with this journal."
    }

    if ($Write) {
        throw $message
    }

    if (-not $script:SchemaWarnedPaths.ContainsKey($Path)) {
        $script:SchemaWarnedPaths[$Path] = $true
        Write-Warning $message
    }
}