All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sap.cds.ql.impl.DeepUpdateSplitter Maven / Gradle / Ivy

There is a newer version: 3.4.0
Show newest version
/************************************************************************
 * © 2021-2024 SAP SE or an SAP affiliate company. All rights reserved. *
 ************************************************************************/
package com.sap.cds.ql.impl;

import static com.sap.cds.impl.EntityCascader.cascadeDelete;
import static com.sap.cds.impl.EntityCascader.EntityKeys.keys;
import static com.sap.cds.impl.EntityCascader.EntityOperation.insert;
import static com.sap.cds.impl.EntityCascader.EntityOperation.nop;
import static com.sap.cds.impl.EntityCascader.EntityOperation.root;
import static com.sap.cds.impl.EntityCascader.EntityOperation.updateOrInsert;
import static com.sap.cds.impl.EntityCascader.EntityOperation.upsert;
import static com.sap.cds.impl.RowImpl.row;
import static com.sap.cds.reflect.CdsAnnotatable.byAnnotation;
import static com.sap.cds.util.CdsModelUtils.concreteKeyNames;
import static com.sap.cds.util.CdsModelUtils.isCascading;
import static com.sap.cds.util.CdsModelUtils.isReverseAssociation;
import static com.sap.cds.util.CdsModelUtils.isSingleValued;
import static com.sap.cds.util.CqnStatementUtils.hasInfixFilter;
import static com.sap.cds.util.DataUtils.generateUuidKeys;
import static com.sap.cds.util.DataUtils.isFkUpdate;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.sap.cds.CdsData;
import com.sap.cds.CdsDataStore;
import com.sap.cds.CdsList;
import com.sap.cds.Result;
import com.sap.cds.Row;
import com.sap.cds.SessionContext;
import com.sap.cds.impl.Cascader;
import com.sap.cds.impl.EntityCascader.EntityKeys;
import com.sap.cds.impl.EntityCascader.EntityOperation;
import com.sap.cds.impl.EntityCascader.EntityOperation.Operation;
import com.sap.cds.impl.EntityCascader.EntityOperations;
import com.sap.cds.impl.builder.model.ExpressionImpl;
import com.sap.cds.impl.builder.model.StructuredTypeRefBuilder;
import com.sap.cds.impl.parser.token.RefSegmentBuilder;
import com.sap.cds.ql.CQL;
import com.sap.cds.ql.CdsDataException;
import com.sap.cds.ql.RefBuilder;
import com.sap.cds.ql.RefBuilder.RefSegment;
import com.sap.cds.ql.Select;
import com.sap.cds.ql.StructuredType;
import com.sap.cds.ql.StructuredTypeRef;
import com.sap.cds.ql.cqn.CqnEtagPredicate;
import com.sap.cds.ql.cqn.CqnPredicate;
import com.sap.cds.ql.cqn.CqnSelect;
import com.sap.cds.ql.cqn.CqnSelectListItem;
import com.sap.cds.ql.cqn.CqnStructuredTypeRef;
import com.sap.cds.ql.cqn.CqnUpdate;
import com.sap.cds.ql.cqn.CqnUpsert;
import com.sap.cds.reflect.CdsAssociationType;
import com.sap.cds.reflect.CdsElement;
import com.sap.cds.reflect.CdsEntity;
import com.sap.cds.util.CdsModelUtils;
import com.sap.cds.util.CdsModelUtils.CascadeType;
import com.sap.cds.util.CqnStatementUtils;
import com.sap.cds.util.DataUtils;
import com.sap.cds.util.OccUtils;
import com.sap.cds.util.OnConditionAnalyzer;

public class DeepUpdateSplitter {
	private static final Logger logger = LoggerFactory.getLogger(DeepUpdateSplitter.class);
	private final CdsDataStore dataStore;
	private final SessionContext session;
	private CdsEntity entity;
	private EntityOperations operations;
	private boolean deepUpsert;

	public DeepUpdateSplitter(CdsDataStore dataStore) {
		this.dataStore = dataStore;
		this.session = dataStore.getSessionContext();
	}

	public EntityOperations computeOperations(CdsEntity targetEntity, CqnUpdate update,
			Map targetKeys) {
		StructuredType path = targetRef(update);

		return computeOps(Operation.UPDATE, path, targetEntity, targetKeys, update.entries());
	}

	public EntityOperations computeOperations(CdsEntity targetEntity, CqnUpsert upsert,
			Map targetKeys) {
		this.deepUpsert = true;
		StructuredType path = CQL.to(RefSegmentBuilder.copy(upsert.ref().segments()));

		return computeOps(Operation.UPSERT, path, targetEntity, targetKeys, upsert.entries());
	}

	private EntityOperations computeOps(Operation op, StructuredType path, CdsEntity targetEntity,
			Map targetKeys, List> entries) {
		entity = targetEntity;
		operations = new EntityOperations();
		operations.entries(entries);
		List> updateEntries = determineEntries(targetKeys, path);

		// move ETag predicate to data
		if (!updateEntries.isEmpty()) {
			moveETagToEntries(updateEntries, path);
		}

		for (Map entry : updateEntries) {
			EntityKeys entityId = keys(entity, entry);
			operations.add(root(entityId, op, session).update(entry, emptyMap()));
		}
		if (!updateEntries.isEmpty()) { // compute ops for child entities
			entity.associations().forEach(assoc -> cascade(path, assoc, updateEntries));
		}

		return operations;
	}

	private void moveETagToEntries(List> updateEntries, StructuredType path) {
		OccUtils.getVersionElement(entity).ifPresent(el -> {
			Optional filter = path.asRef().targetSegment().filter();
			Optional eTagPredicate = filter.flatMap(OccUtils::eTagPredicate);
			// existance of oldVersionParam indicates that no ETag * was present
			boolean oldVersionParam = filter.map(p -> OccUtils.containsVersionParam(p, el)).orElse(false);
			if (eTagPredicate.isPresent() || !oldVersionParam) {
				List values = eTagPredicate.map(OccUtils::concreteEtagValues).orElse(List.of());
				if (values.size() == 1) {
					Object versionValue = values.get(0);
					// set data with checked eTag value
					updateEntries.forEach(e -> e.put(el.getName(), versionValue));
				} else {
					// eTag was ambiguuous or force update, can't represent in data
					updateEntries.forEach(e -> e.remove(el.getName()));
				}
			}
		});
	}

	private List> determineEntries(Map targetKeys, StructuredType path) {
		Set keyElements = concreteKeyNames(entity);
		if (!hasInfixFilter(path.asRef())) {
			operations.entries().forEach(e -> addKeyValues(entity, targetKeys, e));
			if (entriesContainValuesFor(keyElements)) {
				// key values in data, no restricting filter
				return operations.entries();
			}
		}
		Set targetKeyElements = targetKeys.keySet();
		Set missingKeys = Sets.filter(keyElements, k -> !targetKeyElements.contains(k));
		if (!entriesContainValuesFor(missingKeys)) {
			// missing key values (searched update) -> select w/ filter
			return selectKeyValues(path, missingKeys);
		}
		// filter condition may restrict the update
		return evaluateFilter(path, targetKeys, keyElements);
	}

	private boolean entriesContainValuesFor(Set elements) {
		return operations.entries().stream().allMatch(e -> e.keySet().containsAll(elements));
	}

	private List> selectKeyValues(StructuredType path, Set missingKeys) {
		if (operations.entries().size() == 1) {
			logger.warn("Update data is missing key values of entity {}. Executing query to determine key values.",
					entity.getQualifiedName());
			Select select = Select.from(path).columns(missingKeys.stream().map(CQL::get));
			Result result = dataStore.execute(select);
			Map singleEntry = operations.entries().get(0);
			List> updateEntries = new ArrayList<>();
			if (result.rowCount() == 1) {
				Row keyValues = result.single();
				singleEntry.putAll(keyValues);
				updateEntries.add(singleEntry);
			} else if (result.rowCount() > 1) { // searched deep update
				result.forEach(row -> row.putAll(DataUtils.copyMap(singleEntry)));
				updateEntries.addAll(result.list());
			}
			return updateEntries;
		}
		throw new CdsDataException("Update data is missing key values " + missingKeys + " of entity " + entity);
	}

	private List> evaluateFilter(StructuredType path, Map targetKeys,
			Set keyElements) {
		logger.debug("Executing query to evaluate update filter condition {}", path);
		if (!targetKeys.isEmpty()) {
			// add key values from infix filter to each data entry
			operations.entries().forEach(e -> addKeyValues(entity, targetKeys, e));
		}
		List> updateEntries = new ArrayList<>();
		Map entityKeys = selectKeysMatchingPathFilter(path, keyElements, operations.entries());
		Map> updateData = operations.entries().stream()
				.collect(toMap(row -> index(row, keyElements), Function.identity()));
		entityKeys.forEach((hash, key) -> {
			updateEntries.add(updateData.get(hash));
		});
		logger.debug("Update filter condition fulfilled by {} entities", entityKeys.size());
		return updateEntries;
	}

	private static void addKeyValues(CdsEntity entity, Map keyValues, Map target) {
		keyValues.forEach((key, valInRef) -> {
			Object valInData = target.put(key, valInRef);
			if (valInData != null && !valInData.equals(valInRef)) {
				throw new CdsDataException("Values for key element '" + key
						+ "' in update data do not match values in update ref or where clause");
			}
		});
		target.putAll(Maps.filterValues(DataUtils.keyValues(entity, target), v -> !Objects.isNull(v)));
	}

	private static StructuredType targetRef(CqnUpdate update) {
		if (!CqnStatementUtils.containsPathExpression(update.where())) {
			return CqnStatementUtils.targetRef(update);
		}
		throw new UnsupportedOperationException("Deep updates with path in where clause are not supported");
	}

	private Map selectKeysMatchingPathFilter(StructuredType path, Set keys,
			Iterable> keyValueSets) {
		Select select = Select.from(filteredPath(path, keys)).columns(keys.stream().map(CQL::get));
		Iterable> params = OccUtils.addOldVersionParam(entity, keyValueSets);
		Result entries = dataStore.execute(select, params);

		return entries.stream().collect(toMap(row -> index(row, keys), Function.identity()));
	}

	private CqnStructuredTypeRef filteredPath(StructuredType path, Set filterParams) {
		RefBuilder refBuilder = StructuredTypeRefBuilder.copy(path.asRef());
		RefSegment targetSegment = refBuilder.targetSegment();
		targetSegment.filter(ExpressionImpl.byParams(filterParams).and(targetSegment.filter().orElse(null)));

		return refBuilder.build();
	}

	private void cascade(StructuredType path, CdsElement assoc, List> entries) {
		Iterable> updateEntries = Iterables.filter(entries, e -> e.containsKey(assoc.getName()));
		if (!Iterables.isEmpty(updateEntries)) {
			CdsEntity targetEntity = assoc.getType().as(CdsAssociationType.class).getTarget();
			if (isSingleValued(assoc.getType())) {
				toOne(path, assoc, targetEntity, updateEntries);
			} else {
				toMany(path, assoc, targetEntity, updateEntries);
			}
		}
	}

	/*
	 * Computes operations for to-one associations / compositions - remove / delete
	 * if association is mapped to null - insert / update if association is mapped
	 * to data map
	 */
	private void toOne(StructuredType path, CdsElement assoc, CdsEntity child,
			Iterable> parentEntries) {

		CdsEntity parent = assoc.getDeclaringType();
		boolean forward = !isReverseAssociation(assoc);
		boolean composition = assoc.getType().as(CdsAssociationType.class).isComposition();
		Set parentKeys = concreteKeyNames(parent);

		Map expandedParentEntries = deepUpsert ? emptyMap()
				// execute batch select to determine the association's target key values
				: selectPathExpandToOneTargetKeys(path, assoc, forward, parentKeys, parentEntries);

		List> childEntries = new ArrayList<>(Iterables.size(parentEntries));
		for (Map parentEntry : parentEntries) {
			Map childEntry = getDataFor(assoc, parentEntry);

			if (deepUpsert) {
				upsertToOne(parent, child, assoc, forward, composition, parentEntry, childEntry, childEntries);
			} else {
				updateToOne(parent, child, assoc, forward, composition, parentKeys, expandedParentEntries, parentEntry,
						childEntry, childEntries);
			}
		}
		// cascade over associations
		child.associations().forEach(a -> cascade(path.to(assoc.getName()), a, childEntries));
	}

	private void upsertToOne(CdsEntity parent, CdsEntity child, CdsElement assoc, boolean forward, boolean composition,
			Map parentEntry, Map childEntry, List> childEntries) {
		if (childEntry == null) {
			// clear FKs & delete target when cascading
			remove(assoc, forward, parent, singletonList(parentEntry), assoc.getName());

		} else if (isCascading(CascadeType.INSERT, assoc) || isCascading(CascadeType.UPDATE, assoc)) {
			childEntries.add(childEntry);
			// TODO allow to delete old child via annotation
			operations.add(xsert(parentEntry, assoc, forward, child, childEntry, false));
		} else if (forward && !composition) {
			// set ref to existing target
			removeNonFkValues(assoc, childEntry);
		}
	}

	private void updateToOne(CdsEntity parentEntity, CdsEntity targetEntity, CdsElement assoc, boolean forward,
			boolean composition, Set parentKeys, Map expandedParentEntries,
			Map parentUpdateEntry, Map targetUpdateEntry,
			List> targetUpdateEntries) {
		Row parentEntry = expandedParentEntries.getOrDefault(index(parentUpdateEntry, parentKeys), row(emptyMap()));
		Map targetEntry = getDataFor(assoc, parentEntry);

		if (targetEntry != null && !targetEntry.isEmpty()) {
			// assoc points to an existing target entity
			if (targetUpdateEntry == null) {
				// clear FKs & delete target when cascading
				remove(assoc, forward, targetEntity, singletonList(targetEntry), null);

			} else if (targetChange(assoc, forward && !composition, targetEntity, targetEntry, targetUpdateEntry)) {
				// target key values changed
				if (isCascading(CascadeType.INSERT, assoc) || isCascading(CascadeType.UPDATE, assoc)) {
					// composition -> insert / assoc -> update or insert
					operations.add(
							xsert(parentUpdateEntry, assoc, forward, targetEntity, targetUpdateEntry, composition));
					targetUpdateEntries.add(targetUpdateEntry);
				} else { // update FK (relaxed data)
					removeNonFkValues(assoc, targetUpdateEntry);
				}
				if (composition) { // delete old target
					delete(targetEntity, targetEntry, null);
				}

			} else if (isCascading(CascadeType.UPDATE, assoc)) { // update target
				operations.add(update(targetEntity, targetEntry, targetUpdateEntry, emptyMap()));
				targetUpdateEntries.add(targetUpdateEntry);
			}
			// same target, not cascading update -> ignore

		} else if (targetUpdateEntry != null) { // assoc is null
			if (forward && !composition && !isCascading(CascadeType.INSERT, assoc)) {
				// set ref to existing target
				removeNonFkValues(assoc, targetUpdateEntry);
			} else { // insert or update target
				boolean generatedKey = generateUuidKeys(targetEntity, targetUpdateEntry);
				if (forward && generatedKey) { // update parent with target ref (FK) values
					Map parentKeyValues = keys(parentEntity, parentUpdateEntry);
					Map targetRefValues = fkValues(assoc, !forward, targetUpdateEntry);
					operations.add(update(parentEntity, parentKeyValues, new HashMap<>(), targetRefValues));
				}
				boolean insertOnly = composition || generatedKey;
				operations.add(xsert(parentUpdateEntry, assoc, forward, targetEntity, targetUpdateEntry, insertOnly));
				targetUpdateEntries.add(targetUpdateEntry);
			}
		}
	}

	private static boolean targetChange(CdsElement assoc, boolean forward, CdsEntity targetEntity,
			Map oldEntry, Map newEntry) {
		Set refElements = forward ? refElements(assoc, forward) : concreteKeyNames(targetEntity);
		return refElements.stream().anyMatch(k -> {
			Object newVal = newEntry.get(k);
			return newVal != null && !newVal.equals(oldEntry.get(k));
		});
	}

	private static void removeNonFkValues(CdsElement assoc, Map data) {
		Set assocKeys = refElements(assoc, true);
		data.keySet().retainAll(assocKeys);
	}

	private Map selectPathExpandToOneTargetKeys(StructuredType path, CdsElement assoc,
			boolean forwardMapped, Set parentKeys, Iterable> keyValueSets) {
		logger.debug("Executing query to determine target entity of {}", assoc.getQualifiedName());
		List slis = parentKeys.stream().map(CQL::get).collect(toList());
		Set refElements = refElements(assoc, forwardMapped);
		CdsModelUtils.targetKeys(assoc).forEach(refElements::add);
		slis.add(CQL.to(assoc.getName()).expand(refElements.toArray(new String[refElements.size()])));
		CqnSelect select = Select.from(filteredPath(path, parentKeys)).columns(slis);
		Iterable> params = OccUtils.addOldVersionParam(entity, keyValueSets);
		Result targetEntries = dataStore.execute(select, params);

		return targetEntries.stream().collect(toMap(row -> index(row, parentKeys), Function.identity()));
	}

	private Map fkValues(CdsElement assoc, boolean reverseMapped, Map data) {
		return new OnConditionAnalyzer(assoc, reverseMapped, session).getFkValues(data);
	}

	/*
	 * Computes operations for to-many associations / compositions - insert / update
	 * entities that are in the update data list - remove / delete entities that are
	 * not in the update data list
	 */
	private void toMany(StructuredType path, CdsElement assoc, CdsEntity targetEntity,
			Iterable> parentEntries) {
		boolean composition = assoc.getType().as(CdsAssociationType.class).isComposition();
		Set parentKeys = concreteKeyNames(assoc.getDeclaringType());
		Set targetKeys = CdsModelUtils.targetKeys(assoc);
		// execute batch select to determine the association target entities of
		// non-delta entries
		Map fullSetTargetEntries = selectTargetEntries(assoc, parentKeys, targetKeys, parentEntries);

		List> updateEntries = new ArrayList<>(Iterables.size(parentEntries));
		for (Map parentEntry : parentEntries) {
			List> targetUpdateEntries = getDataFor(assoc, parentEntry);
			if (targetUpdateEntries == null) {
				throw new CdsDataException("Value for to-many association '" + assoc.getDeclaringType() + "." + assoc
						+ "' must not be null.");
			}
			Map parentRefValues = new OnConditionAnalyzer(assoc, true, session)
					.getFkValues(parentEntry);
			if (parentRefValues.containsValue(null)) {
				throw new CdsDataException("Values of ref elements " + parentRefValues.keySet() + " for mapping "
						+ targetEntity + " to " + assoc.getDeclaringType() + " cannot be determined from update data.");
			}
			boolean delta = isDelta(targetUpdateEntries);
			for (Map updateEntry : targetUpdateEntries) {
				Map updateEntryWithFks = new HashMap<>(updateEntry);
				updateEntryWithFks.putAll(parentRefValues);
				EntityOperation operation;
				if (delta) { // DELTA: remove/delete via annotation
					if (isRemove(updateEntry)) {
						remove(assoc, false, targetEntity, singletonList(updateEntryWithFks), null);
						continue;
					}
					operation = xsert(assoc, targetEntity, updateEntry, parentRefValues, false);

				} else { // FULL-SET: remove/delete if not present in update data
					boolean targetPresent = fullSetTargetEntries.remove(index(updateEntryWithFks, targetKeys)) != null;
					if (deepUpsert) {
						// Do DB upsert to deal with concurrent inserts/deletes
						operation = xsert(assoc, targetEntity, updateEntry, parentRefValues, false);
					} else {
						boolean generatedKey = !targetPresent && generateUuidKeys(targetEntity, updateEntry);
						if (generatedKey) {
							updateEntryWithFks.putAll(updateEntry);
						}
						EntityKeys targetId = keys(targetEntity, updateEntryWithFks);
						if (generatedKey) { // insert
							operation = EntityOperation.insert(targetId, updateEntry, parentRefValues, session);
						} else if (targetPresent) { // nop or update
							operation = nop(targetId, null, session).update(updateEntry, emptyMap());
						} else { // assoc: insert or update / composition: insert
							operation = xsert(assoc, targetEntity, updateEntry, parentRefValues, composition);
						}
					}
				}
				assertCascading(operation, assoc);
				operations.add(operation);
				updateEntries.add(updateEntryWithFks);
			}
		}
		// full-sets: remove/delete all entities that are not included in the update
		// data list
		remove(assoc, false, targetEntity, fullSetTargetEntries.values(), null);

		// cascade over associations
		targetEntity.associations().forEach(a -> cascade(path.to(assoc.getName()), a, updateEntries));
	}

	private static boolean isDelta(List> entries) {
		if (entries instanceof CdsList list) {
			return list.isDelta();
		}
		return false;
	}

	private static boolean isRemove(Map entry) {
		if (entry instanceof CdsData data) {
			return data.isForRemoval();
		}
		return false;
	}

	private Map selectTargetEntries(CdsElement assoc, Set parentKeys, Set targetKeys,
			Iterable> keyValueSets) {
		logger.debug("Executing query to determine target entity of {}", assoc.getQualifiedName());
		StructuredType path = CQL.entity(assoc.getDeclaringType().getQualifiedName()).filterByParams(parentKeys)
				.to(assoc.getName());
		CqnSelect select = Select.from(path).columns(targetKeys.stream().map(CQL::get));
		Result targetEntries = dataStore.execute(select,
				Iterables.filter(keyValueSets, entry -> !isDelta(getDataFor(assoc, entry))));

		return targetEntries.stream().collect(toMap(row -> index(row, targetKeys), Function.identity()));
	}

	private EntityOperation update(CdsEntity target, Map entry, Map updateData,
			Map fkValues) {
		EntityOperation op = nop(keys(target, entry), null, entry, session).update(updateData, fkValues);
		updateData.putAll(op.targetKeys());

		return op;
	}

	private void remove(CdsElement association, boolean forwardMapped, CdsEntity targetEntity,
			Collection> keyValues, String path) {
		if (isCascading(CascadeType.DELETE, association)) {
			keyValues.forEach(k -> delete(targetEntity, k, path));
		} else if (!forwardMapped) {
			assertCascading(CascadeType.UPDATE, association);
			Set parentRefElements = refElements(association, forwardMapped);
			keyValues.forEach(k -> {
				operations.add(nop(keys(targetEntity, k), path, session).updateToNull(parentRefElements));
			});
		}
	}

	private void delete(CdsEntity entity, Map data, String path) {
		EntityKeys rootKey = keys(entity, data);
		CdsEntity target = entity;
		if (path != null) {
			target = entity.getTargetOf(path);
		}
		boolean cascaded = Cascader.create(CascadeType.DELETE, target).from(path)
				.cascade(p -> operations.add(EntityOperation.delete(rootKey, p.asRef().path(), session)));
		if (!cascaded) {
			// fallback for cyclic models and subqueries in where
			// compute object graph (select key values)
			cascadeDelete(dataStore, rootKey, path).forEach(operations::add);
		}
		operations.add(EntityOperation.delete(rootKey, path, session));
	}

	private EntityOperation xsert(Map parentData, CdsElement association, boolean forwardMapped,
			CdsEntity entity, Map entityData, boolean insertOnly) {
		Map fkValues = emptyMap();
		if (!forwardMapped) { // set parent ref (FK) in target entity
			fkValues = fkValues(association, !forwardMapped, parentData);
		}
		return xsert(association, entity, entityData, fkValues, insertOnly);
	}

	private EntityOperation xsert(CdsElement association, CdsEntity entity, Map entityData,
			Map fkValues, boolean insertOnly) {
		Map data = new HashMap<>(entityData);
		data.putAll(fkValues);
		EntityKeys targetEntity = keys(entity, data);
		boolean insert = isCascading(CascadeType.INSERT, association);
		boolean update = !insertOnly && isCascading(CascadeType.UPDATE, association);
		if (!deepUpsert && insert && update && containsStream(entityData)) {
			// InputStreams can only be consumed once
			// Determine if target exists to decide between insert and update
			CqnSelect select = Select.from(entity).columns(CQL.plain("1").as("1")).matching(targetEntity);
			update = dataStore.execute(select).rowCount() > 0;
			insert = !update;
		}
		if (insert && update) {
			if (deepUpsert || !hasDefaultValues(entity, entityData)) {
				return upsert(targetEntity, entityData, fkValues, session);
			}
			return updateOrInsert(targetEntity, entityData, fkValues, session);
		}
		if (insert) {
			return insert(targetEntity, entityData, fkValues, session);
		}
		if (update) {
			return nop(targetEntity, null, session).update(entityData, fkValues);
		}
		if (CdsModelUtils.managedToOne(association.getType()) && isFkUpdate(association, entityData, session)) {
			return nop(targetEntity, null, session);
		}
		CdsEntity target = association.getType().as(CdsAssociationType.class).getTarget();
		throw new CdsDataException(
				"UPSERT entity '%s' via association '%s.%s' is not allowed. The association does not cascade insert or update.".formatted(
				target, association.getDeclaringType(), association));
	}

	@SuppressWarnings("unchecked")
	private static boolean containsStream(Map entityData) {
		return entityData.values().stream()
				.anyMatch(v -> v instanceof InputStream || v instanceof Map m && containsStream(m));
	}

	private static boolean hasDefaultValues(CdsEntity entity, Map entityData) {
		boolean defaultValue = entity.concreteNonAssociationElements().filter(e -> e.defaultValue().isPresent())
				.anyMatch(e -> !entityData.containsKey(e.getName()));
		return defaultValue || entity.concreteNonAssociationElements().filter(byAnnotation("cds.on.insert"))
				.anyMatch(e -> !entityData.containsKey(e.getName()));
	}

	private static void assertCascading(EntityOperation op, CdsElement association) {
		switch (op.operation()) {
		case INSERT:
			assertCascading(CascadeType.INSERT, association);
			break;
		case UPDATE:
			assertCascading(CascadeType.UPDATE, association);
			break;
		case DELETE:
			assertCascading(CascadeType.DELETE, association);
			break;
		case UPDATE_OR_INSERT:
		case UPSERT:
			assertCascading(CascadeType.UPDATE, association);
			assertCascading(CascadeType.INSERT, association);
		}
	}

	private static void assertCascading(CascadeType cascadeType, CdsElement association) {
		if (!isCascading(cascadeType, association)) {
			CdsEntity target = association.getType().as(CdsAssociationType.class).getTarget();
			throw new CdsDataException(
					"%s entity '%s' via association '%s.%s' is not allowed. The association does not cascade %s.".formatted(
					cascadeType.name(), target, association.getDeclaringType(), association, cascadeType));
		}
	}

	private static Set refElements(CdsElement assoc, boolean forwardMapped) {
		HashMap mapping = new HashMap<>();
		new OnConditionAnalyzer(assoc, !forwardMapped).getFkMapping().forEach((fk, val) -> {
			if (val.isRef() && !val.asRef().firstSegment().startsWith("$")) {
				mapping.put(fk, val.asRef().lastSegment());
			}
		});
		if (forwardMapped) {
			return new HashSet<>(mapping.values());
		}
		return new HashSet<>(mapping.keySet());
	}

	public static  T getDataFor(CdsElement assoc, Map data) {
		return DataUtils.getOrDefault(data, assoc.getName(), null);
	}

	public static String index(Map values, Set keys) {
		return values.entrySet().stream().filter(e -> keys.contains(e.getKey()))
				.map(e -> e.getKey() + ":" + e.getValue()).sorted().collect(joining("-"));
	}

}