Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ jobs:
- name: Replay the DOS-Me search rating proposal (Docker)
run: node scripts/test-search-rating-score-proposal.mjs

- name: Replay the DOS-Me review value proposal (Docker)
run: pnpm --filter @repo/shared-data build && node scripts/test-review-value-proposal.mjs

- name: Check generated rate schema types match the migrations
run: pnpm check:db-types

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ playwright-report-item-model/
!Docs/Proposals/2026-11-02-dos-me-search-rating-score.sql
!Docs/Proposals/2026-11-02-dos-me-search-rating-score-rollback.sql
!Docs/Proposals/2026-11-02-dos-me-search-rating-score-verify.sql
!Docs/Proposals/2026-11-02-dos-me-review-value-score.sql
!Docs/Proposals/2026-11-02-dos-me-review-value-score-rollback.sql
!Docs/Proposals/2026-11-02-dos-me-review-value-score-verify.sql
* - Copy*
* - Bản sao*
*.dump
Expand Down
109 changes: 109 additions & 0 deletions Docs/Proposals/2026-11-02-dos-me-review-value-score-rollback.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
-- ROLLBACK of the PROPOSAL 2026-11-02-dos-me-review-value-score.sql (DOS/Rate#1547).
-- Two transactions:
-- 1. puts back P2's rating function (byte for byte from 20261101120000_rate_criteria_ratings.sql),
-- drops the triggers on rate.review_values, and refreshes every master a stored value could
-- have lifted; no table lock beyond the rows it updates;
-- 2. drops the writer, the reader, the factor, rate.review_values (its values are lost: the
-- scoring job recomputes them), the tier column and its CHECK, under a short ACCESS
-- EXCLUSIVE lock with a 5 s timeout (re-run the file if it times out). The CHECK keeping
-- P2's public number columns empty stays (D7).
-- The refresh skips a master another transaction holds at that moment (FOR UPDATE SKIP LOCKED in
-- P2), so stop the scoring job first and afterwards run V5 of the verify file: any master it
-- counts needs SELECT rate.refresh_master_rating_extras(id).
-- Safe to run twice.

BEGIN;

CREATE OR REPLACE FUNCTION rate.master_rating_extras(p_master_id uuid)
RETURNS TABLE (rating_score numeric, rating_weight_sum numeric, criteria_scores jsonb)
LANGUAGE plpgsql
STABLE
SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
v_item uuid := rate.master_rating_item(p_master_id);
BEGIN
RETURN QUERY EXECUTE format($q$
WITH eligible AS (
SELECT LEAST(3.0, r.helpfulness_weight)::numeric AS w,
COALESCE(r.rating_exact, r.rating_star)::numeric AS star,
r.criteria_ratings
FROM rate.products p
JOIN rate.reviews r ON r.product_id = p.id
JOIN rate.review_flags f ON f.review_id = r.id
WHERE %s = $1
AND p.origin = 'crawl'
AND r.origin IN ('crawl', 'community')
AND r.reviewer_type = 'user'
AND COALESCE(f.include_in_rating, true)
AND r.helpfulness_weight > 0
),
overall AS (
SELECT round(COALESCE(sum(w), 0), 2) AS weight_sum,
CASE WHEN sum(w) > 0 THEN round((3 * 4.2 + sum(w * star)) / (3 + sum(w)), 3) END AS score
FROM eligible
WHERE star BETWEEN 1 AND 5
),
per_criterion AS (
SELECT c.key,
count(*) AS writers,
round(sum(e.w), 2) AS weight_sum,
sum(LEAST(1.0, e.w)) AS shown_sum,
round((3 * 4.2 + sum(e.w * (c.value #>> '{}')::numeric)) / (3 + sum(e.w)), 3) AS score
FROM eligible e
CROSS JOIN LATERAL jsonb_each(
CASE WHEN jsonb_typeof(e.criteria_ratings) = 'object' THEN e.criteria_ratings
ELSE '{}'::jsonb END
) AS c(key, value)
WHERE c.key ~ '^[a-z_]{2,32}$'
AND jsonb_typeof(c.value) = 'number'
AND (c.value #>> '{}')::numeric BETWEEN 1 AND 5
GROUP BY c.key
)
SELECT o.score,
o.weight_sum,
COALESCE(
(SELECT jsonb_object_agg(pc.key, jsonb_build_object(
'score', pc.score, 'weight_sum', pc.weight_sum, 'writers', pc.writers))
FROM per_criterion pc
WHERE pc.writers >= 3 AND pc.shown_sum >= 1.5),
'{}'::jsonb)
FROM overall o
$q$, CASE WHEN v_item IS NULL THEN 'p.cluster_id' ELSE 'p.item_id' END)
USING COALESCE(v_item, p_master_id);
END $$;

DROP TRIGGER IF EXISTS trg_review_values_rating_extras_ins ON rate.review_values;
DROP TRIGGER IF EXISTS trg_review_values_rating_extras_upd ON rate.review_values;
DROP TRIGGER IF EXISTS trg_review_values_rating_extras_del ON rate.review_values;
DROP FUNCTION IF EXISTS rate.review_values_refresh_rating_extras();

DO $$
BEGIN
IF to_regclass('rate.review_values') IS NOT NULL THEN
-- Only reviews whose value lifted them (the factor still exists until transaction 2).
PERFORM rate.refresh_rating_extras_of_products(ARRAY(
SELECT DISTINCT r.product_id
FROM rate.review_values v JOIN rate.reviews r ON r.id = v.review_id
WHERE rate.review_completeness_factor(v.value_breakdown) > 1));
END IF;
END $$;

COMMIT;

BEGIN;

SET LOCAL lock_timeout = '5s';

DROP FUNCTION IF EXISTS rate.store_review_values(jsonb);
DROP FUNCTION IF EXISTS rate.my_review_values(uuid[]);
DROP FUNCTION IF EXISTS rate.review_completeness_factor(jsonb);
DROP TABLE IF EXISTS rate.review_values;

-- reviews_value_private stays: P2's public number columns must remain empty after a rollback
-- too (D7), and rate_worker can still write rate.reviews.
ALTER TABLE rate.reviews DROP CONSTRAINT IF EXISTS reviews_value_tier_known;
ALTER TABLE rate.reviews DROP COLUMN IF EXISTS value_tier;

COMMIT;
97 changes: 97 additions & 0 deletions Docs/Proposals/2026-11-02-dos-me-review-value-score-verify.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
-- VERIFY the PROPOSAL 2026-11-02-dos-me-review-value-score.sql (DOS/Rate#1547). Read-only.
-- Run after the migration on preview, beta and production. V1 to V7 return 0.
-- Run as postgres or service_role: V1, V5 and V8 call functions only service_role may execute
-- (a role without them fails loudly, never passes). V5 recomputes every master: run it off-peak.

-- V1 the factor is the D6 formula on weightCompleteness
-- (packages/shared-data completenessWeightFactor); expect 0
SELECT count(*) FROM (VALUES
(NULL::jsonb, 1.0000), ('{}', 1.0000), ('[]', 1.0000), ('{"weightCompleteness": "35"}', 1.0000),
('{"weightCompleteness": 0}', 1.0000), ('{"weightCompleteness": 25}', 1.1786),
('{"weightCompleteness": 35}', 1.2500), ('{"weightCompleteness": 99}', 1.2500),
('{"weightCompleteness": -4}', 1.0000)
) AS t(breakdown, expected)
WHERE rate.review_completeness_factor(t.breakdown) IS DISTINCT FROM t.expected;

-- V2 the rating reads the private values' factor, and the three triggers on them exist; expect 0
SELECT 4 - (
SELECT count(*) FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE n.nspname = 'rate' AND p.proname = 'master_rating_extras'
AND p.prosrc LIKE '%r.helpfulness_weight * rate.review_completeness_factor(v.value_breakdown)%'
AND p.prosrc LIKE '%LEFT JOIN rate.review_values v ON v.review_id = r.id%'
) - (
SELECT count(*) FROM pg_trigger
WHERE tgrelid = 'rate.review_values'::regclass AND NOT tgisinternal
AND tgname IN ('trg_review_values_rating_extras_ins', 'trg_review_values_rating_extras_upd',
'trg_review_values_rating_extras_del')
);

-- V3 D7: anon and authenticated cannot read rate.review_values or call the writer, and anon
-- cannot call the reader; rate_worker reads the table but writes only through the writer; the
-- functions are security definer with an empty search_path; expect 0
SELECT count(*) FROM (
SELECT p.proname::text AS problem FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE n.nspname = 'rate' AND p.proname IN ('store_review_values', 'my_review_values')
AND NOT (p.prosecdef AND p.proconfig @> ARRAY['search_path=""'])
UNION ALL
SELECT r.role FROM unnest(ARRAY['anon', 'authenticated']) AS r(role)
WHERE has_table_privilege(r.role, 'rate.review_values', 'SELECT')
OR has_table_privilege(r.role, 'rate.review_values', 'INSERT')
OR has_table_privilege(r.role, 'rate.review_values', 'UPDATE')
OR has_table_privilege(r.role, 'rate.review_values', 'DELETE')
OR has_table_privilege(r.role, 'rate.review_values', 'TRUNCATE')
OR has_function_privilege(r.role, 'rate.store_review_values(jsonb)', 'EXECUTE')
OR has_function_privilege(r.role, 'rate.review_completeness_factor(jsonb)', 'EXECUTE')
UNION ALL
SELECT 'anon reader' WHERE has_function_privilege('anon', 'rate.my_review_values(uuid[])', 'EXECUTE')
UNION ALL
SELECT 'authenticated reader'
WHERE NOT has_function_privilege('authenticated', 'rate.my_review_values(uuid[])', 'EXECUTE')
UNION ALL
SELECT 'rate_worker' WHERE NOT has_table_privilege('rate_worker', 'rate.review_values', 'SELECT')
OR has_table_privilege('rate_worker', 'rate.review_values', 'INSERT')
OR has_table_privilege('rate_worker', 'rate.review_values', 'UPDATE')
OR has_table_privilege('rate_worker', 'rate.review_values', 'DELETE')
OR has_table_privilege('rate_worker', 'rate.review_values', 'TRUNCATE')
OR NOT has_function_privilege('rate_worker', 'rate.store_review_values(jsonb)', 'EXECUTE')
UNION ALL
SELECT 'rls' FROM pg_class WHERE oid = 'rate.review_values'::regclass AND NOT relrowsecurity
) x;

-- V4 the CHECKs exist; expect 0
SELECT 3 - count(*) FROM pg_constraint
WHERE (conrelid = 'rate.reviews'::regclass
AND conname IN ('reviews_value_tier_known', 'reviews_value_private'))
OR (conrelid = 'rate.review_values'::regclass AND conname = 'review_values_completeness');

-- V5 every master's stored rating equals what the functions compute; expect 0
SELECT count(*) FROM rate.master_products m
CROSS JOIN LATERAL rate.master_rating_extras(m.id) x
WHERE (m.rating_score, m.rating_weight_sum, m.criteria_scores)
IS DISTINCT FROM (x.rating_score, x.rating_weight_sum, x.criteria_scores);

-- V6 the number never reaches the public table: P2's columns are empty on every row (the CHECK
-- is NOT VALID), and every tier is known; expect 0
SELECT count(*) FROM rate.reviews r
WHERE r.value_score IS NOT NULL OR r.value_breakdown IS NOT NULL
OR (r.value_tier IS NOT NULL AND r.value_tier NOT IN ('basic', 'complete', 'excellent'));

-- V7 the public tier is the stored breakdown's, and follows the module's rule: none on a gated
-- review, else basic under completeness 30, excellent from score 70, complete between; a tier
-- only with a value; expect 0
SELECT count(*) FROM rate.reviews r
LEFT JOIN rate.review_values v ON v.review_id = r.id
WHERE (v.review_id IS NULL AND r.value_tier IS NOT NULL)
OR (v.review_id IS NOT NULL AND (
r.value_tier IS DISTINCT FROM (v.value_breakdown ->> 'tier')
OR (v.value_breakdown ->> 'tier') IS DISTINCT FROM CASE
WHEN v.value_breakdown -> 'gated' = 'true'::jsonb THEN NULL
WHEN (v.value_breakdown ->> 'completeness')::numeric < 30 THEN 'basic'
WHEN v.value_score >= 70 THEN 'excellent'
ELSE 'complete' END));

-- V8 (information) reviews scored, their mean score and how many are lifted
SELECT count(*) AS scored,
round(avg(value_score), 2) AS mean_score,
count(*) FILTER (WHERE rate.review_completeness_factor(value_breakdown) > 1) AS lifted
FROM rate.review_values;
Loading
Loading