Skip to content
Open
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
111 changes: 111 additions & 0 deletions migrations/Version20260720102000.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

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

final class Version20260720102000 extends AbstractMigration
{
private const ADDRESSBOOK_SYNC_INDEX = 'idx_addressbookchanges_book_sync';

public function getDescription(): string
{
return 'Store DAV sync tokens as integers so range queries use numeric ordering';
}

public function up(Schema $schema): void
{
$engine = $this->connection->getDatabasePlatform()->getName();

if ('mysql' === $engine) {
$this->addSql('ALTER TABLE addressbooks MODIFY synctoken INT DEFAULT 1 NOT NULL');
$this->addSql('ALTER TABLE calendars MODIFY synctoken INT DEFAULT 1 NOT NULL');
$this->addSql('ALTER TABLE addressbookchanges MODIFY synctoken INT DEFAULT 1 NOT NULL');

return;
}

if ('postgresql' === $engine) {
foreach (['addressbooks', 'calendars', 'addressbookchanges'] as $table) {
$this->addSql(sprintf('ALTER TABLE %s ALTER COLUMN synctoken TYPE INT USING synctoken::integer', $table));
$this->addSql(sprintf('ALTER TABLE %s ALTER COLUMN synctoken SET DEFAULT 1', $table));
}

return;
}

if ('sqlite' === $engine) {
$restoreSyncIndex = $this->hasAddressBookSyncIndex();
if ($restoreSyncIndex) {
$this->addSql('DROP INDEX '.self::ADDRESSBOOK_SYNC_INDEX);
}

foreach (['addressbooks', 'calendars', 'addressbookchanges'] as $table) {
$this->replaceSqliteSyncToken($table, 'INTEGER', 'INTEGER', '1');
}

if ($restoreSyncIndex) {
$this->addSql('CREATE INDEX '.self::ADDRESSBOOK_SYNC_INDEX.' ON addressbookchanges (addressbookid, synctoken)');
}
}
}

public function down(Schema $schema): void
{
$engine = $this->connection->getDatabasePlatform()->getName();

if ('mysql' === $engine) {
$this->addSql('ALTER TABLE addressbooks MODIFY synctoken VARCHAR(255) NOT NULL');
$this->addSql('ALTER TABLE calendars MODIFY synctoken VARCHAR(255) NOT NULL');
$this->addSql('ALTER TABLE addressbookchanges MODIFY synctoken VARCHAR(255) NOT NULL');

return;
}

if ('postgresql' === $engine) {
foreach (['addressbooks', 'calendars', 'addressbookchanges'] as $table) {
$this->addSql(sprintf('ALTER TABLE %s ALTER COLUMN synctoken DROP DEFAULT', $table));
$this->addSql(sprintf('ALTER TABLE %s ALTER COLUMN synctoken TYPE VARCHAR(255) USING synctoken::varchar', $table));
}

return;
}

if ('sqlite' === $engine) {
$restoreSyncIndex = $this->hasAddressBookSyncIndex();
if ($restoreSyncIndex) {
$this->addSql('DROP INDEX '.self::ADDRESSBOOK_SYNC_INDEX);
}

foreach (['addressbooks', 'calendars', 'addressbookchanges'] as $table) {
$this->replaceSqliteSyncToken($table, 'VARCHAR(255)', 'TEXT', "'1'");
}

if ($restoreSyncIndex) {
$this->addSql('CREATE INDEX '.self::ADDRESSBOOK_SYNC_INDEX.' ON addressbookchanges (addressbookid, synctoken)');
}
}
}

private function replaceSqliteSyncToken(string $table, string $type, string $cast, string $default): void
{
$this->addSql(sprintf('ALTER TABLE %s ADD COLUMN new_synctoken %s DEFAULT %s NOT NULL', $table, $type, $default));
$this->addSql(sprintf('UPDATE %s SET new_synctoken = CAST(synctoken AS %s)', $table, $cast));
$this->addSql(sprintf('ALTER TABLE %s RENAME COLUMN synctoken TO old_synctoken', $table));
$this->addSql(sprintf('ALTER TABLE %s RENAME COLUMN new_synctoken TO synctoken', $table));
$this->addSql(sprintf('ALTER TABLE %s DROP COLUMN old_synctoken', $table));
}

private function hasAddressBookSyncIndex(): bool
{
$indexes = array_change_key_case(
$this->connection->createSchemaManager()->listTableIndexes('addressbookchanges'),
CASE_LOWER,
);

return isset($indexes[self::ADDRESSBOOK_SYNC_INDEX]);
}
}
6 changes: 3 additions & 3 deletions src/Entity/AddressBook.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class AddressBook
#[ORM\Column(type: 'text', nullable: true)]
private $description;

#[ORM\Column(type: 'string', length: 255)]
#[ORM\Column(type: 'integer', options: ['default' => 1])]
private $synctoken;

#[ORM\Column(type: 'boolean', nullable: true, options: ['default' => false])]
Expand Down Expand Up @@ -116,12 +116,12 @@ public function setDescription(string $description): self
return $this;
}

public function getSynctoken(): ?string
public function getSynctoken(): ?int
{
return $this->synctoken;
}

public function setSynctoken(string $synctoken): self
public function setSynctoken(int $synctoken): self
{
$this->synctoken = $synctoken;

Expand Down
6 changes: 3 additions & 3 deletions src/Entity/AddressBookChange.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class AddressBookChange
#[ORM\Column(type: 'string', length: 255)]
private $uri;

#[ORM\Column(type: 'string', length: 255)]
#[ORM\Column(type: 'integer', options: ['default' => 1])]
private $synctoken;

#[ORM\ManyToOne(targetEntity: "App\Entity\AddressBook", inversedBy: 'changes')]
Expand All @@ -43,12 +43,12 @@ public function setUri(string $uri): self
return $this;
}

public function getSynctoken(): ?string
public function getSynctoken(): ?int
{
return $this->synctoken;
}

public function setSynctoken(string $synctoken): self
public function setSynctoken(int $synctoken): self
{
$this->synctoken = $synctoken;

Expand Down
6 changes: 3 additions & 3 deletions src/Entity/Calendar.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class Calendar
#[ORM\Column(type: 'integer')]
private $id;

#[ORM\Column(type: 'string', length: 255)]
#[ORM\Column(type: 'integer', options: ['default' => 1])]
private $synctoken;

#[ORM\Column(type: 'string', length: 255, nullable: true)]
Expand Down Expand Up @@ -47,12 +47,12 @@ public function getId(): ?int
return $this->id;
}

public function getSynctoken(): ?string
public function getSynctoken(): ?int
{
return $this->synctoken;
}

public function setSynctoken(string $synctoken): self
public function setSynctoken(int $synctoken): self
{
$this->synctoken = $synctoken;

Expand Down
2 changes: 1 addition & 1 deletion tests/Functional/Commands/SyncBirthdayCalendarTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ private function createAddressBookWithCard(string $username, string $cardUri, st
->setUri('default')
->setDisplayName('Default')
->setDescription('')
->setSynctoken('1')
->setSynctoken(1)
->setIncludedInBirthdayCalendar(true);
$this->em->persist($addressBook);

Expand Down
2 changes: 1 addition & 1 deletion tests/Functional/Service/BirthdayServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ private function createAddressBook(
->setUri('default')
->setDisplayName('Default')
->setDescription('')
->setSynctoken('1')
->setSynctoken(1)
->setIncludedInBirthdayCalendar($includedInBirthdayCalendar);
$this->em->persist($addressBook);
$this->em->flush();
Expand Down
42 changes: 42 additions & 0 deletions tests/Functional/SyncTokenTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace App\Tests\Functional;

use Doctrine\ORM\EntityManagerInterface;
use Sabre\CardDAV\Backend\PDO as CardDavBackend;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

class SyncTokenTest extends KernelTestCase
{
public function testAddressBookChangesUseNumericTokenOrdering(): void
{
self::bootKernel();

$entityManager = self::getContainer()->get(EntityManagerInterface::class);
$pdo = $entityManager->getConnection()->getNativeConnection();

$statement = $pdo->prepare(<<<'SQL'
INSERT INTO addressbooks (principaluri, displayname, uri, description, synctoken)
VALUES (?, ?, ?, ?, ?)
SQL);
$statement->execute(['principals/sync-token-test', 'Sync token test', 'sync-token-test', '', 11]);

$statement = $pdo->prepare('SELECT id FROM addressbooks WHERE principaluri = ? AND uri = ?');
$statement->execute(['principals/sync-token-test', 'sync-token-test']);
$addressBookId = (int) $statement->fetchColumn();

$statement = $pdo->prepare(<<<'SQL'
INSERT INTO addressbookchanges (addressbookid, uri, synctoken, operation)
VALUES (?, ?, ?, ?)
SQL);
$statement->execute([$addressBookId, 'added.vcf', 9, 1]);
$statement->execute([$addressBookId, 'changed.vcf', 10, 2]);

$changes = (new CardDavBackend($pdo))->getChangesForAddressBook($addressBookId, '9', 1);

self::assertSame(11, (int) $changes['syncToken']);
self::assertSame(['added.vcf'], $changes['added']);
self::assertSame(['changed.vcf'], $changes['modified']);
self::assertSame([], $changes['deleted']);
}
}
Loading