Commands/Resolve-SqlMigrations.ps1

<#
    Copyright 2023 Subatomix Research Inc.

    Permission to use, copy, modify, and distribute this software for any
    purpose with or without fee is hereby granted, provided that the above
    copyright notice and this permission notice appear in all copies.

    THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
    WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
    MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
    ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
    WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
    ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#>


function Resolve-SqlMigrations {
    <#
    .SYNOPSIS
        Validates a set of migrations produced by Merge-SqlMigrations.
    #>

    [CmdletBinding()]
    param (
        # Set of migrations to validate.
        [Parameter(Mandatory)]
        [System.Collections.Specialized.OrderedDictionary]
        $Migrations
    )

    process {
        $HasWarnings = $false

        foreach ($Migration in $Migrations.Values) {

            # If the migration has been applied already (partially or fully), then
            # it is possible that the migration's code has changed in the interim.
            # That suggests an accidental change to an old migration -- an error.
            # To detect this, we store a hash value in the target database's
            # migration record and compare it (in Merge-SqlMigrations) to the
            # current files' hash. If the hashes are different, the HasChanges
            # property is set to $true.
            if ($Migration.State -gt 0 -and $Migration.HasChanged) {
                Write-Warning (
                    "Migration '", $Migration.Name, "' has been applied ",
                    "through phase ", $Migration.State, " of 3, ",
                    "but its code in the source directory does not match the code previously used. ",
                    "To resolve, verify that no accidental changes have occurred to this migration's code. ",
                    "Then update the migration's expected hash in the _deploy.Migration table. ",
                    "Expected hash: ", $Migration.Hash `
                    -join ""
                )
                $HasWarnings = $true
            }

            # How much of migration has been applied to the target database?
            if ($Migration.State -ge 3 <# Done #>) {
                # Fully applied; no further work to do in this function.
                continue
            }

            # Migration is partially applied, or perhaps never applied.
            # Check if the path to its code is known.
            if (-not $Migration.Path) {
                Write-Warning (
                    "Migration ", $Migration.Name, " is partially applied ",
                    "(through phase ", $Migration.State, " of 3), ",
                    "but no code for it was found in the source directory. ",
                    "It is not possible to complete this migration." `
                    -join ""
                )
                $HasWarnings = $true
                continue
            }

            # Path to migration code is known.

            # Read and collate migration SQL phases
            $Sql = Read-SqlMigration $Migration.Path
            $Migration.PreSql  = $Sql.PreSql
            $Migration.CoreSql = $Sql.CoreSql
            $Migration.PostSql = $Sql.PostSql

            # Resolve migration dependencies
            $Migration.Depends = @($Sql.Depends | ForEach-Object {
                # Look up the dependency
                $Depend = $Migrations[$_]

                # Verify dependency was found
                if (-not $Depend) {
                    Write-Warning (
                        "Migration ", $Migration.Name, " claims to depend on $_, ",
                        "but no migration by that name was found. ",
                        "The dependency cannot be satisfied." `
                        -join ""
                    )
                    $HasWarnings = $true
                    continue
                }

                # Verify dependency runs earlier than depending migration
                if ($Depend.Name -ge $Migration.Name -and $Depend.State -ne 3 <# Done #>) {
                    Write-Warning (
                        "Migration ", $Migration.Name, " claims to depend on ", $Depend.Name, ", ",
                        "but that migration must run later in the sequence. ",
                        "The dependency cannot be satisfied." `
                        -join ""
                    )
                    $HasWarnings = $true
                    continue
                }

                $Depend.Name
            })
        }

        if ($HasWarnings) {
            $PSCmdlet.ThrowTerminatingError((New-Error `
                ( [System.Data.DataException] `
                    "Migration(s) failed validation. Address warnings and try again." ) `
                -Id PSql.Deploy.InvalidMigrations
            ))
        }
    }
}