Public/Initialize-VBEnrichmentDatabase.ps1

function Initialize-VBEnrichmentDatabase {
<#
.SYNOPSIS
    Create or migrate the SQLite database used by VB.DNSEnrichment.
 
.DESCRIPTION
    Idempotent. Safe to call on every module run. Creates the database file (and
    parent folder) if missing, applies any pending migrations from Sql/*.sql in
    numeric order, and records each applied version in the SchemaVersion table.
 
    Migration files must be named NNN_description.sql (e.g. 001_init.sql,
    002_add_index.sql). The numeric prefix is the version number. Files are
    discovered from $PSScriptRoot\..\Sql relative to this function's location.
 
.PARAMETER DatabasePath
    Full path to the SQLite database file. The parent folder is created if missing.
    Defaults to "$env:LOCALAPPDATA\VB.DNSEnrichment\enrichment.db".
 
.OUTPUTS
    [PSCustomObject] -- ComputerName, Status, Path, Created, Version, MigrationsApplied, Error, CollectionTime.
 
.EXAMPLE
    Initialize-VBEnrichmentDatabase
 
.EXAMPLE
    Initialize-VBEnrichmentDatabase -DatabasePath 'D:\Data\enrichment.db'
 
.NOTES
    Version: 1.0.0
    MinPSVersion: 5.1
    Author: VB
    ChangeLog:
        1.0.0 -- 2026-05-10 -- Initial release
#>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter()]
        [string]$DatabasePath = (Join-Path $env:LOCALAPPDATA 'VB.DNSEnrichment\enrichment.db')
    )

    $computer       = $env:COMPUTERNAME
    $collectionTime = (Get-Date).ToString('o')

    try {
        # --- Step 1 -- Ensure parent folder exists ---
        $parent = Split-Path -Path $DatabasePath -Parent
        if (-not (Test-Path -LiteralPath $parent)) {
            New-Item -Path $parent -ItemType Directory -Force | Out-Null
            Write-Verbose "[Init-DB] Created folder: $parent"
        }

        $created = -not (Test-Path -LiteralPath $DatabasePath)
        if ($created) {
            Write-Verbose "[Init-DB] Database file does not exist; will be created on first query"
        }

        # --- Step 2 -- Locate the Sql migration folder ---
        $sqlFolder = Join-Path $PSScriptRoot '..\Sql'
        $sqlFolder = (Resolve-Path -LiteralPath $sqlFolder -ErrorAction Stop).Path

        $migrationFiles = Get-ChildItem -Path $sqlFolder -Filter '*.sql' -File |
            Where-Object { $_.Name -match '^(\d+)_' } |
            Sort-Object { [int]($_.Name -replace '^(\d+)_.*', '$1') }

        if (-not $migrationFiles) {
            throw "No migration files found in $sqlFolder"
        }

        # --- Step 3 -- Bootstrap SchemaVersion table on first run ---
        # If the database is brand new there's no SchemaVersion table yet, so the
        # SELECT below will fail. Create it pre-emptively as a no-op DDL.
        $bootstrapSql = @'
CREATE TABLE IF NOT EXISTS SchemaVersion (
    Version INTEGER PRIMARY KEY,
    AppliedAt TEXT NOT NULL
);
'@

        Invoke-VBSqliteCommand -DatabasePath $DatabasePath-Query $bootstrapSql | Out-Null

        # --- Step 4 -- Read currently-applied versions ---
        $appliedRows = Invoke-VBSqliteCommand -DatabasePath $DatabasePath `
            -Query 'SELECT Version FROM SchemaVersion'
        $applied = @{}
        foreach ($row in $appliedRows) {
            $applied[[int]$row.Version] = $true
        }

        # --- Step 5 -- Apply each pending migration in order ---
        $migrationsApplied = @()
        foreach ($file in $migrationFiles) {
            $version = [int]($file.Name -replace '^(\d+)_.*', '$1')
            if ($applied.ContainsKey($version)) {
                Write-Verbose "[Init-DB] Migration $version already applied -- skipping"
                continue
            }

            Write-Verbose "[Init-DB] Applying migration $($file.Name)"
            $sql = Get-Content -LiteralPath $file.FullName -Raw -Encoding UTF8
            Invoke-VBSqliteCommand -DatabasePath $DatabasePath-Query $sql | Out-Null

            Invoke-VBSqliteCommand -DatabasePath $DatabasePath`
                -Query 'INSERT OR IGNORE INTO SchemaVersion (Version, AppliedAt) VALUES (@v, @t)' `
                -SqlParameters @{ v = $version; t = (Get-Date).ToString('o') } | Out-Null

            $migrationsApplied += $file.Name
        }

        # --- Step 6 -- Read the final highest applied version ---
        $current = Invoke-VBSqliteCommand -DatabasePath $DatabasePath `
            -Query 'SELECT MAX(Version) AS V FROM SchemaVersion'
        $currentVersion = if ($current -and $current.V) { [int]$current.V } else { 0 }

        [PSCustomObject]@{
            ComputerName      = $computer
            Status            = 'Success'
            Path              = $DatabasePath
            Created           = $created
            Version           = $currentVersion
            MigrationsApplied = $migrationsApplied
            Error             = $null
            CollectionTime    = $collectionTime
        }
    }
    catch {
        [PSCustomObject]@{
            ComputerName      = $computer
            Status            = 'Failed'
            Path              = $DatabasePath
            Created           = $false
            Version           = 0
            MigrationsApplied = @()
            Error             = $_.Exception.Message
            CollectionTime    = $collectionTime
        }
    }
}