Skip to content

Commit 7ca8673

Browse files
committed
chore(git): merge dev branch updates
Signed-off-by: Gabriel Harris-Rouquette <gabizou@me.com>
2 parents 0956a10 + 25e9027 commit 7ca8673

8 files changed

Lines changed: 73 additions & 86 deletions

File tree

cmd/frontend/main.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"time"
1111

1212
"github.com/exaring/otelpgx"
13-
"github.com/go-chi/httplog/v2"
13+
"github.com/go-chi/httplog/v3"
1414
"github.com/go-slog/otelslog"
1515
"github.com/jackc/pgx/v5/pgxpool"
1616
"github.com/klauspost/compress/gzhttp"
@@ -114,21 +114,21 @@ func NewOTel(lc fx.Lifecycle, logs *logging.Result) *otelsetup.Result {
114114

115115
func NewMux(fe *frontend.Server, otel *otelsetup.Result) http.Handler {
116116
mux := http.NewServeMux()
117-
118117
fe.RegisterRoutes(mux)
119118

120-
logger := httplog.NewLogger("soad-frontend", httplog.Options{
121-
LogLevel: slog.LevelInfo,
122-
JSON: true,
123-
Concise: true,
124-
RequestHeaders: true,
125-
Writer: os.Stderr,
126-
})
119+
logger := slog.New(slog.NewJSONHandler(os.Stderr, nil)).
120+
With(slog.String("service", "soad-frontend"))
127121

128122
handler := gzhttp.GzipHandler(otelhttp.NewHandler(mux, "soad-frontend"))
129-
loggedHandler := httplog.RequestLogger(logger)(handler)
130-
131-
// Outer mux: /healthz and /metrics bypass request logging.
123+
loggedHandler := httplog.RequestLogger(logger, &httplog.Options{
124+
Level: slog.LevelInfo,
125+
Schema: httplog.SchemaOTEL,
126+
RecoverPanics: true,
127+
LogRequestHeaders: []string{"User-Agent", "Referer"},
128+
})(handler)
129+
130+
// Outer mux: /healthz and /metrics bypass tracing, gzip, and request logging
131+
// to keep probe/scrape traffic off the observability pipelines.
132132
outer := http.NewServeMux()
133133
outer.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
134134
w.WriteHeader(http.StatusOK)

cmd/server/main.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
"time"
1212

1313
"github.com/exaring/otelpgx"
14-
"github.com/go-chi/httplog/v2"
14+
"github.com/go-chi/httplog/v3"
1515
"github.com/go-slog/otelslog"
1616
"github.com/jackc/pgx/v5/pgxpool"
1717
"github.com/spongepowered/systemofadownload/internal/logging"
@@ -172,18 +172,19 @@ func NewMux(h *httpapi.Handler, cfg *Config, otel *otelsetup.Result) http.Handle
172172
apiHandler := api.NewStrictHandler(h, middlewares)
173173
mux := http.NewServeMux()
174174

175-
logger := httplog.NewLogger("soad-server", httplog.Options{
176-
LogLevel: slog.LevelInfo,
177-
JSON: true,
178-
Concise: true,
179-
RequestHeaders: true,
180-
Writer: os.Stderr,
181-
})
175+
logger := slog.New(slog.NewJSONHandler(os.Stderr, nil)).
176+
With(slog.String("service", "soad-server"))
182177

183178
handler := otelhttp.NewHandler(api.HandlerFromMux(apiHandler, mux), "soad-server")
184-
loggedHandler := httplog.RequestLogger(logger)(handler)
185-
186-
// Outer mux: /healthz and /metrics bypass request logging.
179+
loggedHandler := httplog.RequestLogger(logger, &httplog.Options{
180+
Level: slog.LevelInfo,
181+
Schema: httplog.SchemaOTEL,
182+
RecoverPanics: true,
183+
LogRequestHeaders: []string{"User-Agent", "Referer"},
184+
})(handler)
185+
186+
// Outer mux: /healthz and /metrics bypass tracing and request logging
187+
// to keep probe/scrape traffic off the observability pipelines.
187188
outer := http.NewServeMux()
188189
outer.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
189190
w.WriteHeader(http.StatusOK)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DROP INDEX IF EXISTS idx_versioned_tags_av_key_value;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
CREATE INDEX idx_versioned_tags_av_key_value
2+
ON artifact_versioned_tags (artifact_version_id, tag_key, tag_value);

db/schema.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,7 @@ CREATE TABLE artifact_versioned_tags (
4646
);
4747

4848
CREATE INDEX idx_versioned_tags_key_value ON artifact_versioned_tags(tag_key, tag_value, artifact_version_id);
49+
CREATE INDEX idx_versioned_tags_av_key_value ON artifact_versioned_tags(artifact_version_id, tag_key, tag_value);
50+
CREATE INDEX idx_versioned_assets_version_id ON artifact_versioned_assets(artifact_version_id);
4951
CREATE INDEX idx_versions_artifact_sort ON artifact_versions(artifact_id, sort_order DESC);
5052
CREATE INDEX idx_versions_artifact_recommended_sort ON artifact_versions(artifact_id, recommended, sort_order DESC);

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.26.1
44

55
require (
66
github.com/exaring/otelpgx v0.10.0
7-
github.com/go-chi/httplog/v2 v2.1.1
7+
github.com/go-chi/httplog/v3 v3.3.0
88
github.com/go-git/go-git/v5 v5.18.0
99
github.com/go-slog/otelslog v0.3.0
1010
github.com/google/go-cmp v0.7.0

go.sum

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -69,16 +69,14 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
6969
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
7070
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
7171
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
72-
github.com/go-chi/httplog/v2 v2.1.1 h1:ojojiu4PIaoeJ/qAO4GWUxJqvYUTobeo7zmuHQJAxRk=
73-
github.com/go-chi/httplog/v2 v2.1.1/go.mod h1:/XXdxicJsp4BA5fapgIC3VuTD+z0Z/VzukoB3VDc1YE=
72+
github.com/go-chi/httplog/v3 v3.3.0 h1:Gr6Y7nSzbpyCyRwKPOVKjDH3BH6TH5uvRNDsTZWDpvU=
73+
github.com/go-chi/httplog/v3 v3.3.0/go.mod h1:N/J1l5l1fozUrqIVuT8Z/HzNeSy8TF2EFyokPLe6y2w=
7474
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
7575
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
7676
github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0=
7777
github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY=
7878
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
7979
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
80-
github.com/go-git/go-git/v5 v5.17.2 h1:B+nkdlxdYrvyFK4GPXVU8w1U+YkbsgciIR7f2sZJ104=
81-
github.com/go-git/go-git/v5 v5.17.2/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo=
8280
github.com/go-git/go-git/v5 v5.18.0 h1:O831KI+0PR51hM2kep6T8k+w0/LIAD490gvqMCvL5hM=
8381
github.com/go-git/go-git/v5 v5.18.0/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo=
8482
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -238,42 +236,24 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
238236
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
239237
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
240238
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
241-
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
242-
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
243239
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
244240
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
245-
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 h1:H7O6RlGOMTizyl3R08Kn5pdM06bnH8oscSj7o11tmLA=
246-
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0/go.mod h1:mBFWu/WOVDkWWsR7Tx7h6EpQB8wsv7P0Yrh0Pb7othc=
247241
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY=
248242
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg=
249-
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=
250-
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw=
251243
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
252244
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
253-
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o=
254-
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc=
255245
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
256246
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
257247
go.opentelemetry.io/otel/exporters/prometheus v0.64.0 h1:g0LRDXMX/G1SEZtK8zl8Chm4K6GBwRkjPKE36LxiTYs=
258248
go.opentelemetry.io/otel/exporters/prometheus v0.64.0/go.mod h1:UrgcjnarfdlBDP3GjDIJWe6HTprwSazNjwsI+Ru6hro=
259-
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
260-
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
261249
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
262250
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
263-
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
264-
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
265251
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
266252
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
267-
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
268-
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
269253
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
270254
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
271-
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
272-
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
273255
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
274256
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
275-
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
276-
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
277257
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
278258
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
279259
go.temporal.io/api v1.62.7 h1:joCtF30Dr+ynzrFJySewZsWbyf4AETZpuizHhFIyj/o=
@@ -355,18 +335,12 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
355335
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
356336
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
357337
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
358-
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
359-
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
360-
google.golang.org/genproto/googleapis/api v0.0.0-20260319164105-cd36c79ee9b0 h1:f3D8mgSgZxY7SMrLKd02nLQ9izWkzkvisJvVNgkts1w=
361-
google.golang.org/genproto/googleapis/api v0.0.0-20260319164105-cd36c79ee9b0/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y=
338+
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
339+
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
362340
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
363341
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
364-
google.golang.org/genproto/googleapis/rpc v0.0.0-20260319164105-cd36c79ee9b0 h1:WrVi7iN1ydLVeyPGOLBXhf09uZtKZEpS69YmVVnF1cg=
365-
google.golang.org/genproto/googleapis/rpc v0.0.0-20260319164105-cd36c79ee9b0/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
366342
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
367343
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
368-
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
369-
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
370344
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
371345
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
372346
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

internal/repository/repository.go

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -316,21 +316,20 @@ type VersionsWithAssetsResult struct {
316316

317317
// ListVersionsWithAssets returns paginated versions with pre-aggregated assets in a single query.
318318
// This is a frontend-optimized query that eliminates the N+1 pattern for the downloads page.
319+
//
320+
// Shape: filter versions in a CTE, paginate, THEN join assets — so the asset
321+
// LEFT JOIN and jsonb_agg only run for the current page, not the whole match set.
322+
// Each tag is an EXISTS subquery that hits the (artifact_version_id, tag_key)
323+
// primary key on artifact_versioned_tags.
319324
func (q *querierWithConn) ListVersionsWithAssets(ctx context.Context, params VersionQueryParams) (*VersionsWithAssetsResult, error) {
320325
var args []any
321326
argN := 1
322327

323-
query := `SELECT av.id, av.version, av.recommended, av.commit_body,
324-
COUNT(*) OVER() AS total_count,
325-
COALESCE(jsonb_agg(jsonb_build_object(
326-
'classifier', ava.classifier,
327-
'download_url', ava.download_url,
328-
'extension', ava.extension
329-
)) FILTER (WHERE ava.id IS NOT NULL), '[]'::jsonb) AS assets_json
330-
FROM artifact_versions av
331-
JOIN artifacts a ON av.artifact_id = a.id
332-
LEFT JOIN artifact_versioned_assets ava ON ava.artifact_version_id = av.id
333-
WHERE a.group_id = $` + strconv.Itoa(argN)
328+
query := `WITH filtered AS (
329+
SELECT av.id, av.version, av.recommended, av.commit_body, av.sort_order
330+
FROM artifact_versions av
331+
JOIN artifacts a ON av.artifact_id = a.id
332+
WHERE a.group_id = $` + strconv.Itoa(argN)
334333
args = append(args, params.GroupID)
335334
argN++
336335

@@ -344,31 +343,39 @@ WHERE a.group_id = $` + strconv.Itoa(argN)
344343
argN++
345344
}
346345

347-
if len(params.Tags) > 0 {
348-
query += ` AND av.id IN (SELECT t.artifact_version_id FROM artifact_versioned_tags t WHERE (`
349-
i := 0
350-
for key, value := range params.Tags {
351-
if i > 0 {
352-
query += ` OR `
353-
}
354-
query += `(t.tag_key = $` + strconv.Itoa(argN) +
355-
` AND (t.tag_value = $` + strconv.Itoa(argN+1) +
356-
` OR t.tag_value LIKE $` + strconv.Itoa(argN+2) + `))`
357-
args = append(args, key, value, value+".%")
358-
argN += 3
359-
i++
360-
}
361-
query += `) GROUP BY t.artifact_version_id HAVING COUNT(DISTINCT t.tag_key) = $` + strconv.Itoa(argN) + `)`
362-
args = append(args, len(params.Tags))
363-
argN++
346+
// Prefix matching with dot boundary: "minecraft:1.12" matches "1.12" and
347+
// "1.12.2" but NOT "1.120". Each tag becomes an independent EXISTS hitting
348+
// the (artifact_version_id, tag_key) PK on artifact_versioned_tags.
349+
for key, value := range params.Tags {
350+
query += ` AND EXISTS (SELECT 1 FROM artifact_versioned_tags` +
351+
` WHERE artifact_version_id = av.id` +
352+
` AND tag_key = $` + strconv.Itoa(argN) +
353+
` AND (tag_value = $` + strconv.Itoa(argN+1) +
354+
` OR tag_value LIKE $` + strconv.Itoa(argN+2) + `))`
355+
args = append(args, key, value, value+".%")
356+
argN += 3
364357
}
365358

366-
query += ` GROUP BY av.id, av.version, av.recommended, av.commit_body, av.sort_order`
367-
query += ` ORDER BY av.sort_order DESC`
368-
query += ` LIMIT $` + strconv.Itoa(argN)
359+
query += `
360+
), page AS (
361+
SELECT f.*, COUNT(*) OVER() AS total_count
362+
FROM filtered f
363+
ORDER BY f.sort_order DESC
364+
LIMIT $` + strconv.Itoa(argN)
369365
args = append(args, params.Limit)
370366
argN++
371-
query += ` OFFSET $` + strconv.Itoa(argN)
367+
query += ` OFFSET $` + strconv.Itoa(argN) + `
368+
)
369+
SELECT p.id, p.version, p.recommended, p.commit_body, p.total_count,
370+
COALESCE(jsonb_agg(jsonb_build_object(
371+
'classifier', ava.classifier,
372+
'download_url', ava.download_url,
373+
'extension', ava.extension
374+
)) FILTER (WHERE ava.id IS NOT NULL), '[]'::jsonb) AS assets_json
375+
FROM page p
376+
LEFT JOIN artifact_versioned_assets ava ON ava.artifact_version_id = p.id
377+
GROUP BY p.id, p.version, p.recommended, p.commit_body, p.total_count, p.sort_order
378+
ORDER BY p.sort_order DESC`
372379
args = append(args, params.Offset)
373380

374381
rows, err := q.conn.Query(ctx, query, args...)

0 commit comments

Comments
 (0)