From 1c7d2a0c8a80dd1cb268bbbbaf8293aec76dd60b Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Sat, 7 Jun 2025 10:47:53 -0400 Subject: [PATCH] Removes duplicates when listing software titles on 'All teams' (#29459) For #26375. When listing software titles for 'All teams', do not join against software installers nor vpps to avoid duplicates. Since filters related to software installers/VPP apps are no longer used when viewing titles for 'All teams', the filter dropdown is disabled if 'All teams' is selected. --- ...es-duplicates-when-listing-software-titles | 1 + .../SoftwareTable/SoftwareTable.tsx | 1 + server/datastore/mysql/software_titles.go | 252 ++++++++++-------- .../datastore/mysql/software_titles_test.go | 220 +++++++++++++++ .../select_software_titles_sql_fixture.gz | Bin 0 -> 29298 bytes server/datastore/mysql/testing_utils.go | 42 +++ server/service/integration_enterprise_test.go | 10 +- 7 files changed, 407 insertions(+), 119 deletions(-) create mode 100644 changes/26375-fixes-duplicates-when-listing-software-titles create mode 100644 server/datastore/mysql/testdata/select_software_titles_sql_fixture.gz diff --git a/changes/26375-fixes-duplicates-when-listing-software-titles b/changes/26375-fixes-duplicates-when-listing-software-titles new file mode 100644 index 0000000000..8effd5f842 --- /dev/null +++ b/changes/26375-fixes-duplicates-when-listing-software-titles @@ -0,0 +1 @@ +* Fixed bug when listing software titles for 'All teams' which caused duplicated entries. \ No newline at end of file diff --git a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTable.tsx b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTable.tsx index caecfe1de3..2d5b475400 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTable.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTable.tsx @@ -293,6 +293,7 @@ const SoftwareTable = ({ value={softwareFilter} className={`${baseClass}__software-filter`} options={SOFTWARE_TITLES_DROPDOWN_OPTIONS} + isDisabled={teamId === undefined} onChange={(newValue: SingleValue) => newValue && handleCustomFilterDropdownChange( diff --git a/server/datastore/mysql/software_titles.go b/server/datastore/mysql/software_titles.go index 25e46269f3..8251eb16c8 100644 --- a/server/datastore/mysql/software_titles.go +++ b/server/datastore/mysql/software_titles.go @@ -1,11 +1,13 @@ package mysql import ( + "bytes" "context" "database/sql" "fmt" "slices" "strings" + "text/template" "time" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" @@ -111,7 +113,10 @@ func (ds *Datastore) ListSoftwareTitles( } dbReader := ds.reader(ctx) - getTitlesStmt, args := selectSoftwareTitlesSQL(opt) + getTitlesStmt, args, err := selectSoftwareTitlesSQL(opt) + if err != nil { + return nil, 0, nil, ctxerr.Wrap(ctx, err, "building software titles select statement") + } // build the count statement before adding the pagination constraints to `getTitlesStmt` getTitlesCountStmt := fmt.Sprintf(`SELECT COUNT(DISTINCT s.id) FROM (%s) AS s`, getTitlesStmt) @@ -306,149 +311,166 @@ func spliceSecondaryOrderBySoftwareTitlesSQL(stmt string, opts fleet.ListOptions return strings.Replace(stmt, targetSubstr, targetSubstr+secondaryOrderBy, 1) } -func selectSoftwareTitlesSQL(opt fleet.SoftwareTitleListOptions) (string, []any) { +func selectSoftwareTitlesSQL(opt fleet.SoftwareTitleListOptions) (string, []any, error) { stmt := ` SELECT - st.id, - st.name, - st.source, - st.browser, - st.bundle_identifier, - MAX(COALESCE(sthc.hosts_count, 0)) as hosts_count, - MAX(COALESCE(sthc.updated_at, date('0001-01-01 00:00:00'))) as counts_updated_at, - si.self_service as package_self_service, - si.filename as package_name, - si.version as package_version, - si.platform as package_platform, - si.url AS package_url, - si.install_during_setup as package_install_during_setup, - si.storage_id as package_storage_id, - si.fleet_maintained_app_id, - vat.self_service as vpp_app_self_service, - vat.adam_id as vpp_app_adam_id, - vat.install_during_setup as vpp_install_during_setup, - vap.latest_version as vpp_app_version, - vap.platform as vpp_app_platform, - vap.icon_url as vpp_app_icon_url + st.id + ,st.name + ,st.source + ,st.browser + ,st.bundle_identifier + ,MAX(COALESCE(sthc.hosts_count, 0)) as hosts_count + ,MAX(COALESCE(sthc.updated_at, date('0001-01-01 00:00:00'))) as counts_updated_at + {{if hasTeamID .}} + ,si.self_service as package_self_service + ,si.filename as package_name + ,si.version as package_version + ,si.platform as package_platform + ,si.url AS package_url + ,si.install_during_setup as package_install_during_setup + ,si.storage_id as package_storage_id + ,si.fleet_maintained_app_id + ,vat.self_service as vpp_app_self_service + ,vat.adam_id as vpp_app_adam_id + ,vat.install_during_setup as vpp_install_during_setup + ,vap.latest_version as vpp_app_version + ,vap.platform as vpp_app_platform + ,vap.icon_url as vpp_app_icon_url + {{end}} FROM software_titles st -LEFT JOIN software_installers si ON si.title_id = st.id AND %s -LEFT JOIN vpp_apps vap ON vap.title_id = st.id AND %s -LEFT JOIN vpp_apps_teams vat ON vat.adam_id = vap.adam_id AND vat.platform = vap.platform AND %s -LEFT JOIN software_titles_host_counts sthc ON sthc.software_title_id = st.id AND (%s) --- placeholder for JOIN on software/software_cve -%s --- placeholder for optional extra WHERE filter -WHERE %s --- placeholder for filter based on software installed on hosts + software installers -AND (%s) -GROUP BY st.id, package_self_service, package_name, package_version, package_platform, package_url, package_install_during_setup, package_storage_id, fleet_maintained_app_id, vpp_app_self_service, vpp_app_adam_id, vpp_app_version, vpp_app_platform, vpp_app_icon_url, vpp_install_during_setup` - - cveJoinType := "LEFT" - if opt.VulnerableOnly { - cveJoinType = "INNER" - } - - countsJoin := "TRUE" - softwareInstallersJoinCond := "TRUE" - vppAppsJoinCond := "TRUE" - vppAppsTeamsJoinCond := "TRUE" - includeVPPAppsAndSoftwareInstallers := "TRUE" - switch { - case opt.TeamID == nil: - countsJoin = "sthc.team_id = 0 AND sthc.global_stats = 1" - // When opt.TeamID is nil (aka "All teams") we do not include VPP-apps/installers - // that are not installed on any host. - includeVPPAppsAndSoftwareInstallers = "FALSE" - case *opt.TeamID == 0: - countsJoin = "sthc.team_id = 0 AND sthc.global_stats = 0" - softwareInstallersJoinCond = fmt.Sprintf("si.global_or_team_id = %d", *opt.TeamID) - vppAppsTeamsJoinCond = fmt.Sprintf("vat.global_or_team_id = %d", *opt.TeamID) - case *opt.TeamID > 0: - countsJoin = fmt.Sprintf("sthc.team_id = %d AND sthc.global_stats = 0", *opt.TeamID) - softwareInstallersJoinCond = fmt.Sprintf("si.global_or_team_id = %d", *opt.TeamID) - vppAppsTeamsJoinCond = fmt.Sprintf("vat.global_or_team_id = %d", *opt.TeamID) - } - - if opt.PackagesOnly { - vppAppsJoinCond = "FALSE" - vppAppsTeamsJoinCond = "FALSE" - } - - additionalWhere := "TRUE" - match := opt.ListOptions.MatchQuery - softwareJoin := "" - if match != "" || opt.VulnerableOnly { - // if we do a match but not vulnerable only, we want a LEFT JOIN on - // software because software installers may not have entries in software - // for their software title. If we do want vulnerable only, then we have to - // INNER JOIN because a CVE implies a specific software version. - softwareJoin = fmt.Sprintf(` - %s JOIN software s ON s.title_id = st.id - -- placeholder for changing the JOIN type to filter vulnerable software - %[1]s JOIN software_cve scve ON s.id = scve.software_id - `, cveJoinType) - } - + {{if hasTeamID .}} + LEFT JOIN software_installers si ON si.title_id = st.id AND si.global_or_team_id = {{teamID .}} + LEFT JOIN vpp_apps vap ON vap.title_id = st.id AND {{yesNo .PackagesOnly "FALSE" "TRUE"}} + LEFT JOIN vpp_apps_teams vat ON vat.adam_id = vap.adam_id AND vat.platform = vap.platform AND + {{if .PackagesOnly}} FALSE {{else}} vat.global_or_team_id = {{teamID .}}{{end}} + {{end}} + LEFT JOIN software_titles_host_counts sthc ON sthc.software_title_id = st.id AND + (sthc.team_id = {{teamID .}} AND sthc.global_stats = {{if hasTeamID .}} 0 {{else}} 1 {{end}}) +{{with $softwareJoin := " "}} + {{if or $.ListOptions.MatchQuery $.VulnerableOnly}} + -- If we do a match but not vulnerable only, we want a LEFT JOIN on + -- software because software installers may not have entries in software + -- for their software title. If we do want vulnerable only, then we have to + -- INNER JOIN because a CVE implies a specific software version. + {{$cveJoin := yesNo $.VulnerableOnly "INNER" "LEFT"}} + {{$softwareJoin = printf "%s JOIN software s ON s.title_id = st.id %[1]s JOIN software_cve scve ON s.id = scve.software_id" $cveJoin }} + {{end}} + {{if and $.VulnerableOnly (or $.KnownExploit $.MinimumCVSS $.MaximumCVSS)}} + {{$softwareJoin = printf "%s INNER JOIN cve_meta cm ON scve.cve = cm.cve" $softwareJoin}} + {{if $.KnownExploit}} + {{$softwareJoin = printf "%s AND cm.cisa_known_exploit = 1" $softwareJoin}} + {{end}} + {{if $.MinimumCVSS}} + {{$softwareJoin = printf "%s AND cm.cvss_score >= ?" $softwareJoin}} + {{end}} + {{if $.MaximumCVSS}} + {{$softwareJoin = printf "%s AND cm.cvss_score <= ?" $softwareJoin}} + {{end}} + {{end}} + {{$softwareJoin}} +{{end}} +WHERE + {{with $additionalWhere := "TRUE"}} + {{if $.ListOptions.MatchQuery}} + {{$additionalWhere = "(st.name LIKE ? OR scve.cve LIKE ?)"}} + {{end}} + {{if and (hasTeamID $) $.Platform}} + {{$postfix := printf " AND (si.platform IN (%s) OR vap.platform IN (%[1]s))" (placeholders $.Platform)}} + {{$additionalWhere = printf "%s %s" $additionalWhere $postfix}} + {{end}} + {{$additionalWhere}} + {{end}} + -- If teamID is set, defaults to "a software installer or VPP app exists", and see next condition. + {{with $defFilter := yesNo (hasTeamID .) "(si.id IS NOT NULL OR vat.adam_id IS NOT NULL)" "FALSE"}} + -- add software installed for hosts if we're not filtering for "available for install" only + {{if not $.AvailableForInstall}} + {{$defFilter = $defFilter | printf " ( %s OR sthc.hosts_count > 0 ) "}} + {{ end }} + {{if and $.SelfServiceOnly (hasTeamID $)}} + {{$defFilter = $defFilter | printf "%s AND ( si.self_service = 1 OR vat.self_service = 1 ) "}} + {{end}} + AND ({{$defFilter}}) + {{end}} +GROUP BY + st.id + {{if hasTeamID .}} + ,package_self_service + ,package_name + ,package_version + ,package_platform + ,package_url + ,package_install_during_setup + ,package_storage_id + ,fleet_maintained_app_id + ,vpp_app_self_service + ,vpp_app_adam_id + ,vpp_app_version + ,vpp_app_platform + ,vpp_app_icon_url + ,vpp_install_during_setup + {{end}} +` var args []any if opt.VulnerableOnly && (opt.KnownExploit || opt.MinimumCVSS > 0 || opt.MaximumCVSS > 0) { - softwareJoin += ` - INNER JOIN cve_meta cm ON scve.cve = cm.cve - ` - if opt.KnownExploit { - softwareJoin += ` - AND cm.cisa_known_exploit = 1 - ` - } if opt.MinimumCVSS > 0 { - softwareJoin += ` - AND cm.cvss_score >= ? - ` args = append(args, opt.MinimumCVSS) } if opt.MaximumCVSS > 0 { - softwareJoin += ` - AND cm.cvss_score <= ? - ` args = append(args, opt.MaximumCVSS) } } - if match != "" { - additionalWhere = " (st.name LIKE ? OR scve.cve LIKE ?)" - match = likePattern(match) + if opt.ListOptions.MatchQuery != "" { + match := likePattern(opt.ListOptions.MatchQuery) args = append(args, match, match) } if opt.Platform != "" { platforms := strings.Split(strings.ReplaceAll(opt.Platform, "macos", "darwin"), ",") platformPlaceholders := strings.TrimSuffix(strings.Repeat("?,", len(platforms)), ",") - - additionalWhere += fmt.Sprintf(` AND (si.platform IN (%s) OR vap.platform IN (%s))`, platformPlaceholders, platformPlaceholders) args = slices.Grow(args, len(platformPlaceholders)*2) - for _, platform := range platforms { // for software installers + // for software installers + for _, platform := range platforms { args = append(args, platform) } - for _, platform := range platforms { // for VPP apps; could micro-optimize later by dropping non-Apple platforms + // for VPP apps; could micro-optimize later by dropping non-Apple platforms + for _, platform := range platforms { args = append(args, platform) } } - // default to "a software installer or VPP app exists", and see next condition. - defaultFilter := fmt.Sprintf(` - ((si.id IS NOT NULL OR vat.adam_id IS NOT NULL) AND %s) - `, includeVPPAppsAndSoftwareInstallers) - - // add software installed for hosts if we're not filtering for "available for install" only - if !opt.AvailableForInstall { - defaultFilter = ` ( ` + defaultFilter + ` OR sthc.hosts_count > 0 ) ` - } - if opt.SelfServiceOnly { - defaultFilter += ` AND ( si.self_service = 1 OR vat.self_service = 1 ) ` + t, err := template.New("stm").Funcs(map[string]any{ + "yesNo": func(b bool, yes string, no string) string { + if b { + return yes + } + return no + }, + "placeholders": func(val string) string { + vals := strings.Split(val, ",") + return strings.TrimSuffix(strings.Repeat("?,", len(vals)), ",") + }, + "hasTeamID": func(q fleet.SoftwareTitleListOptions) bool { + return q.TeamID != nil + }, + "teamID": func(q fleet.SoftwareTitleListOptions) uint { + if q.TeamID == nil { + return 0 + } + return *q.TeamID + }, + }).Parse(stmt) + if err != nil { + return "", nil, err } - stmt = fmt.Sprintf(stmt, softwareInstallersJoinCond, vppAppsJoinCond, vppAppsTeamsJoinCond, countsJoin, softwareJoin, additionalWhere, defaultFilter) - return stmt, args + var buff bytes.Buffer + if err = t.Execute(&buff, opt); err != nil { + return "", nil, err + } + + return buff.String(), args, nil } func (ds *Datastore) selectSoftwareVersionsSQL(titleIDs []uint, teamID *uint, tmFilter fleet.TeamFilter, withCounts bool) ( diff --git a/server/datastore/mysql/software_titles_test.go b/server/datastore/mysql/software_titles_test.go index 5ca78cdb4e..1ddc6bf810 100644 --- a/server/datastore/mysql/software_titles_test.go +++ b/server/datastore/mysql/software_titles_test.go @@ -1,10 +1,16 @@ package mysql import ( + "compress/gzip" "context" "database/sql" + "encoding/json" "fmt" + "os" + "path/filepath" + "reflect" "sort" + "strings" "testing" "time" @@ -34,6 +40,7 @@ func TestSoftwareTitles(t *testing.T) { {"UploadedSoftwareExists", testUploadedSoftwareExists}, {"ListSoftwareTitlesVulnerabilityFilters", testListSoftwareTitlesVulnerabilityFilters}, {"UpdateSoftwareTitleName", testUpdateSoftwareTitleName}, + {"ListSoftwareTitlesDoesnotIncludeDuplicates", testListSoftwareTitlesDoesnotIncludeDuplicates}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -1792,3 +1799,216 @@ func testUpdateSoftwareTitleName(t *testing.T, ds *Datastore) { require.NoError(t, err) require.Equal(t, "installer2", title2.Name) } + +func testListSoftwareTitlesDoesnotIncludeDuplicates(t *testing.T, ds *Datastore) { + ctx := context.Background() + + host := test.NewHost(t, ds, "host1", "1", "host1key", "host1uuid", time.Now()) + + _, err := ds.UpdateHostSoftware(ctx, host.ID, []fleet.Software{ + {Name: "Santa", Version: "2025.4", Source: "apps", BundleIdentifier: "com.northpolesec.santa"}, + }) + require.NoError(t, err) + + var sw []fleet.Software + err = ds.writer(ctx).SelectContext(ctx, &sw, + `SELECT id, name, version, bundle_identifier, source, browser, title_id FROM software ORDER BY name, source, browser, version`) + require.NoError(t, err) + require.Len(t, sw, 1) + require.NotNil(t, sw[0].TitleID) + + user := test.NewUser(t, ds, "Alice", "alice@example.com", true) + tfr1, err := fleet.NewTempFileReader(strings.NewReader("hello"), t.TempDir) + require.NoError(t, err) + + // same bundle identifier, different name + team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 1"}) + require.NoError(t, err) + _, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ + InstallerFile: tfr1, + BundleIdentifier: "com.northpolesec.santa", + Title: "Santa", + Version: "2025.2", + Extension: "pkg", + StorageID: "storage0", + Filename: "santa123", + Source: "pkg_packages", + UserID: user.ID, + TeamID: &team1.ID, + ValidatedLabels: &fleet.LabelIdentsWithScope{}, + }) + require.NoError(t, err) + + team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 2"}) + require.NoError(t, err) + _, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ + InstallerFile: tfr1, + BundleIdentifier: "com.northpolesec.santa", + Title: "Santa", + Version: "2025.3", + Extension: "pkg", + StorageID: "storage0", + Filename: "santa123", + Source: "pkg_packages", + UserID: user.ID, + TeamID: &team2.ID, + ValidatedLabels: &fleet.LabelIdentsWithScope{}, + }) + require.NoError(t, err) + + // We should only have a single title on the DB ... + var swt []fleet.SoftwareTitle + err = ds.writer(ctx).SelectContext(ctx, &swt, + `SELECT id, name, bundle_identifier, source, browser FROM software_titles ORDER BY name, source, browser`) + require.NoError(t, err) + require.Len(t, swt, 1) + + require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now())) + require.NoError(t, ds.ReconcileSoftwareTitles(ctx)) + require.NoError(t, ds.SyncHostsSoftwareTitles(ctx, time.Now())) + + titles, _, _, err := ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{ + ListOptions: fleet.ListOptions{ + OrderKey: "name", + OrderDirection: fleet.OrderAscending, + }, + }, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}) + require.NoError(t, err) + // We should have a single software title since when specifying 'All Teams' (TeamID = nil). + // installers are excluded + require.Len(t, titles, 1) +} + +func TestSelectSoftwareTitlesSQLGeneration(t *testing.T) { + fixturePath := filepath.Join("testdata", "select_software_titles_sql_fixture.gz") + + testData := []struct { + Args []any + Opts fleet.SoftwareTitleListOptions + Fingerprint string + }{} + + file, err := os.Open(fixturePath) + require.NoError(t, err) + defer file.Close() + + gzipReader, err := gzip.NewReader(file) + require.NoError(t, err) + defer gzipReader.Close() + + decoder := json.NewDecoder(gzipReader) + err = decoder.Decode(&testData) + require.NoError(t, err) + + for _, tt := range testData { + stm, args, err := selectSoftwareTitlesSQL(tt.Opts) + require.NoError(t, err) + require.Equal(t, tt.Fingerprint, NormalizeSQL(stm), tt.Opts) + require.Equal(t, tt.Args, args) + } +} + +// Use this to generate the select_software_titles_sql_fixture.gz testdata fixture. +// It generates a bunch of SoftwareTitleListOptions combinations, all SQL statements +// generated are normalized and written to the fixture file. +func generateSelectSoftwareTitlesSQLFixture(t *testing.T) { //nolint: unused + queryParams := struct { + Match []string + Platforms []string + VulnerableOnly []bool + AvailableForInstall []bool + SelfService []bool + KnownExploit []bool + MinVCSScores []float64 + MaxVCSScores []float64 + PackagesOnly []bool + TeamIDs []*uint + }{ + Match: []string{"", "chrome"}, + Platforms: []string{"", "darwin,linux"}, + VulnerableOnly: []bool{true, false}, + AvailableForInstall: []bool{true, false}, + SelfService: []bool{true, false}, + KnownExploit: []bool{true, false}, + MinVCSScores: []float64{0, 5.0}, + MaxVCSScores: []float64{0, 5.0}, + PackagesOnly: []bool{true, false}, + TeamIDs: []*uint{nil, ptr.Uint(0), ptr.Uint(1)}, + } + combinations := make([]fleet.SoftwareTitleListOptions, 0) + currentValues := make(map[string]interface{}) + + generateSoftwareTitleListOptionsCombinations( + reflect.ValueOf(queryParams), + currentValues, + &combinations, + ) + + testData := []struct { + Args []any + Opts fleet.SoftwareTitleListOptions + Fingerprint string + }{} + + for _, c := range combinations { + sqlStm, args, err := selectSoftwareTitlesSQL(c) + testData = append(testData, struct { + Args []any + Opts fleet.SoftwareTitleListOptions + Fingerprint string + }{Args: args, Opts: c, Fingerprint: NormalizeSQL(sqlStm)}) + require.NoError(t, err) + } + + asJSON, err := json.Marshal(testData) + require.NoError(t, err) + + fPath := filepath.Join("testdata", "select_software_titles_sql_fixture.gz") + + file, err := os.Create(fPath) + require.NoError(t, err) + defer file.Close() + + gzipWriter := gzip.NewWriter(file) + defer gzipWriter.Close() + + _, err = gzipWriter.Write(asJSON) + require.NoError(t, err) +} + +// nolint: unused +func generateSoftwareTitleListOptionsCombinations( + v reflect.Value, + currentValues map[string]interface{}, + combinations *[]fleet.SoftwareTitleListOptions, +) { + t := v.Type() + if len(currentValues) == t.NumField() { + opt := &fleet.SoftwareTitleListOptions{ + TeamID: currentValues["TeamIDs"].(*uint), + Platform: currentValues["Platforms"].(string), + VulnerableOnly: currentValues["VulnerableOnly"].(bool), + PackagesOnly: currentValues["PackagesOnly"].(bool), + SelfServiceOnly: currentValues["SelfService"].(bool), + AvailableForInstall: currentValues["AvailableForInstall"].(bool), + MinimumCVSS: currentValues["MinVCSScores"].(float64), + MaximumCVSS: currentValues["MaxVCSScores"].(float64), + KnownExploit: currentValues["KnownExploit"].(bool), + ListOptions: fleet.ListOptions{ + MatchQuery: currentValues["Match"].(string), + }, + } + *combinations = append(*combinations, *opt) + return + } + + fieldIndex := len(currentValues) + field := t.Field(fieldIndex) + slice := v.Field(fieldIndex) + + for i := 0; i < slice.Len(); i++ { + currentValues[field.Name] = slice.Index(i).Interface() + generateSoftwareTitleListOptionsCombinations(v, currentValues, combinations) + } + delete(currentValues, field.Name) +} diff --git a/server/datastore/mysql/testdata/select_software_titles_sql_fixture.gz b/server/datastore/mysql/testdata/select_software_titles_sql_fixture.gz new file mode 100644 index 0000000000000000000000000000000000000000..aaad2d7228e45935e73f9d372e888ed0bc3466f7 GIT binary patch literal 29298 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 YHEH=(=I{F`D8GL