public function set($document, array $fields, array $options = [])
    {
        $merge = $this->pluck('merge', $options, false) ?: false;

        // Record whether the document is empty before any filtering.
        $emptyDocument = count($fields) === 0;

        list ($fields, $sentinels, $metadata) = $this->filterFields($fields);

        if (!$merge && $metadata['hasDelete']) {
            throw new \InvalidArgumentException('Delete cannot appear in data unless `$options[\'merge\']` is set.');
        }

        // Enqueue a write if any of the following conditions are met
        // - if there are still fields remaining after sentinels were removed
        // - if the user provided an empty set to begin with
        // - if the user provided only transform sentinel values AND did not specify merge behavior
        // - if the user provided only delete sentinel field values.

        $updateNotRequired = count($fields) === 0
            && !$emptyDocument
            && !$metadata['hasUpdateMask']
            && $metadata['hasTransform'];

        $shouldEnqueueUpdate = $fields
            || $emptyDocument
            || ($updateNotRequired && !$merge)
            || $metadata['hasUpdateMask'];

        if ($shouldEnqueueUpdate) {
            $write = [
                'fields' => $this->valueMapper->encodeValues($fields),
            ];

            if ($merge) {
                $write['updateMask'] = $this->pathsToStrings($this->encodeFieldPaths($fields), $sentinels);
            }

            $this->writes[] = $this->createDatabaseWrite(self::TYPE_UPDATE, $document, $write, $options);
        }

        // document transform operations are enqueued as a separate mutation.
        $this->enqueueTransforms($document, $sentinels, $options);

        return $this;
    }