Skip to content

Commit 4e84a2e

Browse files
committed
perf: optimize updateDocument() calls to use sparse documents
Optimize updateDocument() calls across the codebase to pass only changed attributes as sparse Document objects rather than full documents. This is more efficient because updateDocument() internally performs array_merge(). Changes: - Updated 58 files to use sparse Document objects - Added Performance Patterns section to AGENTS.md with optimization guidelines - Applied pattern to Workers, Functions, Sites, Teams, VCS modules - Updated app/controllers/api files (account, users, messaging) - Updated app infrastructure files (realtime, general, init/resources, shared/api) Exceptions maintained: - Migration files (need full document updates by design) - Cases with 6+ attributes (marginal benefit) - Complex nested relationship logic
1 parent 2306072 commit 4e84a2e

59 files changed

Lines changed: 502 additions & 174 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,36 @@ Examples:
4747
'resourceType' => 'deployments'
4848
```
4949

50+
## Performance Patterns
51+
52+
### Document Update Optimization
53+
54+
When updating documents, always pass only the changed attributes as a sparse `Document` rather than the full document. This is more efficient because `updateDocument()` internally performs `array_merge($old, $new)`.
55+
56+
**Correct Pattern:**
57+
```php
58+
// Good: Pass only changed attributes directly
59+
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
60+
'name' => $name,
61+
'email' => $email,
62+
]));
63+
```
64+
65+
**Incorrect Pattern:**
66+
```php
67+
$user->setAttribute('name', $name);
68+
$user->setAttribute('email', $email);
69+
70+
// Bad: Passing full document is inefficient
71+
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
72+
```
73+
74+
**Exceptions:**
75+
- Migration files (need full document updates by design)
76+
- Cases already using `array_merge()` with `getArrayCopy()`
77+
- Updates with 6+ attributes changing simultaneously (marginal benefit)
78+
- Complex nested relationship logic where full document state is required
79+
5080
## Security Considerations
5181

5282
### Critical Security Practices

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@AGENTS.md

app/controllers/api/account.php

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,10 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, arr
288288
}
289289

290290
try {
291-
$dbForProject->updateDocument('users', $user->getId(), $user);
291+
$dbForProject->updateDocument('users', $user->getId(), new Document([
292+
'emailVerification' => $user->getAttribute('emailVerification'),
293+
'phoneVerification' => $user->getAttribute('phoneVerification'),
294+
]));
292295
} catch (\Throwable $th) {
293296
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed saving user to DB');
294297
}
@@ -1032,7 +1035,11 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, arr
10321035
->setAttribute('password', $proofForPasswordUpdated->hash($password))
10331036
->setAttribute('hash', $proofForPasswordUpdated->getHash()->getName())
10341037
->setAttribute('hashOptions', $proofForPasswordUpdated->getHash()->getOptions());
1035-
$dbForProject->updateDocument('users', $user->getId(), $user);
1038+
$dbForProject->updateDocument('users', $user->getId(), new Document([
1039+
'password' => $user->getAttribute('password'),
1040+
'hash' => $user->getAttribute('hash'),
1041+
'hashOptions' => $user->getAttribute('hashOptions'),
1042+
]));
10361043
}
10371044

10381045
$dbForProject->purgeCachedDocument('users', $user->getId());
@@ -1822,7 +1829,11 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, arr
18221829
->setAttribute('providerAccessToken', $accessToken)
18231830
->setAttribute('providerRefreshToken', $refreshToken)
18241831
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int) $accessTokenExpiry));
1825-
$dbForProject->updateDocument('identities', $identity->getId(), $identity);
1832+
$dbForProject->updateDocument('identities', $identity->getId(), new Document([
1833+
'providerAccessToken' => $identity->getAttribute('providerAccessToken'),
1834+
'providerRefreshToken' => $identity->getAttribute('providerRefreshToken'),
1835+
'providerAccessTokenExpiry' => $identity->getAttribute('providerAccessTokenExpiry'),
1836+
]));
18261837
}
18271838

18281839
if (empty($user->getAttribute('email'))) {
@@ -1960,7 +1971,10 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, arr
19601971
->setAttribute('sessionId', $session->getId())
19611972
->setAttribute('sessionInternalId', $session->getSequence());
19621973

1963-
$dbForProject->updateDocument('targets', $target->getId(), $target);
1974+
$dbForProject->updateDocument('targets', $target->getId(), new Document([
1975+
'sessionId' => $target->getAttribute('sessionId'),
1976+
'sessionInternalId' => $target->getAttribute('sessionInternalId'),
1977+
]));
19641978
}
19651979
}
19661980

@@ -3145,7 +3159,9 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, arr
31453159

31463160
$user->setAttribute('name', $name);
31473161

3148-
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
3162+
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
3163+
'name' => $user->getAttribute('name'),
3164+
]));
31493165

31503166
$queueForEvents->setParam('userId', $user->getId());
31513167

@@ -3798,13 +3814,15 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, arr
37983814

37993815
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]);
38003816

3801-
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile
3802-
->setAttribute('password', $newPassword)
3803-
->setAttribute('passwordHistory', $history)
3804-
->setAttribute('passwordUpdate', DateTime::now())
3805-
->setAttribute('hash', $proofForPassword->getHash()->getName())
3806-
->setAttribute('hashOptions', $proofForPassword->getHash()->getOptions())
3807-
->setAttribute('emailVerification', true));
3817+
$profile = $dbForProject->updateDocument('users', $profile->getId(), new Document(
3818+
[
3819+
'password' => $newPassword,
3820+
'passwordHistory' => $history,
3821+
'passwordUpdate' => DateTime::now(),
3822+
'hash' => $proofForPassword->getHash()->getName(),
3823+
'hashOptions' => $proofForPassword->getHash()->getOptions(),
3824+
'emailVerification' => true]
3825+
));
38083826

38093827
$user->setAttributes($profile->getArrayCopy());
38103828

@@ -4126,7 +4144,7 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, arr
41264144

41274145
$authorization->addRole(Role::user($profile->getId())->toString());
41284146

4129-
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('emailVerification', true));
4147+
$profile = $dbForProject->updateDocument('users', $profile->getId(), new Document(['emailVerification' => true]));
41304148

41314149
$user->setAttributes($profile->getArrayCopy());
41324150

@@ -4342,7 +4360,7 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, arr
43424360

43434361
$authorization->addRole(Role::user($profile->getId())->toString());
43444362

4345-
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('phoneVerification', true));
4363+
$profile = $dbForProject->updateDocument('users', $profile->getId(), new Document(['phoneVerification' => true]));
43464364

43474365
$user->setAttributes($profile->getArrayCopy());
43484366

@@ -4500,7 +4518,11 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, arr
45004518

45014519
$target->setAttribute('name', "{$device['deviceBrand']} {$device['deviceModel']}");
45024520

4503-
$target = $dbForProject->updateDocument('targets', $target->getId(), $target);
4521+
$target = $dbForProject->updateDocument('targets', $target->getId(), new Document([
4522+
'identifier' => $target->getAttribute('identifier'),
4523+
'expired' => $target->getAttribute('expired'),
4524+
'name' => $target->getAttribute('name'),
4525+
]));
45044526

45054527
$dbForProject->purgeCachedDocument('users', $user->getId());
45064528

app/controllers/api/messaging.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2692,7 +2692,10 @@
26922692
$topic->setAttribute('subscribe', $subscribe);
26932693
}
26942694

2695-
$topic = $dbForProject->updateDocument('topics', $topicId, $topic);
2695+
$topic = $dbForProject->updateDocument('topics', $topicId, new Document([
2696+
'name' => $topic->getAttribute('name'),
2697+
'subscribe' => $topic->getAttribute('subscribe'),
2698+
]));
26962699

26972700
$queueForEvents
26982701
->setParam('topicId', $topic->getId());

app/controllers/api/users.php

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,7 +1161,7 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
11611161
throw new Exception(Exception::USER_NOT_FOUND);
11621162
}
11631163

1164-
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('status', (bool) $status));
1164+
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['status' => (bool) $status]));
11651165

11661166
$queueForEvents
11671167
->setParam('userId', $user->getId());
@@ -1204,7 +1204,7 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
12041204

12051205
$user->setAttribute('labels', (array) \array_values(\array_unique($labels)));
12061206

1207-
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
1207+
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['labels' => $user->getAttribute('labels')]));
12081208

12091209
$queueForEvents
12101210
->setParam('userId', $user->getId());
@@ -1245,7 +1245,7 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
12451245
throw new Exception(Exception::USER_NOT_FOUND);
12461246
}
12471247

1248-
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('phoneVerification', $phoneVerification));
1248+
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['phoneVerification' => $phoneVerification]));
12491249

12501250
$queueForEvents
12511251
->setParam('userId', $user->getId());
@@ -1289,7 +1289,7 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
12891289

12901290
$user->setAttribute('name', $name);
12911291

1292-
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
1292+
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['name' => $user->getAttribute('name')]));
12931293

12941294
$queueForEvents->setParam('userId', $user->getId());
12951295

@@ -1344,7 +1344,10 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
13441344
->setAttribute('password', '')
13451345
->setAttribute('passwordUpdate', DateTime::now());
13461346

1347-
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
1347+
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
1348+
'password' => $user->getAttribute('password'),
1349+
'passwordUpdate' => $user->getAttribute('passwordUpdate'),
1350+
]));
13481351
$queueForEvents->setParam('userId', $user->getId());
13491352
$response->dynamic($user, Response::MODEL_USER);
13501353
}
@@ -1377,7 +1380,13 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
13771380
->setAttribute('hash', $hasher->getName())
13781381
->setAttribute('hashOptions', $hasher->getOptions());
13791382

1380-
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
1383+
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
1384+
'password' => $user->getAttribute('password'),
1385+
'passwordHistory' => $user->getAttribute('passwordHistory'),
1386+
'passwordUpdate' => $user->getAttribute('passwordUpdate'),
1387+
'hash' => $user->getAttribute('hash'),
1388+
'hashOptions' => $user->getAttribute('hashOptions'),
1389+
]));
13811390

13821391
$sessions = $user->getAttribute('sessions', []);
13831392
$invalidate = $project->getAttribute('auths', default: [])['invalidateSessions'] ?? false;
@@ -1469,15 +1478,23 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
14691478
;
14701479

14711480
try {
1472-
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
1481+
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
1482+
'email' => $user->getAttribute('email'),
1483+
'emailVerification' => $user->getAttribute('emailVerification'),
1484+
'emailCanonical' => $user->getAttribute('emailCanonical'),
1485+
'emailIsCanonical' => $user->getAttribute('emailIsCanonical'),
1486+
'emailIsCorporate' => $user->getAttribute('emailIsCorporate'),
1487+
'emailIsDisposable' => $user->getAttribute('emailIsDisposable'),
1488+
'emailIsFree' => $user->getAttribute('emailIsFree'),
1489+
]));
14731490
/**
14741491
* @var Document $oldTarget
14751492
*/
14761493
$oldTarget = $user->find('identifier', $oldEmail, 'targets');
14771494

14781495
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
14791496
if (\strlen($email) !== 0) {
1480-
$dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email));
1497+
$dbForProject->updateDocument('targets', $oldTarget->getId(), new Document(['identifier' => $email]));
14811498
} else {
14821499
$dbForProject->deleteDocument('targets', $oldTarget->getId());
14831500
}
@@ -1558,15 +1575,18 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
15581575
}
15591576

15601577
try {
1561-
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
1578+
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
1579+
'phone' => $user->getAttribute('phone'),
1580+
'phoneVerification' => $user->getAttribute('phoneVerification'),
1581+
]));
15621582
/**
15631583
* @var Document $oldTarget
15641584
*/
15651585
$oldTarget = $user->find('identifier', $oldPhone, 'targets');
15661586

15671587
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
15681588
if (\strlen($number) !== 0) {
1569-
$dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $number));
1589+
$dbForProject->updateDocument('targets', $oldTarget->getId(), new Document(['identifier' => $number]));
15701590
} else {
15711591
$dbForProject->deleteDocument('targets', $oldTarget->getId());
15721592
}
@@ -1630,7 +1650,7 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
16301650
throw new Exception(Exception::USER_NOT_FOUND);
16311651
}
16321652

1633-
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', $emailVerification));
1653+
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['emailVerification' => $emailVerification]));
16341654

16351655
$queueForEvents->setParam('userId', $user->getId());
16361656

@@ -1668,7 +1688,7 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
16681688
throw new Exception(Exception::USER_NOT_FOUND);
16691689
}
16701690

1671-
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('prefs', $prefs));
1691+
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['prefs' => $prefs]));
16721692

16731693
$queueForEvents
16741694
->setParam('userId', $user->getId());
@@ -1768,7 +1788,13 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
17681788
$target->setAttribute('name', $name);
17691789
}
17701790

1771-
$target = $dbForProject->updateDocument('targets', $target->getId(), $target);
1791+
$target = $dbForProject->updateDocument('targets', $target->getId(), new Document([
1792+
'identifier' => $target->getAttribute('identifier'),
1793+
'expired' => $target->getAttribute('expired'),
1794+
'providerId' => $target->getAttribute('providerId'),
1795+
'providerInternalId' => $target->getAttribute('providerInternalId'),
1796+
'name' => $target->getAttribute('name'),
1797+
]));
17721798
$dbForProject->purgeCachedDocument('users', $user->getId());
17731799

17741800
$queueForEvents
@@ -1836,7 +1862,7 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
18361862

18371863
$user->setAttribute('mfa', $mfa);
18381864

1839-
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
1865+
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['mfa' => $user->getAttribute('mfa')]));
18401866

18411867
$queueForEvents->setParam('userId', $user->getId());
18421868

@@ -2024,7 +2050,7 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
20242050

20252051
$mfaRecoveryCodes = Type::generateBackupCodes();
20262052
$user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes);
2027-
$dbForProject->updateDocument('users', $user->getId(), $user);
2053+
$dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes]));
20282054

20292055
$queueForEvents->setParam('userId', $user->getId());
20302056

@@ -2096,7 +2122,7 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
20962122

20972123
$mfaRecoveryCodes = Type::generateBackupCodes();
20982124
$user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes);
2099-
$dbForProject->updateDocument('users', $user->getId(), $user);
2125+
$dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes]));
21002126

21012127
$queueForEvents->setParam('userId', $user->getId());
21022128

app/controllers/general.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1650,7 +1650,10 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
16501650
->setAttribute('pingedAt', $pingedAt);
16511651

16521652
$authorization->skip(function () use ($dbForPlatform, $project) {
1653-
$dbForPlatform->updateDocument('projects', $project->getId(), $project);
1653+
$dbForPlatform->updateDocument('projects', $project->getId(), new Document([
1654+
'pingCount' => $project->getAttribute('pingCount'),
1655+
'pingedAt' => $project->getAttribute('pingedAt')
1656+
]));
16541657
});
16551658

16561659
$queueForEvents

app/controllers/shared/api.php

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -372,9 +372,13 @@
372372
$user->setAttribute('accessedAt', DateTime::now());
373373

374374
if ($project->getId() !== 'console' && APP_MODE_ADMIN !== $mode) {
375-
$dbForProject->updateDocument('users', $user->getId(), $user);
375+
$dbForProject->updateDocument('users', $user->getId(), new Document([
376+
'accessedAt' => $user->getAttribute('accessedAt')
377+
]));
376378
} else {
377-
$authorization->skip(fn () => $dbForPlatform->updateDocument('users', $user->getId(), $user));
379+
$authorization->skip(fn () => $dbForPlatform->updateDocument('users', $user->getId(), new Document([
380+
'accessedAt' => $user->getAttribute('accessedAt')
381+
])));
378382
}
379383
}
380384
}
@@ -650,7 +654,9 @@
650654
$transformedAt = $file->getAttribute('transformedAt', '');
651655
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) {
652656
$file->setAttribute('transformedAt', DateTime::now());
653-
$authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $file->getAttribute('bucketInternalId'), $file->getId(), $file));
657+
$authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $file->getAttribute('bucketInternalId'), $file->getId(), new Document([
658+
'transformedAt' => $file->getAttribute('transformedAt')
659+
])));
654660
}
655661
}
656662
}
@@ -949,7 +955,9 @@
949955
}
950956
} elseif (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) {
951957
$cacheLog->setAttribute('accessedAt', $now);
952-
$authorization->skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog));
958+
$authorization->skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), new Document([
959+
'accessedAt' => $cacheLog->getAttribute('accessedAt')
960+
])));
953961
// Overwrite the file every APP_CACHE_UPDATE seconds to update the file modified time that is used in the TTL checks in cache->load()
954962
$cache->save($key, $data['payload']);
955963
}

0 commit comments

Comments
 (0)