Skip to content

Commit a7a7d34

Browse files
authored
fix(scoop-search): Select latest search result semantically (#6643)
1 parent 3533442 commit a7a7d34

3 files changed

Lines changed: 151 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
- **checkver:** Remove redundant always-true condition in GitHub checkver logic ([#6571](https://github.com/ScoopInstaller/Scoop/issues/6571))
3636
- **core:** Give `dark` higher priority when use `Extract-DarkArchive` ([#6637](https://github.com/ScoopInstaller/Scoop/issues/6637))
3737
- **checkver:** Harden github checkver ([#6641](https://github.com/ScoopInstaller/Scoop/issues/6641))
38+
- **scoop-search:** Select latest search result semantically ([#6643](https://github.com/ScoopInstaller/Scoop/issues/6643))
3839

3940
### Code Refactoring
4041

lib/database.ps1

Lines changed: 93 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,6 @@ function Find-ScoopDBItem {
277277
$dbAdapter = New-Object -TypeName System.Data.SQLite.SQLiteDataAdapter
278278
$result = New-Object System.Data.DataTable
279279
$dbQuery = "SELECT * FROM app WHERE $(($From -join ' LIKE @Pattern OR ') + ' LIKE @Pattern')"
280-
$dbQuery = "SELECT * FROM ($($dbQuery + ' ORDER BY version DESC')) GROUP BY name, bucket"
281280
$dbCommand = $db.CreateCommand()
282281
$dbCommand.CommandText = $dbQuery
283282
$dbCommand.CommandType = [System.Data.CommandType]::Text
@@ -291,7 +290,7 @@ function Find-ScoopDBItem {
291290
$dbCommand.Dispose()
292291
$dbAdapter.Dispose()
293292
$db.Dispose()
294-
return $result
293+
return Select-LatestScoopDBRow -Table $result -GroupBy @('name', 'bucket')
295294
}
296295
}
297296

@@ -336,8 +335,6 @@ function Get-ScoopDBItem {
336335
$dbQuery = 'SELECT * FROM app WHERE name = @Name AND bucket = @Bucket'
337336
if ($Version) {
338337
$dbQuery += ' AND version = @Version'
339-
} else {
340-
$dbQuery += ' ORDER BY version DESC LIMIT 1'
341338
}
342339
$dbCommand = $db.CreateCommand()
343340
$dbCommand.CommandText = $dbQuery
@@ -356,10 +353,101 @@ function Get-ScoopDBItem {
356353
$dbCommand.Dispose()
357354
$dbAdapter.Dispose()
358355
$db.Dispose()
359-
return $result
356+
# With $Version, the PRIMARY KEY guarantees at most one row; without it, the
357+
# query is already limited to one name+bucket pair, so selecting latest needs no -GroupBy.
358+
if ($Version) {
359+
return $result
360+
}
361+
362+
return Select-LatestScoopDBRow -Table $result
360363
}
361364
}
362365

366+
<#
367+
.SYNOPSIS
368+
Get the latest row from a set of Scoop database rows.
369+
.DESCRIPTION
370+
Compares the `version` property of each row semantically and returns the
371+
latest row. Returns `$null` when no rows are provided.
372+
.PARAMETER Rows
373+
System.Object[]
374+
The rows to evaluate for the latest version. Each row must have a `version` property.
375+
.INPUTS
376+
System.Object[]
377+
.OUTPUTS
378+
System.Object
379+
The latest row based on semantic versioning, or `$null` if no rows are provided.
380+
#>
381+
function Get-LatestScoopDBRow {
382+
param(
383+
[Parameter(Mandatory)]
384+
[AllowEmptyCollection()]
385+
[object[]]
386+
$Rows
387+
)
388+
389+
if (-not $Rows -or $Rows.Count -eq 0) {
390+
return $null
391+
}
392+
393+
if (-not (Get-Command Compare-Version -ErrorAction Ignore)) {
394+
. "$PSScriptRoot\versions.ps1"
395+
}
396+
397+
$latest = $Rows[0]
398+
for ($i = 1; $i -lt $Rows.Count; $i++) {
399+
$row = $Rows[$i]
400+
if ((Compare-Version -ReferenceVersion $latest.version -DifferenceVersion $row.version) -gt 0) {
401+
$latest = $row
402+
}
403+
}
404+
405+
return $latest
406+
}
407+
408+
<#
409+
.SYNOPSIS
410+
Return the semantically latest row or rows from a Scoop database result set.
411+
.DESCRIPTION
412+
Clones the schema of `Table` and imports the latest row per group when
413+
`GroupBy` is supplied, or the single latest row across the whole table when
414+
it is omitted. Returns an empty cloned table when the source table has no rows.
415+
.PARAMETER Table
416+
System.Data.DataTable
417+
The source table returned from a Scoop database query.
418+
.PARAMETER GroupBy
419+
System.String[]
420+
Optional column names used to group rows before semantic latest-row selection.
421+
.OUTPUTS
422+
System.Data.DataTable
423+
A cloned table containing the latest matching row for each requested scope.
424+
#>
425+
function Select-LatestScoopDBRow {
426+
param(
427+
[Parameter(Mandatory)]
428+
[System.Data.DataTable]
429+
$Table,
430+
[string[]]
431+
$GroupBy
432+
)
433+
434+
$latestRows = $Table.Clone()
435+
$rows = @($Table.Rows)
436+
if ($rows.Count -eq 0) {
437+
return $latestRows
438+
}
439+
440+
if ($GroupBy -and $GroupBy.Count -gt 0) {
441+
foreach ($group in ($rows | Group-Object -Property $GroupBy)) {
442+
$latestRows.ImportRow((Get-LatestScoopDBRow -Rows @($group.Group)))
443+
}
444+
} else {
445+
$latestRows.ImportRow((Get-LatestScoopDBRow -Rows $rows))
446+
}
447+
448+
return $latestRows
449+
}
450+
363451
<#
364452
.SYNOPSIS
365453
Remove Scoop database item(s).

test/Scoop-Database.Tests.ps1

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
Describe 'database version selection' -Tag 'Scoop' {
2+
BeforeAll {
3+
. "$PSScriptRoot\Scoop-TestLib.ps1"
4+
. "$PSScriptRoot\..\lib\core.ps1"
5+
. "$PSScriptRoot\..\lib\versions.ps1"
6+
. "$PSScriptRoot\..\lib\database.ps1"
7+
}
8+
9+
It 'chooses the semantically latest row within one result set' {
10+
$rows = @(
11+
[pscustomobject]@{ version = '1.0.7' }
12+
[pscustomobject]@{ version = '1.0.31' }
13+
)
14+
15+
(Get-LatestScoopDBRow -Rows $rows).version | Should -Be '1.0.31'
16+
}
17+
18+
It 'returns null when no rows are provided' {
19+
Get-LatestScoopDBRow -Rows @() | Should -Be $null
20+
}
21+
22+
It 'returns the latest semantic version per name and bucket' {
23+
$result = New-Object System.Data.DataTable
24+
[void]$result.Columns.Add('name', [string])
25+
[void]$result.Columns.Add('version', [string])
26+
[void]$result.Columns.Add('bucket', [string])
27+
[void]$result.Columns.Add('binary', [string])
28+
[void]$result.Rows.Add('copilot-cli', '1.0.7', 'main', 'copilot')
29+
[void]$result.Rows.Add('copilot-cli', '1.0.31', 'main', 'copilot')
30+
[void]$result.Rows.Add('zotero', '7.0.9', 'extras', 'zotero')
31+
[void]$result.Rows.Add('zotero', '7.0.20', 'extras', 'zotero')
32+
[void]$result.Rows.Add('zotero', '7.0.9', 'he0119', 'zotero')
33+
[void]$result.Rows.Add('zotero', '7.0.20', 'he0119', 'zotero')
34+
35+
$latest = @(Select-LatestScoopDBRow -Table $result -GroupBy @('name', 'bucket'))
36+
37+
$latest.Count | Should -Be 3
38+
(@($latest | Where-Object { $_.name -eq 'copilot-cli' -and $_.bucket -eq 'main' })[0]).version | Should -Be '1.0.31'
39+
(@($latest | Where-Object { $_.name -eq 'zotero' -and $_.bucket -eq 'extras' })[0]).version | Should -Be '7.0.20'
40+
(@($latest | Where-Object { $_.name -eq 'zotero' -and $_.bucket -eq 'he0119' })[0]).version | Should -Be '7.0.20'
41+
}
42+
43+
It 'returns the latest semantic version when no grouping is requested' {
44+
$result = New-Object System.Data.DataTable
45+
[void]$result.Columns.Add('name', [string])
46+
[void]$result.Columns.Add('version', [string])
47+
[void]$result.Columns.Add('bucket', [string])
48+
[void]$result.Columns.Add('binary', [string])
49+
[void]$result.Rows.Add('zotero', '7.0.9', 'extras', 'zotero')
50+
[void]$result.Rows.Add('zotero', '7.0.20', 'extras', 'zotero')
51+
52+
$latest = @(Select-LatestScoopDBRow -Table $result)
53+
54+
$latest.Count | Should -Be 1
55+
$latest[0].version | Should -Be '7.0.20'
56+
}
57+
}

0 commit comments

Comments
 (0)