Private
Public Access
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:
@@ -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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user