MergeOptionalIntoPrecedingGraphTransform.java

/*******************************************************************************
 * Copyright (c) 2025 Eclipse RDF4J contributors.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Distribution License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 *******************************************************************************/
package org.eclipse.rdf4j.queryrender.sparql.ir.util.transform;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.rdf4j.queryrender.sparql.ir.IrBGP;
import org.eclipse.rdf4j.queryrender.sparql.ir.IrFilter;
import org.eclipse.rdf4j.queryrender.sparql.ir.IrGraph;
import org.eclipse.rdf4j.queryrender.sparql.ir.IrMinus;
import org.eclipse.rdf4j.queryrender.sparql.ir.IrNode;
import org.eclipse.rdf4j.queryrender.sparql.ir.IrOptional;
import org.eclipse.rdf4j.queryrender.sparql.ir.IrPathTriple;
import org.eclipse.rdf4j.queryrender.sparql.ir.IrService;
import org.eclipse.rdf4j.queryrender.sparql.ir.IrStatementPattern;
import org.eclipse.rdf4j.queryrender.sparql.ir.IrSubSelect;
import org.eclipse.rdf4j.queryrender.sparql.ir.IrUnion;

/**
 * Merge a simple OPTIONAL body that explicitly targets the same GRAPH as the preceding GRAPH block into that block,
 * i.e.,
 *
 * GRAPH ?g { ... } OPTIONAL { GRAPH ?g { simple } }
 *
 * ��� GRAPH ?g { ... OPTIONAL { simple } }
 *
 * Only applies to "simple" OPTIONAL bodies to avoid changing intended scoping or reordering more complex shapes.
 */
public final class MergeOptionalIntoPrecedingGraphTransform extends BaseTransform {
	private MergeOptionalIntoPrecedingGraphTransform() {
	}

	public static IrBGP apply(IrBGP bgp) {
		if (bgp == null) {
			return null;
		}
		final List<IrNode> in = bgp.getLines();
		final List<IrNode> out = new ArrayList<>();
		for (int i = 0; i < in.size(); i++) {
			IrNode n = in.get(i);
			if (n instanceof IrGraph && i + 1 < in.size() && in.get(i + 1) instanceof IrOptional) {
				IrGraph g = (IrGraph) n;
				// Only merge when the preceding GRAPH has a single simple line. This preserves cases where the
				// original query intentionally kept OPTIONAL outside the GRAPH that already groups multiple lines.
				final IrBGP gInner = g.getWhere();
				if (gInner == null || gInner.getLines().size() != 1) {
					// do not merge; keep original placement
					out.add(n);
					continue;
				}
				IrOptional opt = (IrOptional) in.get(i + 1);
				IrBGP ow = opt.getWhere();
				IrBGP simpleOw = null;
				// Only merge when OPTIONAL body explicitly targets the same GRAPH context. Do not merge a plain
				// OPTIONAL body without an explicit GRAPH wrapper; keep it outside to match original structure.
				if (ow != null && ow.getLines().size() == 1 && ow.getLines().get(0) instanceof IrGraph) {
					// Handle OPTIONAL { GRAPH ?g { simple } } ��� OPTIONAL { simple } when graph matches
					IrGraph inner = (IrGraph) ow.getLines().get(0);
					if (sameVarOrValue(g.getGraph(), inner.getGraph()) && isSimpleOptionalBody(inner.getWhere())) {
						simpleOw = inner.getWhere();
					}
				} else if (ow != null && !ow.getLines().isEmpty()) {
					// Handle OPTIONAL bodies that contain exactly one GRAPH ?g { simple } plus one or more FILTER
					// lines.
					// Merge into the preceding GRAPH and keep the FILTER(s) inside the OPTIONAL block.
					IrGraph innerGraph = null;
					final List<IrFilter> filters = new ArrayList<>();
					boolean ok = true;
					for (IrNode ln : ow.getLines()) {
						if (ln instanceof IrGraph) {
							if (innerGraph != null) {
								ok = false; // more than one graph inside OPTIONAL -> bail
								break;
							}
							innerGraph = (IrGraph) ln;
							if (!sameVarOrValue(g.getGraph(), innerGraph.getGraph())) {
								ok = false;
								break;
							}
							continue;
						}
						if (ln instanceof IrFilter) {
							filters.add((IrFilter) ln);
							continue;
						}
						ok = false; // unexpected node type inside OPTIONAL body
						break;
					}
					if (ok && innerGraph != null && isSimpleOptionalBody(innerGraph.getWhere())) {
						IrBGP body = new IrBGP(bgp.isNewScope());
						// simple triples/paths first, then original FILTER lines
						for (IrNode gln : innerGraph.getWhere().getLines()) {
							body.add(gln);
						}
						for (IrFilter fl : filters) {
							body.add(fl);
						}
						simpleOw = body;
					}
				}
				if (simpleOw != null) {
					// Build merged graph body
					IrBGP merged = new IrBGP(bgp.isNewScope());
					for (IrNode gl : g.getWhere().getLines()) {
						merged.add(gl);
					}
					IrOptional no = new IrOptional(simpleOw, opt.isNewScope());
					no.setNewScope(opt.isNewScope());
					merged.add(no);
					// Debug marker (harmless): indicate we applied the merge
					// System.out.println("# IrTransforms: merged OPTIONAL into preceding GRAPH");
					out.add(new IrGraph(g.getGraph(), merged, g.isNewScope()));
					i += 1;
					continue;
				}
			}
			// Recurse into containers
			if (n instanceof IrBGP || n instanceof IrGraph || n instanceof IrOptional || n instanceof IrUnion
					|| n instanceof IrMinus || n instanceof IrService || n instanceof IrSubSelect) {
				n = n.transformChildren(child -> {
					if (child instanceof IrBGP) {
						return MergeOptionalIntoPrecedingGraphTransform.apply((IrBGP) child);
					}
					return child;
				});
			}
			out.add(n);
		}
		return BaseTransform.bgpWithLines(bgp, out);
	}

	public static boolean isSimpleOptionalBody(IrBGP ow) {
		if (ow == null) {
			return false;
		}
		if (ow.getLines().isEmpty()) {
			return false;
		}
		for (IrNode ln : ow.getLines()) {
			if (!(ln instanceof IrStatementPattern || ln instanceof IrPathTriple)) {
				return false;
			}
		}
		return true;
	}

}