Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 83 additions & 5 deletions lib/database.ps1
Original file line number Diff line number Diff line change
@@ -1,5 +1,80 @@
# Description: Functions for interacting with the Scoop database cache

if (-not (Get-Command Compare-Version -ErrorAction Ignore)) {
. "$PSScriptRoot\versions.ps1"
}

<#
.SYNOPSIS
Get the latest row from a set of Scoop database rows.
.DESCRIPTION
Compares the `version` property of each row semantically and returns the
latest row. Returns `$null` when no rows are provided.
#>
function Get-LatestScoopDBRow {
param(
[Parameter(Mandatory)]
[AllowEmptyCollection()]
[object[]]
$Rows
)

if (-not $Rows -or $Rows.Count -eq 0) {
return $null
}

$latest = $Rows[0]
for ($i = 1; $i -lt $Rows.Count; $i++) {
$row = $Rows[$i]
if ((Compare-Version -ReferenceVersion $latest.version -DifferenceVersion $row.version) -gt 0) {
$latest = $row
}
}

return $latest
}

<#
.SYNOPSIS
Return the semantically latest row or rows from a Scoop database result set.
.DESCRIPTION
Clones the schema of `Table` and imports the latest row per group when
`GroupBy` is supplied, or the single latest row across the whole table when
it is omitted. Returns an empty cloned table when the source table has no rows.
.PARAMETER Table
The source `System.Data.DataTable` returned from a Scoop database query.
.PARAMETER GroupBy
Optional column names used to group rows before semantic latest-row selection.
.OUTPUTS
System.Data.DataTable
A cloned table containing the latest matching row for each requested scope.
#>
function Select-LatestScoopDBRow {
param(
[Parameter(Mandatory)]
[System.Data.DataTable]
$Table,
[string[]]
$GroupBy
)

$latestRows = $Table.Clone()
$rows = @($Table.Rows)
if ($rows.Count -eq 0) {
return $latestRows
}

if ($GroupBy -and $GroupBy.Count -gt 0) {
foreach ($group in ($rows | Group-Object -Property $GroupBy)) {
$latestRows.ImportRow((Get-LatestScoopDBRow -Rows @($group.Group)))
}
} else {
$latestRows.ImportRow((Get-LatestScoopDBRow -Rows $rows))
}

return $latestRows
}

<#
.SYNOPSIS
Get SQLite .NET driver
Expand Down Expand Up @@ -277,7 +352,6 @@ function Find-ScoopDBItem {
$dbAdapter = New-Object -TypeName System.Data.SQLite.SQLiteDataAdapter
$result = New-Object System.Data.DataTable
$dbQuery = "SELECT * FROM app WHERE $(($From -join ' LIKE @Pattern OR ') + ' LIKE @Pattern')"
$dbQuery = "SELECT * FROM ($($dbQuery + ' ORDER BY version DESC')) GROUP BY name, bucket"
$dbCommand = $db.CreateCommand()
$dbCommand.CommandText = $dbQuery
$dbCommand.CommandType = [System.Data.CommandType]::Text
Expand All @@ -291,7 +365,7 @@ function Find-ScoopDBItem {
$dbCommand.Dispose()
$dbAdapter.Dispose()
$db.Dispose()
return $result
return Select-LatestScoopDBRow -Table $result -GroupBy @('name', 'bucket')
}
}

Expand Down Expand Up @@ -336,8 +410,6 @@ function Get-ScoopDBItem {
$dbQuery = 'SELECT * FROM app WHERE name = @Name AND bucket = @Bucket'
if ($Version) {
$dbQuery += ' AND version = @Version'
} else {
$dbQuery += ' ORDER BY version DESC LIMIT 1'
}
$dbCommand = $db.CreateCommand()
$dbCommand.CommandText = $dbQuery
Expand All @@ -356,7 +428,13 @@ function Get-ScoopDBItem {
$dbCommand.Dispose()
$dbAdapter.Dispose()
$db.Dispose()
return $result
# With $Version, the PRIMARY KEY guarantees at most one row; without it, the
# query is already limited to one name+bucket pair, so selecting latest needs no -GroupBy.
if ($Version) {
return $result
}

return Select-LatestScoopDBRow -Table $result
}
}

Expand Down
57 changes: 57 additions & 0 deletions test/Scoop-Database.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
Describe 'database version selection' -Tag 'Scoop' {
BeforeAll {
. "$PSScriptRoot\Scoop-TestLib.ps1"
. "$PSScriptRoot\..\lib\core.ps1"
. "$PSScriptRoot\..\lib\versions.ps1"
. "$PSScriptRoot\..\lib\database.ps1"
}

It 'chooses the semantically latest row within one result set' {
$rows = @(
[pscustomobject]@{ version = '1.0.7' }
[pscustomobject]@{ version = '1.0.31' }
)

(Get-LatestScoopDBRow -Rows $rows).version | Should -Be '1.0.31'
}

It 'returns null when no rows are provided' {
Get-LatestScoopDBRow -Rows @() | Should -Be $null
}

It 'returns the latest semantic version per name and bucket' {
$result = New-Object System.Data.DataTable
[void]$result.Columns.Add('name', [string])
[void]$result.Columns.Add('version', [string])
[void]$result.Columns.Add('bucket', [string])
[void]$result.Columns.Add('binary', [string])
[void]$result.Rows.Add('copilot-cli', '1.0.7', 'main', 'copilot')
[void]$result.Rows.Add('copilot-cli', '1.0.31', 'main', 'copilot')
[void]$result.Rows.Add('zotero', '7.0.9', 'extras', 'zotero')
[void]$result.Rows.Add('zotero', '7.0.20', 'extras', 'zotero')
[void]$result.Rows.Add('zotero', '7.0.9', 'he0119', 'zotero')
[void]$result.Rows.Add('zotero', '7.0.20', 'he0119', 'zotero')

$latest = @(Select-LatestScoopDBRow -Table $result -GroupBy @('name', 'bucket'))

$latest.Count | Should -Be 3
(@($latest | Where-Object { $_.name -eq 'copilot-cli' -and $_.bucket -eq 'main' })[0]).version | Should -Be '1.0.31'
(@($latest | Where-Object { $_.name -eq 'zotero' -and $_.bucket -eq 'extras' })[0]).version | Should -Be '7.0.20'
(@($latest | Where-Object { $_.name -eq 'zotero' -and $_.bucket -eq 'he0119' })[0]).version | Should -Be '7.0.20'
}

It 'returns the latest semantic version when no grouping is requested' {
$result = New-Object System.Data.DataTable
[void]$result.Columns.Add('name', [string])
[void]$result.Columns.Add('version', [string])
[void]$result.Columns.Add('bucket', [string])
[void]$result.Columns.Add('binary', [string])
[void]$result.Rows.Add('zotero', '7.0.9', 'extras', 'zotero')
[void]$result.Rows.Add('zotero', '7.0.20', 'extras', 'zotero')

$latest = @(Select-LatestScoopDBRow -Table $result)

$latest.Count | Should -Be 1
$latest[0].version | Should -Be '7.0.20'
}
}
Loading