diff --git a/tests/bulkwritecommand/bulkwritecommand-debug-003.phpt b/tests/bulkwritecommand/bulkwritecommand-debug-003.phpt
new file mode 100644
index 000000000..971f55381
--- /dev/null
+++ b/tests/bulkwritecommand/bulkwritecommand-debug-003.phpt
@@ -0,0 +1,106 @@
+--TEST--
+MongoDB\Driver\BulkWriteCommand debug output before execution
+--FILE--
+<?php
+
+$tests = [
+    [],
+    ['ordered' => true],
+    ['ordered' => false],
+    ['bypassDocumentValidation' => true],
+    ['bypassDocumentValidation' => false],
+    ['comment' => ['foo' => 1]],
+    ['let' => ['id' => 1, 'x' => 'foo']],
+];
+
+foreach ($tests as $options) {
+    var_dump(new MongoDB\Driver\BulkWriteCommand($options));
+}
+
+?>
+===DONE===
+<?php exit(0); ?>
+--EXPECTF--
+object(MongoDB\Driver\BulkWriteCommand)#%d (%d) {
+  ["bypassDocumentValidation"]=>
+  NULL
+  ["ordered"]=>
+  bool(true)
+  ["verboseResults"]=>
+  bool(false)
+  ["session"]=>
+  NULL
+}
+object(MongoDB\Driver\BulkWriteCommand)#%d (%d) {
+  ["bypassDocumentValidation"]=>
+  NULL
+  ["ordered"]=>
+  bool(true)
+  ["verboseResults"]=>
+  bool(false)
+  ["session"]=>
+  NULL
+}
+object(MongoDB\Driver\BulkWriteCommand)#%d (%d) {
+  ["bypassDocumentValidation"]=>
+  NULL
+  ["ordered"]=>
+  bool(false)
+  ["verboseResults"]=>
+  bool(false)
+  ["session"]=>
+  NULL
+}
+object(MongoDB\Driver\BulkWriteCommand)#%d (%d) {
+  ["bypassDocumentValidation"]=>
+  bool(true)
+  ["ordered"]=>
+  bool(true)
+  ["verboseResults"]=>
+  bool(false)
+  ["session"]=>
+  NULL
+}
+object(MongoDB\Driver\BulkWriteCommand)#%d (%d) {
+  ["bypassDocumentValidation"]=>
+  bool(false)
+  ["ordered"]=>
+  bool(true)
+  ["verboseResults"]=>
+  bool(false)
+  ["session"]=>
+  NULL
+}
+object(MongoDB\Driver\BulkWriteCommand)#%d (%d) {
+  ["bypassDocumentValidation"]=>
+  NULL
+  ["comment"]=>
+  object(stdClass)#2 (1) {
+    ["foo"]=>
+    int(1)
+  }
+  ["ordered"]=>
+  bool(true)
+  ["verboseResults"]=>
+  bool(false)
+  ["session"]=>
+  NULL
+}
+object(MongoDB\Driver\BulkWriteCommand)#%d (%d) {
+  ["bypassDocumentValidation"]=>
+  NULL
+  ["let"]=>
+  object(stdClass)#2 (2) {
+    ["id"]=>
+    int(1)
+    ["x"]=>
+    string(3) "foo"
+  }
+  ["ordered"]=>
+  bool(true)
+  ["verboseResults"]=>
+  bool(false)
+  ["session"]=>
+  NULL
+}
+===DONE===
diff --git a/tests/bulkwritecommand/bulkwritecommand-deleteOne-001.phpt b/tests/bulkwritecommand/bulkwritecommand-deleteOne-001.phpt
new file mode 100644
index 000000000..eaa45f502
--- /dev/null
+++ b/tests/bulkwritecommand/bulkwritecommand-deleteOne-001.phpt
@@ -0,0 +1,85 @@
+--TEST--
+MongoDB\Driver\BulkWriteCommand::deleteOne() should always encode __pclass for Persistable objects
+--SKIPIF--
+<?php require __DIR__ . "/../utils/basic-skipif.inc"; ?>
+<?php skip_if_not_live(); ?>
+<?php skip_if_server_version('<', '8.0'); ?>
+<?php skip_if_not_clean(); ?>
+--FILE--
+<?php
+require_once __DIR__ . "/../utils/basic.inc";
+
+class MyClass implements MongoDB\BSON\Persistable
+{
+    private $id;
+    private $child;
+
+    public function __construct($id, ?MyClass $child = null)
+    {
+        $this->id = $id;
+        $this->child = $child;
+    }
+
+    public function bsonSerialize(): array
+    {
+        return [
+            '_id' => $this->id,
+            'child' => $this->child,
+        ];
+    }
+
+    public function bsonUnserialize(array $data): void
+    {
+        $this->id = $data['_id'];
+        $this->child = $data['child'];
+    }
+}
+
+$manager = create_test_manager();
+
+$document = new MyClass('foo', new MyClass('bar', new MyClass('baz')));
+
+$bulk = new MongoDB\Driver\BulkWriteCommand();
+$bulk->insertOne(NS, $document);
+$result = $manager->executeBulkWriteCommand($bulk);
+printf("Inserted %d document(s)\n", $result->getInsertedCount());
+
+$cursor = $manager->executeQuery(NS, new MongoDB\Driver\Query([]));
+var_dump($cursor->toArray());
+
+$bulk = new MongoDB\Driver\BulkWriteCommand();
+$bulk->deleteOne(NS, $document);
+$result = $manager->executeBulkWriteCommand($bulk);
+printf("Deleted %d document(s)\n", $result->getDeletedCount());
+
+$cursor = $manager->executeQuery(NS, new MongoDB\Driver\Query([]));
+var_dump($cursor->toArray());
+
+?>
+===DONE===
+<?php exit(0); ?>
+--EXPECTF--
+Inserted 1 document(s)
+array(1) {
+  [0]=>
+  object(MyClass)#%d (%d) {
+    ["id":"MyClass":private]=>
+    string(3) "foo"
+    ["child":"MyClass":private]=>
+    object(MyClass)#%d (%d) {
+      ["id":"MyClass":private]=>
+      string(3) "bar"
+      ["child":"MyClass":private]=>
+      object(MyClass)#%d (%d) {
+        ["id":"MyClass":private]=>
+        string(3) "baz"
+        ["child":"MyClass":private]=>
+        NULL
+      }
+    }
+  }
+}
+Deleted 1 document(s)
+array(0) {
+}
+===DONE===
diff --git a/tests/bulkwritecommand/bulkwritecommand-deleteOne-002.phpt b/tests/bulkwritecommand/bulkwritecommand-deleteOne-002.phpt
new file mode 100644
index 000000000..c53ec4208
--- /dev/null
+++ b/tests/bulkwritecommand/bulkwritecommand-deleteOne-002.phpt
@@ -0,0 +1,55 @@
+--TEST--
+MongoDB\Driver\BulkWriteCommand::deleteOne() with hint option
+--SKIPIF--
+<?php require __DIR__ . "/../utils/basic-skipif.inc"; ?>
+<?php skip_if_not_live(); ?>
+<?php skip_if_server_version('<', '8.0'); ?>
+<?php skip_if_not_clean(); ?>
+--FILE--
+<?php
+require_once __DIR__ . "/../utils/basic.inc";
+
+class CommandLogger implements MongoDB\Driver\Monitoring\CommandSubscriber
+{
+    public function commandStarted(MongoDB\Driver\Monitoring\CommandStartedEvent $event): void
+    {
+        if ($event->getCommandName() !== 'bulkWrite') {
+            return;
+        }
+
+        printf("delete included hint: %s\n", json_encode($event->getCommand()->ops[0]->hint));
+    }
+
+    public function commandSucceeded(MongoDB\Driver\Monitoring\CommandSucceededEvent $event): void
+    {
+    }
+
+    public function commandFailed(MongoDB\Driver\Monitoring\CommandFailedEvent $event): void
+    {
+    }
+}
+
+$manager = create_test_manager();
+
+$bulk = new MongoDB\Driver\BulkWriteCommand();
+$bulk->insertOne(NS, ['x' => 1]);
+$bulk->insertOne(NS, ['x' => 2]);
+$manager->executeBulkWriteCommand($bulk);
+
+MongoDB\Driver\Monitoring\addSubscriber(new CommandLogger);
+
+$bulk = new MongoDB\Driver\BulkWriteCommand;
+$bulk->deleteOne(NS, ['_id' => 1], ['hint' => '_id_']);
+$manager->executeBulkWriteCommand($bulk);
+
+$bulk = new MongoDB\Driver\BulkWriteCommand;
+$bulk->deleteOne(NS, ['_id' => 2], ['hint' => ['_id' => 1]]);
+$manager->executeBulkWriteCommand($bulk);
+
+?>
+===DONE===
+<?php exit(0); ?>
+--EXPECTF--
+delete included hint: "_id_"
+delete included hint: {"_id":1}
+===DONE===
diff --git a/tests/bulkwritecommand/bulkwritecommand-deleteOne-003.phpt b/tests/bulkwritecommand/bulkwritecommand-deleteOne-003.phpt
new file mode 100644
index 000000000..172a7a6ef
--- /dev/null
+++ b/tests/bulkwritecommand/bulkwritecommand-deleteOne-003.phpt
@@ -0,0 +1,33 @@
+--TEST--
+MongoDB\Driver\BulkWriteCommand::deleteOne() $filter is MongoDB\BSON\Document
+--SKIPIF--
+<?php require __DIR__ . "/../utils/basic-skipif.inc"; ?>
+<?php skip_if_not_live(); ?>
+<?php skip_if_server_version('<', '8.0'); ?>
+<?php skip_if_not_clean(); ?>
+--FILE--
+<?php
+
+require_once __DIR__ . "/../utils/basic.inc";
+
+$manager = create_test_manager();
+
+$bulk = new MongoDB\Driver\BulkWriteCommand();
+$bulk->insertOne(NS, ['_id' => 1]);
+$bulk->insertOne(NS, ['_id' => 2]);
+$manager->executeBulkWriteCommand($bulk);
+
+$filter = MongoDB\BSON\Document::fromJSON('{ "_id": { "$gt": 1 } }');
+
+$bulk = new MongoDB\Driver\BulkWriteCommand;
+$bulk->deleteOne(NS, $filter);
+$result = $manager->executeBulkWriteCommand($bulk);
+
+var_dump($result->getDeletedCount());
+
+?>
+===DONE===
+<?php exit(0); ?>
+--EXPECT--
+int(1)
+===DONE===
diff --git a/tests/bulkwritecommand/bulkwritecommand-deleteOne_error-001.phpt b/tests/bulkwritecommand/bulkwritecommand-deleteOne_error-001.phpt
new file mode 100644
index 000000000..aaae4bdca
--- /dev/null
+++ b/tests/bulkwritecommand/bulkwritecommand-deleteOne_error-001.phpt
@@ -0,0 +1,27 @@
+--TEST--
+MongoDB\Driver\BulkWriteCommand::deleteOne() with invalid options
+--FILE--
+<?php
+
+require_once __DIR__ . '/../utils/basic.inc';
+
+$bulk = new MongoDB\Driver\BulkWriteCommand;
+
+echo throws(function() use ($bulk) {
+    $bulk->deleteOne(NS, ['x' => 1], ['collation' => 1]);
+}, 'MongoDB\Driver\Exception\InvalidArgumentException'), "\n\n";
+
+echo throws(function() use ($bulk) {
+    $bulk->deleteOne(NS, ['x' => 1], ['hint' => 1]);
+}, 'MongoDB\Driver\Exception\InvalidArgumentException'), "\n";
+
+?>
+===DONE===
+<?php exit(0); ?>
+--EXPECT--
+OK: Got MongoDB\Driver\Exception\InvalidArgumentException
+Expected "collation" option to be array or object, int given
+
+OK: Got MongoDB\Driver\Exception\InvalidArgumentException
+Expected "hint" option to be string, array, or object, int given
+===DONE===
diff --git a/tests/bulkwritecommand/bulkwritecommand-deleteOne_error-002.phpt b/tests/bulkwritecommand/bulkwritecommand-deleteOne_error-002.phpt
new file mode 100644
index 000000000..f59ab761e
--- /dev/null
+++ b/tests/bulkwritecommand/bulkwritecommand-deleteOne_error-002.phpt
@@ -0,0 +1,27 @@
+--TEST--
+MongoDB\Driver\BulkWriteCommand::deleteOne() with BSON encoding error (invalid UTF-8 string)
+--FILE--
+<?php
+
+require_once __DIR__ . '/../utils/basic.inc';
+
+$bulk = new MongoDB\Driver\BulkWriteCommand;
+
+echo throws(function() use ($bulk) {
+    $bulk->deleteOne(NS, ['x' => "\xc3\x28"]);
+}, 'MongoDB\Driver\Exception\UnexpectedValueException'), "\n\n";
+
+echo throws(function() use ($bulk) {
+    $bulk->deleteOne(NS, ['x' => 1], ['collation' => ['locale' => "\xc3\x28"]]);
+}, 'MongoDB\Driver\Exception\UnexpectedValueException'), "\n";
+
+?>
+===DONE===
+<?php exit(0); ?>
+--EXPECTF--
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+Detected invalid UTF-8 for field path "x": %s
+
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+Detected invalid UTF-8 for field path "locale": %s
+===DONE===
diff --git a/tests/bulkwritecommand/bulkwritecommand-deleteOne_error-003.phpt b/tests/bulkwritecommand/bulkwritecommand-deleteOne_error-003.phpt
new file mode 100644
index 000000000..c6b7ba1b1
--- /dev/null
+++ b/tests/bulkwritecommand/bulkwritecommand-deleteOne_error-003.phpt
@@ -0,0 +1,55 @@
+--TEST--
+MongoDB\Driver\BulkWriteCommand::deleteOne() with BSON encoding error (null bytes in keys)
+--FILE--
+<?php
+
+require_once __DIR__ . '/../utils/basic.inc';
+
+$bulk = new MongoDB\Driver\BulkWriteCommand;
+
+echo throws(function() use ($bulk) {
+    $bulk->deleteOne(NS, ["\0" => 1]);
+}, 'MongoDB\Driver\Exception\UnexpectedValueException'), "\n\n";
+
+echo throws(function() use ($bulk) {
+    $bulk->deleteOne(NS, ["x\0" => 1]);
+}, 'MongoDB\Driver\Exception\UnexpectedValueException'), "\n\n";
+
+echo throws(function() use ($bulk) {
+    $bulk->deleteOne(NS, ["\0\0\0" => 1]);
+}, 'MongoDB\Driver\Exception\UnexpectedValueException'), "\n\n";
+
+echo throws(function() use ($bulk) {
+    $bulk->deleteOne(NS, ['x' => 1], ['collation' => ["\0" => 1]]);
+}, 'MongoDB\Driver\Exception\UnexpectedValueException'), "\n\n";
+
+echo throws(function() use ($bulk) {
+    $bulk->deleteOne(NS, ['x' => 1], ['collation' => ["x\0" => 1]]);
+}, 'MongoDB\Driver\Exception\UnexpectedValueException'), "\n\n";
+
+echo throws(function() use ($bulk) {
+    $bulk->deleteOne(NS, ['x' => 1], ['collation' => ["\0\0\0" => 1]]);
+}, 'MongoDB\Driver\Exception\UnexpectedValueException'), "\n";
+
+?>
+===DONE===
+<?php exit(0); ?>
+--EXPECT--
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+BSON keys cannot contain null bytes. Unexpected null byte after "".
+
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+BSON keys cannot contain null bytes. Unexpected null byte after "x".
+
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+BSON keys cannot contain null bytes. Unexpected null byte after "".
+
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+BSON keys cannot contain null bytes. Unexpected null byte after "".
+
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+BSON keys cannot contain null bytes. Unexpected null byte after "x".
+
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+BSON keys cannot contain null bytes. Unexpected null byte after "".
+===DONE===
diff --git a/tests/bulkwritecommand/bulkwritecommand-deleteOne_error-004.phpt b/tests/bulkwritecommand/bulkwritecommand-deleteOne_error-004.phpt
new file mode 100644
index 000000000..52abea739
--- /dev/null
+++ b/tests/bulkwritecommand/bulkwritecommand-deleteOne_error-004.phpt
@@ -0,0 +1,33 @@
+--TEST--
+MongoDB\Driver\BulkWriteCommand::deleteOne() prohibits PackedArray for document values
+--FILE--
+<?php
+require_once __DIR__ . "/../utils/basic.inc";
+
+$bulk = new MongoDB\Driver\BulkWriteCommand;
+
+echo throws(function() use ($bulk) {
+    $bulk->deleteOne(NS, MongoDB\BSON\PackedArray::fromPHP([]));
+}, MongoDB\Driver\Exception\UnexpectedValueException::class), "\n";
+
+echo throws(function() use ($bulk) {
+    $bulk->deleteOne(NS, [], ['collation' => MongoDB\BSON\PackedArray::fromPHP([])]);
+}, MongoDB\Driver\Exception\UnexpectedValueException::class), "\n";
+
+// @TODO: ALMOST: Got MongoDB\Driver\Exception\InvalidArgumentException - expected MongoDB\Driver\Exception\UnexpectedValueException
+// Expected "hint" option to yield string or document but got "array"
+echo throws(function() use ($bulk) {
+    $bulk->deleteOne(NS, [], ['hint' => MongoDB\BSON\PackedArray::fromPHP([])]);
+}, MongoDB\Driver\Exception\InvalidArgumentException::class), "\n";
+
+?>
+===DONE===
+<?php exit(0); ?>
+--EXPECT--
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+MongoDB\BSON\PackedArray cannot be serialized as a root document
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+MongoDB\BSON\PackedArray cannot be serialized as a root document
+OK: Got MongoDB\Driver\Exception\InvalidArgumentException
+Expected "hint" option to yield string or document but got "array"
+===DONE===
diff --git a/tests/bulkwritecommand/bulkwritecommand-insertOne-001.phpt b/tests/bulkwritecommand/bulkwritecommand-insertOne-001.phpt
new file mode 100644
index 000000000..9a9a77d0d
--- /dev/null
+++ b/tests/bulkwritecommand/bulkwritecommand-insertOne-001.phpt
@@ -0,0 +1,72 @@
+--TEST--
+MongoDB\Driver\BulkWriteCommand::insertOne() should always encode __pclass for Persistable objects
+--SKIPIF--
+<?php require __DIR__ . "/../utils/basic-skipif.inc"; ?>
+<?php skip_if_not_live(); ?>
+<?php skip_if_server_version('<', '8.0'); ?>
+<?php skip_if_not_clean(); ?>
+--FILE--
+<?php
+require_once __DIR__ . "/../utils/basic.inc";
+
+class MyClass implements MongoDB\BSON\Persistable
+{
+    private $id;
+    private $child;
+
+    public function __construct($id, ?MyClass $child = null)
+    {
+        $this->id = $id;
+        $this->child = $child;
+    }
+
+    public function bsonSerialize(): array
+    {
+        return [
+            '_id' => $this->id,
+            'child' => $this->child,
+        ];
+    }
+
+    public function bsonUnserialize(array $data): void
+    {
+        $this->id = $data['_id'];
+        $this->child = $data['child'];
+    }
+}
+
+$manager = create_test_manager();
+
+$bulk = new MongoDB\Driver\BulkWriteCommand();
+$bulk->insertOne(NS, new MyClass('foo', new MyClass('bar', new MyClass('baz'))));
+$result = $manager->executeBulkWriteCommand($bulk);
+printf("Inserted %d document(s)\n", $result->getInsertedCount());
+
+$cursor = $manager->executeQuery(NS, new MongoDB\Driver\Query([]));
+var_dump($cursor->toArray());
+
+?>
+===DONE===
+<?php exit(0); ?>
+--EXPECTF--
+Inserted 1 document(s)
+array(1) {
+  [0]=>
+  object(MyClass)#%d (%d) {
+    ["id":"MyClass":private]=>
+    string(3) "foo"
+    ["child":"MyClass":private]=>
+    object(MyClass)#%d (%d) {
+      ["id":"MyClass":private]=>
+      string(3) "bar"
+      ["child":"MyClass":private]=>
+      object(MyClass)#%d (%d) {
+        ["id":"MyClass":private]=>
+        string(3) "baz"
+        ["child":"MyClass":private]=>
+        NULL
+      }
+    }
+  }
+}
+===DONE===
diff --git a/tests/bulkwritecommand/bulkwritecommand-insertOne-002.phpt b/tests/bulkwritecommand/bulkwritecommand-insertOne-002.phpt
new file mode 100644
index 000000000..b69431c74
--- /dev/null
+++ b/tests/bulkwritecommand/bulkwritecommand-insertOne-002.phpt
@@ -0,0 +1,30 @@
+--TEST--
+MongoDB\Driver\BulkWriteCommand::insertOne() $document is MongoDB\BSON\Document
+--SKIPIF--
+<?php require __DIR__ . "/../utils/basic-skipif.inc"; ?>
+<?php skip_if_not_live(); ?>
+<?php skip_if_server_version('<', '8.0'); ?>
+<?php skip_if_not_clean(); ?>
+--FILE--
+<?php
+
+require_once __DIR__ . "/../utils/basic.inc";
+
+$manager = create_test_manager();
+
+$document = MongoDB\BSON\Document::fromJSON('{ "_id": 2 }');
+
+$bulk = new MongoDB\Driver\BulkWriteCommand();
+$insertedId = $bulk->insertOne(NS, $document);
+$result = $manager->executeBulkWriteCommand($bulk);
+
+var_dump($insertedId);
+var_dump($result->getInsertedCount());
+
+?>
+===DONE===
+<?php exit(0); ?>
+--EXPECT--
+int(2)
+int(1)
+===DONE===
diff --git a/tests/bulkwritecommand/bulkwritecommand-insertOne-003.phpt b/tests/bulkwritecommand/bulkwritecommand-insertOne-003.phpt
new file mode 100644
index 000000000..f4497591a
--- /dev/null
+++ b/tests/bulkwritecommand/bulkwritecommand-insertOne-003.phpt
@@ -0,0 +1,54 @@
+--TEST--
+MongoDB\Driver\BulkWriteCommand::insertOne() appends ID when inserting Document instances
+--SKIPIF--
+<?php require __DIR__ . "/../utils/basic-skipif.inc"; ?>
+<?php skip_if_not_live(); ?>
+<?php skip_if_server_version('<', '8.0'); ?>
+<?php skip_if_not_clean(); ?>
+--FILE--
+<?php
+require_once __DIR__ . "/../utils/basic.inc";
+
+$manager = create_test_manager();
+
+$document = MongoDB\BSON\Document::fromPHP(['foo' => 'bar']);
+
+$bulk = new MongoDB\Driver\BulkWriteCommand();
+
+$insertedId = $bulk->insertOne(NS, $document);
+
+$result = $manager->executeBulkWriteCommand($bulk);
+printf("Inserted %d document(s)\n", $result->getInsertedCount());
+
+var_dump($insertedId);
+var_dump($document->toPHP());
+
+$cursor = $manager->executeQuery(NS, new MongoDB\Driver\Query([]));
+var_dump($cursor->toArray());
+
+?>
+===DONE===
+<?php exit(0); ?>
+--EXPECTF--
+Inserted 1 document(s)
+object(MongoDB\BSON\ObjectId)#%d (%d) {
+  ["oid"]=>
+  string(24) "%x"
+}
+object(stdClass)#%d (%d) {
+  ["foo"]=>
+  string(3) "bar"
+}
+array(1) {
+  [0]=>
+  object(stdClass)#%d (%d) {
+    ["_id"]=>
+    object(MongoDB\BSON\ObjectId)#%d (%d) {
+      ["oid"]=>
+      string(24) "%x"
+    }
+    ["foo"]=>
+    string(3) "bar"
+  }
+}
+===DONE===
diff --git a/tests/bulkwritecommand/bulkwritecommand-insertOne-004.phpt b/tests/bulkwritecommand/bulkwritecommand-insertOne-004.phpt
new file mode 100644
index 000000000..9c9363600
--- /dev/null
+++ b/tests/bulkwritecommand/bulkwritecommand-insertOne-004.phpt
@@ -0,0 +1,126 @@
+--TEST--
+MongoDB\Driver\BulkWriteCommand::insertOne() returns "_id" of inserted document
+--SKIPIF--
+<?php require __DIR__ . "/../utils/basic-skipif.inc"; ?>
+<?php skip_if_not_live(); ?>
+<?php skip_if_server_version('<', '8.0'); ?>
+<?php skip_if_not_clean(); ?>
+--FILE--
+<?php
+require_once __DIR__ . "/../utils/basic.inc";
+
+class MySerializableId implements MongoDB\BSON\Serializable
+{
+    public $id;
+
+    public function __construct($id)
+    {
+        $this->id = $id;
+    }
+
+    public function bsonSerialize(): array
+    {
+        return ['id' => $this->id];
+    }
+}
+
+class MyPersistableId extends MySerializableId implements MongoDB\BSON\Persistable
+{
+    public function bsonUnserialize(array $data): void
+    {
+        $this->id = $data['id'];
+    }
+}
+
+$documents = [
+    ['x' => 1],
+    ['_id' => new MongoDB\BSON\ObjectId('590b72d606e9660190656a55')],
+    ['_id' => ['foo' => 1]],
+    ['_id' => new MySerializableId('foo')],
+    ['_id' => new MyPersistableId('bar')],
+];
+
+$manager = create_test_manager();
+
+$bulk = new MongoDB\Driver\BulkWriteCommand();
+
+foreach ($documents as $document) {
+    var_dump($bulk->insertOne(NS, $document));
+}
+
+$result = $manager->executeBulkWriteCommand($bulk);
+printf("Inserted %d document(s)\n", $result->getInsertedCount());
+
+$cursor = $manager->executeQuery(NS, new MongoDB\Driver\Query([]));
+var_dump($cursor->toArray());
+
+?>
+===DONE===
+<?php exit(0); ?>
+--EXPECTF--
+object(MongoDB\BSON\ObjectId)#%d (%d) {
+  ["oid"]=>
+  string(24) "%x"
+}
+object(MongoDB\BSON\ObjectId)#%d (%d) {
+  ["oid"]=>
+  string(24) "590b72d606e9660190656a55"
+}
+object(stdClass)#%d (%d) {
+  ["foo"]=>
+  int(1)
+}
+object(stdClass)#%d (%d) {
+  ["id"]=>
+  string(3) "foo"
+}
+object(MyPersistableId)#%d (%d) {
+  ["id"]=>
+  string(3) "bar"
+}
+Inserted 5 document(s)
+array(5) {
+  [0]=>
+  object(stdClass)#%d (%d) {
+    ["_id"]=>
+    object(MongoDB\BSON\ObjectId)#%d (%d) {
+      ["oid"]=>
+      string(24) "%x"
+    }
+    ["x"]=>
+    int(1)
+  }
+  [1]=>
+  object(stdClass)#%d (%d) {
+    ["_id"]=>
+    object(MongoDB\BSON\ObjectId)#%d (%d) {
+      ["oid"]=>
+      string(24) "590b72d606e9660190656a55"
+    }
+  }
+  [2]=>
+  object(stdClass)#%d (%d) {
+    ["_id"]=>
+    object(stdClass)#%d (%d) {
+      ["foo"]=>
+      int(1)
+    }
+  }
+  [3]=>
+  object(stdClass)#%d (%d) {
+    ["_id"]=>
+    object(stdClass)#%d (%d) {
+      ["id"]=>
+      string(3) "foo"
+    }
+  }
+  [4]=>
+  object(stdClass)#%d (%d) {
+    ["_id"]=>
+    object(MyPersistableId)#%d (%d) {
+      ["id"]=>
+      string(3) "bar"
+    }
+  }
+}
+===DONE===
diff --git a/tests/bulkwritecommand/bulkwritecommand-insertOne_error-001.phpt b/tests/bulkwritecommand/bulkwritecommand-insertOne_error-001.phpt
new file mode 100644
index 000000000..135378a88
--- /dev/null
+++ b/tests/bulkwritecommand/bulkwritecommand-insertOne_error-001.phpt
@@ -0,0 +1,20 @@
+--TEST--
+MongoDB\Driver\BulkWriteCommand::insertOne() prohibits PackedArray for document
+--FILE--
+<?php
+
+require_once __DIR__ . '/../utils/basic.inc';
+
+$bulk = new MongoDB\Driver\BulkWriteCommand;
+
+echo throws(function() use ($bulk) {
+    $bulk->insertOne(NS, MongoDB\BSON\PackedArray::fromPHP([]));
+}, MongoDB\Driver\Exception\UnexpectedValueException::class), "\n";
+
+?>
+===DONE===
+<?php exit(0); ?>
+--EXPECT--
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+MongoDB\BSON\PackedArray cannot be serialized as a root document
+===DONE===
diff --git a/tests/bulkwritecommand/bulkwritecommand-insertOne_error-002.phpt b/tests/bulkwritecommand/bulkwritecommand-insertOne_error-002.phpt
new file mode 100644
index 000000000..04d9a9fb8
--- /dev/null
+++ b/tests/bulkwritecommand/bulkwritecommand-insertOne_error-002.phpt
@@ -0,0 +1,20 @@
+--TEST--
+MongoDB\Driver\BulkWriteCommand::insertOne() with BSON encoding error (invalid UTF-8 string)
+--FILE--
+<?php
+
+require_once __DIR__ . '/../utils/basic.inc';
+
+$bulk = new MongoDB\Driver\BulkWriteCommand;
+
+echo throws(function() use ($bulk) {
+    $bulk->insertOne(NS, ['x' => "\xc3\x28"]);
+}, 'MongoDB\Driver\Exception\UnexpectedValueException'), "\n";
+
+?>
+===DONE===
+<?php exit(0); ?>
+--EXPECTF--
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+Detected invalid UTF-8 for field path "x": %s
+===DONE===
diff --git a/tests/bulkwritecommand/bulkwritecommand-insertOne_error-003.phpt b/tests/bulkwritecommand/bulkwritecommand-insertOne_error-003.phpt
new file mode 100644
index 000000000..26e8b9b41
--- /dev/null
+++ b/tests/bulkwritecommand/bulkwritecommand-insertOne_error-003.phpt
@@ -0,0 +1,34 @@
+--TEST--
+MongoDB\Driver\BulkWriteCommand::insertOne() with BSON encoding error (null bytes in keys)
+--FILE--
+<?php
+
+require_once __DIR__ . '/../utils/basic.inc';
+
+$bulk = new MongoDB\Driver\BulkWriteCommand;
+
+echo throws(function() use ($bulk) {
+    $bulk->insertOne(NS, ["\0" => 1]);
+}, 'MongoDB\Driver\Exception\UnexpectedValueException'), "\n\n";
+
+echo throws(function() use ($bulk) {
+    $bulk->insertOne(NS, ["x\0" => 1]);
+}, 'MongoDB\Driver\Exception\UnexpectedValueException'), "\n\n";
+
+echo throws(function() use ($bulk) {
+    $bulk->insertOne(NS, ["\0\0\0" => 1]);
+}, 'MongoDB\Driver\Exception\UnexpectedValueException'), "\n";
+
+?>
+===DONE===
+<?php exit(0); ?>
+--EXPECT--
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+BSON keys cannot contain null bytes. Unexpected null byte after "".
+
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+BSON keys cannot contain null bytes. Unexpected null byte after "x".
+
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+BSON keys cannot contain null bytes. Unexpected null byte after "".
+===DONE===
diff --git a/tests/bulkwritecommand/bulkwritecommand-insertone-001.phpt b/tests/bulkwritecommand/bulkwritecommand-insertone-001.phpt
deleted file mode 100644
index 3698a1c3e..000000000
--- a/tests/bulkwritecommand/bulkwritecommand-insertone-001.phpt
+++ /dev/null
@@ -1,25 +0,0 @@
---TEST--
-MongoDB\Driver\BulkWriteCommand::insertOne() returns document ID
---SKIPIF--
-<?php require __DIR__ . "/../utils/basic-skipif.inc"; ?>
---FILE--
-<?php
-
-require_once __DIR__ . "/../utils/basic.inc";
-
-$bulk = new MongoDB\Driver\BulkWriteCommand();
-
-var_dump($bulk->insertOne(NS, ['_id' => 1]));
-var_dump($bulk->insertOne(NS, []));
-
-
-?>
-===DONE===
-<?php exit(0); ?>
---EXPECTF--
-int(1)
-object(MongoDB\BSON\ObjectId)#%d (%d) {
-  ["oid"]=>
-  string(24) "%x"
-}
-===DONE===
diff --git a/tests/bulkwritecommand/bulkwritecommand-updateOne-001.phpt b/tests/bulkwritecommand/bulkwritecommand-updateOne-001.phpt
new file mode 100644
index 000000000..60ecbe600
--- /dev/null
+++ b/tests/bulkwritecommand/bulkwritecommand-updateOne-001.phpt
@@ -0,0 +1,109 @@
+--TEST--
+MongoDB\Driver\BulkWriteCommand::updateOne() should always encode __pclass for Persistable objects
+--SKIPIF--
+<?php require __DIR__ . "/../utils/basic-skipif.inc"; ?>
+<?php skip_if_not_live(); ?>
+<?php skip_if_server_version('<', '8.0'); ?>
+<?php skip_if_not_clean(); ?>
+--FILE--
+<?php
+require_once __DIR__ . "/../utils/basic.inc";
+
+class MyClass implements MongoDB\BSON\Persistable
+{
+    private $id;
+    private $child;
+
+    public function __construct($id, ?MyClass $child = null)
+    {
+        $this->id = $id;
+        $this->child = $child;
+    }
+
+    public function bsonSerialize(): array
+    {
+        return [
+            '_id' => $this->id,
+            'child' => $this->child,
+        ];
+    }
+
+    public function bsonUnserialize(array $data): void
+    {
+        $this->id = $data['_id'];
+        $this->child = $data['child'];
+    }
+}
+
+$manager = create_test_manager();
+
+$document = new MyClass('foo', new MyClass('bar', new MyClass('baz')));
+
+$bulk = new MongoDB\Driver\BulkWriteCommand();
+$bulk->updateOne(NS,
+    ['_id' => 'foo'],
+    ['$set' => $document],
+    ['upsert' => true]
+);
+$result = $manager->executeBulkWriteCommand($bulk);
+printf("Upserted %d document(s)\n", $result->getUpsertedCount());
+
+$cursor = $manager->executeQuery(NS, new MongoDB\Driver\Query([]));
+var_dump($cursor->toArray());
+
+$bulk = new MongoDB\Driver\BulkWriteCommand();
+$bulk->updateOne(NS,
+    $document,
+    ['$set' => ['child' => new MyClass('yip', new MyClass('yap'))]]
+);
+$result = $manager->executeBulkWriteCommand($bulk);
+printf("Modified %d document(s)\n", $result->getModifiedCount());
+
+$cursor = $manager->executeQuery(NS, new MongoDB\Driver\Query([]));
+var_dump($cursor->toArray());
+
+?>
+===DONE===
+<?php exit(0); ?>
+--EXPECTF--
+Upserted 1 document(s)
+array(1) {
+  [0]=>
+  object(MyClass)#%d (%d) {
+    ["id":"MyClass":private]=>
+    string(3) "foo"
+    ["child":"MyClass":private]=>
+    object(MyClass)#%d (%d) {
+      ["id":"MyClass":private]=>
+      string(3) "bar"
+      ["child":"MyClass":private]=>
+      object(MyClass)#%d (%d) {
+        ["id":"MyClass":private]=>
+        string(3) "baz"
+        ["child":"MyClass":private]=>
+        NULL
+      }
+    }
+  }
+}
+Modified 1 document(s)
+array(1) {
+  [0]=>
+  object(MyClass)#%d (%d) {
+    ["id":"MyClass":private]=>
+    string(3) "foo"
+    ["child":"MyClass":private]=>
+    object(MyClass)#%d (%d) {
+      ["id":"MyClass":private]=>
+      string(3) "yip"
+      ["child":"MyClass":private]=>
+      object(MyClass)#%d (%d) {
+        ["id":"MyClass":private]=>
+        string(3) "yap"
+        ["child":"MyClass":private]=>
+        NULL
+      }
+    }
+  }
+}
+===DONE===
diff --git a/tests/bulkwritecommand/bulkwritecommand-updateOne-002.phpt b/tests/bulkwritecommand/bulkwritecommand-updateOne-002.phpt
new file mode 100644
index 000000000..9a5d147e9
--- /dev/null
+++ b/tests/bulkwritecommand/bulkwritecommand-updateOne-002.phpt
@@ -0,0 +1,84 @@
+--TEST--
+MongoDB\Driver\BulkWriteCommand::updateOne() with arrayFilters option
+--SKIPIF--
+<?php require __DIR__ . "/../utils/basic-skipif.inc"; ?>
+<?php skip_if_not_live(); ?>
+<?php skip_if_server_version('<', '8.0'); ?>
+<?php skip_if_not_clean(); ?>
+--FILE--
+<?php
+require_once __DIR__ . "/../utils/basic.inc";
+
+$manager = create_test_manager();
+
+$bulk = new MongoDB\Driver\BulkWriteCommand();
+
+$bulk->insertOne(NS, [ '_id' => 1, 'grades' => [ 95, 92, 90 ] ]);
+$bulk->insertOne(NS, [ '_id' => 2, 'grades' => [ 98, 100, 102 ] ]);
+$bulk->insertOne(NS, [ '_id' => 3, 'grades' => [ 95, 110, 100 ] ]);
+
+$manager->executeBulkWriteCommand($bulk);
+
+$updateBulk = new MongoDB\Driver\BulkWriteCommand();
+
+$query = ['grades' => ['$gte' => 100]];
+$update = [ '$set' => [ 'grades.$[element]' => 100 ] ];
+$options = [
+    'arrayFilters' => [ [ 'element' => [ '$gte' => 100 ] ] ],
+    'multi' => true
+];
+
+$updateBulk->updateMany(NS, $query, $update, $options);
+$manager->executeBulkWriteCommand($updateBulk);
+
+$cursor = $manager->executeQuery(NS, new \MongoDB\Driver\Query([]));
+var_dump($cursor->toArray());
+?>
+===DONE===
+<?php exit(0); ?>
+--EXPECTF--
+array(%d) {
+  [0]=>
+  object(stdClass)#%d (%d) {
+    ["_id"]=>
+    int(1)
+    ["grades"]=>
+    array(%d) {
+      [0]=>
+      int(95)
+      [1]=>
+      int(92)
+      [2]=>
+      int(90)
+    }
+  }
+  [1]=>
+  object(stdClass)#%d (%d) {
+    ["_id"]=>
+    int(2)
+    ["grades"]=>
+    array(%d) {
+      [0]=>
+      int(98)
+      [1]=>
+      int(100)
+      [2]=>
+      int(100)
+    }
+  }
+  [2]=>
+  object(stdClass)#%d (%d) {
+    ["_id"]=>
+    int(3)
+    ["grades"]=>
+    array(%d) {
+      [0]=>
+      int(95)
+      [1]=>
+      int(100)
+      [2]=>
+      int(100)
+    }
+  }
+}
+===DONE===
diff --git a/tests/bulkwritecommand/bulkwritecommand-updateOne-003.phpt b/tests/bulkwritecommand/bulkwritecommand-updateOne-003.phpt
new file mode 100644
index 000000000..1f44fee44
--- /dev/null
+++ b/tests/bulkwritecommand/bulkwritecommand-updateOne-003.phpt
@@ -0,0 +1,65 @@
+--TEST--
+MongoDB\Driver\BulkWriteCommand::updateOne() with pipeline option
+--SKIPIF--
+<?php require __DIR__ . "/../utils/basic-skipif.inc"; ?>
+<?php skip_if_not_live(); ?>
+<?php skip_if_server_version('<', '8.0'); ?>
+<?php skip_if_not_clean(); ?>
+--FILE--
+<?php
+require_once __DIR__ . "/../utils/basic.inc";
+
+$manager = create_test_manager();
+
+$bulk = new MongoDB\Driver\BulkWriteCommand();
+
+$bulk->insertOne(NS, [ '_id' => 1, 'x' => 1, 'y' => 1, 't' => [ 'u' => [ 'v' => 1 ] ] ]);
+$bulk->insertOne(NS, [ '_id' => 2, 'x' => 2, 'y' => 1]);
+
+$manager->executeBulkWriteCommand($bulk);
+
+$updateBulk = new MongoDB\Driver\BulkWriteCommand();
+
+$query = ['_id' => 1];
+$update = [
+    [
+        '$replaceRoot' => [ 'newRoot' => '$t' ],
+    ],
+    [
+        '$addFields' => [ 'foo' => 1 ],
+    ],
+];
+
+$updateBulk->updateMany(NS, $query, $update);
+$manager->executeBulkWriteCommand($updateBulk);
+
+$cursor = $manager->executeQuery(NS, new \MongoDB\Driver\Query([]));
+var_dump($cursor->toArray());
+?>
+===DONE===
+<?php exit(0); ?>
+--EXPECTF--
+array(%d) {
+  [0]=>
+  object(stdClass)#%d (%d) {
+    ["_id"]=>
+    int(1)
+    ["u"]=>
+    object(stdClass)#%d (%d) {
+      ["v"]=>
+      int(1)
+    }
+    ["foo"]=>
+    int(1)
+  }
+  [1]=>
+  object(stdClass)#%d (%d) {
+    ["_id"]=>
+    int(2)
+    ["x"]=>
+    int(2)
+    ["y"]=>
+    int(1)
+  }
+}
+===DONE===
diff --git a/tests/bulkwritecommand/bulkwritecommand-updateOne-004.phpt b/tests/bulkwritecommand/bulkwritecommand-updateOne-004.phpt
new file mode 100644
index 000000000..a1850f387
--- /dev/null
+++ b/tests/bulkwritecommand/bulkwritecommand-updateOne-004.phpt
@@ -0,0 +1,55 @@
+--TEST--
+MongoDB\Driver\BulkWriteCommand::updateOne() with hint option
+--SKIPIF--
+<?php require __DIR__ . "/../utils/basic-skipif.inc"; ?>
+<?php skip_if_not_live(); ?>
+<?php skip_if_server_version('<', '8.0'); ?>
+<?php skip_if_not_clean(); ?>
+--FILE--
+<?php
+require_once __DIR__ . "/../utils/basic.inc";
+
+class CommandLogger implements MongoDB\Driver\Monitoring\CommandSubscriber
+{
+    public function commandStarted(MongoDB\Driver\Monitoring\CommandStartedEvent $event): void
+    {
+        if ($event->getCommandName() !== 'bulkWrite') {
+            return;
+        }
+
+        printf("update included hint: %s\n", json_encode($event->getCommand()->ops[0]->hint));
+    }
+
+    public function commandSucceeded(MongoDB\Driver\Monitoring\CommandSucceededEvent $event): void
+    {
+    }
+
+    public function commandFailed(MongoDB\Driver\Monitoring\CommandFailedEvent $event): void
+    {
+    }
+}
+
+$manager = create_test_manager();
+
+$bulk = new MongoDB\Driver\BulkWriteCommand();
+$bulk->insertOne(NS, ['x' => 1]);
+$bulk->insertOne(NS, ['x' => 2]);
+$manager->executeBulkWriteCommand($bulk);
+
+MongoDB\Driver\Monitoring\addSubscriber(new CommandLogger);
+
+$bulk = new MongoDB\Driver\BulkWriteCommand;
+$bulk->updateOne(NS, ['_id' => 1], ['$set' => ['x' => 11]], ['hint' => '_id_']);
+$manager->executeBulkWriteCommand($bulk);
+
+$bulk = new MongoDB\Driver\BulkWriteCommand;
+$bulk->updateOne(NS, ['_id' => 2], ['$set' => ['x' => 22]], ['hint' => ['_id' => 1]]);
+$manager->executeBulkWriteCommand($bulk);
+
+?>
+===DONE===
+<?php exit(0); ?>
+--EXPECTF--
+update included hint: "_id_"
+update included hint: {"_id":1}
+===DONE===
diff --git a/tests/bulkwritecommand/bulkwritecommand-updateOne-005.phpt b/tests/bulkwritecommand/bulkwritecommand-updateOne-005.phpt
new file mode 100644
index 000000000..ab01dc0d0
--- /dev/null
+++ b/tests/bulkwritecommand/bulkwritecommand-updateOne-005.phpt
@@ -0,0 +1,36 @@
+--TEST--
+MongoDB\Driver\BulkWriteCommand::updateOne() $filter and $newObj are MongoDB\BSON\Document
+--SKIPIF--
+<?php require __DIR__ . "/../utils/basic-skipif.inc"; ?>
+<?php skip_if_not_live(); ?>
+<?php skip_if_server_version('<', '8.0'); ?>
+<?php skip_if_not_clean(); ?>
+--FILE--
+<?php
+
+require_once __DIR__ . "/../utils/basic.inc";
+
+$manager = create_test_manager();
+
+$bulk = new MongoDB\Driver\BulkWriteCommand();
+$bulk->insertOne(NS, ['_id' => 1]);
+$bulk->insertOne(NS, ['_id' => 2]);
+$manager->executeBulkWriteCommand($bulk);
+
+$filter = MongoDB\BSON\Document::fromJSON('{ "_id": { "$gt": 1 } }');
+$newObj = MongoDB\BSON\Document::fromJSON('{ "$set": { "x": 1 } }');
+
+$bulk = new MongoDB\Driver\BulkWriteCommand;
+$bulk->updateMany(NS, $filter, $newObj);
+$result = $manager->executeBulkWriteCommand($bulk);
+
+var_dump($result->getMatchedCount());
+var_dump($result->getModifiedCount());
+
+?>
+===DONE===
+<?php exit(0); ?>
+--EXPECT--
+int(1)
+int(1)
+===DONE===
diff --git a/tests/bulkwritecommand/bulkwritecommand-updateOne-006.phpt b/tests/bulkwritecommand/bulkwritecommand-updateOne-006.phpt
new file mode 100644
index 000000000..d572cb489
--- /dev/null
+++ b/tests/bulkwritecommand/bulkwritecommand-updateOne-006.phpt
@@ -0,0 +1,65 @@
+--TEST--
+MongoDB\Driver\BulkWriteCommand::updateOne() PackedArray for update pipeline
+--SKIPIF--
+<?php require __DIR__ . "/../utils/basic-skipif.inc"; ?>
+<?php skip_if_not_live(); ?>
+<?php skip_if_server_version('<', '8.0'); ?>
+<?php skip_if_not_clean(); ?>
+--FILE--
+<?php
+require_once __DIR__ . "/../utils/basic.inc";
+
+$manager = create_test_manager();
+
+$bulk = new MongoDB\Driver\BulkWriteCommand();
+
+$bulk->insertOne(NS, ['_id' => 1, 'x' => 1, 'y' => 1, 't' => ['u' => ['v' => 1]]]);
+$bulk->insertOne(NS, ['_id' => 2, 'x' => 2, 'y' => 1]);
+
+$manager->executeBulkWriteCommand($bulk);
+
+$updateBulk = new MongoDB\Driver\BulkWriteCommand();
+
+$updateBulk->updateOne(
+    NS,
+    ['_id' => 1],
+    MongoDB\BSON\PackedArray::fromPHP([
+        ['$replaceRoot' => ['newRoot' => '$t']],
+        ['$addFields' => ['foo' => 1]],
+    ])
+);
+
+$manager->executeBulkWriteCommand($updateBulk);
+
+$cursor = $manager->executeQuery(NS, new MongoDB\Driver\Query([]));
+
+var_dump($cursor->toArray());
+
+?>
+===DONE===
+<?php exit(0); ?>
+--EXPECTF--
+array(%d) {
+  [0]=>
+  object(stdClass)#%d (%d) {
+    ["_id"]=>
+    int(1)
+    ["u"]=>
+    object(stdClass)#%d (%d) {
+      ["v"]=>
+      int(1)
+    }
+    ["foo"]=>
+    int(1)
+  }
+  [1]=>
+  object(stdClass)#%d (%d) {
+    ["_id"]=>
+    int(2)
+    ["x"]=>
+    int(2)
+    ["y"]=>
+    int(1)
+  }
+}
+===DONE===
diff --git a/tests/bulkwritecommand/bulkwritecommand-updateOne-007.phpt b/tests/bulkwritecommand/bulkwritecommand-updateOne-007.phpt
new file mode 100644
index 000000000..984e1234b
--- /dev/null
+++ b/tests/bulkwritecommand/bulkwritecommand-updateOne-007.phpt
@@ -0,0 +1,87 @@
+--TEST--
+MongoDB\Driver\BulkWriteCommand::updateOne() PackedArray for arrayFilters option
+--SKIPIF--
+<?php require __DIR__ . "/../utils/basic-skipif.inc"; ?>
+<?php skip_if_not_live(); ?>
+<?php skip_if_server_version('<', '8.0'); ?>
+<?php skip_if_not_clean(); ?>
+--FILE--
+<?php
+require_once __DIR__ . "/../utils/basic.inc";
+
+$manager = create_test_manager();
+
+$bulk = new MongoDB\Driver\BulkWriteCommand();
+
+$bulk->insertOne(NS, ['_id' => 1, 'grades' => [95, 92, 90]]);
+$bulk->insertOne(NS, ['_id' => 2, 'grades' => [98, 100, 102]]);
+$bulk->insertOne(NS, ['_id' => 3, 'grades' => [95, 110, 100]]);
+
+$manager->executeBulkWriteCommand($bulk);
+
+$updateBulk = new MongoDB\Driver\BulkWriteCommand();
+
+$updateBulk->updateMany(NS,
+    ['grades' => ['$gte' => 100]],
+    ['$set' => ['grades.$[element]' => 100]],
+    [
+        'arrayFilters' => MongoDB\BSON\PackedArray::fromPHP([['element' => ['$gte' => 100]]]),
+        'multi' => true,
+    ]
+);
+
+$manager->executeBulkWriteCommand($updateBulk);
+
+$cursor = $manager->executeQuery(NS, new MongoDB\Driver\Query([]));
+
+var_dump($cursor->toArray());
+
+?>
+===DONE===
+<?php exit(0); ?>
+--EXPECTF--
+array(%d) {
+  [0]=>
+  object(stdClass)#%d (%d) {
+    ["_id"]=>
+    int(1)
+    ["grades"]=>
+    array(%d) {
+      [0]=>
+      int(95)
+      [1]=>
+      int(92)
+      [2]=>
+      int(90)
+    }
+  }
+  [1]=>
+  object(stdClass)#%d (%d) {
+    ["_id"]=>
+    int(2)
+    ["grades"]=>
+    array(%d) {
+      [0]=>
+      int(98)
+      [1]=>
+      int(100)
+      [2]=>
+      int(100)
+    }
+  }
+  [2]=>
+  object(stdClass)#%d (%d) {
+    ["_id"]=>
+    int(3)
+    ["grades"]=>
+    array(%d) {
+      [0]=>
+      int(95)
+      [1]=>
+      int(100)
+      [2]=>
+      int(100)
+    }
+  }
+}
+===DONE===
diff --git a/tests/bulkwritecommand/bulkwritecommand-updateOne-008.phpt b/tests/bulkwritecommand/bulkwritecommand-updateOne-008.phpt
new file mode 100644
index 000000000..d1c12cf73
--- /dev/null
+++ b/tests/bulkwritecommand/bulkwritecommand-updateOne-008.phpt
@@ -0,0 +1,81 @@
+--TEST--
+MongoDB\Driver\BulkWriteCommand::updateOne() with sort option
+--SKIPIF--
+<?php require __DIR__ . "/../utils/basic-skipif.inc"; ?>
+<?php skip_if_not_live(); ?>
+<?php skip_if_server_version('<', '8.0'); ?>
+<?php skip_if_not_clean(); ?>
+--FILE--
+<?php
+require_once __DIR__ . "/../utils/basic.inc";
+
+class CommandLogger implements MongoDB\Driver\Monitoring\CommandSubscriber
+{
+    public function commandStarted(MongoDB\Driver\Monitoring\CommandStartedEvent $event): void
+    {
+        if ($event->getCommandName() !== 'bulkWrite') {
+            return;
+        }
+
+        printf("update included sort: %s\n", json_encode($event->getCommand()->ops[0]->sort));
+    }
+
+    public function commandSucceeded(MongoDB\Driver\Monitoring\CommandSucceededEvent $event): void
+    {
+    }
+
+    public function commandFailed(MongoDB\Driver\Monitoring\CommandFailedEvent $event): void
+    {
+    }
+}
+
+$manager = create_test_manager();
+
+$bulk = new MongoDB\Driver\BulkWriteCommand();
+$bulk->insertOne(NS, ['_id' => 1]);
+$bulk->insertOne(NS, ['_id' => 2]);
+$bulk->insertOne(NS, ['_id' => 3]);
+$manager->executeBulkWriteCommand($bulk);
+
+MongoDB\Driver\Monitoring\addSubscriber(new CommandLogger);
+
+$bulk = new MongoDB\Driver\BulkWriteCommand;
+$bulk->updateOne(NS, ['_id' => ['$gt' => 1]], ['$set' => ['x' => 11]], ['sort' => ['_id' => 1]]);
+$manager->executeBulkWriteCommand($bulk);
+
+$bulk = new MongoDB\Driver\BulkWriteCommand;
+$bulk->updateOne(NS, ['_id' => ['$gt' => 1]], ['$set' => ['x' => 22]], ['sort' => ['_id' => -1]]);
+$manager->executeBulkWriteCommand($bulk);
+
+$cursor = $manager->executeQuery(NS, new MongoDB\Driver\Query([]));
+
+var_dump($cursor->toArray());
+
+?>
+===DONE===
+<?php exit(0); ?>
+--EXPECTF--
+update included sort: {"_id":1}
+update included sort: {"_id":-1}
+array(3) {
+  [0]=>
+  object(stdClass)#%d (%d) {
+    ["_id"]=>
+    int(1)
+  }
+  [1]=>
+  object(stdClass)#%d (%d) {
+    ["_id"]=>
+    int(2)
+    ["x"]=>
+    int(11)
+  }
+  [2]=>
+  object(stdClass)#%d (%d) {
+    ["_id"]=>
+    int(3)
+    ["x"]=>
+    int(22)
+  }
+}
+===DONE===
diff --git a/tests/bulkwritecommand/bulkwritecommand-updateOne_error-003.phpt b/tests/bulkwritecommand/bulkwritecommand-updateOne_error-003.phpt
new file mode 100644
index 000000000..3a8f3f5a0
--- /dev/null
+++ b/tests/bulkwritecommand/bulkwritecommand-updateOne_error-003.phpt
@@ -0,0 +1,48 @@
+--TEST--
+MongoDB\Driver\BulkWriteCommand::updateOne() with invalid options
+--FILE--
+<?php
+
+require_once __DIR__ . '/../utils/basic.inc';
+
+$bulk = new MongoDB\Driver\BulkWriteCommand;
+
+echo throws(function() use ($bulk) {
+    $bulk->updateOne(NS, ['x' => 1], ['$set' => ['y' => 1]], ['collation' => 1]);
+}, 'MongoDB\Driver\Exception\InvalidArgumentException'), "\n\n";
+
+echo throws(function() use ($bulk) {
+    $bulk->updateOne(NS, ['x' => 1], ['$set' => ['y' => 1]], ['collation' => 1]);
+}, 'MongoDB\Driver\Exception\InvalidArgumentException'), "\n\n";
+
+echo throws(function() use ($bulk) {
+    $bulk->updateOne(NS, ['x' => 1], ['$set' => ['y' => 1]], ['arrayFilters' => 1]);
+}, 'MongoDB\Driver\Exception\InvalidArgumentException'), "\n\n";
+
+echo throws(function() use ($bulk) {
+    $bulk->updateOne(NS, ['x' => 1], ['$set' => ['y' => 1]], ['arrayFilters' => ['foo' => 'bar']]);
+}, 'MongoDB\Driver\Exception\InvalidArgumentException'), "\n\n";
+
+echo throws(function() use ($bulk) {
+    $bulk->updateOne(NS, ['x' => 1], ['$set' => ['y' => 1]], ['hint' => 1]);
+}, 'MongoDB\Driver\Exception\InvalidArgumentException'), "\n";
+
+?>
+===DONE===
+<?php exit(0); ?>
+--EXPECT--
+OK: Got MongoDB\Driver\Exception\InvalidArgumentException
+Expected "collation" option to be array or object, int given
+
+OK: Got MongoDB\Driver\Exception\InvalidArgumentException
+Expected "collation" option to be array or object, int given
+
+OK: Got MongoDB\Driver\Exception\InvalidArgumentException
+Expected "arrayFilters" option to be array or object, int given
+
+OK: Got MongoDB\Driver\Exception\InvalidArgumentException
+Expected "arrayFilters" option to yield array but got non-sequential keys
+
+OK: Got MongoDB\Driver\Exception\InvalidArgumentException
+Expected "hint" option to be string, array, or object, int given
+===DONE===
diff --git a/tests/bulkwritecommand/bulkwritecommand-updateOne_error-004.phpt b/tests/bulkwritecommand/bulkwritecommand-updateOne_error-004.phpt
new file mode 100644
index 000000000..cc7a91a5a
--- /dev/null
+++ b/tests/bulkwritecommand/bulkwritecommand-updateOne_error-004.phpt
@@ -0,0 +1,41 @@
+--TEST--
+MongoDB\Driver\BulkWriteCommand::updateOne() with BSON encoding error (invalid UTF-8 string)
+--FILE--
+<?php
+
+require_once __DIR__ . '/../utils/basic.inc';
+
+$bulk = new MongoDB\Driver\BulkWriteCommand;
+
+echo throws(function() use ($bulk) {
+    $bulk->updateOne(NS, ['x' => "\xc3\x28"], ['x' => 1]);
+}, 'MongoDB\Driver\Exception\UnexpectedValueException'), "\n\n";
+
+echo throws(function() use ($bulk) {
+    $bulk->updateOne(NS, ['x' => 1], ['x' => "\xc3\x28"]);
+}, 'MongoDB\Driver\Exception\UnexpectedValueException'), "\n\n";
+
+echo throws(function() use ($bulk) {
+    $bulk->updateOne(NS, ['x' => 1], ['$set' => ['x' => "\xc3\x28"]]);
+}, 'MongoDB\Driver\Exception\UnexpectedValueException'), "\n\n";
+
+echo throws(function() use ($bulk) {
+    $bulk->updateOne(NS, ['x' => 1], ['y' => 1], ['collation' => ['locale' => "\xc3\x28"]]);
+}, 'MongoDB\Driver\Exception\UnexpectedValueException'), "\n";
+
+?>
+===DONE===
+<?php exit(0); ?>
+--EXPECTF--
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+Detected invalid UTF-8 for field path "x": %s
+
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+Detected invalid UTF-8 for field path "x": %s
+
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+Detected invalid UTF-8 for field path "$set.x": %s
+
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+Detected invalid UTF-8 for field path "locale": %s
+===DONE===
diff --git a/tests/bulkwritecommand/bulkwritecommand-updateOne_error-005.phpt b/tests/bulkwritecommand/bulkwritecommand-updateOne_error-005.phpt
new file mode 100644
index 000000000..0cb3dfd8c
--- /dev/null
+++ b/tests/bulkwritecommand/bulkwritecommand-updateOne_error-005.phpt
@@ -0,0 +1,76 @@
+--TEST--
+MongoDB\Driver\BulkWriteCommand::updateOne() with BSON encoding error (null bytes in keys)
+--FILE--
+<?php
+
+require_once __DIR__ . '/../utils/basic.inc';
+
+$bulk = new MongoDB\Driver\BulkWriteCommand;
+
+echo throws(function() use ($bulk) {
+    $bulk->updateOne(NS, ["\0" => 1], ['x' => 1]);
+}, 'MongoDB\Driver\Exception\UnexpectedValueException'), "\n\n";
+
+echo throws(function() use ($bulk) {
+    $bulk->updateOne(NS, ["x\0" => 1], ['x' => 1]);
+}, 'MongoDB\Driver\Exception\UnexpectedValueException'), "\n\n";
+
+echo throws(function() use ($bulk) {
+    $bulk->updateOne(NS, ["\0\0\0" => 1], ['x' => 1]);
+}, 'MongoDB\Driver\Exception\UnexpectedValueException'), "\n\n";
+
+echo throws(function() use ($bulk) {
+    $bulk->updateOne(NS, ['x' => 1], ["\0" => 1]);
+}, 'MongoDB\Driver\Exception\UnexpectedValueException'), "\n\n";
+
+echo throws(function() use ($bulk) {
+    $bulk->updateOne(NS, ['x' => 1], ["x\0" => 1]);
+}, 'MongoDB\Driver\Exception\UnexpectedValueException'), "\n\n";
+
+echo throws(function() use ($bulk) {
+    $bulk->updateOne(NS, ['x' => 1], ["\0\0\0" => 1]);
+}, 'MongoDB\Driver\Exception\UnexpectedValueException'), "\n\n";
+
+echo throws(function() use ($bulk) {
+    $bulk->updateOne(NS, ['x' => 1], ['y' => 1], ['collation' => ["\0" => 1]]);
+}, 'MongoDB\Driver\Exception\UnexpectedValueException'), "\n\n";
+
+echo throws(function() use ($bulk) {
+    $bulk->updateOne(NS, ['x' => 1], ['y' => 1], ['collation' => ["x\0" => 1]]);
+}, 'MongoDB\Driver\Exception\UnexpectedValueException'), "\n\n";
+
+echo throws(function() use ($bulk) {
+    $bulk->updateOne(NS, ['x' => 1], ['y' => 1], ['collation' => ["\0\0\0" => 1]]);
+}, 'MongoDB\Driver\Exception\UnexpectedValueException'), "\n";
+
+?>
+===DONE===
+<?php exit(0); ?>
+--EXPECT--
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+BSON keys cannot contain null bytes. Unexpected null byte after "".
+
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+BSON keys cannot contain null bytes. Unexpected null byte after "x".
+
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+BSON keys cannot contain null bytes. Unexpected null byte after "".
+
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+BSON keys cannot contain null bytes. Unexpected null byte after "".
+
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+BSON keys cannot contain null bytes. Unexpected null byte after "x".
+
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+BSON keys cannot contain null bytes. Unexpected null byte after "".
+
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+BSON keys cannot contain null bytes. Unexpected null byte after "".
+
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+BSON keys cannot contain null bytes. Unexpected null byte after "x".
+
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+BSON keys cannot contain null bytes. Unexpected null byte after "".
+===DONE===
diff --git a/tests/bulkwritecommand/bulkwritecommand-updateOne_error-006.phpt b/tests/bulkwritecommand/bulkwritecommand-updateOne_error-006.phpt
new file mode 100644
index 000000000..20ca761ea
--- /dev/null
+++ b/tests/bulkwritecommand/bulkwritecommand-updateOne_error-006.phpt
@@ -0,0 +1,31 @@
+--TEST--
+MongoDB\Driver\BulkWriteCommand::updateOne() prohibits PackedArray for document values
+--FILE--
+<?php
+require_once __DIR__ . "/../utils/basic.inc";
+
+$bulk = new MongoDB\Driver\BulkWriteCommand;
+
+echo throws(function() use ($bulk) {
+    $bulk->updateOne(NS, MongoDB\BSON\PackedArray::fromPHP([]), []);
+}, MongoDB\Driver\Exception\UnexpectedValueException::class), "\n";
+
+echo throws(function() use ($bulk) {
+    $bulk->updateOne(NS, [], [], ['collation' => MongoDB\BSON\PackedArray::fromPHP([])]);
+}, MongoDB\Driver\Exception\UnexpectedValueException::class), "\n";
+
+echo throws(function() use ($bulk) {
+    $bulk->updateOne(NS, [], [], ['hint' => MongoDB\BSON\PackedArray::fromPHP([])]);
+}, MongoDB\Driver\Exception\InvalidArgumentException::class), "\n";
+
+?>
+===DONE===
+<?php exit(0); ?>
+--EXPECT--
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+MongoDB\BSON\PackedArray cannot be serialized as a root document
+OK: Got MongoDB\Driver\Exception\UnexpectedValueException
+MongoDB\BSON\PackedArray cannot be serialized as a root document
+OK: Got MongoDB\Driver\Exception\InvalidArgumentException
+Expected "hint" option to yield string or document but got "array"
+===DONE===