Upgrade-DuckDBAssemblies.ps1

<#
.SYNOPSIS
Upgrades DuckDB.NET assemblies to the latest stable (non-beta) version.
 
.DESCRIPTION
This script downloads and installs the latest stable versions of DuckDB.NET.Data
and DuckDB.NET.Bindings NuGet packages. It organizes assemblies into subdirectories:
- lib/core - Native DuckDB library (duckdb.dll)
- lib/net6 - .NET 6.0 managed assemblies
- lib/net8 - .NET 8.0 managed assemblies (required for UDF support)
- lib/net10 - .NET 10.0 managed assemblies (required for UDF support)
 
The script also compiles the C# wrapper classes (ScalarFunctionWrapper, TableFunctionWrapper)
to DLLs for faster module loading.
 
.PARAMETER Force
Forces re-download even if assemblies already exist.
 
.PARAMETER SkipCompile
Skips compilation of wrapper DLLs.
 
.EXAMPLE
.\Upgrade-DuckDBAssemblies.ps1
Downloads and installs the latest stable DuckDB.NET assemblies.
 
.EXAMPLE
.\Upgrade-DuckDBAssemblies.ps1 -Force
Forces re-download of all assemblies.
 
.NOTES
Run this script in a FRESH PowerShell session (no PaperinikDB module loaded).
Requires internet access to download from NuGet.
#>


param(
    [switch]$Force,
    [switch]$SkipCompile
)

$ErrorActionPreference = 'Stop'

# Get script location
$scriptPath = $PSScriptRoot
if (-not $scriptPath) {
    $scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
}

$libPath = Join-Path $scriptPath 'lib'
$corePath = Join-Path $libPath 'core'
$net6Path = Join-Path $libPath 'net6'
$net8Path = Join-Path $libPath 'net8'
$net10Path = Join-Path $libPath 'net10'
$tempPath = Join-Path $libPath 'temp'

Write-Host '=== DuckDB.NET Assembly Upgrade Tool ===' -ForegroundColor Cyan
Write-Host "Library path: $libPath"

#region Helper Functions

function Get-LatestNuGetVersion {
    param(
        [string]$PackageId
    )
    
    Write-Host " Checking latest stable version for $PackageId..." -ForegroundColor Gray
    
    $url = "https://api.nuget.org/v3-flatcontainer/$($PackageId.ToLower())/index.json"
    
    try {
        $response = Invoke-RestMethod -Uri $url -ErrorAction Stop
        
        # Filter out pre-release versions (contain -, alpha, beta, rc, preview)
        $stableVersions = $response.versions | Where-Object {
            $_ -notmatch '[-]|alpha|beta|rc|preview'
        }
        
        if ($stableVersions.Count -eq 0) {
            Write-Warning "No stable versions found for $PackageId"
            return $null
        }
        
        # Get the latest stable version (last in sorted list)
        $latestVersion = $stableVersions | Sort-Object { [Version]($_ -replace '[^0-9.]', '') } | Select-Object -Last 1
        
        Write-Host " Latest stable version: $latestVersion" -ForegroundColor Green
        return $latestVersion
    } catch {
        Write-Warning "Failed to check NuGet for $PackageId : $_"
        return $null
    }
}

function Get-NuGetPackage {
    param(
        [string]$PackageId,
        [string]$Version,
        [string]$OutputPath
    )
    
    $packageUrl = "https://api.nuget.org/v3-flatcontainer/$($PackageId.ToLower())/$Version/$($PackageId.ToLower()).$Version.nupkg"
    $nupkgPath = Join-Path $OutputPath "$PackageId.$Version.nupkg"
    $extractPath = Join-Path $OutputPath $PackageId
    
    Write-Host " Downloading $PackageId v$Version..." -ForegroundColor Gray
    
    # Download
    Invoke-WebRequest -Uri $packageUrl -OutFile $nupkgPath -ErrorAction Stop
    
    # Extract
    if (Test-Path $extractPath) {
        Remove-Item $extractPath -Recurse -Force
    }
    
    Expand-Archive -Path $nupkgPath -DestinationPath $extractPath -Force
    
    return $extractPath
}

#endregion

#region Create Directory Structure

Write-Host "`nCreating directory structure..." -ForegroundColor Yellow

foreach ($dir in @($corePath, $net6Path, $net8Path, $net10Path, $tempPath)) {
    if (-not (Test-Path $dir)) {
        New-Item -ItemType Directory -Path $dir -Force | Out-Null
        Write-Host " Created: $dir"
    }
}

#endregion

#region Download Packages

Write-Host "`nChecking for latest versions..." -ForegroundColor Yellow

$dataVersion = Get-LatestNuGetVersion -PackageId 'DuckDB.NET.Data.Full'
$bindingsVersion = Get-LatestNuGetVersion -PackageId 'DuckDB.NET.Bindings.Full'

if (-not $dataVersion -or -not $bindingsVersion) {
    throw 'Failed to determine latest versions. Check your internet connection.'
}

Write-Host "`nDownloading packages..." -ForegroundColor Yellow

$dataPath = Get-NuGetPackage -PackageId 'DuckDB.NET.Data.Full' -Version $dataVersion -OutputPath $tempPath
$bindingsPath = Get-NuGetPackage -PackageId 'DuckDB.NET.Bindings.Full' -Version $bindingsVersion -OutputPath $tempPath

Write-Host ' Downloaded successfully!' -ForegroundColor Green

#endregion

#region Install Assemblies

Write-Host "`nInstalling assemblies..." -ForegroundColor Yellow

# Install native library to core
$nativePattern = Join-Path $bindingsPath 'runtimes\win-x64\native\duckdb.dll'
if (Test-Path $nativePattern) {
    Copy-Item $nativePattern $corePath -Force
    Write-Host ' Installed: duckdb.dll -> lib/core'
}

# Install .NET 6.0 assemblies
$net6DataDll = Join-Path $dataPath 'lib\net6.0\DuckDB.NET.Data.dll'
$net6BindingsDll = Join-Path $bindingsPath 'lib\net6.0\DuckDB.NET.Bindings.dll'

if (Test-Path $net6DataDll) {
    Copy-Item $net6DataDll $net6Path -Force
    Write-Host ' Installed: DuckDB.NET.Data.dll -> lib/net6'
}
if (Test-Path $net6BindingsDll) {
    Copy-Item $net6BindingsDll $net6Path -Force
    Write-Host ' Installed: DuckDB.NET.Bindings.dll -> lib/net6'
}

# Install .NET 8.0 assemblies
$net8DataDll = Join-Path $dataPath 'lib\net8.0\DuckDB.NET.Data.dll'
$net8BindingsDll = Join-Path $bindingsPath 'lib\net8.0\DuckDB.NET.Bindings.dll'

if (Test-Path $net8DataDll) {
    Copy-Item $net8DataDll $net8Path -Force
    Write-Host ' Installed: DuckDB.NET.Data.dll -> lib/net8'
}
if (Test-Path $net8BindingsDll) {
    Copy-Item $net8BindingsDll $net8Path -Force
    Write-Host ' Installed: DuckDB.NET.Bindings.dll -> lib/net8'
}

# Install .NET 10.0 assemblies
$net10DataDll = Join-Path $dataPath 'lib\net10.0\DuckDB.NET.Data.dll'
$net10BindingsDll = Join-Path $bindingsPath 'lib\net10.0\DuckDB.NET.Bindings.dll'

if (Test-Path $net10DataDll) {
    Copy-Item $net10DataDll $net10Path -Force
    Write-Host ' Installed: DuckDB.NET.Data.dll -> lib/net10'
}
if (Test-Path $net10BindingsDll) {
    Copy-Item $net10BindingsDll $net10Path -Force
    Write-Host ' Installed: DuckDB.NET.Bindings.dll -> lib/net10'
}

# Also copy to lib root for backward compatibility (may fail if files are locked)
try {
    Copy-Item (Join-Path $net8Path 'DuckDB.NET.Data.dll') $libPath -Force -ErrorAction Stop
    Copy-Item (Join-Path $net8Path 'DuckDB.NET.Bindings.dll') $libPath -Force -ErrorAction Stop
    Copy-Item (Join-Path $corePath 'duckdb.dll') $libPath -Force -ErrorAction Stop
    Write-Host ' Installed: Backward-compatible copies -> lib/'
} catch {
    Write-Warning 'Could not update lib/ root (files may be locked). Run in a fresh PowerShell session.'
}

#endregion

#region Compile Wrapper DLLs

$compiledFramework = $null

if (-not $SkipCompile) {
    Write-Host "`nDetecting available .NET runtimes for compilation..." -ForegroundColor Yellow

    # Map framework folder names to their .NET major version numbers
    $frameworkVersionMap = [ordered]@{
        'net6'  = 6
        'net8'  = 8
        'net10' = 10
    }

    # Find which lib/netX directories contain downloaded DuckDB assemblies
    $availableFrameworks = [ordered]@{}
    foreach ($fw in $frameworkVersionMap.Keys) {
        $fwPath = Join-Path $libPath $fw
        if (Test-Path (Join-Path $fwPath 'DuckDB.NET.Data.dll')) {
            $availableFrameworks[$fw] = $fwPath
            Write-Host " Found assemblies: lib/$fw" -ForegroundColor Gray
        }
    }

    if ($availableFrameworks.Count -eq 0) {
        Write-Warning 'No DuckDB assemblies found in lib/net* directories. Skipping compilation.'
    } else {
        # Only frameworks whose major version <= current PS .NET runtime can be loaded by Add-Type
        $currentRuntimeMajor = [System.Environment]::Version.Major
        Write-Host " Current PowerShell .NET runtime: $currentRuntimeMajor" -ForegroundColor Gray

        $viableTargets = [ordered]@{}
        foreach ($fw in $availableFrameworks.Keys) {
            if ($frameworkVersionMap[$fw] -le $currentRuntimeMajor) {
                $viableTargets[$fw] = $availableFrameworks[$fw]
            }
        }

        if ($viableTargets.Count -eq 0) {
            Write-Warning "No downloaded assemblies are compatible with the current .NET $currentRuntimeMajor runtime. Skipping compilation."
        } else {
            # Choose compilation target
            $orderedKeys = @($viableTargets.Keys)
            $chosenFramework = $null
            $chosenPath = $null

            if ($viableTargets.Count -eq 1) {
                $chosenFramework = $orderedKeys[0]
                $chosenPath = $viableTargets[$chosenFramework]
                Write-Host " Using runtime: $chosenFramework" -ForegroundColor Green
            } else {
                Write-Host ''
                Write-Host 'Multiple .NET runtimes are available for compilation:' -ForegroundColor Cyan
                for ($i = 0; $i -lt $orderedKeys.Count; $i++) {
                    Write-Host " [$($i + 1)] $($orderedKeys[$i])" -ForegroundColor White
                }

                $choice = 0
                do {
                    $userInput = Read-Host "Choose a runtime to compile wrapper DLLs for (1-$($viableTargets.Count))"
                    if ([int]::TryParse($userInput, [ref]$choice)) {
                        if ($choice -lt 1 -or $choice -gt $viableTargets.Count) {
                            Write-Host " Please enter a number between 1 and $($viableTargets.Count)" -ForegroundColor Yellow
                            $choice = 0
                        }
                    } else {
                        Write-Host ' Please enter a valid number' -ForegroundColor Yellow
                        $choice = 0
                    }
                } while ($choice -lt 1)

                $chosenFramework = $orderedKeys[$choice - 1]
                $chosenPath = $viableTargets[$chosenFramework]
                Write-Host " Selected: $chosenFramework" -ForegroundColor Green
            }

            Write-Host "`nCompiling wrapper DLLs for $chosenFramework..." -ForegroundColor Yellow

            # Load the assemblies for the chosen framework
            Add-Type -Path (Join-Path $chosenPath 'DuckDB.NET.Data.dll')
            Add-Type -Path (Join-Path $chosenPath 'DuckDB.NET.Bindings.dll')

            # Load wrapper source code from .cs files
            $csPath = Join-Path $scriptPath 'cs'
            $ScalarFunctionWrapperCode = Get-Content -Path (Join-Path $csPath 'ScalarFunctionWrapper.cs') -Raw
            $TableFunctionWrapperCode = Get-Content -Path (Join-Path $csPath 'TableFunctionWrapper.cs') -Raw

            # Get referenced assemblies
            $duckDbDataAssembly = [DuckDB.NET.Data.DuckDBConnection].Assembly
            $duckDbBindingsAssembly = [DuckDB.NET.Native.NativeMethods].Assembly

            $referencedAssemblies = @(
                [System.Management.Automation.PSObject].Assembly.Location,
                [System.Object].Assembly.Location,
                [System.Collections.Generic.List[object]].Assembly.Location,
                [System.Collections.Generic.IList[object]].Assembly.Location,
                [System.Runtime.CompilerServices.DynamicAttribute].Assembly.Location,
                [Microsoft.CSharp.RuntimeBinder.Binder].Assembly.Location,
                $duckDbDataAssembly.Location,
                $duckDbBindingsAssembly.Location
            ) | Select-Object -Unique

            # Compile ScalarFunctionWrapper
            $scalarDllPath = Join-Path $chosenPath 'PaperinikDB.ScalarFunctionWrapper.dll'
            try {
                Add-Type -TypeDefinition $ScalarFunctionWrapperCode `
                    -ReferencedAssemblies $referencedAssemblies `
                    -OutputAssembly $scalarDllPath `
                    -IgnoreWarnings `
                    -ErrorAction Stop
                Write-Host " Compiled: PaperinikDB.ScalarFunctionWrapper.dll -> lib/$chosenFramework" -ForegroundColor Green
            } catch {
                Write-Warning "Failed to compile ScalarFunctionWrapper: $_"
            }

            # Compile TableFunctionWrapper
            $tableDllPath = Join-Path $chosenPath 'PaperinikDB.TableFunctionWrapper.dll'
            try {
                Add-Type -TypeDefinition $TableFunctionWrapperCode `
                    -ReferencedAssemblies $referencedAssemblies `
                    -OutputAssembly $tableDllPath `
                    -IgnoreWarnings `
                    -ErrorAction Stop
                Write-Host " Compiled: PaperinikDB.TableFunctionWrapper.dll -> lib/$chosenFramework" -ForegroundColor Green
            } catch {
                Write-Warning "Failed to compile TableFunctionWrapper: $_"
            }

            $compiledFramework = $chosenFramework
        }
    }
}

#endregion

#region Cleanup

Write-Host "`nCleaning up..." -ForegroundColor Yellow
Remove-Item $tempPath -Recurse -Force -ErrorAction SilentlyContinue
Write-Host ' Removed temporary files'

#endregion

#region Summary

Write-Host "`n=== Upgrade Complete! ===" -ForegroundColor Green
Write-Host ''
Write-Host 'Installed versions:' -ForegroundColor Yellow
Write-Host " DuckDB.NET.Data: $dataVersion"
Write-Host " DuckDB.NET.Bindings: $bindingsVersion"
Write-Host ''
Write-Host 'Directory structure:' -ForegroundColor Yellow
Write-Host ' lib/core - Native DuckDB library'
Write-Host ' lib/net6 - .NET 6.0 assemblies'
Write-Host ' lib/net8 - .NET 8.0 assemblies'
Write-Host ' lib/net10 - .NET 10.0 assemblies'
if ($compiledFramework) {
    Write-Host " Wrapper DLLs compiled to: lib/$compiledFramework" -ForegroundColor Green
}
Write-Host ''
Write-Host 'To verify, run in a new PowerShell session:' -ForegroundColor Yellow
Write-Host ' Import-Module PaperinikDB -Force'
Write-Host ' $conn = New-DuckDBConnection'
Write-Host ' $conn.CreateFunction("test", { param($x) $x * 2 }, @([int]), [int])'
Write-Host ' $conn.sql("SELECT test(21)")'

#endregion