Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
- **checkver:** Remove redundant always-true condition in GitHub checkver logic ([#6571](https://github.com/ScoopInstaller/Scoop/issues/6571))
- **core:** Give `dark` higher priority when use `Extract-DarkArchive` ([#6637](https://github.com/ScoopInstaller/Scoop/issues/6637))
- **checkver:** Harden github checkver ([#6641](https://github.com/ScoopInstaller/Scoop/issues/6641))
- **scoop-search:** Select latest search result semantically ([#6643](https://github.com/ScoopInstaller/Scoop/issues/6643))

### Code Refactoring

Expand Down
98 changes: 93 additions & 5 deletions lib/database.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,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 +290,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 +335,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,10 +353,101 @@ 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
}
}

<#
.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.
.PARAMETER Rows
System.Object[]
The rows to evaluate for the latest version. Each row must have a `version` property.
.INPUTS
System.Object[]
.OUTPUTS
System.Object
The latest row based on semantic versioning, or `$null` if no rows are provided.
#>
function Get-LatestScoopDBRow {
param(
[Parameter(Mandatory)]
[AllowEmptyCollection()]
[object[]]
$Rows
)

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

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

$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
System.Data.DataTable
The source table returned from a Scoop database query.
.PARAMETER GroupBy
System.String[]
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
Remove Scoop database item(s).
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