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

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

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

import static com.sap.cds.util.CdsModelUtils.entity;
import static com.sap.cds.util.CdsModelUtils.isContextElementRef;
import static com.sap.cds.util.CqnStatementUtils.resolveStar;
import static com.sap.cds.util.CqnStatementUtils.selectedRefs;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.stream.Collectors.toList;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.IntStream;

import com.sap.cds.impl.builder.model.Conjunction;
import com.sap.cds.impl.util.Stack;
import com.sap.cds.ql.cqn.AnalysisResult;
import com.sap.cds.ql.cqn.CqnAnalyzer.CqnAnalyzerSPI;
import com.sap.cds.ql.cqn.CqnComparisonPredicate;
import com.sap.cds.ql.cqn.CqnConnectivePredicate;
import com.sap.cds.ql.cqn.CqnConnectivePredicate.Operator;
import com.sap.cds.ql.cqn.CqnContainmentTest;
import com.sap.cds.ql.cqn.CqnDelete;
import com.sap.cds.ql.cqn.CqnElementRef;
import com.sap.cds.ql.cqn.CqnInPredicate;
import com.sap.cds.ql.cqn.CqnListValue;
import com.sap.cds.ql.cqn.CqnNegation;
import com.sap.cds.ql.cqn.CqnPredicate;
import com.sap.cds.ql.cqn.CqnReference.Segment;
import com.sap.cds.ql.cqn.CqnSelect;
import com.sap.cds.ql.cqn.CqnSelectListItem;
import com.sap.cds.ql.cqn.CqnSelectListValue;
import com.sap.cds.ql.cqn.CqnSource;
import com.sap.cds.ql.cqn.CqnStatement;
import com.sap.cds.ql.cqn.CqnStructuredTypeRef;
import com.sap.cds.ql.cqn.CqnUpdate;
import com.sap.cds.ql.cqn.CqnValue;
import com.sap.cds.ql.cqn.CqnVisitor;
import com.sap.cds.ql.cqn.ResolvedRefItem;
import com.sap.cds.ql.cqn.ResolvedSegment;
import com.sap.cds.reflect.CdsElement;
import com.sap.cds.reflect.CdsEntity;
import com.sap.cds.reflect.CdsModel;
import com.sap.cds.util.CdsModelUtils;
import com.sap.cds.util.CqnStatementUtils;

public class CqnAnalyzerImpl implements CqnAnalyzerSPI {

	private static final String COUNT = "COUNT";
	private static final String COUNT_DISTINCT = "countDistinct";
	private static final Object AMBIGUOUS = new Object();
	private static final Map GIVE_UP = new HashMap<>();

	@Override
	public boolean isCountQuery(CqnStatement cqn) {
		if (cqn.isSelect() && cqn.asSelect().items().size() == 1) {
			CqnSelectListItem item = cqn.asSelect().items().get(0);
			if (item.isValue()) {
				CqnValue val = item.asValue().value();
				if (val.isFunction()) {
					String func = val.asFunction().func();
					return COUNT.equals(func.toUpperCase(Locale.US)) || COUNT_DISTINCT.equals(func);
				}
			}
		}
		return false;
	}

	@Override
	public AnalysisResult analyze(CdsModel model, CqnStructuredTypeRef ref) {
		List segments = resolveSegments(model, ref.segments());
		return new Result(segments, Optional.empty());
	}

	@Override
	public AnalysisResult analyze(CdsModel model, CqnSelect select) {
		List segments = resolveSegments(model, select.ref().segments());
		return new Result(segments, select.where());
	}

	@Override
	public AnalysisResult analyze(CdsModel model, CqnUpdate update) {
		List segments = resolveSegments(model, update.ref().segments());
		return new Result(segments, update.where());
	}

	@Override
	public AnalysisResult analyze(CdsModel model, CqnDelete delete) {
		List segments = resolveSegments(model, delete.ref().segments());
		return new Result(segments, delete.where());
	}

	@Override
	public Iterable resolveRefItems(CdsModel model, CqnSelect query) {
		return resolveRefItems(model, query, i -> true);
	}

	private static List resolveRefItems(CdsModel model, CqnSelect query,
			Predicate filter) {
		CqnSource from = query.from();
		if (from.isRef()) {
			CdsEntity target = entity(model, from.asRef());
			query = resolveStar(query, target);

			return selectedRefs(query).filter(filter).map(r -> new ResolvedRef(model, target, r)).collect(toList());
		}
		return emptyList();
	}

	private List resolveSegments(CdsModel model, List segments) {
		List entities = CdsModelUtils.entities(model, segments);

		return IntStream.range(0, segments.size())
				.mapToObj(i -> new ResolvedSeg(segments.get(i), entities.get(i), Optional.empty())).collect(toList());
	}

	public static Map extractKeys(Segment seg, CdsEntity entity) {
		Optional filter = seg.filter();
		if (filter.isPresent()) {
			return new ResolvedSeg(seg, entity, filter).keys();
		}
		return emptyMap();
	}

	private static class ResolvedRef implements ResolvedRefItem {
		private final CdsModel model;
		private final CdsEntity entity;
		private final CqnSelectListValue value;
		private final CqnElementRef ref;

		private ResolvedRef(CdsModel model, CdsEntity entity, CqnSelectListValue value) {
			this.model = model;
			this.entity = entity;
			this.value = value;
			this.ref = value.value().asRef();
		}

		@Override
		public String displayName() {
			return value.displayName();
		}

		@Override
		public CqnElementRef ref() {
			return ref;
		}

		@Override
		public CdsElement element() {
			return CdsModelUtils.element(entity, ref);
		}

		@Override
		public Optional origin() {
			Optional projection = entity.query();
			if (projection.isPresent()) {
				List refs = resolveRefItems(model, projection.get(),
						i -> i.displayName().equals(ref.lastSegment()));
				if (!refs.isEmpty()) {
					return Optional.of(refs.get(0));
				}
			}
			return Optional.empty();
		}
	}

	private static class Result extends PathImpl implements AnalysisResult {
		private final Optional where;

		public Result(List segments, Optional where) {
			super(new LinkedList<>(segments));
			this.where = where;
		}

		@Override
		public CdsEntity rootEntity() {
			return root().entity();
		}

		@Override
		public Map rootKeys() {
			return root().keys();
		}

		@Override
		public CdsEntity targetEntity() {
			return target().entity();
		}

		@Override
		public Map targetKeys() {
			return target(ResolvedSegment::keys);
		}

		@Override
		public Map targetKeyValues() {
			return target(ResolvedSegment::keyValues);
		}

		@Override
		public Map targetValues() {
			return target(ResolvedSegment::values);
		}

		private Map target(Function> filter) {
			ResolvedSegment target = target();
			if (where.isPresent()) {
				target = new ResolvedSeg(target.segment(), target.entity(), where);
			}
			return filter.apply(target);
		}
	}

	private static class ResolvedSeg extends PathImpl.ResolvedSegImpl {
		private final CqnPredicate filter;

		private ResolvedSeg(Segment segment, CdsEntity entity, Optional where) {
			super(segment, entity);
			CqnPredicate combined = Conjunction.and(segment.filter(), where).orElse(null);
			filter = CqnStatementUtils.simplifyPredicate(combined);
		}

		@Override
		public Map values() {
			if (filter == null || filter.isLiteral() && filter.asLiteral().isBoolean()) {
				return new HashMap<>();
			}
			ValueExtractor extractor = new ValueExtractor();
			filter.accept(extractor);

			return extractor.values();
		}
	}

	private static final class ValueExtractor implements CqnVisitor {
		private Stack> stack = new Stack<>();

		private Map values() {
			Map map = stack.pop();
			if (map == GIVE_UP) {
				return emptyMap();
			}

			Map result = new HashMap<>(map);
			result.entrySet().removeIf(e -> e.getValue() == AMBIGUOUS);

			return result;
		}

		@Override
		public void visit(CqnComparisonPredicate predicate) {
			CqnValue left = predicate.left();
			CqnValue right = predicate.right();
			Map map = emptyMap();
			switch (predicate.operator()) {
			case EQ:
			case IS:
				if (left.isList() && right.isList()) {
					CqnListValue lList = left.asList();
					CqnListValue rList = right.asList();
					List> mappings = new ArrayList<>(lList.size());
					Iterator ls = lList.values().iterator();
					Iterator rs = rList.values().iterator();
					while (ls.hasNext()) {
						mappings.add(eq(ls.next(), rs.next()));
					}
					map = conjunction(mappings);
				} else {
					map = eq(left, right);
				}
				break;
			case IS_NOT:
				if (right.isNullValue()) {
					break;
				}
			default:
				if (isElementRef(left)) {
					map = ambiguous(left.asRef());
				} else if (isElementRef(right)) {
					map = ambiguous(right.asRef());
				}
			}
			stack.push(map);
		}

		private Map eq(CqnValue left, CqnValue right) {
			if (isElementRef(left)) {
				return mapping(left.asRef(), right);
			} else if (isElementRef(right)) {
				return mapping(right.asRef(), left);
			}

			return emptyMap();
		}

		private static boolean isElementRef(CqnValue val) {
			return val.isRef() && !isContextElementRef(val.asRef());
		}

		@Override
		public void visit(CqnInPredicate in) {
			CqnValue val = in.value();
			if (val.isRef() && in.valueSet().isList()) {
				List> maps = new ArrayList<>();
				in.values().forEach(v -> maps.add(mapping(val.asRef(), v)));
				stack.push(disjunction(maps));
			} else {
				stack.push(emptyMap());
			}
		}

		@Override
		public void visit(CqnPredicate pred) {
			stack.push(GIVE_UP);
		}

		@Override
		public void visit(CqnContainmentTest test) {
			CqnValue val = test.value();
			if (val.isRef()) {
				stack.push(ambiguous(val.asRef()));
			} else {
				stack.push(GIVE_UP);
			}
		}

		@Override
		public void visit(CqnConnectivePredicate con) {
			int n = con.predicates().size();
			List> maps = stack.pop(n);

			if (con.operator() == Operator.OR) {
				stack.push(disjunction(maps));
			} else {
				// AND
				stack.push(conjunction(maps));
			}
		}

		private static Map disjunction(List> maps) {
			if (anyGiveUp(maps)) {
				return GIVE_UP;
			}

			// keep mappings that are common to all maps
			// set all other mappings to AMBIGOUS
			Set allKeys = new HashSet<>();
			maps.forEach(m -> allKeys.addAll(m.keySet()));
			Map result = new HashMap<>(allKeys.size());
			for (String key : allKeys) {
				Object val = null;
				boolean first = true;
				for (Map map : maps) {
					Object v = map.getOrDefault(key, AMBIGUOUS);
					if (first) {
						val = v;
						first = false;
					} else if (!Objects.equals(val, v)) {
						val = AMBIGUOUS;
						break;
					}
				}
				result.put(key, val);
			}
			return result;
		}

		private static Map conjunction(List> maps) {
			// collect/merge values, ignore AMBIGOUS
			Map result = new HashMap<>();
			for (Map map : maps) {
				map.forEach((k, v) -> {
					if (v != AMBIGUOUS) {
						result.putIfAbsent(k, v);
					}
				});
			}
			return result;
		}

		private static boolean anyGiveUp(List> maps) {
			return maps.stream().anyMatch(m -> m == GIVE_UP);
		}

		@Override
		public void visit(CqnNegation neg) {
			Map map = stack.pop();
			if (map == GIVE_UP) {
				stack.push(GIVE_UP);
				return;
			}

			Map result = new HashMap<>();
			map.keySet().forEach(k -> result.put(k, AMBIGUOUS));
			stack.push(result);
		}

		private Map mapping(CqnElementRef ref, CqnValue value) {
			String path = path(ref);
			Object val = value(value);
			return mapping(path, val);
		}

		private Map ambiguous(CqnElementRef ref) {
			return Collections.singletonMap(path(ref), AMBIGUOUS);
		}

		private Map mapping(String name, Object val) {
			return Collections.singletonMap(name, val);
		}

		private static Object value(CqnValue value) {
			if (value.isLiteral()) {
				return value.asLiteral().value();
			}
			if (value.isNullValue()) {
				return null;
			}
			if (value.isParameter() || value.isRef() && isContextElementRef(value.asRef())) {
				return value;
			}
			return AMBIGUOUS; // ref, func, ...
		}

		private static String path(CqnElementRef ref) {
			return ref.path();
		}
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy