Skip to content

Commit

Permalink
Split the problem text content from the problem in the database.
Browse files Browse the repository at this point in the history
This is to prepare for the next Doctrine release, which doesn't allow partial queries anymore.
This is also to make it consistent with other blobs in the database like submission files and problem attachments.

This is preparation for #2069.
  • Loading branch information
nickygerritsen committed Nov 24, 2023
1 parent df78c81 commit 748eabe
Show file tree
Hide file tree
Showing 12 changed files with 164 additions and 50 deletions.
42 changes: 42 additions & 0 deletions webapp/migrations/Version20231124133426.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20231124133426 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE problem_text_content (probid INT UNSIGNED NOT NULL COMMENT \'Problem ID\', content LONGBLOB NOT NULL COMMENT \'Text content(DC2Type:blobtext)\', PRIMARY KEY(probid)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB COMMENT = \'Stores contents of problem texts\' ');
$this->addSql('INSERT INTO problem_text_content (probid, content) SELECT probid, problemtext FROM problem');
$this->addSql('ALTER TABLE problem_text_content ADD CONSTRAINT FK_21B6AD6BEF049279 FOREIGN KEY (probid) REFERENCES problem (probid) ON DELETE CASCADE');
$this->addSql('ALTER TABLE problem DROP problemtext');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE problem ADD problemtext LONGBLOB DEFAULT NULL COMMENT \'Problem text in HTML/PDF/ASCII\'');
$this->addSql('UPDATE problem INNER JOIN problem_text_content USING (probid) SET problem.problemtext = problem_text_content.content');
$this->addSql('ALTER TABLE problem_text_content DROP FOREIGN KEY FK_21B6AD6BEF049279');
$this->addSql('DROP TABLE problem_text_content');
}

public function isTransactional(): bool
{
return false;
}
}
5 changes: 3 additions & 2 deletions webapp/src/Controller/API/ProblemController.php
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,8 @@ public function singleAction(Request $request, string $id): Response
public function statementAction(Request $request, string $id): Response
{
$queryBuilder = $this->getQueryBuilder($request)
->addSelect('partial p.{probid,problemtext}')
->leftJoin('p.problemTextContent', 'content')
->addSelect('content')
->setParameter('id', $id)
->andWhere(sprintf('%s = :id', $this->getIdField()));

Expand Down Expand Up @@ -448,7 +449,7 @@ protected function getQueryBuilder(Request $request): QueryBuilder
->from(ContestProblem::class, 'cp')
->join('cp.problem', 'p')
->leftJoin('p.testcases', 'tc')
->select('cp, partial p.{probid,externalid,name,timelimit,memlimit,problemtext_type}, COUNT(tc.testcaseid) AS testdatacount')
->select('cp, p, COUNT(tc.testcaseid) AS testdatacount')
->andWhere('cp.contest = :cid')
->andWhere('cp.allowSubmit = 1')
->setParameter('cid', $contestId)
Expand Down
2 changes: 1 addition & 1 deletion webapp/src/Controller/Jury/ClarificationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ protected function getClarificationFormData(?Team $team = null): array
/** @var ContestProblem[] $contestproblems */
$contestproblems = $this->em->createQueryBuilder()
->from(ContestProblem::class, 'cp')
->select('cp, partial p.{probid,externalid,name}')
->select('cp, p')
->innerJoin('cp.problem', 'p')
->where('cp.contest IN (:contests)')
->setParameter('contests', $contests)
Expand Down
2 changes: 1 addition & 1 deletion webapp/src/Controller/Jury/ContestController.php
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@ public function viewAction(Request $request, int $contestId): Response
$problems = $this->em->createQueryBuilder()
->from(ContestProblem::class, 'cp')
->join('cp.problem', 'p')
->select('cp', 'partial p.{probid,externalid,name,timelimit,memlimit,problemtext_type}')
->select('cp', 'p')
->andWhere('cp.contest = :contest')
->setParameter('contest', $contest)
->orderBy('cp.shortname')
Expand Down
7 changes: 4 additions & 3 deletions webapp/src/Controller/Jury/ProblemController.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public function __construct(
public function indexAction(): Response
{
$problems = $this->em->createQueryBuilder()
->select('partial p.{probid,externalid,name,timelimit,memlimit,outputlimit,problemtext_type}', 'COUNT(tc.testcaseid) AS testdatacount')
->select('p', 'COUNT(tc.testcaseid) AS testdatacount')
->from(Problem::class, 'p')
->leftJoin('p.testcases', 'tc')
->orderBy('p.probid', 'ASC')
Expand Down Expand Up @@ -239,7 +239,8 @@ public function exportAction(int $problemId): StreamedResponse
$problem = $this->em->createQueryBuilder()
->from(Problem::class, 'p')
->leftJoin('p.contest_problems', 'cp', Join::WITH, 'cp.contest = :contest')
->select('p', 'cp')
->leftJoin('p.problemTextContent', 'content')
->select('p', 'cp', 'content')
->andWhere('p.probid = :problemId')
->setParameter('problemId', $problemId)
->setParameter('contest', $this->dj->getCurrentContest())
Expand Down Expand Up @@ -295,7 +296,7 @@ public function exportAction(int $problemId): StreamedResponse

if (!empty($problem->getProblemtext())) {
$zip->addFromString('problem.' . $problem->getProblemtextType(),
stream_get_contents($problem->getProblemtext()));
$problem->getProblemtext());
}

foreach ([true, false] as $isSample) {
Expand Down
81 changes: 48 additions & 33 deletions webapp/src/Entity/Problem.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,6 @@ class Problem extends BaseApiEntity
#[Serializer\Exclude]
private bool $combined_run_compare = false;

/**
* @var resource|string|null
*/
#[ORM\Column(
type: 'blob',
nullable: true,
options: ['comment' => 'Problem text in HTML/PDF/ASCII']
)]
#[Serializer\Exclude]
private mixed $problemtext = null;

#[Assert\File]
#[Serializer\Exclude]
private ?UploadedFile $problemtextFile = null;
Expand Down Expand Up @@ -155,6 +144,22 @@ class Problem extends BaseApiEntity
#[Serializer\Exclude]
private Collection $testcases;

/**
* @var Collection<int, ProblemTextContent>
*
* We use a OneToMany instead of a OneToOne here, because otherwise this
* relation will always be loaded. See the commit message of commit
* 9e421f96691ec67ed62767fe465a6d8751edd884 for a more elaborate explanation
*/
#[ORM\OneToMany(
mappedBy: 'problem',
targetEntity: ProblemTextContent::class,
cascade: ['persist'],
orphanRemoval: true
)]
#[Serializer\Exclude]
private Collection $problemTextContent;

/**
* @var Collection<int, ProblemAttachment>
*/
Expand Down Expand Up @@ -259,39 +264,30 @@ public function getCombinedRunCompare(): bool
return $this->combined_run_compare;
}

/**
* @param resource|string|null $problemtext
*/
public function setProblemtext($problemtext): Problem
{
$this->problemtext = $problemtext;
return $this;
}

public function setProblemtextFile(?UploadedFile $problemtextFile): Problem
{
$this->problemtextFile = $problemtextFile;

// Clear the problem text to make sure the entity is modified.
$this->problemtext = '';
$this->setProblemTextContent(null);

return $this;
}

public function setClearProblemtext(bool $clearProblemtext): Problem
{
$this->clearProblemtext = $clearProblemtext;
$this->problemtext = null;
$this->setProblemTextContent(null);

return $this;
}

/**
* @return resource|string
* @return string|null
*/
public function getProblemtext()
public function getProblemtext(): ?string
{
return $this->problemtext;
return $this->getProblemTextContent()?->getContent();
}

public function getProblemtextFile(): ?UploadedFile
Expand Down Expand Up @@ -339,11 +335,12 @@ public function getRunExecutable(): ?Executable

public function __construct()
{
$this->testcases = new ArrayCollection();
$this->submissions = new ArrayCollection();
$this->clarifications = new ArrayCollection();
$this->contest_problems = new ArrayCollection();
$this->attachments = new ArrayCollection();
$this->testcases = new ArrayCollection();
$this->submissions = new ArrayCollection();
$this->clarifications = new ArrayCollection();
$this->contest_problems = new ArrayCollection();
$this->attachments = new ArrayCollection();
$this->problemTextContent = new ArrayCollection();
}

public function addTestcase(Testcase $testcase): Problem
Expand Down Expand Up @@ -433,13 +430,29 @@ public function getAttachments(): Collection
return $this->attachments;
}

public function setProblemTextContent(?ProblemTextContent $content): self
{
$this->problemTextContent->clear();
if ($content) {
$this->problemTextContent->add($content);
$content->setProblem($this);
}

return $this;
}

public function getProblemTextContent(): ?ProblemTextContent
{
return $this->problemTextContent->first() ?: null;
}

#[ORM\PrePersist]
#[ORM\PreUpdate]
public function processProblemText(): void
{
if ($this->isClearProblemtext()) {
$this
->setProblemtext(null)
->setProblemTextContent(null)
->setProblemtextType(null);
} elseif ($this->getProblemtextFile()) {
$content = file_get_contents($this->getProblemtextFile()->getRealPath());
Expand Down Expand Up @@ -476,8 +489,10 @@ public function processProblemText(): void
throw new Exception('Problem statement has unknown file type.');
}

$problemTextContent = (new ProblemTextContent())
->setContent($content);
$this
->setProblemtext($content)
->setProblemTextContent($problemTextContent)
->setProblemtextType($problemTextType);
}
}
Expand All @@ -492,7 +507,7 @@ public function getProblemTextStreamedResponse(): StreamedResponse
};

$filename = sprintf('prob-%s.%s', $this->getName(), $this->getProblemtextType());
$problemText = stream_get_contents($this->getProblemtext());
$problemText = $this->getProblemtext();

$response = new StreamedResponse();
$response->setCallback(function () use ($problemText) {
Expand Down
51 changes: 51 additions & 0 deletions webapp/src/Entity/ProblemTextContent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php declare(strict_types=1);

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(options: [
'collation' => 'utf8mb4_unicode_ci',
'charset' => 'utf8mb4',
'comment' => 'Stores contents of problem texts',
])]
class ProblemTextContent
{
/**
* We use a ManyToOne instead of a OneToOne here, because otherwise the
* reverse of this relation will always be loaded. See the commit message of commit
* 9e421f96691ec67ed62767fe465a6d8751edd884 for a more elaborate explanation.
*/
#[ORM\Id]
#[ORM\ManyToOne(inversedBy: 'problemTextContent')]
#[ORM\JoinColumn(name: 'probid', referencedColumnName: 'probid', onDelete: 'CASCADE')]
private Problem $problem;

#[ORM\Column(type: 'blobtext', options: ['comment' => 'Text content'])]
private string $content;

public function getProblem(): Problem
{
return $this->problem;
}

public function setProblem(Problem $problem): self
{
$this->problem = $problem;

return $this;
}

public function getContent(): string
{
return $this->content;
}

public function setContent(string $content): self
{
$this->content = $content;

return $this;
}
}
7 changes: 4 additions & 3 deletions webapp/src/Service/DOMJudgeService.php
Original file line number Diff line number Diff line change
Expand Up @@ -839,7 +839,8 @@ public function getSamplesZipForContest(Contest $contest): StreamedResponse
->innerJoin('c.problems', 'cp')
->innerJoin('cp.problem', 'p')
->leftJoin('p.attachments', 'a')
->select('c', 'cp', 'p', 'a')
->leftJoin('p.problemTextContent', 'content')
->select('c', 'cp', 'p', 'a', 'content')
->andWhere('c.cid = :cid')
->setParameter('cid', $contest->getCid())
->getQuery()
Expand All @@ -861,7 +862,7 @@ public function getSamplesZipForContest(Contest $contest): StreamedResponse

if ($problem->getProblem()->getProblemtextType()) {
$filename = sprintf('%s/statement.%s', $problem->getShortname(), $problem->getProblem()->getProblemtextType());
$zip->addFromString($filename, stream_get_contents($problem->getProblem()->getProblemtext()));
$zip->addFromString($filename, $problem->getProblem()->getProblemtext());
}

/** @var ProblemAttachment $attachment */
Expand Down Expand Up @@ -972,7 +973,7 @@ public function getTwigDataForProblemsAction(
->join('cp.problem', 'p')
->leftJoin('p.testcases', 'tc')
->leftJoin('p.attachments', 'a')
->select('partial p.{probid,name,externalid,problemtext_type,timelimit,memlimit,combined_run_compare}', 'cp', 'a')
->select('p', 'cp', 'a')
->andWhere('cp.contest = :contest')
->andWhere('cp.allowSubmit = 1')
->setParameter('contest', $contest)
Expand Down
7 changes: 5 additions & 2 deletions webapp/src/Service/ImportProblemService.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use App\Entity\Problem;
use App\Entity\ProblemAttachment;
use App\Entity\ProblemAttachmentContent;
use App\Entity\ProblemTextContent;
use App\Entity\Submission;
use App\Entity\Team;
use App\Entity\Testcase;
Expand Down Expand Up @@ -207,7 +208,7 @@ public function importZippedProblem(
->setCombinedRunCompare(false)
->setMemlimit(null)
->setOutputlimit(null)
->setProblemtext(null)
->setProblemTextContent(null)
->setProblemtextType(null);

$contestProblem
Expand Down Expand Up @@ -311,8 +312,10 @@ public function importZippedProblem(
$filename = sprintf('%sproblem.%s', $dir, $type);
$text = $zip->getFromName($filename);
if ($text !== false) {
$content = (new ProblemTextContent())
->setContent($text);
$problem
->setProblemtext($text)
->setProblemTextContent($content)
->setProblemtextType($type);
$messages['info'][] = "Added/updated problem statement from: $filename";
break 2;
Expand Down
2 changes: 1 addition & 1 deletion webapp/src/Service/ScoreboardService.php
Original file line number Diff line number Diff line change
Expand Up @@ -1001,7 +1001,7 @@ protected function getProblems(Contest $contest): array
{
$queryBuilder = $this->em->createQueryBuilder()
->from(ContestProblem::class, 'cp')
->select('cp, partial p.{probid,externalid,name,problemtext_type}')
->select('cp, p')
->innerJoin('cp.problem', 'p')
->andWhere('cp.allowSubmit = 1')
->andWhere('cp.contest = :contest')
Expand Down
2 changes: 1 addition & 1 deletion webapp/src/Service/StatisticsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ public function getTeamStats(Contest $contest, Team $team): array
// - The submission was made by a team in a visible category
/** @var Judging[] $judgings */
$judgings = $this->em->createQueryBuilder()
->select('j, jr', 's', 'team', 'partial p.{timelimit,name,probid}')
->select('j, jr', 's', 'team', 'p')
->from(Judging::class, 'j')
->join('j.submission', 's')
->join('s.problem', 'p')
Expand Down
Loading

0 comments on commit 748eabe

Please sign in to comment.