Skip to content

Commit c0f1fe7

Browse files
committed
feat(dav): store multiple default calendar alarms as JSON
Replace singular default-alarm-* integer properties with plural default-alarms-* JSON text columns. Migrate existing int defaults with two set-based QueryBuilder UPDATEs, then drop the legacy columns. No dual-write or server-side validation; propertyMap stores/retrieves JSON strings only (NC 35+ clients). Assisted-by: Grok:grok-4.5
1 parent ff020ad commit c0f1fe7

6 files changed

Lines changed: 177 additions & 20 deletions

File tree

‎apps/dav/composer/composer/autoload_classmap.php‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,8 @@
396396
'OCA\\DAV\\Migration\\Version1036Date20251202000000' => $baseDir . '/../lib/Migration/Version1036Date20251202000000.php',
397397
'OCA\\DAV\\Migration\\Version1038Date20260302000000' => $baseDir . '/../lib/Migration/Version1038Date20260302000000.php',
398398
'OCA\\DAV\\Migration\\Version1039Date20260408000000' => $baseDir . '/../lib/Migration/Version1039Date20260408000000.php',
399+
'OCA\\DAV\\Migration\\Version1040Date20260703120000' => $baseDir . '/../lib/Migration/Version1040Date20260703120000.php',
400+
'OCA\\DAV\\Migration\\Version1041Date20260703120100' => $baseDir . '/../lib/Migration/Version1041Date20260703120100.php',
399401
'OCA\\DAV\\Model\\ExampleEvent' => $baseDir . '/../lib/Model/ExampleEvent.php',
400402
'OCA\\DAV\\Paginate\\LimitedCopyIterator' => $baseDir . '/../lib/Paginate/LimitedCopyIterator.php',
401403
'OCA\\DAV\\Paginate\\PaginateCache' => $baseDir . '/../lib/Paginate/PaginateCache.php',

‎apps/dav/composer/composer/autoload_static.php‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,8 @@ class ComposerStaticInitDAV
411411
'OCA\\DAV\\Migration\\Version1036Date20251202000000' => __DIR__ . '/..' . '/../lib/Migration/Version1036Date20251202000000.php',
412412
'OCA\\DAV\\Migration\\Version1038Date20260302000000' => __DIR__ . '/..' . '/../lib/Migration/Version1038Date20260302000000.php',
413413
'OCA\\DAV\\Migration\\Version1039Date20260408000000' => __DIR__ . '/..' . '/../lib/Migration/Version1039Date20260408000000.php',
414+
'OCA\\DAV\\Migration\\Version1040Date20260703120000' => __DIR__ . '/..' . '/../lib/Migration/Version1040Date20260703120000.php',
415+
'OCA\\DAV\\Migration\\Version1041Date20260703120100' => __DIR__ . '/..' . '/../lib/Migration/Version1041Date20260703120100.php',
414416
'OCA\\DAV\\Model\\ExampleEvent' => __DIR__ . '/..' . '/../lib/Model/ExampleEvent.php',
415417
'OCA\\DAV\\Paginate\\LimitedCopyIterator' => __DIR__ . '/..' . '/../lib/Paginate/LimitedCopyIterator.php',
416418
'OCA\\DAV\\Paginate\\PaginateCache' => __DIR__ . '/..' . '/../lib/Paginate/PaginateCache.php',

‎apps/dav/lib/CalDAV/CalDavBackend.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
152152
'{http://apple.com/ns/ical/}calendar-order' => ['calendarorder', 'int'],
153153
'{http://apple.com/ns/ical/}calendar-color' => ['calendarcolor', 'string'],
154154
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => ['deleted_at', 'int'],
155-
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}default-alarm-part-day' => ['default_alarm_pday', 'int'],
156-
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}default-alarm-full-day' => ['default_alarm_fday', 'int'],
155+
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}default-alarms-part-day' => ['default_alarms_pday', 'string'],
156+
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}default-alarms-full-day' => ['default_alarms_fday', 'string'],
157157
];
158158

159159
/**
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\DAV\Migration;
11+
12+
use Closure;
13+
use OCP\DB\ISchemaWrapper;
14+
use OCP\DB\Types;
15+
use OCP\IDBConnection;
16+
use OCP\Migration\Attributes\AddColumn;
17+
use OCP\Migration\Attributes\ColumnType;
18+
use OCP\Migration\Attributes\DataCleansing;
19+
use OCP\Migration\IOutput;
20+
use OCP\Migration\SimpleMigrationStep;
21+
use Override;
22+
23+
/**
24+
* Add JSON default-alarm columns and copy legacy single-int defaults in bulk.
25+
*
26+
* Conversion uses two set-based UPDATEs so large calendars tables stay fast
27+
* (avoids per-row PHP updates).
28+
*/
29+
#[DataCleansing(table: 'calendars', description: 'Migrate legacy default_alarm_* integers to default_alarms_* JSON')]
30+
#[AddColumn(table: 'calendars', name: 'default_alarms_pday', type: ColumnType::TEXT)]
31+
#[AddColumn(table: 'calendars', name: 'default_alarms_fday', type: ColumnType::TEXT)]
32+
class Version1040Date20260703120000 extends SimpleMigrationStep {
33+
public function __construct(
34+
private IDBConnection $db,
35+
) {
36+
}
37+
38+
#[Override]
39+
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
40+
/** @var ISchemaWrapper $schema */
41+
$schema = $schemaClosure();
42+
43+
$calendarsTable = $schema->getTable('calendars');
44+
45+
if (!$calendarsTable->hasColumn('default_alarms_pday')) {
46+
$calendarsTable->addColumn('default_alarms_pday', Types::TEXT, [
47+
'notnull' => false,
48+
'default' => null,
49+
]);
50+
}
51+
52+
if (!$calendarsTable->hasColumn('default_alarms_fday')) {
53+
$calendarsTable->addColumn('default_alarms_fday', Types::TEXT, [
54+
'notnull' => false,
55+
'default' => null,
56+
]);
57+
}
58+
59+
return $schema;
60+
}
61+
62+
#[Override]
63+
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
64+
/** @var ISchemaWrapper $schema */
65+
$schema = $schemaClosure();
66+
$calendarsTable = $schema->getTable('calendars');
67+
68+
if ($calendarsTable->hasColumn('default_alarm_pday') && $calendarsTable->hasColumn('default_alarms_pday')) {
69+
$this->migrateLegacyIntColumn('default_alarm_pday', 'default_alarms_pday');
70+
}
71+
72+
if ($calendarsTable->hasColumn('default_alarm_fday') && $calendarsTable->hasColumn('default_alarms_fday')) {
73+
$this->migrateLegacyIntColumn('default_alarm_fday', 'default_alarms_fday');
74+
}
75+
}
76+
77+
/**
78+
* Encode a single legacy trigger int as [{"trigger":N,"action":"DISPLAY"}]
79+
* for all rows still missing the JSON column value.
80+
*/
81+
private function migrateLegacyIntColumn(string $legacyColumn, string $jsonColumn): void {
82+
$qb = $this->db->getQueryBuilder();
83+
$qb->update('calendars')
84+
->set($jsonColumn, $qb->func()->concat(
85+
$qb->expr()->literal('[{"trigger":'),
86+
$legacyColumn,
87+
$qb->expr()->literal(',"action":"DISPLAY"}]'),
88+
))
89+
->where($qb->expr()->isNotNull($legacyColumn))
90+
->andWhere($qb->expr()->isNull($jsonColumn));
91+
$qb->executeStatement();
92+
}
93+
}
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+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\DAV\Migration;
11+
12+
use Closure;
13+
use OCP\DB\ISchemaWrapper;
14+
use OCP\Migration\Attributes\DropColumn;
15+
use OCP\Migration\IOutput;
16+
use OCP\Migration\SimpleMigrationStep;
17+
use Override;
18+
19+
/**
20+
* Drop legacy single-int default alarm columns after JSON migration.
21+
*/
22+
#[DropColumn(table: 'calendars', name: 'default_alarm_pday', description: 'Replaced by default_alarms_pday JSON')]
23+
#[DropColumn(table: 'calendars', name: 'default_alarm_fday', description: 'Replaced by default_alarms_fday JSON')]
24+
class Version1041Date20260703120100 extends SimpleMigrationStep {
25+
#[Override]
26+
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
27+
/** @var ISchemaWrapper $schema */
28+
$schema = $schemaClosure();
29+
30+
$calendarsTable = $schema->getTable('calendars');
31+
32+
if ($calendarsTable->hasColumn('default_alarm_pday')) {
33+
$calendarsTable->dropColumn('default_alarm_pday');
34+
}
35+
36+
if ($calendarsTable->hasColumn('default_alarm_fday')) {
37+
$calendarsTable->dropColumn('default_alarm_fday');
38+
}
39+
40+
return $schema;
41+
}
42+
}

‎apps/dav/tests/unit/CalDAV/CalDavBackendTest.php‎

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1944,46 +1944,64 @@ public function testUnshare(): void {
19441944

19451945
}
19461946

1947-
public function testDefaultAlarmProperties(): void {
1947+
public function testDefaultAlarmsProperties(): void {
19481948
$calendarId = $this->createTestCalendar();
19491949

1950-
// Test setting both default alarm properties
1950+
$partDayProperty = '{http://nextcloud.com/ns}default-alarms-part-day';
1951+
$fullDayProperty = '{http://nextcloud.com/ns}default-alarms-full-day';
1952+
1953+
$partDayJson = json_encode([
1954+
['trigger' => -86400, 'action' => 'EMAIL'],
1955+
['trigger' => -900, 'action' => 'DISPLAY'],
1956+
], JSON_THROW_ON_ERROR);
1957+
$fullDayJson = json_encode([
1958+
['trigger' => -3600, 'action' => 'EMAIL'],
1959+
], JSON_THROW_ON_ERROR);
1960+
1961+
// Set plural default-alarms properties
19511962
$patch = new PropPatch([
1952-
'{http://nextcloud.com/ns}default-alarm-part-day' => -900,
1953-
'{http://nextcloud.com/ns}default-alarm-full-day' => -3600,
1963+
$partDayProperty => $partDayJson,
1964+
$fullDayProperty => $fullDayJson,
19541965
]);
19551966
$this->backend->updateCalendar($calendarId, $patch);
19561967
$patch->commit();
19571968

1958-
// Verify the properties were set
19591969
$calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
19601970
$this->assertCount(1, $calendars);
1961-
$this->assertEquals(-900, $calendars[0]['{http://nextcloud.com/ns}default-alarm-part-day']);
1962-
$this->assertEquals(-3600, $calendars[0]['{http://nextcloud.com/ns}default-alarm-full-day']);
1963-
1964-
// Test updating to different values
1971+
$this->assertEquals($partDayJson, $calendars[0][$partDayProperty]);
1972+
$this->assertEquals($fullDayJson, $calendars[0][$fullDayProperty]);
1973+
$this->assertArrayNotHasKey('{http://nextcloud.com/ns}default-alarm-part-day', $calendars[0]);
1974+
$this->assertArrayNotHasKey('{http://nextcloud.com/ns}default-alarm-full-day', $calendars[0]);
1975+
1976+
// Update to different values
1977+
$updatedPartDayJson = json_encode([
1978+
['trigger' => -1800, 'action' => 'DISPLAY'],
1979+
], JSON_THROW_ON_ERROR);
1980+
$updatedFullDayJson = json_encode([
1981+
['trigger' => -43200, 'action' => 'DISPLAY'],
1982+
], JSON_THROW_ON_ERROR);
19651983
$patch = new PropPatch([
1966-
'{http://nextcloud.com/ns}default-alarm-part-day' => -86400,
1967-
'{http://nextcloud.com/ns}default-alarm-full-day' => -43200,
1984+
$partDayProperty => $updatedPartDayJson,
1985+
$fullDayProperty => $updatedFullDayJson,
19681986
]);
19691987
$this->backend->updateCalendar($calendarId, $patch);
19701988
$patch->commit();
19711989

19721990
$calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
1973-
$this->assertEquals(-86400, $calendars[0]['{http://nextcloud.com/ns}default-alarm-part-day']);
1974-
$this->assertEquals(-43200, $calendars[0]['{http://nextcloud.com/ns}default-alarm-full-day']);
1991+
$this->assertEquals($updatedPartDayJson, $calendars[0][$partDayProperty]);
1992+
$this->assertEquals($updatedFullDayJson, $calendars[0][$fullDayProperty]);
19751993

1976-
// Test setting to null
1994+
// Clear properties
19771995
$patch = new PropPatch([
1978-
'{http://nextcloud.com/ns}default-alarm-part-day' => null,
1979-
'{http://nextcloud.com/ns}default-alarm-full-day' => null,
1996+
$partDayProperty => null,
1997+
$fullDayProperty => null,
19801998
]);
19811999
$this->backend->updateCalendar($calendarId, $patch);
19822000
$patch->commit();
19832001

19842002
$calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
1985-
$this->assertNull($calendars[0]['{http://nextcloud.com/ns}default-alarm-part-day']);
1986-
$this->assertNull($calendars[0]['{http://nextcloud.com/ns}default-alarm-full-day']);
2003+
$this->assertNull($calendars[0][$partDayProperty]);
2004+
$this->assertNull($calendars[0][$fullDayProperty]);
19872005

19882006
// Clean up
19892007
$this->backend->deleteCalendar($calendars[0]['id'], true);

0 commit comments

Comments
 (0)