@@ -417,6 +417,8 @@ class Database
417417
418418 protected bool $ preserveDates = false ;
419419
420+ protected bool $ skipDuplicates = false ;
421+
420422 protected bool $ preserveSequence = false ;
421423
422424 protected int $ maxQueryValues = 5000 ;
@@ -842,6 +844,29 @@ public function skipRelationshipsExistCheck(callable $callback): mixed
842844 }
843845 }
844846
847+ public function skipDuplicates (callable $ callback ): mixed
848+ {
849+ $ previous = $ this ->skipDuplicates ;
850+ $ this ->skipDuplicates = true ;
851+
852+ try {
853+ return $ callback ();
854+ } finally {
855+ $ this ->skipDuplicates = $ previous ;
856+ }
857+ }
858+
859+ /**
860+ * Build a tenant-aware identity key for a document.
861+ * Returns "<tenant>:<id>" in tenant-per-document shared-table mode, otherwise just the id.
862+ */
863+ private function tenantKey (Document $ document ): string
864+ {
865+ return ($ this ->adapter ->getSharedTables () && $ this ->adapter ->getTenantPerDocument ())
866+ ? $ document ->getTenant () . ': ' . $ document ->getId ()
867+ : $ document ->getId ();
868+ }
869+
845870 /**
846871 * Trigger callback for events
847872 *
@@ -5700,9 +5725,11 @@ public function createDocuments(
57005725 }
57015726
57025727 foreach (\array_chunk ($ documents , $ batchSize ) as $ chunk ) {
5703- $ batch = $ this ->withTransaction (function () use ($ collection , $ chunk ) {
5704- return $ this ->adapter ->createDocuments ($ collection , $ chunk );
5705- });
5728+ $ insert = fn () => $ this ->withTransaction (fn () => $ this ->adapter ->createDocuments ($ collection , $ chunk ));
5729+ // Set adapter flag before withTransaction so Mongo can opt out of a real txn.
5730+ $ batch = $ this ->skipDuplicates
5731+ ? $ this ->adapter ->skipDuplicates ($ insert )
5732+ : $ insert ();
57065733
57075734 $ batch = $ this ->adapter ->getSequences ($ collection ->getId (), $ batch );
57085735
@@ -7116,18 +7143,57 @@ public function upsertDocumentsWithIncrease(
71167143 $ created = 0 ;
71177144 $ updated = 0 ;
71187145 $ seenIds = [];
7119- foreach ($ documents as $ key => $ document ) {
7120- if ($ this ->getSharedTables () && $ this ->getTenantPerDocument ()) {
7121- $ old = $ this ->authorization ->skip (fn () => $ this ->withTenant ($ document ->getTenant (), fn () => $ this ->silent (fn () => $ this ->getDocument (
7122- $ collection ->getId (),
7123- $ document ->getId (),
7124- ))));
7125- } else {
7126- $ old = $ this ->authorization ->skip (fn () => $ this ->silent (fn () => $ this ->getDocument (
7127- $ collection ->getId (),
7128- $ document ->getId (),
7129- )));
7146+
7147+ // Batch-fetch existing documents in one query instead of N individual getDocument() calls.
7148+ // tenantPerDocument: group ids by tenant and run one find() per tenant under withTenant,
7149+ // so cross-tenant batches (e.g. StatsUsage worker) don't get silently scoped to the
7150+ // session tenant and miss rows belonging to other tenants.
7151+ $ existingDocs = [];
7152+
7153+ if ($ this ->getSharedTables () && $ this ->getTenantPerDocument ()) {
7154+ $ idsByTenant = [];
7155+ foreach ($ documents as $ doc ) {
7156+ if ($ doc ->getId () !== '' ) {
7157+ $ idsByTenant [$ doc ->getTenant ()][] = $ doc ->getId ();
7158+ }
7159+ }
7160+ foreach ($ idsByTenant as $ tenant => $ tenantIds ) {
7161+ $ tenantIds = \array_values (\array_unique ($ tenantIds ));
7162+ foreach (\array_chunk ($ tenantIds , \max (1 , $ this ->maxQueryValues )) as $ chunk ) {
7163+ $ found = $ this ->authorization ->skip (fn () => $ this ->withTenant ($ tenant , fn () => $ this ->silent (
7164+ fn () => $ this ->find ($ collection ->getId (), [
7165+ Query::equal ('$id ' , $ chunk ),
7166+ Query::limit ($ this ->maxQueryValues ),
7167+ ])
7168+ )));
7169+ foreach ($ found as $ doc ) {
7170+ $ existingDocs [$ this ->tenantKey ($ doc )] = $ doc ;
7171+ }
7172+ }
71307173 }
7174+ } else {
7175+ $ docIds = \array_values (\array_unique (\array_filter (
7176+ \array_map (fn (Document $ doc ) => $ doc ->getId (), $ documents ),
7177+ fn ($ id ) => $ id !== ''
7178+ )));
7179+
7180+ if (!empty ($ docIds )) {
7181+ foreach (\array_chunk ($ docIds , \max (1 , $ this ->maxQueryValues )) as $ chunk ) {
7182+ $ existing = $ this ->authorization ->skip (fn () => $ this ->silent (
7183+ fn () => $ this ->find ($ collection ->getId (), [
7184+ Query::equal ('$id ' , $ chunk ),
7185+ Query::limit ($ this ->maxQueryValues ),
7186+ ])
7187+ ));
7188+ foreach ($ existing as $ doc ) {
7189+ $ existingDocs [$ this ->tenantKey ($ doc )] = $ doc ;
7190+ }
7191+ }
7192+ }
7193+ }
7194+
7195+ foreach ($ documents as $ key => $ document ) {
7196+ $ old = $ existingDocs [$ this ->tenantKey ($ document )] ?? new Document ();
71317197
71327198 // Extract operators early to avoid comparison issues
71337199 $ documentArray = $ document ->getArrayCopy ();
@@ -7294,7 +7360,7 @@ public function upsertDocumentsWithIncrease(
72947360 $ document = $ this ->silent (fn () => $ this ->createDocumentRelationships ($ collection , $ document ));
72957361 }
72967362
7297- $ seenIds [] = $ document -> getId ( );
7363+ $ seenIds [] = $ this -> tenantKey ( $ document );
72987364 $ old = $ this ->adapter ->castingBefore ($ collection , $ old );
72997365 $ document = $ this ->adapter ->castingBefore ($ collection , $ document );
73007366
0 commit comments