🐛 Fix deduplicated storage overcounting in media-storage-bytes-per-team quote

The SQL query sql:get-media-storage-bytes-per-team used UNION ALL
across six SELECT branches that each produce a so_id reference.
When deduplication causes multiple file_media_object or
team_font_variant rows to point at the same storage_object, UNION ALL
counts that objects size once per reference — inflating "used bytes"
and causing false :max-quote-reached rejections.

Change all five UNION ALL to UNION so that duplicate so_id values are
collapsed before the JOIN storage_object / SUM(so.size).

Add a test (media-storage-bytes-quote-deduped) that creates one
storage_object referenced by two file_media_object rows and asserts
the computed usage reflects the deduplicated physical size, not 2x.

AI-assisted-by: mimo-v2.5
This commit is contained in:
Andrey Antukh 2026-08-17 12:01:03 +00:00
parent 049205d0fc
commit 6d481eb15b
2 changed files with 51 additions and 10 deletions

View File

@ -568,8 +568,8 @@
WHERE p.team_id = ?
AND fmo.deleted_at IS NULL
AND f.deleted_at IS NULL
UNION ALL
SELECT fmo.thumbnail_id AS so_id
UNION
SELECT fmo.thumbnail_id AS so_id
FROM file_media_object AS fmo
JOIN file AS f ON (f.id = fmo.file_id)
JOIN project AS p ON (p.id = f.project_id)
@ -577,26 +577,26 @@
AND fmo.thumbnail_id IS NOT NULL
AND fmo.deleted_at IS NULL
AND f.deleted_at IS NULL
UNION ALL
SELECT v.otf_file_id AS so_id
UNION
SELECT v.otf_file_id AS so_id
FROM team_font_variant AS v
WHERE v.team_id = ?
AND v.otf_file_id IS NOT NULL
AND v.deleted_at IS NULL
UNION ALL
SELECT v.ttf_file_id AS so_id
UNION
SELECT v.ttf_file_id AS so_id
FROM team_font_variant AS v
WHERE v.team_id = ?
AND v.ttf_file_id IS NOT NULL
AND v.deleted_at IS NULL
UNION ALL
SELECT v.woff1_file_id AS so_id
UNION
SELECT v.woff1_file_id AS so_id
FROM team_font_variant AS v
WHERE v.team_id = ?
AND v.woff1_file_id IS NOT NULL
AND v.deleted_at IS NULL
UNION ALL
SELECT v.woff2_file_id AS so_id
UNION
SELECT v.woff2_file_id AS so_id
FROM team_font_variant AS v
WHERE v.team_id = ?
AND v.woff2_file_id IS NOT NULL

View File

@ -400,6 +400,47 @@
;; total=0, incr=500, best quote=1000 → 0+500 <= 1000 → ok
(check-ok! "allowed by team+profile quote"))))
(t/deftest media-storage-bytes-quote-deduped
(with-mocks [mock {:target 'app.config/get
:return (th/config-get-mock
{:quotes-media-storage-bytes-per-team 1100})}]
(let [prof (th/create-profile* 1)
team-id (:default-team-id prof)
proj (th/create-project* 1 {:profile-id (:id prof)
:team-id team-id})
file1 (th/create-file* 1 {:profile-id (:id prof)
:project-id (:id proj)
:is-shared false})
file2 (th/create-file* 2 {:profile-id (:id prof)
:project-id (:id proj)
:is-shared false})
;; One physical storage object of 500 bytes
so-id (uuid/random)
_ (th/db-insert! :storage-object {:id so-id
:size 500
:backend "test"})
;; Two file_media_object rows pointing at the SAME storage object
;; (simulates the deduplication path: same content uploaded twice)
_ (th/create-file-media-object*
{:file-id (:id file1) :media-id so-id
:name "icon" :mtype "image/svg+xml"})
_ (th/create-file-media-object*
{:file-id (:id file2) :media-id so-id
:name "icon" :mtype "image/svg+xml"})
data {::quotes/id ::quotes/media-storage-bytes-per-team
::quotes/profile-id (:id prof)
::quotes/team-id team-id
::quotes/incr 200}]
;; Physical size is 500. With UNION (correct), total=500, 500+200=700 ≤ 1100 → ok.
;; With UNION ALL (buggy), total=1000, 1000+200=1200 > 1100 → rejected.
(quotes/check! th/*system* data)
(t/is (true? true) "deduped storage counted once, under quota"))))
(t/deftest media-upload-enforces-storage-quote
(with-mocks [mock {:target 'app.config/get
:return (th/config-get-mock