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

com.sap.cds.jdbc.hana.hierarchies.HanaHierarchyResolver Maven / Gradle / Ivy

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

import static com.sap.cds.ql.cqn.transformation.CqnTransformation.Kind.IDENTITY;
import static com.sap.cds.ql.cqn.transformation.CqnTransformation.Kind.ORDERBY;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.function.Function;

import com.sap.cds.impl.builder.model.InSubquery;
import com.sap.cds.ql.CQL;
import com.sap.cds.ql.ElementRef;
import com.sap.cds.ql.Literal;
import com.sap.cds.ql.Predicate;
import com.sap.cds.ql.Select;
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.CqnSelectListValue;
import com.sap.cds.ql.cqn.CqnSortSpecification;
import com.sap.cds.ql.cqn.transformation.CqnAncestorsTransformation;
import com.sap.cds.ql.cqn.transformation.CqnDescendantsTransformation;
import com.sap.cds.ql.cqn.transformation.CqnFilterTransformation;
import com.sap.cds.ql.cqn.transformation.CqnHierarchySubsetTransformation;
import com.sap.cds.ql.cqn.transformation.CqnSearchTransformation;
import com.sap.cds.ql.cqn.transformation.CqnTopLevelsTransformation;
import com.sap.cds.ql.cqn.transformation.CqnTransformation;
import com.sap.cds.ql.cqn.transformation.CqnTransformation.Kind;
import com.sap.cds.ql.hana.HANA;
import com.sap.cds.ql.hana.Hierarchy;
import com.sap.cds.ql.hana.HierarchySubset;
import com.sap.cds.ql.impl.SelectBuilder;
import com.sap.cds.reflect.CdsBaseType;
import com.sap.cds.util.CaseBuilder;
import com.sap.cds.util.transformations.TransformationToSelect;

public class HanaHierarchyResolver extends TransformationToSelect {

	private static final Literal ZERO = CQL.constant(0);
	private static final Literal ONE = CQL.constant(1);

	// HANA elements
	private static final String PARENT_ID = "parent_id";
	private static final String NODE_ID = "node_id";
	private static final String HIERARCHY_TREE_SIZE = "hierarchy_tree_size";
	private static final String HIERARCHY_LEVEL = "hierarchy_level";
	private static final String HIERARCHY_RANK = "hierarchy_rank";

	// OData elements
	private static final String DISTANCE_FROM_ROOT = "DistanceFromRoot";
	private static final String LIMITED_DESCENDANT_COUNT = "LimitedDescendantCount";
	private static final String DESCENDANT_COUNT = "DescendantCount";
	private static final String DRILL_STATE = "DrillState";

	private boolean isHierarchicalSelect;

	// drill states
	private static final Literal COLLAPSED = CQL.constant("collapsed");
	private static final Literal EXPANDED = CQL.constant("expanded");
	private static final Literal LEAF = CQL.constant("leaf");

	private Hierarchy sourceHierarchy;
	private List transformations;
	private List originalItems;

	public HanaHierarchyResolver(Select select) {
		super(select);
	}

	@Override
	protected void before(CqnSelect original) {
		originalItems = original.items();
		isHierarchicalSelect = original.transformations().stream().anyMatch(HanaHierarchyResolver::isHierarchical);
		if (isHierarchicalSelect) {
			this.transformations = original.transformations();
			Select hierarchySource = select;
			sourceHierarchy = HANA.hierarchy(hierarchySource);
			select = SelectBuilder.from(sourceHierarchy)
					.columns(CQL.star(), 
							descendantCount(), //
							distanceFromRoot()) //
					.excluding(HIERARCHY_TREE_SIZE, HIERARCHY_LEVEL, HIERARCHY_RANK);
		}
	}

	@Override
	protected void after() {
		if (isHierarchicalSelect) {
			var iter = transformations.listIterator(transformations.size());
			while (iter.hasPrevious()) {
				CqnTransformation t = iter.previous();
				if (t.kind() == Kind.DESCENDANTS) {
					addDescendantsDrillState((SelectBuilder) select, t);
					break;
				}
			}
		}
	}

	@Override
	protected void copySelectList(List slis) {
		if (!isHierarchicalSelect) {
			super.copySelectList(slis);
		}
		// don't modify select list, as we add calculated elements
	}

	private static boolean isHierarchical(CqnTransformation t) {
		return switch (t.kind()) {
			case TOPLEVELS, ANCESTORS, DESCENDANTS -> true;
			default -> false;
		};
	}

	@Override
	protected void applyTopLevels(CqnTopLevelsTransformation topLevels) {
		wrapUnless(topLevels, IDENTITY, ORDERBY);
		previous = CqnTransformation.IDENTITY; // avoid wrapping in case of ORDER BYs

		Predicate filter = CQL.TRUE;
		// TODO: optimize to use depth instead where clause
		long levels = topLevels.levels();
		if (levels > 0) {
			// sub-select aliases {hierarchy_level} to {DistanceFromRoot = hierarchy_level - 1}
			filter = select.from().isSelect()
					? filter.and(CQL.get(DISTANCE_FROM_ROOT).lt(levels))
					: filter.and(CQL.get(HIERARCHY_LEVEL).le(levels));
		}
		CqnPredicate f = filter.or(expandFilter(topLevels.expandLevels().keySet()));

		applyFilter(() -> f);

		// wrap into hierarchy select(hierarchy(select))
		List siblingOrderBy = new ArrayList<>(select.orderBy());
		if (siblingOrderBy.isEmpty()) {
			siblingOrderBy = List.of(CQL.get(NODE_ID).asc());
		}
		select.orderBy(List.of());

		Hierarchy hierarchy = HANA.hierarchy(select).orderBy(siblingOrderBy);
		select = SelectBuilder.from(hierarchy).columns(computeVirtual(originalItems));
	}

	private static List computeVirtual(List slis) {
		if (slis.isEmpty()) {
			List selectList = new ArrayList<>(3);
			selectList.add(CQL.star());
			selectList.add(limitedDescendantCount());
			selectList.add(drillState());
			return selectList;
		}
		return slis.stream().map(HanaHierarchyResolver::computeVirtual).toList();
	}

	private static CqnSelectListItem computeVirtual(CqnSelectListItem sli) {
		if (sli.isRef()) {
			String path = sli.asRef().path();
			if (LIMITED_DESCENDANT_COUNT.equals(path)) {
				return limitedDescendantCount();
			}
			if (DRILL_STATE.equals(path)) {
				return drillState();
			}
		}
		return sli;
	}

	private static CqnSelectListValue distanceFromRoot() {
		return CQL.get(HIERARCHY_LEVEL).minus(ONE).as(DISTANCE_FROM_ROOT);
	}

	private static CqnSelectListValue descendantCount() {
		return CQL.get(HIERARCHY_TREE_SIZE).minus(ONE).as(DESCENDANT_COUNT);
	}

	private static CqnSelectListValue limitedDescendantCount() {
		return CQL.get(HIERARCHY_TREE_SIZE).minus(ONE).as(LIMITED_DESCENDANT_COUNT);
	}

	private static CqnSelectListItem drillState() {
		Predicate hasDescendants = CQL.get(DESCENDANT_COUNT).gt(ZERO);
		ElementRef treeSize = CQL.get(HIERARCHY_TREE_SIZE);
		return CaseBuilder.cases() //
				.when(hasDescendants.and(treeSize.eq(ONE))).then(COLLAPSED) //
				.when(hasDescendants.and(treeSize.gt(ONE))).then(EXPANDED) //
				.orElse(LEAF) //
				.end().type(CdsBaseType.STRING).as(DRILL_STATE);
	}

	private static void addDescendantsDrillState(SelectBuilder select, CqnTransformation t) {
		CqnDescendantsTransformation d = (CqnDescendantsTransformation) t;
		int v = d.distanceFromStart();
		if (v > 1 || d.keepStart()) {
			return; // not yet implemented
		}

		CqnSelectListValue drillState = CaseBuilder.cases()
				.when(CQL.get(HIERARCHY_TREE_SIZE).gt(ONE)).then(COLLAPSED) //
				.orElse(LEAF) //
				.end().type(CdsBaseType.STRING).as(DRILL_STATE);
		select.addItem(drillState);
	}

	/*
	 * Identify the nodes to be expanded
	 */
	private static Predicate expandFilter(Set ids) {
		List expandIds = ids.stream().toList();
		return CQL.get(PARENT_ID).in(expandIds).or(CQL.get(NODE_ID).in(expandIds));
	}

	@Override
	protected void applyAncestors(CqnAncestorsTransformation transformation) {
		subSet(transformation, HANA::ancestors);
	}

	@Override
	protected void applyDescendants(CqnDescendantsTransformation transformation) {
		subSet(transformation, HANA::descendants);
	}

	private void subSet(CqnHierarchySubsetTransformation subsetTrafo, Function factory) {
		HierarchySubset subset = factory.apply(sourceHierarchy);

		subset.distance(subsetTrafo.distanceFromStart(), subsetTrafo.keepStart());

		CqnPredicate startWhere = CQL.TRUE;
		for (CqnTransformation nested : subsetTrafo.transformations()) {
			if (nested instanceof CqnFilterTransformation filter) {
				startWhere = CQL.and(startWhere, filter.filter());
			} else if (nested instanceof CqnSearchTransformation search) {
				startWhere = CQL.and(startWhere, search.search());
			}
		}
		subset.startWhere(startWhere);

		CqnSelect subquery = SelectBuilder.from(subset).columns(NODE_ID).distinct();

		applyFilter(() -> InSubquery.in(CQL.get(NODE_ID), subquery));
	}
}