chg: dev: create the UserStats entity what is a Materialized View to store Profile stats for every user - & massive ProfileController refactor #8

This commit is contained in:
Lang
2026-04-20 20:44:33 +02:00
parent 6be0d52fb7
commit 6a5ba84b5e
13 changed files with 827 additions and 502 deletions
@@ -0,0 +1,117 @@
<?php declare(strict_types=1);
/**
* This file is part of the SplendidBear Websites' projects.
*
* Copyright (c) 2026 @ www.splendidbear.org
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Migrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Class Version20260420120000
*
* Creates user_stats materialized view for ProfileController optimization.
*
* @package App\Migrations
* @author Lang <https://www.splendidbear.org>
* @category Class
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
* @link www.splendidbear.org
* @since 2026. 04. 20.
*/
final class Version20260420120000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Create user_stats materialized view for profile statistics.';
}
public function up(Schema $schema): void
{
$this->addSql('
CREATE MATERIALIZED VIEW user_stats AS
SELECT
u.id AS user_id,
COUNT(CASE WHEN pg.red_id = u.id OR pg.blue_id = u.id THEN 1 END) AS total_games,
COUNT(CASE WHEN
pg.red_id = u.id AND (pg.red_points > pg.blue_points AND pg.resign IS NULL)
OR pg.blue_id = u.id AND (pg.blue_points > pg.red_points AND pg.resign IS NULL)
OR pg.red_id = u.id AND pg.resign = \'blue\'
OR pg.blue_id = u.id AND pg.resign = \'red\'
THEN 1 END) AS wins,
COUNT(CASE WHEN
pg.red_id = u.id AND (pg.blue_points > pg.red_points AND pg.resign IS NULL)
OR pg.blue_id = u.id AND (pg.red_points > pg.blue_points AND pg.resign IS NULL)
OR pg.red_id = u.id AND pg.resign = \'red\'
OR pg.blue_id = u.id AND pg.resign = \'blue\'
THEN 1 END) AS losses,
COUNT(CASE WHEN
(pg.red_id = u.id OR pg.blue_id = u.id)
AND pg.red_points IS NOT NULL
AND pg.blue_points IS NOT NULL
AND pg.resign IS NULL
AND pg.red_points = pg.blue_points
THEN 1 END) AS draws,
COALESCE(SUM(
CASE WHEN pg.red_id = u.id THEN pg.red_points ELSE pg.blue_points END
), 0) AS total_mines,
COALESCE(SUM(
CASE WHEN pg.red_id = u.id THEN pg.red_bonus_points ELSE pg.blue_bonus_points END
), 0)::numeric(10,1) AS total_bonus_points,
COALESCE(AVG(
CASE WHEN pg.red_id = u.id THEN pg.red_bonus_points ELSE pg.blue_bonus_points END
), 0)::numeric(10,1) AS avg_bonus,
COALESCE(MAX(
CASE WHEN pg.red_id = u.id THEN (pg.red_bonus_stats->>\'chainBest\')::int ELSE (pg.blue_bonus_stats->>\'chainBest\')::int END
), 0) AS best_chain,
COALESCE(SUM(
CASE WHEN pg.red_id = u.id THEN (pg.red_bonus_stats->>\'blindHits\')::int ELSE (pg.blue_bonus_stats->>\'blindHits\')::int END
), 0) AS blind_hits,
COALESCE(SUM(
CASE WHEN pg.red_id = u.id THEN (pg.red_bonus_stats->>\'edgeMines\')::int ELSE (pg.blue_bonus_stats->>\'edgeMines\')::int END
), 0) AS edge_mines,
COUNT(CASE WHEN
pg.red_id = u.id AND pg.red_points IS NOT NULL
OR pg.blue_id = u.id AND pg.blue_points IS NOT NULL
THEN 1 END) AS games_with_scores,
MAX(pg.updated) AS last_game_at
FROM app_user u
LEFT JOIN played_game pg ON (pg.red_id = u.id OR pg.blue_id = u.id)
AND (pg.red_points IS NOT NULL OR pg.blue_points IS NOT NULL OR pg.resign IS NOT NULL)
GROUP BY u.id
');
$this->addSql('CREATE UNIQUE INDEX idx_user_stats_user_id ON user_stats (user_id)');
$this->addSql('CREATE INDEX idx_user_stats_last_game_at ON user_stats (last_game_at DESC)');
$this->addSql('
CREATE OR REPLACE FUNCTION refresh_user_stats()
RETURNS TRIGGER AS $$
BEGIN
REFRESH MATERIALIZED VIEW CONCURRENTLY user_stats;
RETURN NULL;
END;
$$ LANGUAGE plpgsql
');
$this->addSql('
CREATE TRIGGER trigger_refresh_user_stats
AFTER INSERT OR UPDATE OR DELETE ON played_game
FOR EACH STATEMENT
EXECUTE FUNCTION refresh_user_stats()
');
}
public function down(Schema $schema): void
{
$this->addSql('DROP TRIGGER IF EXISTS trigger_refresh_user_stats ON played_game');
$this->addSql('DROP FUNCTION IF EXISTS refresh_user_stats()');
$this->addSql('DROP MATERIALIZED VIEW IF EXISTS user_stats');
}
}