From edcb5ba9c7e0e04d65e2eaaaef236a07b9edbd3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Thu, 30 Apr 2026 12:25:30 +0400 Subject: [PATCH] Add CookiePatternFilter to push adoption filtering to SQL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The adoptUncategorisedPatterns method loaded all patterns for a banner then filtered in Go. This adds a CookiePatternFilter (match_type + cookie_category_id) and wires it into LoadAllByCookieBannerID so the two targeted loads only fetch the rows they need. Signed-off-by: Émile Ré --- pkg/cookiebanner/service.go | 2 +- pkg/cookiebanner/worker.go | 93 ++++++++++++++++++++++++++- pkg/coredata/cookie_pattern.go | 5 +- pkg/coredata/cookie_pattern_filter.go | 83 ++++++++++++++++++++++++ 4 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 pkg/coredata/cookie_pattern_filter.go diff --git a/pkg/cookiebanner/service.go b/pkg/cookiebanner/service.go index 272368b86..8d777c876 100644 --- a/pkg/cookiebanner/service.go +++ b/pkg/cookiebanner/service.go @@ -531,7 +531,7 @@ func (s *Service) ensureDraftVersionForBanner( } var allPatterns coredata.CookiePatterns - if err := allPatterns.LoadAllByCookieBannerID(ctx, tx, scope, bannerID); err != nil { + if err := allPatterns.LoadAllByCookieBannerID(ctx, tx, scope, bannerID, nil); err != nil { return nil, fmt.Errorf("cannot load cookie patterns: %w", err) } diff --git a/pkg/cookiebanner/worker.go b/pkg/cookiebanner/worker.go index d5c1c20a8..0f313bbcb 100644 --- a/pkg/cookiebanner/worker.go +++ b/pkg/cookiebanner/worker.go @@ -19,6 +19,7 @@ import ( "errors" "fmt" "sort" + "strings" "time" "go.gearno.de/kit/log" @@ -85,7 +86,7 @@ func (h *patternAnalysisHandler) Process(ctx context.Context, banner coredata.Co scope := coredata.NewScopeFromObjectID(banner.ID) var patterns coredata.CookiePatterns - if err := patterns.LoadAllByCookieBannerID(ctx, tx, scope, banner.ID); err != nil { + if err := patterns.LoadAllByCookieBannerID(ctx, tx, scope, banner.ID, nil); err != nil { return fmt.Errorf("cannot load patterns: %w", err) } @@ -147,6 +148,14 @@ func (h *patternAnalysisHandler) Process(ctx context.Context, banner coredata.Co ) } + adopted, err := h.adoptUncategorisedPatterns(ctx, tx, scope, banner) + if err != nil { + return fmt.Errorf("cannot adopt uncategorised patterns: %w", err) + } + if adopted { + merged = true + } + if merged { if _, err := h.svc.ensureDraftVersionForBanner(ctx, tx, scope, banner.ID); err != nil { return fmt.Errorf("cannot ensure draft version: %w", err) @@ -273,3 +282,85 @@ func mostCommonMaxAge(patterns []*coredata.CookiePattern) *int { v := entries[0].k.val return &v } + +func (h *patternAnalysisHandler) adoptUncategorisedPatterns( + ctx context.Context, + tx pg.Tx, + scope coredata.Scoper, + banner coredata.CookieBanner, +) (bool, error) { + var uncategorised coredata.CookieCategory + if err := uncategorised.LoadUncategorisedByCookieBannerID(ctx, tx, scope, banner.ID); err != nil { + if errors.Is(err, coredata.ErrResourceNotFound) { + return false, nil + } + return false, fmt.Errorf("cannot load uncategorised category: %w", err) + } + + prefixMatchType := coredata.CookiePatternMatchTypePrefix + var prefixPatterns coredata.CookiePatterns + if err := prefixPatterns.LoadAllByCookieBannerID( + ctx, + tx, + scope, + banner.ID, + coredata.NewCookiePatternFilter(&prefixMatchType, nil), + ); err != nil { + return false, fmt.Errorf("cannot load prefix patterns: %w", err) + } + + if len(prefixPatterns) == 0 { + return false, nil + } + + sort.Slice(prefixPatterns, func(i, j int) bool { + return len(prefixPatterns[i].Pattern) > len(prefixPatterns[j].Pattern) + }) + + exactMatchType := coredata.CookiePatternMatchTypeExact + var uncategorisedExact coredata.CookiePatterns + if err := uncategorisedExact.LoadAllByCookieBannerID( + ctx, + tx, + scope, + banner.ID, + coredata.NewCookiePatternFilter(&exactMatchType, &uncategorised.ID), + ); err != nil { + return false, fmt.Errorf("cannot load uncategorised exact patterns: %w", err) + } + + adopted := false + for _, ep := range uncategorisedExact { + var match *coredata.CookiePattern + for _, pp := range prefixPatterns { + if strings.HasPrefix(ep.Pattern, pp.Pattern) { + match = pp + break + } + } + + if match == nil { + continue + } + + var cookies coredata.Cookies + if err := cookies.RelinkByCookiePatternID(ctx, tx, scope, ep.ID, match.ID); err != nil { + return false, fmt.Errorf("cannot relink cookies from pattern %q: %w", ep.Pattern, err) + } + + if err := ep.Delete(ctx, tx, scope); err != nil { + return false, fmt.Errorf("cannot delete adopted exact pattern %q: %w", ep.Pattern, err) + } + + adopted = true + h.logger.InfoCtx( + ctx, + "adopted uncategorised exact pattern into prefix pattern", + log.String("exact_pattern", ep.Pattern), + log.String("prefix_pattern", match.Pattern), + log.String("banner_id", banner.ID.String()), + ) + } + + return adopted, nil +} diff --git a/pkg/coredata/cookie_pattern.go b/pkg/coredata/cookie_pattern.go index 8bbb24dc2..87cb5641a 100644 --- a/pkg/coredata/cookie_pattern.go +++ b/pkg/coredata/cookie_pattern.go @@ -331,6 +331,7 @@ func (cps *CookiePatterns) LoadAllByCookieBannerID( conn pg.Querier, scope Scoper, cookieBannerID gid.GID, + filter *CookiePatternFilter, ) error { q := ` SELECT @@ -351,14 +352,16 @@ FROM WHERE %s AND cookie_banner_id = @cookie_banner_id + AND %s ORDER BY created_at ASC, id ASC; ` - q = fmt.Sprintf(q, scope.SQLFragment()) + q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment()) args := pgx.StrictNamedArgs{"cookie_banner_id": cookieBannerID} maps.Copy(args, scope.SQLArguments()) + maps.Copy(args, filter.SQLArguments()) rows, err := conn.Query(ctx, q, args) if err != nil { diff --git a/pkg/coredata/cookie_pattern_filter.go b/pkg/coredata/cookie_pattern_filter.go new file mode 100644 index 000000000..31ba41118 --- /dev/null +++ b/pkg/coredata/cookie_pattern_filter.go @@ -0,0 +1,83 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package coredata + +import ( + "github.com/jackc/pgx/v5" + "go.probo.inc/probo/pkg/gid" +) + +type CookiePatternFilter struct { + matchType *CookiePatternMatchType + cookieCategoryID *gid.GID +} + +func NewCookiePatternFilter( + matchType *CookiePatternMatchType, + cookieCategoryID *gid.GID, +) *CookiePatternFilter { + return &CookiePatternFilter{ + matchType: matchType, + cookieCategoryID: cookieCategoryID, + } +} + +func (f *CookiePatternFilter) SQLFragment() string { + if f == nil { + return "TRUE" + } + + return ` +( + CASE + WHEN @has_match_type_filter::boolean = false THEN TRUE + WHEN @has_match_type_filter::boolean = true THEN + match_type = @filter_match_type::cookie_pattern_match_type + ELSE TRUE + END + AND + CASE + WHEN @has_cookie_category_id_filter::boolean = false THEN TRUE + WHEN @has_cookie_category_id_filter::boolean = true THEN + cookie_category_id = @filter_cookie_category_id::text + ELSE TRUE + END +)` +} + +func (f *CookiePatternFilter) SQLArguments() pgx.StrictNamedArgs { + if f == nil { + return pgx.StrictNamedArgs{} + } + + args := pgx.StrictNamedArgs{ + "has_match_type_filter": false, + "filter_match_type": nil, + "has_cookie_category_id_filter": false, + "filter_cookie_category_id": nil, + } + + if f.matchType != nil { + args["has_match_type_filter"] = true + args["filter_match_type"] = string(*f.matchType) + } + + if f.cookieCategoryID != nil { + args["has_cookie_category_id_filter"] = true + args["filter_cookie_category_id"] = *f.cookieCategoryID + } + + return args +}