imbajin commented on code in PR #3370: URL: https://github.com/apache/hugegraph/pull/3370#discussion_r4305155194
########## hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherTemporalFunctions.java: ########## @@ -0,0 +1,330 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.opencypher; + +import static org.slf4j.LoggerFactory.getLogger; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.opencypher.gremlin.translation.CypherAst; +import org.opencypher.v9_0.expressions.FunctionInvocation; +import org.opencypher.v9_0.expressions.Parameter; +import org.opencypher.v9_0.expressions.StringLiteral; +import org.opencypher.v9_0.util.InputPosition; +import org.opencypher.v9_0.util.symbols.AnyType; +import org.slf4j.Logger; + +import scala.Option; +import scala.Product; +import scala.Some; +import scala.Tuple2; +import scala.collection.JavaConverters; +import scala.collection.Seq; + +/** + * Rewrites the Cypher temporal constructor functions that the cypher-for-gremlin + * translator does not implement into parameters bound to native date values. + * <p> + * The translator's function list is fixed inside its own dependency, so the + * supported constructors (date, datetime, localdatetime) are folded into the + * parsed statement right before translation: each call with a foldable argument + * becomes a synthetic bound parameter and the date value is added to the + * bindings. Calls that cannot be folded are left in place and keep failing + * with the translator's original error, as do all other temporal functions. + */ +public final class CypherTemporalFunctions { + + private static final Logger logger = getLogger(CypherTemporalFunctions.class); + + private static final String DATE = "date"; + private static final String DATETIME = "datetime"; + private static final String LOCALDATETIME = "localdatetime"; + + // Two leading spaces: parameter names coming from a Cypher query can + // never collide with this prefix, like the translator's own reserved names. + private static final String SYNTHETIC_PREFIX = " cypher.fold."; + + private CypherTemporalFunctions() { + } + + /** + * Folds the supported temporal constructors in the parsed statement and + * returns the effective request parameters for translation: the same + * instance when nothing was folded, a copy carrying the synthetic + * bindings otherwise. The parsed statement is only replaced when the + * rewrite completed without errors, so a fold failure leaves the query + * exactly as it was parsed. + */ + public static Map<String, Object> fold(CypherAst ast, Map<String, Object> parameters) { + TemporalFold fold = new TemporalFold(parameters); + try { + Field statementField = CypherAst.class.getDeclaredField("statement"); + Field parametersField = CypherAst.class.getDeclaredField("parameters"); + statementField.setAccessible(true); + parametersField.setAccessible(true); + + Object folded = fold.copy(ast.statement()); + if (fold.bindings.isEmpty()) { + return parameters; + } + Object merged = mergeParameters(parametersField.get(ast), fold.bindings); + statementField.set(ast, folded); + parametersField.set(ast, merged); + } catch (Exception | LinkageError e) { + logger.warn("Failed to fold temporal functions, keeping the parsed statement", e); + return parameters; + } + Map<String, Object> effective = new LinkedHashMap<>(parameters); + effective.putAll(fold.bindings); + return effective; + } + + @SuppressWarnings("unchecked") + private static Object mergeParameters(Object parameters, Map<String, Object> bindings) { + scala.collection.immutable.Map<String, Object> merged = + (scala.collection.immutable.Map<String, Object>) parameters; + for (Map.Entry<String, Object> binding : bindings.entrySet()) { + merged = merged.$plus(new Tuple2<>(binding.getKey(), binding.getValue())); + } + return merged; + } + + private static final class TemporalFold { + + private final Map<String, Object> parameters; + private final Map<String, Object> bindings = new LinkedHashMap<>(); + private int counter = 0; + + private TemporalFold(Map<String, Object> parameters) { + this.parameters = parameters; + } + + /** + * Returns a copy of the node with temporal constructors folded in, or + * the same instance when the subtree does not change. Subtrees that + * cannot be rebuilt are shared with the original statement, so the + * copy stays minimal. + */ + private Object copy(Object node) throws Exception { + if (node == null || node instanceof String || node instanceof Number || + node instanceof Boolean || node instanceof Character) { + return node; + } + if (node instanceof FunctionInvocation) { + Object folded = foldCall((FunctionInvocation) node); + if (folded != null) { + return folded; + } + } + if (node instanceof Option) { + Option<?> option = (Option<?>) node; + if (!option.isEmpty()) { + Object value = option.get(); + Object copied = copy(value); + if (copied != value) { + return new Some<>(copied); + } + } + return node; + } + if (node instanceof scala.collection.IndexedSeq) { + return copyIndexedSeq((scala.collection.IndexedSeq<?>) node); + } + if (node instanceof Seq) { + return copySeq((Seq<?>) node); + } + if (node instanceof scala.collection.immutable.Set) { + return copySet((scala.collection.immutable.Set<?>) node); + } + if (node instanceof Tuple2) { + Tuple2<?, ?> tuple = (Tuple2<?, ?>) node; + Object first = copy(tuple._1()); + Object second = copy(tuple._2()); + return first == tuple._1() && second == tuple._2() ? node : new Tuple2<>(first, second); + } + if (node instanceof Product) { + return rebuild((Product) node); + } + return node; + } + + private Object foldCall(FunctionInvocation call) { + String name = call.name(); + if (!DATE.equals(name) && !DATETIME.equals(name) && !LOCALDATETIME.equals(name)) { + return null; + } + if (call.distinct() || !call.namespace().parts().isEmpty()) { + return null; + } + Seq<?> arguments = call.args(); + Date value; + if (arguments.isEmpty()) { + value = currentValue(name); + } else if (arguments.size() == 1) { + String literal = literalOf(arguments.head()); + if (literal == null) { + return null; + } + value = parse(name, literal); + if (value == null) { + return null; + } + } else { + return null; + } + String binding = SYNTHETIC_PREFIX + name + '.' + counter++; + this.bindings.put(binding, value); + return new Parameter(binding, AnyType.instance(), call.position()); Review Comment: [P2] Avoid overwriting existing escaped parameter names The two-space prefix is expressible using a backtick-escaped Cypher parameter: ```cypher RETURN date('2024-02-05') AS d, $` cypher.fold.date.0` AS other ``` Parameters: ```json {" cypher.fold.date.0":"original"} ``` The actual parser accepts the query, but folding replaces the existing binding with the constructed Date, so both translated output bindings use that Date instead of preserving `original` for `other`. The same parameter without a temporal constructor remains unchanged. `CypherOpProcessor` accepts string keys and `ParameterNormalizer` preserves this name. Generate synthetic names by checking request parameters and accumulated synthetic bindings and skipping occupied names; add literal-constructor and parameter-constructor collision regressions. Evidence: independently reproduced by two reviewers on the actual parser/fold/translation/binding paths at [these lines](https://github.com/apache/hugegraph/blob/c983b270f4a633207ebd4bd08e160106c1a8b8bc/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherTemporalFunctions.java#L200-L202). No live database execution is claimed. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
