Skip to content

Commit 541fe1d

Browse files
nickygerritsenvmcj
authored andcommitted
Split the problem text content from the problem in the database.
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.
1 parent f2cbf64 commit 541fe1d

12 files changed

+172
-61
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DoctrineMigrations;
6+
7+
use Doctrine\DBAL\Schema\Schema;
8+
use Doctrine\Migrations\AbstractMigration;
9+
10+
/**
11+
* Auto-generated Migration: Please modify to your needs!
12+
*/
13+
final class Version20231124133426 extends AbstractMigration
14+
{
15+
public function getDescription(): string
16+
{
17+
return 'Split problem text content from problem';
18+
}
19+
20+
public function up(Schema $schema): void
21+
{
22+
// this up() migration is auto-generated, please modify it to your needs
23+
$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\' ');
24+
$this->addSql('INSERT INTO problem_text_content (probid, content) SELECT probid, problemtext FROM problem');
25+
$this->addSql('ALTER TABLE problem_text_content ADD CONSTRAINT FK_21B6AD6BEF049279 FOREIGN KEY (probid) REFERENCES problem (probid) ON DELETE CASCADE');
26+
$this->addSql('ALTER TABLE problem DROP problemtext');
27+
}
28+
29+
public function down(Schema $schema): void
30+
{
31+
// this down() migration is auto-generated, please modify it to your needs
32+
$this->addSql('ALTER TABLE problem ADD problemtext LONGBLOB DEFAULT NULL COMMENT \'Problem text in HTML/PDF/ASCII\'');
33+
$this->addSql('UPDATE problem INNER JOIN problem_text_content USING (probid) SET problem.problemtext = problem_text_content.content');
34+
$this->addSql('ALTER TABLE problem_text_content DROP FOREIGN KEY FK_21B6AD6BEF049279');
35+
$this->addSql('DROP TABLE problem_text_content');
36+
}
37+
38+
public function isTransactional(): bool
39+
{
40+
return false;
41+
}
42+
}

webapp/src/Controller/API/ProblemController.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,8 @@ public function singleAction(Request $request, string $id): Response
414414
public function statementAction(Request $request, string $id): Response
415415
{
416416
$queryBuilder = $this->getQueryBuilder($request)
417-
->addSelect('partial p.{probid,problemtext}')
417+
->leftJoin('p.problemTextContent', 'content')
418+
->addSelect('content')
418419
->setParameter('id', $id)
419420
->andWhere(sprintf('%s = :id', $this->getIdField()));
420421

@@ -448,7 +449,7 @@ protected function getQueryBuilder(Request $request): QueryBuilder
448449
->from(ContestProblem::class, 'cp')
449450
->join('cp.problem', 'p')
450451
->leftJoin('p.testcases', 'tc')
451-
->select('cp, partial p.{probid,externalid,name,timelimit,memlimit,problemtext_type}, COUNT(tc.testcaseid) AS testdatacount')
452+
->select('cp, p, COUNT(tc.testcaseid) AS testdatacount')
452453
->andWhere('cp.contest = :cid')
453454
->andWhere('cp.allowSubmit = 1')
454455
->setParameter('cid', $contestId)

webapp/src/Controller/Jury/ClarificationController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ protected function getClarificationFormData(?Team $team = null): array
242242
/** @var ContestProblem[] $contestproblems */
243243
$contestproblems = $this->em->createQueryBuilder()
244244
->from(ContestProblem::class, 'cp')
245-
->select('cp, partial p.{probid,externalid,name}')
245+
->select('cp, p')
246246
->innerJoin('cp.problem', 'p')
247247
->where('cp.contest IN (:contests)')
248248
->setParameter('contests', $contests)

webapp/src/Controller/Jury/ContestController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,7 @@ public function viewAction(Request $request, int $contestId): Response
403403
$problems = $this->em->createQueryBuilder()
404404
->from(ContestProblem::class, 'cp')
405405
->join('cp.problem', 'p')
406-
->select('cp', 'partial p.{probid,externalid,name,timelimit,memlimit,problemtext_type}')
406+
->select('cp', 'p')
407407
->andWhere('cp.contest = :contest')
408408
->setParameter('contest', $contest)
409409
->orderBy('cp.shortname')

webapp/src/Controller/Jury/ProblemController.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public function __construct(
6262
public function indexAction(): Response
6363
{
6464
$problems = $this->em->createQueryBuilder()
65-
->select('partial p.{probid,externalid,name,timelimit,memlimit,outputlimit,problemtext_type}', 'COUNT(tc.testcaseid) AS testdatacount')
65+
->select('p', 'COUNT(tc.testcaseid) AS testdatacount')
6666
->from(Problem::class, 'p')
6767
->leftJoin('p.testcases', 'tc')
6868
->orderBy('p.probid', 'ASC')
@@ -239,7 +239,8 @@ public function exportAction(int $problemId): StreamedResponse
239239
$problem = $this->em->createQueryBuilder()
240240
->from(Problem::class, 'p')
241241
->leftJoin('p.contest_problems', 'cp', Join::WITH, 'cp.contest = :contest')
242-
->select('p', 'cp')
242+
->leftJoin('p.problemTextContent', 'content')
243+
->select('p', 'cp', 'content')
243244
->andWhere('p.probid = :problemId')
244245
->setParameter('problemId', $problemId)
245246
->setParameter('contest', $this->dj->getCurrentContest())
@@ -295,7 +296,7 @@ public function exportAction(int $problemId): StreamedResponse
295296

296297
if (!empty($problem->getProblemtext())) {
297298
$zip->addFromString('problem.' . $problem->getProblemtextType(),
298-
stream_get_contents($problem->getProblemtext()));
299+
$problem->getProblemtext());
299300
}
300301

301302
foreach ([true, false] as $isSample) {

webapp/src/Entity/Problem.php

Lines changed: 47 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -90,17 +90,6 @@ class Problem extends BaseApiEntity
9090
#[Serializer\Exclude]
9191
private bool $combined_run_compare = false;
9292

93-
/**
94-
* @var resource|string|null
95-
*/
96-
#[ORM\Column(
97-
type: 'blob',
98-
nullable: true,
99-
options: ['comment' => 'Problem text in HTML/PDF/ASCII']
100-
)]
101-
#[Serializer\Exclude]
102-
private mixed $problemtext = null;
103-
10493
#[Assert\File]
10594
#[Serializer\Exclude]
10695
private ?UploadedFile $problemtextFile = null;
@@ -155,6 +144,22 @@ class Problem extends BaseApiEntity
155144
#[Serializer\Exclude]
156145
private Collection $testcases;
157146

147+
/**
148+
* @var Collection<int, ProblemTextContent>
149+
*
150+
* We use a OneToMany instead of a OneToOne here, because otherwise this
151+
* relation will always be loaded. See the commit message of commit
152+
* 9e421f96691ec67ed62767fe465a6d8751edd884 for a more elaborate explanation
153+
*/
154+
#[ORM\OneToMany(
155+
mappedBy: 'problem',
156+
targetEntity: ProblemTextContent::class,
157+
cascade: ['persist'],
158+
orphanRemoval: true
159+
)]
160+
#[Serializer\Exclude]
161+
private Collection $problemTextContent;
162+
158163
/**
159164
* @var Collection<int, ProblemAttachment>
160165
*/
@@ -259,39 +264,27 @@ public function getCombinedRunCompare(): bool
259264
return $this->combined_run_compare;
260265
}
261266

262-
/**
263-
* @param resource|string|null $problemtext
264-
*/
265-
public function setProblemtext($problemtext): Problem
266-
{
267-
$this->problemtext = $problemtext;
268-
return $this;
269-
}
270-
271267
public function setProblemtextFile(?UploadedFile $problemtextFile): Problem
272268
{
273269
$this->problemtextFile = $problemtextFile;
274270

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

278274
return $this;
279275
}
280276

281277
public function setClearProblemtext(bool $clearProblemtext): Problem
282278
{
283279
$this->clearProblemtext = $clearProblemtext;
284-
$this->problemtext = null;
280+
$this->setProblemTextContent(null);
285281

286282
return $this;
287283
}
288284

289-
/**
290-
* @return resource|string
291-
*/
292-
public function getProblemtext()
285+
public function getProblemtext(): ?string
293286
{
294-
return $this->problemtext;
287+
return $this->getProblemTextContent()?->getContent();
295288
}
296289

297290
public function getProblemtextFile(): ?UploadedFile
@@ -339,11 +332,12 @@ public function getRunExecutable(): ?Executable
339332

340333
public function __construct()
341334
{
342-
$this->testcases = new ArrayCollection();
343-
$this->submissions = new ArrayCollection();
344-
$this->clarifications = new ArrayCollection();
345-
$this->contest_problems = new ArrayCollection();
346-
$this->attachments = new ArrayCollection();
335+
$this->testcases = new ArrayCollection();
336+
$this->submissions = new ArrayCollection();
337+
$this->clarifications = new ArrayCollection();
338+
$this->contest_problems = new ArrayCollection();
339+
$this->attachments = new ArrayCollection();
340+
$this->problemTextContent = new ArrayCollection();
347341
}
348342

349343
public function addTestcase(Testcase $testcase): Problem
@@ -433,13 +427,29 @@ public function getAttachments(): Collection
433427
return $this->attachments;
434428
}
435429

430+
public function setProblemTextContent(?ProblemTextContent $content): self
431+
{
432+
$this->problemTextContent->clear();
433+
if ($content) {
434+
$this->problemTextContent->add($content);
435+
$content->setProblem($this);
436+
}
437+
438+
return $this;
439+
}
440+
441+
public function getProblemTextContent(): ?ProblemTextContent
442+
{
443+
return $this->problemTextContent->first() ?: null;
444+
}
445+
436446
#[ORM\PrePersist]
437447
#[ORM\PreUpdate]
438448
public function processProblemText(): void
439449
{
440450
if ($this->isClearProblemtext()) {
441451
$this
442-
->setProblemtext(null)
452+
->setProblemTextContent(null)
443453
->setProblemtextType(null);
444454
} elseif ($this->getProblemtextFile()) {
445455
$content = file_get_contents($this->getProblemtextFile()->getRealPath());
@@ -476,8 +486,10 @@ public function processProblemText(): void
476486
throw new Exception('Problem statement has unknown file type.');
477487
}
478488

489+
$problemTextContent = (new ProblemTextContent())
490+
->setContent($content);
479491
$this
480-
->setProblemtext($content)
492+
->setProblemTextContent($problemTextContent)
481493
->setProblemtextType($problemTextType);
482494
}
483495
}
@@ -492,7 +504,7 @@ public function getProblemTextStreamedResponse(): StreamedResponse
492504
};
493505

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

497509
$response = new StreamedResponse();
498510
$response->setCallback(function () use ($problemText) {
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace App\Entity;
4+
5+
use Doctrine\ORM\Mapping as ORM;
6+
7+
#[ORM\Entity]
8+
#[ORM\Table(options: [
9+
'collation' => 'utf8mb4_unicode_ci',
10+
'charset' => 'utf8mb4',
11+
'comment' => 'Stores contents of problem texts',
12+
])]
13+
class ProblemTextContent
14+
{
15+
/**
16+
* We use a ManyToOne instead of a OneToOne here, because otherwise the
17+
* reverse of this relation will always be loaded. See the commit message of commit
18+
* 9e421f96691ec67ed62767fe465a6d8751edd884 for a more elaborate explanation.
19+
*/
20+
#[ORM\Id]
21+
#[ORM\ManyToOne(inversedBy: 'problemTextContent')]
22+
#[ORM\JoinColumn(name: 'probid', referencedColumnName: 'probid', onDelete: 'CASCADE')]
23+
private Problem $problem;
24+
25+
#[ORM\Column(type: 'blobtext', options: ['comment' => 'Text content'])]
26+
private string $content;
27+
28+
public function getProblem(): Problem
29+
{
30+
return $this->problem;
31+
}
32+
33+
public function setProblem(Problem $problem): self
34+
{
35+
$this->problem = $problem;
36+
37+
return $this;
38+
}
39+
40+
public function getContent(): string
41+
{
42+
return $this->content;
43+
}
44+
45+
public function setContent(string $content): self
46+
{
47+
$this->content = $content;
48+
49+
return $this;
50+
}
51+
}

webapp/src/Service/DOMJudgeService.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -839,7 +839,8 @@ public function getSamplesZipForContest(Contest $contest): StreamedResponse
839839
->innerJoin('c.problems', 'cp')
840840
->innerJoin('cp.problem', 'p')
841841
->leftJoin('p.attachments', 'a')
842-
->select('c', 'cp', 'p', 'a')
842+
->leftJoin('p.problemTextContent', 'content')
843+
->select('c', 'cp', 'p', 'a', 'content')
843844
->andWhere('c.cid = :cid')
844845
->setParameter('cid', $contest->getCid())
845846
->getQuery()
@@ -861,7 +862,7 @@ public function getSamplesZipForContest(Contest $contest): StreamedResponse
861862

862863
if ($problem->getProblem()->getProblemtextType()) {
863864
$filename = sprintf('%s/statement.%s', $problem->getShortname(), $problem->getProblem()->getProblemtextType());
864-
$zip->addFromString($filename, stream_get_contents($problem->getProblem()->getProblemtext()));
865+
$zip->addFromString($filename, $problem->getProblem()->getProblemtext());
865866
}
866867

867868
/** @var ProblemAttachment $attachment */
@@ -972,7 +973,7 @@ public function getTwigDataForProblemsAction(
972973
->join('cp.problem', 'p')
973974
->leftJoin('p.testcases', 'tc')
974975
->leftJoin('p.attachments', 'a')
975-
->select('partial p.{probid,name,externalid,problemtext_type,timelimit,memlimit,combined_run_compare}', 'cp', 'a')
976+
->select('p', 'cp', 'a')
976977
->andWhere('cp.contest = :contest')
977978
->andWhere('cp.allowSubmit = 1')
978979
->setParameter('contest', $contest)

webapp/src/Service/ImportProblemService.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use App\Entity\Problem;
1010
use App\Entity\ProblemAttachment;
1111
use App\Entity\ProblemAttachmentContent;
12+
use App\Entity\ProblemTextContent;
1213
use App\Entity\Submission;
1314
use App\Entity\Team;
1415
use App\Entity\Testcase;
@@ -209,7 +210,7 @@ public function importZippedProblem(
209210
->setCombinedRunCompare(false)
210211
->setMemlimit(null)
211212
->setOutputlimit(null)
212-
->setProblemtext(null)
213+
->setProblemTextContent(null)
213214
->setProblemtextType(null);
214215

215216
$contestProblem
@@ -313,8 +314,10 @@ public function importZippedProblem(
313314
$filename = sprintf('%sproblem.%s', $dir, $type);
314315
$text = $zip->getFromName($filename);
315316
if ($text !== false) {
317+
$content = (new ProblemTextContent())
318+
->setContent($text);
316319
$problem
317-
->setProblemtext($text)
320+
->setProblemTextContent($content)
318321
->setProblemtextType($type);
319322
$messages['info'][] = "Added/updated problem statement from: $filename";
320323
break 2;

webapp/src/Service/ScoreboardService.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1003,7 +1003,7 @@ protected function getProblems(Contest $contest): array
10031003
{
10041004
$queryBuilder = $this->em->createQueryBuilder()
10051005
->from(ContestProblem::class, 'cp')
1006-
->select('cp, partial p.{probid,externalid,name,problemtext_type}')
1006+
->select('cp, p')
10071007
->innerJoin('cp.problem', 'p')
10081008
->andWhere('cp.allowSubmit = 1')
10091009
->andWhere('cp.contest = :contest')

0 commit comments

Comments
 (0)