function patch(oPatchGroupLock, bAtFront) {
				var mHeaders = {"If-Match" : oEntity},
					oPatchPromise,
					oRequestLock,
					bSkip; // MUST NOT happen with upsert

				/*
				* Callback to merge the entity's old data into its one remaining PATCH. If
				* no old data from another PATCH is supplied, the PATCH is skipped and returns its
				* old data. Otherwise the given old data from the skipped patch is merged into the
				* surviving PATCH's own old data.
				*
				* @param {object} [oOtherOldData]
				*   Either another PATCH's old data which is to be merged into this one or
				*   <code>undefined</code> if this PATCH is the skipped one and has to return its
				*   own old data.
				* @returns {object}
				*   This PATCH's old data which is to be merged into another one or
				*   <code>undefined</code> if this is the surviving PATCH.
				*/
				function mergePatchRequests(oOtherOldData) {
					if (arguments.length === 0) {
						bSkip = true;
						return oOldData; // my PATCH was merged
					}
					if (oOldData) {
						_Helper.updateNonExisting(oOldData, oOtherOldData);
					} // else: upsert
				}

				/*
				* Synchronous callback to cancel the PATCH request so that it is really gone when
				* resetChangesForPath has been called on the binding or model.
				*/
				function onCancel() {
					_Helper.removeByPath(that.mChangeRequests, sFullPath, oPatchPromise);
					// write the previous value into the cache
					if (bUpsert) {
						// reset to null notifying listeners
						_Helper.updateAll(that.mChangeListeners, sParentPath,
							that.getValue(sParentPath), {[sNavigationProperty] : null});
						fnSetUpsertPromise?.();
					} else {
						_Helper.updateAll(that.mChangeListeners, sEntityPath, oEntity, oOldData);
					}
				}

				/*
				 * Synchronous callback called when the request is put on the wire. Locks the group
				 * so that further requests created via {@link ODataModel#submitBatch} wait until
				 * this request has returned and its response is applied to the cache.
				 */
				function onSubmit() {
					if (bUpsert && that.iActiveUsages) {
						// Note: oPatchGroupLock might refer to a parked group
						that.refreshSingleNoCollection(oGroupLock, sEntityPath)
							.catch(that.oRequestor.getModelInterface().getReporter());
					}
					oRequestLock = that.oRequestor.lockGroup(sGroupId, that, true);
					fnPatchSent();
				}

				if (bUpsert) {
					mHeaders["If-None-Match"] = "*";
					bPatchWithoutSideEffects = true;
				}
				if (bPatchWithoutSideEffects) {
					mHeaders.Prefer = "return=minimal";
				}
				oPatchPromise = that.oRequestor.request("PATCH", sEditUrl, oPatchGroupLock,
					mHeaders, oUpdateData, onSubmit, onCancel, /*sMetaPath*/ undefined,
					_Helper.buildPath(that.sOriginalResourcePath, sEntityPath),
					bAtFront, /*mQueryOptions*/ undefined, /*vOwner*/ undefined,
					mergePatchRequests);
				oPatchPromise.$isKeepAlive = fnIsKeepAlive;
				_Helper.addByPath(that.mChangeRequests, sFullPath, oPatchPromise);
				return SyncPromise.all([
					oPatchPromise,
					that.fetchTypes()
				]).then(function ([oPatchResult, mTypeForMetaPath]) {
					_Helper.removeByPath(that.mChangeRequests, sFullPath, oPatchPromise);
					if (bSkip) {
						// if a PATCH is skipped, because it is merged into another, nothing to do!
						return;
					}
					if (!bPatchWithoutSideEffects) {
						// visit response to report the messages
						that.visitResponse(oPatchResult, mTypeForMetaPath,
							_Helper.getMetaPath(_Helper.buildPath(that.sMetaPath, sEntityPath)),
							sEntityPath
						);
					}
					// update the cache with the PATCH response
					_Helper.updateExisting(that.mChangeListeners, sEntityPath, oEntity,
						bPatchWithoutSideEffects
							? {"@odata.etag" : oPatchResult["@odata.etag"]}
							: oPatchResult);
					if (bUpsert) {
						_Helper.updateAll(that.mChangeListeners, sEntityPath, oEntity,
							{"@$ui5.context.isTransient" : false});
						_Helper.deletePrivateAnnotation(oEntity, "upsert");
					}
				}, function (oError) {
					var sRetryGroupId = sGroupId;

					if (!fnErrorCallback && !oError.canceled) {
						onCancel();
						throw oError;
					}
					_Helper.removeByPath(that.mChangeRequests, sFullPath, oPatchPromise);
					if (oError.canceled) {
						throw oError;
					}
					oRequestLock.unlock();
					oRequestLock = undefined;

					fnErrorCallback(oError);
					if (bSkip) {
						// Note: this PATCH is already merged into another
						// --> return the corresponding PATCH promise
						return that.mEditUrl2PatchPromise[sEditUrl];
					}

					// Note: We arrive here only for the PATCH which was really sent to the server.
					// The other ones which have been merged are still pending on this one!
					// In the end, they will either succeed or be canceled.
					switch (that.oRequestor.getGroupSubmitMode(sGroupId)) {
						case "API":
							break;
						case "Auto":
							if (!that.oRequestor.hasChanges(sGroupId, oEntity)) {
								// park PATCH until another change to this entity happens
								// Note: At most one change per entity is parked (see above), thus
								// order does not matter and bAtFront = true is OK!
								sRetryGroupId = "$parked." + sGroupId;
							}
							break;
						default:
							throw oError; // no retry, just give up
					}

					that.mEditUrl2PatchPromise[sEditUrl]
						= patch(that.oRequestor.lockGroup(sRetryGroupId, that, true, true), true);

					return that.mEditUrl2PatchPromise[sEditUrl];
				}).finally(function () {
					if (oRequestLock) {
						oRequestLock.unlock();
					}
					delete that.mEditUrl2PatchPromise[sEditUrl];
				});
			}
Updates the property of the given name with the given new value (and later with the server's
response), using the given group ID for batch control and the given edit URL to send a PATCH
request.

@param {sap.ui.model.odata.v4.lib._GroupLock} oGroupLock
  A lock for the group ID
@param {string} sPropertyPath
  Path of the property to update, relative to the entity
@param {any} vValue
  The new value
@param {function} [fnErrorCallback]
  A function which is called with an Error object each time a PATCH request fails; if it is
  missing, the PATCH is not retried, but this method's returned promise is rejected
@param {string} sEditUrl
  The edit URL for the entity which is updated via PATCH
@param {string} [sEntityPath]
  Path of the entity, relative to the cache (as used by change listeners)
@param {string} [sUnitOrCurrencyPath]
  Path of the unit or currency for the property, relative to the (entity or complex) type
  which contains the property to update
@param {boolean} [bPatchWithoutSideEffects]
  Whether the PATCH response is ignored, except for a new ETag
@param {function} fnPatchSent
  The function is called just before a back-end request is sent for the first time.
  If no back-end request is needed, the function is not called.
@param {function} fnIsKeepAlive
  A function to tell whether the entity is kept alive
@param {function} fnSetUpsertPromise
  A function to (re)set a sync promise for the "upsert" use case
@returns {sap.ui.base.SyncPromise}
  A promise for the PATCH request (resolves with <code>undefined</code>); rejected in case of
  cancellation or if no <code>fnErrorCallback</code> is given
@throws {Error} If the cache is shared

@public