Skip to content
Merged
52 changes: 29 additions & 23 deletions src/Database/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -5755,6 +5755,7 @@ private function deleteCascade(Document $collection, Document $relatedCollection
* @param array<Query> $queries
* @param int $batchSize
* @param callable|null $onNext
* @param callable|null $onError
* @return int
* @throws AuthorizationException
* @throws DatabaseException
Expand All @@ -5766,6 +5767,7 @@ public function deleteDocuments(
array $queries = [],
int $batchSize = self::DELETE_BATCH_SIZE,
?callable $onNext = null,
?callable $onError = null,
): int {
if ($this->adapter->getSharedTables() && empty($this->adapter->getTenant())) {
throw new DatabaseException('Missing tenant. Tenant must be set when table sharing is enabled.');
Expand Down Expand Up @@ -5846,32 +5848,33 @@ public function deleteDocuments(

$sequences = [];
$permissionIds = [];
foreach ($batch as $document) {
$sequences[] = $document->getSequence();
if (!empty($document->getPermissions())) {
$permissionIds[] = $document->getId();
}

if ($this->resolveRelationships) {
$document = $this->silent(fn () => $this->deleteDocumentRelationships(
$collection,
$document
));
}
$this->withTransaction(function () use ($collection, $sequences, $permissionIds, $batch) {
foreach ($batch as $document) {
$sequences[] = $document->getSequence();
if (!empty($document->getPermissions())) {
$permissionIds[] = $document->getId();
}

// Check if document was updated after the request timestamp
try {
$oldUpdatedAt = new \DateTime($document->getUpdatedAt());
} catch (Exception $e) {
throw new DatabaseException($e->getMessage(), $e->getCode(), $e);
}
if ($this->resolveRelationships) {
$document = $this->silent(fn () => $this->deleteDocumentRelationships(
$collection,
$document
));
}

if (!\is_null($this->timestamp) && $oldUpdatedAt > $this->timestamp) {
throw new ConflictException('Document was updated after the request timestamp');
// Check if document was updated after the request timestamp
try {
$oldUpdatedAt = new \DateTime($document->getUpdatedAt());
} catch (Exception $e) {
throw new DatabaseException($e->getMessage(), $e->getCode(), $e);
}

if (!\is_null($this->timestamp) && $oldUpdatedAt > $this->timestamp) {
throw new ConflictException('Document was updated after the request timestamp');
}
}
}

$this->withTransaction(function () use ($collection, $sequences, $permissionIds) {
$this->adapter->deleteDocuments(
$collection->getId(),
$sequences,
Expand All @@ -5887,8 +5890,11 @@ public function deleteDocuments(
} else {
$this->purgeCachedDocument($collection->getId(), $document->getId());
}

$onNext && $onNext($document);
try {
$onNext && $onNext($document);
} catch (Exception $e) {
$onError ? $onError($e) : throw $e;
}
$modified++;
}

Expand Down
2 changes: 2 additions & 0 deletions src/Database/Mirror.php
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,7 @@ public function deleteDocuments(
array $queries = [],
int $batchSize = self::DELETE_BATCH_SIZE,
?callable $onNext = null,
?callable $onError = null,
): int {
$modified = 0;

Expand All @@ -838,6 +839,7 @@ function ($doc) use (&$modified, $onNext) {
$onNext && $onNext($doc);
$modified++;
},
$onError
);

if (
Expand Down
101 changes: 95 additions & 6 deletions tests/e2e/Adapter/Scopes/DocumentTests.php
Original file line number Diff line number Diff line change
Expand Up @@ -3953,6 +3953,31 @@ public function testDeleteBulkDocuments(): void
*/
$selects = ['$sequence', '$id', '$collection', '$permissions', '$updatedAt'];

try {
// a non existent document to test the error thrown
$database->deleteDocuments(
collection: 'bulk_delete',
queries: [
Query::select([...$selects, '$createdAt']),
Query::lessThan('$createdAt', '1800-01-01'),
Query::orderAsc('$createdAt'),
Query::orderAsc(),
Query::limit(1),
],
batchSize: 1,
onNext: function () {
throw new Exception("Error thrown to test that deletion doesn't stop and error is caught");
}
);
} catch (Exception $e) {
if ($e instanceof Exception) {
$this->assertInstanceOf(Exception::class, $e);
$this->assertEquals("Error thrown to test that deletion doesn't stop and error is caught", $e->getMessage());
} else {
$this->fail("Caught value is not an Exception.");
}
}
Comment thread
ArnabChatterjee20k marked this conversation as resolved.
Outdated

$count = $database->deleteDocuments(
collection: 'bulk_delete',
queries: [
Expand All @@ -3963,13 +3988,33 @@ public function testDeleteBulkDocuments(): void
Query::orderAsc(),
Query::limit(2),
],
batchSize: 1
batchSize: 1,
onNext: function () {
throw new Exception("Error thrown to test that deletion doesn't stop and error is caught");
},
onError:function ($e) {
if ($e instanceof Exception) {
$this->assertInstanceOf(Exception::class, $e);
$this->assertEquals("Error thrown to test that deletion doesn't stop and error is caught", $e->getMessage());
} else {
$this->fail("Caught value is not an Exception.");
}
Comment thread
ArnabChatterjee20k marked this conversation as resolved.
Outdated
}
);

$this->assertEquals(2, $count);

// TEST: Bulk Delete All Documents
$this->assertEquals(8, $database->deleteDocuments('bulk_delete'));
$this->assertEquals(8, $database->deleteDocuments('bulk_delete', onNext: function () {
throw new Exception("Error thrown to test that deletion doesn't stop and error is caught");
}, onError:function ($e) {
if ($e instanceof Exception) {
$this->assertInstanceOf(Exception::class, $e);
$this->assertEquals("Error thrown to test that deletion doesn't stop and error is caught", $e->getMessage());
} else {
$this->fail("Caught value is not an Exception.");
}
Comment thread
ArnabChatterjee20k marked this conversation as resolved.
Outdated
}));

$docs = $database->find('bulk_delete');
$this->assertCount(0, $docs);
Expand All @@ -3982,6 +4027,14 @@ public function testDeleteBulkDocuments(): void
Query::greaterThanEqual('integer', 5)
], onNext: function ($doc) use (&$results) {
$results[] = $doc;
throw new Exception("Error thrown to test that deletion doesn't stop and error is caught");
}, onError:function ($e) {
if ($e instanceof Exception) {
$this->assertInstanceOf(Exception::class, $e);
$this->assertEquals("Error thrown to test that deletion doesn't stop and error is caught", $e->getMessage());
} else {
$this->fail("Caught value is not an Exception.");
}
Comment thread
ArnabChatterjee20k marked this conversation as resolved.
Outdated
});

$this->assertEquals(5, $count);
Expand All @@ -3998,7 +4051,16 @@ public function testDeleteBulkDocuments(): void

try {
$this->getDatabase()->withRequestTimestamp($oneHourAgo, function () {
return $this->getDatabase()->deleteDocuments('bulk_delete');
return $this->getDatabase()->deleteDocuments('bulk_delete', onNext: function () {
throw new Exception("Error thrown to test that deletion doesn't stop and error is caught");
}, onError:function ($e) {
if ($e instanceof Exception) {
$this->assertInstanceOf(Exception::class, $e);
$this->assertEquals("Error thrown to test that deletion doesn't stop and error is caught", $e->getMessage());
} else {
$this->fail("Caught value is not an Exception.");
}
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
});
$this->fail('Failed to throw exception');
} catch (ConflictException $e) {
Expand Down Expand Up @@ -4042,7 +4104,16 @@ public function testDeleteBulkDocuments(): void
Permission::delete(Role::any())
], false);

$database->deleteDocuments('bulk_delete');
$database->deleteDocuments('bulk_delete', onNext: function () {
throw new Exception("Error thrown to test that deletion doesn't stop and error is caught");
}, onError:function ($e) {
if ($e instanceof Exception) {
$this->assertInstanceOf(Exception::class, $e);
$this->assertEquals("Error thrown to test that deletion doesn't stop and error is caught", $e->getMessage());
} else {
$this->fail("Caught value is not an Exception.");
}
Comment thread
ArnabChatterjee20k marked this conversation as resolved.
Outdated
});

$this->assertEquals(0, \count($this->getDatabase()->find('bulk_delete')));

Expand Down Expand Up @@ -4087,10 +4158,28 @@ public function testDeleteBulkDocumentsQueries(): void
// Test limit
$this->propagateBulkDocuments('bulk_delete_queries');

$this->assertEquals(5, $database->deleteDocuments('bulk_delete_queries', [Query::limit(5)]));
$this->assertEquals(5, $database->deleteDocuments('bulk_delete_queries', [Query::limit(5)], onNext: function () {
throw new Exception("Error thrown to test that deletion doesn't stop and error is caught");
}, onError:function ($e) {
if ($e instanceof Exception) {
$this->assertInstanceOf(Exception::class, $e);
$this->assertEquals("Error thrown to test that deletion doesn't stop and error is caught", $e->getMessage());
} else {
$this->fail("Caught value is not an Exception.");
}
Comment thread
ArnabChatterjee20k marked this conversation as resolved.
Outdated
}));
$this->assertEquals(5, \count($database->find('bulk_delete_queries')));

$this->assertEquals(5, $database->deleteDocuments('bulk_delete_queries', [Query::limit(5)]));
$this->assertEquals(5, $database->deleteDocuments('bulk_delete_queries', [Query::limit(5)], onNext: function () {
throw new Exception("Error thrown to test that deletion doesn't stop and error is caught");
}, onError:function ($e) {
if ($e instanceof Exception) {
$this->assertInstanceOf(Exception::class, $e);
$this->assertEquals("Error thrown to test that deletion doesn't stop and error is caught", $e->getMessage());
} else {
$this->fail("Caught value is not an Exception.");
}
Comment thread
ArnabChatterjee20k marked this conversation as resolved.
Outdated
}));
$this->assertEquals(0, \count($database->find('bulk_delete_queries')));

// Test Limit more than batchSize
Expand Down
33 changes: 30 additions & 3 deletions tests/e2e/Adapter/Scopes/Relationships/ManyToManyTests.php
Original file line number Diff line number Diff line change
Expand Up @@ -1581,18 +1581,45 @@ public function testDeleteBulkDocumentsManyToManyRelationship(): void

// Delete person
try {
$this->getDatabase()->deleteDocuments('bulk_delete_person_m2m');
$this->getDatabase()->deleteDocuments('bulk_delete_person_m2m', onNext: function () {
throw new Exception("Error thrown to test that deletion doesn't stop and error is caught");
}, onError:function ($e) {
if ($e instanceof Exception) {
$this->assertInstanceOf(Exception::class, $e);
$this->assertEquals("Error thrown to test that deletion doesn't stop and error is caught", $e->getMessage());
} else {
$this->fail("Caught value is not an Exception.");
}
});
$this->fail('Failed to throw exception');
} catch (RestrictedException $e) {
$this->assertEquals('Cannot delete document because it has at least one related document.', $e->getMessage());
}

// Restrict Cleanup
$this->getDatabase()->deleteRelationship('bulk_delete_person_m2m', 'bulk_delete_library_m2m');
$this->getDatabase()->deleteDocuments('bulk_delete_library_m2m');
$this->getDatabase()->deleteDocuments('bulk_delete_library_m2m', onNext: function () {
throw new Exception("Error thrown to test that deletion doesn't stop and error is caught");
}, onError:function ($e) {
if ($e instanceof Exception) {
$this->assertInstanceOf(Exception::class, $e);
$this->assertEquals("Error thrown to test that deletion doesn't stop and error is caught", $e->getMessage());
} else {
$this->fail("Caught value is not an Exception.");
}
});
$this->assertCount(0, $this->getDatabase()->find('bulk_delete_library_m2m'));

$this->getDatabase()->deleteDocuments('bulk_delete_person_m2m');
$this->getDatabase()->deleteDocuments('bulk_delete_person_m2m', onNext: function () {
throw new Exception("Error thrown to test that deletion doesn't stop and error is caught");
}, onError:function ($e) {
if ($e instanceof Exception) {
$this->assertInstanceOf(Exception::class, $e);
$this->assertEquals("Error thrown to test that deletion doesn't stop and error is caught", $e->getMessage());
} else {
$this->fail("Caught value is not an Exception.");
}
});
$this->assertCount(0, $this->getDatabase()->find('bulk_delete_person_m2m'));
}
}
Loading