_TESTS/PSLocalBase.SecondReview.Tests.ps1

$ErrorActionPreference = 'Stop'
Import-Module (Join-Path (Split-Path $PSScriptRoot -Parent) 'PSLocalBase.psd1') -Force
$script:failed = 0
function Assert($ok, $message) { if (-not $ok) { throw $message } }
function Reject([scriptblock]$body, $pattern = '.') {
    $errorRecord = $null
    try { & $body | Out-Null } catch { $errorRecord = $_ }
    Assert ($null -ne $errorRecord -and $errorRecord.Exception.Message -match $pattern) "Expected rejection ($pattern), got $errorRecord"
}
function Case($name, [scriptblock]$body) {
    $db = Connect-LocalBase ':memory:' -PassThru
    try { & $body $db; Write-Host "[PASS] $name" }
    catch { $script:failed++; Write-Host "[FAIL] ${name}: $_`n$($_.ScriptStackTrace)" }
    finally { Disconnect-LocalBase; $db.close() }
}
Case 'Unsafe existing numbers: comparisons, membership, deletion rollback and indexes' {
    param($db)
    $db.collection('x').doc('safe').set(@{ v = 1; a = @(1) })
    # Simulate legacy/external data, including an existing approximate index.
    $null = $db.query('INSERT INTO x VALUES (''a'', ''{"v":18446744073709551614,"a":[18446744073709551614]}'');')
    $null = $db.query('INSERT INTO x VALUES (''b'', ''{"v":18446744073709551615,"a":[18446744073709551615]}'');')
    $null = $db.query('CREATE INDEX legacy_numeric ON x(json_extract(doc, ''$.v''));')
    foreach ($op in @('==','!=','>','>=','<','<=','in','not-in')) {
        Reject { $db.collection('x').where('v', $op, 1).count() } 'Numeric precision'
        Reject { $db.collection('x').where('v', $op, 1).delete() } 'Numeric precision'
        Assert ($db.collection('x').count() -eq 3) "Partial delete for $op"
    }
    foreach ($op in @('array-contains','array-contains-any')) {
        Reject { $db.collection('x').where('a', $op, 1).delete() } 'Numeric precision'
        Assert ($db.collection('x').count() -eq 3) 'Partial array delete'
    }
    foreach ($value in @([uint64]::MaxValue, [decimal]::MaxValue, [decimal]::Parse('0.10000000000000001', [cultureinfo]::InvariantCulture))) {
        Reject { $db.collection('x').where('v','==',$value).delete() } 'Numeric precision'
        Reject { $db.collection('x').where('a','array-contains-any',@($value)).delete() } 'Numeric precision'
        Assert ($db.collection('x').count() -eq 3) 'Unsafe parameter deleted rows'
    }
    Reject { $db.collection('x').createIndex('v', $true) } 'Numeric precision'
    Reject { $db.collection('x').doc('c').set(@{v=[uint64]::MaxValue}) } 'Numeric precision'
    Assert (-not $db.collection('x').doc('c').exists()) 'Unsafe indexed write persisted'
    $db.collection('d').doc('a').set(@{v=0.1})
    $null = $db.query('INSERT INTO d VALUES (''b'', ''{"v":0.10000000000000001}'');')
    Reject { $db.collection('d').where('v','==',0.1).delete() } 'Numeric precision'
    Assert ($db.collection('d').count() -eq 2) 'Decimal collision lost data'
}
Case 'Supported Int64, uint64 within range, decimals and doubles remain distinct' {
    param($db)
    foreach ($entry in @(@('lo',[long]::MinValue), @('hi',[long]::MaxValue), @('prev',([long]::MaxValue-1)), @('fraction',[decimal]0.1), @('next',[double]::Parse('1.0000000000000002',[cultureinfo]::InvariantCulture)))) {
        $db.collection('x').doc($entry[0]).set(@{v=$entry[1]})
    }
    Assert ($db.collection('x').where('v','==',[uint64][long]::MaxValue).count() -eq 1) 'Int64 boundary rounded'
    Assert ($db.collection('x').where('v','==',[decimal][long]::MaxValue).count() -eq 1) 'Integral decimal parameter rounded'
    Assert ($db.collection('x').where('v','in',@([long]::MinValue,[long]::MaxValue)).count() -eq 2) 'IN lost Int64 precision'
    Assert ($db.collection('x').where('v','==',[decimal]0.1).count() -eq 1) 'Supported decimal failed'
    Assert ($db.collection('x').where('v','==',1.0).count() -eq 0) 'Adjacent double collapsed'
    $db.collection('x').where('v','==',[long]::MaxValue).delete()
    Assert ($db.collection('x').doc('prev').exists()) 'Adjacent Int64 removed'
}
Case 'Reconnect failure preserves ambient connection, data and transaction' {
    param($db)
    $db.beginTransaction()
    $db.collection('x').doc('a').set(@{v=1})
    Reject { Connect-LocalBase -Path ([string][char]0) }
    Reject { Connect-LocalBase ':memory:' -BusyTimeoutMs -1 }
    Reject { Connect-LocalBase ':memory:' -JournalMode INVALID }
    Assert ([object]::ReferenceEquals((Get-LocalBaseConnection), $db)) 'Ambient changed on failure'
    Assert ($db.collection('x').count() -eq 1) 'Pending data lost'
    $db.rollback()
    Assert ($db.collection('x').count() -eq 0) 'Transaction no longer active'
    $replacement = Connect-LocalBase ':memory:' -PassThru
    Assert ($db.DbHandle -eq [IntPtr]::Zero) 'Successful replacement did not close original'
    Assert ($replacement.DbHandle -ne [IntPtr]::Zero) 'New connection closed'
}
Case 'Same file and different file reconnects are validated before replacement' {
    param($db)
    $paths = @((Join-Path $env:TEMP ('pslb-r2-' + [guid]::NewGuid().ToString('N') + '.db')), (Join-Path $env:TEMP ('pslb-r2-' + [guid]::NewGuid().ToString('N') + '.db')))
    try {
        $first = Connect-LocalBase $paths[0] -PassThru
        $first.collection('x').doc('a').set(@{v=1})
        $second = Connect-LocalBase $paths[0] -PassThru
        Assert ($first.DbHandle -eq [IntPtr]::Zero -and $second.collection('x').count() -eq 1) 'Same-file replacement failed'
        $third = Connect-LocalBase $paths[1] -PassThru
        Assert ($second.DbHandle -eq [IntPtr]::Zero -and $third.collection('x').count() -eq 0) 'Different-file replacement failed'
    } finally {
        Disconnect-LocalBase
        foreach ($path in $paths) { foreach ($file in @($path,"$path-wal","$path-shm")) { if (Test-Path -LiteralPath $file) { Remove-Item -LiteralPath $file -Force } } }
    }
}
Case 'Top-level JSON documents preserve shape and trusted identity in point/query pipelines' {
    param($db)
    $jsons = @('[]','[1]','[1,2]','[[1,2],[],[3]]','null','false','42','"text"','{"Value":"business","id":"business"}')
    for ($i=0; $i -lt $jsons.Count; $i++) {
        $db.createCollection('x') | Out-Null
        $null = $db.query("INSERT INTO x VALUES ('$i', '$($jsons[$i])');")
        Get-LocalBaseDocument x -Id "$i" | Set-LocalBaseDocument
        $raw = $db.query("SELECT doc FROM x WHERE key='$i';")
        Assert ($raw[0].doc -ceq $jsons[$i]) "Point pipeline changed $($jsons[$i]) to $($raw[0].doc)"
        Get-LocalBaseDocument x -Id "$i" | Set-LocalBaseDocument x -Id "$i"
        Get-LocalBaseDocument x -Id "$i" | Add-LocalBaseDocument copies -Id "$i" | Out-Null
        Assert (($db.query("SELECT doc FROM copies WHERE key='$i';"))[0].doc -ceq $jsons[$i]) 'Add changed shape'
    }
    Get-LocalBaseDocument x | Set-LocalBaseDocument
    for ($i=0; $i -lt $jsons.Count; $i++) { Assert (($db.query("SELECT doc FROM x WHERE key='$i';"))[0].doc -ceq $jsons[$i]) 'Query pipeline changed shape' }
    Assert (@(Get-LocalBaseDocument x).Count -eq $jsons.Count) 'Array documents enumerated'
    Get-LocalBaseDocument x | Remove-LocalBaseDocument -Confirm:$false
    Assert ($db.collection('x').count() -eq 0) 'Trusted non-object identity lost'
    Assert (@(Get-LocalBaseDocument x -Id missing).Count -eq 0) 'Missing point emitted a document'
    Set-LocalBaseDocument c -Id a -Data @(1,2)
    Get-LocalBaseDocument c -Id a | Set-LocalBaseDocument c -Id a
    Assert (($db.query('SELECT doc FROM c;'))[0].doc -eq '[1,2]') 'Explicit array reproduction failed'
    foreach ($json in @('[]','[1]')) {
        $value = (ConvertFrom-Json ('{"value":'+$json+'}')).value
        $db.collection('c').doc('a').set($value)
        $db.collection('c').doc('b').set($db.collection('c').doc('a').get())
        Assert (($db.query("SELECT doc FROM c WHERE key='b';"))[0].doc -eq $json) 'Fluent array shape lost'
    }
    Set-LocalBaseDocument c -Id explicitNull -Data $null
    Add-LocalBaseDocument c -Id addedNull -Data $null | Out-Null
    Set-LocalBaseDocument c -Id empty -Data @()
    Assert (($db.query("SELECT doc FROM c WHERE key='explicitNull';"))[0].doc -eq 'null') 'Explicit null Set lost'
    Assert (($db.query("SELECT doc FROM c WHERE key='addedNull';"))[0].doc -eq 'null') 'Explicit null Add lost'
    Assert (($db.query("SELECT doc FROM c WHERE key='empty';"))[0].doc -eq '[]') 'Explicit empty array lost'
}
Case 'Distinct rejects modifiers and respects point/filter scope' {
    param($db)
    $db.collection('x').doc('a').set(@{v=1})
    $db.collection('x').doc('b').set(@{v=2})
    Reject { $db.collection('x').limit(0).distinct('v') } 'limit'
    Reject { $db.collection('x').skip(1).distinct('v') } 'skip'
    Reject { $db.collection('x').orderBy('v').distinct('v') } 'orderBy'
    Assert ($db.collection('x').doc('a').where('v','==',1).distinct('v')[0] -eq 1) 'Point/filter distinct scope lost'
    Assert ($db.collection('x').doc('a').where('v','==',2).distinct('v').Count -eq 0) 'Distinct ignored point/filter intersection'
}
Case 'WhatIf collection, index, set and begin transaction have no effects' {
    param($db)
    New-LocalBaseCollection x -WhatIf
    New-LocalBaseIndex x v -WhatIf
    Set-LocalBaseDocument x a -Data @{v=1} -WhatIf
    Start-LocalBaseTransaction -WhatIf
    Assert ($db.getCollections().Count -eq 0) 'WhatIf created schema'
    $db.beginTransaction(); $db.rollback()
}
if ($script:failed) { exit 1 }
exit 0