From c3f1034c9650fa69257c41f667f172562624361e Mon Sep 17 00:00:00 2001 From: Clemens Portele Date: Mon, 19 Oct 2026 19:24:39 +0200 Subject: [PATCH] fix parameters and numbers as arguments of CQL2 functions in stored queries A stored query with a parameter as the argument of a function, e.g. a custom function S_DWITHIN(geometry, $point, $distance), failed with an SQL error, because the parameter was never replaced by its value: the copying visitor returned functions unchanged instead of rebuilding them from the visited arguments. All visitors derived from CqlVisitorCopy now also reach into the arguments of functions. Numeric and boolean literals as arguments of a function were written to CQL2 JSON as strings, so a filter that is written and read again, as stored and ad-hoc queries are, failed the type check of a function that expects a number. The literals are now written with their type. --- .../cql/domain/CqlVisitorCopy.java | 4 +- .../ii/xtraplatform/cql/domain/Function.java | 6 +- .../cql/app/CqlFunctionArgumentsSpec.groovy | 91 +++++++++++++++++++ 3 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 xtraplatform-cql/src/test/groovy/de/ii/xtraplatform/cql/app/CqlFunctionArgumentsSpec.groovy diff --git a/xtraplatform-cql/src/main/java/de/ii/xtraplatform/cql/domain/CqlVisitorCopy.java b/xtraplatform-cql/src/main/java/de/ii/xtraplatform/cql/domain/CqlVisitorCopy.java index 4c27154bd..3a5aa5d75 100644 --- a/xtraplatform-cql/src/main/java/de/ii/xtraplatform/cql/domain/CqlVisitorCopy.java +++ b/xtraplatform-cql/src/main/java/de/ii/xtraplatform/cql/domain/CqlVisitorCopy.java @@ -225,7 +225,9 @@ public CqlNode visit(Bbox bbox, List children) { @Override public CqlNode visit(Function function, List children) { - return function; + return Function.of( + function.getName(), + children.stream().map(Operand.class::cast).collect(Collectors.toList())); } @Override diff --git a/xtraplatform-cql/src/main/java/de/ii/xtraplatform/cql/domain/Function.java b/xtraplatform-cql/src/main/java/de/ii/xtraplatform/cql/domain/Function.java index 81a43ac79..dbf38d6e1 100644 --- a/xtraplatform-cql/src/main/java/de/ii/xtraplatform/cql/domain/Function.java +++ b/xtraplatform-cql/src/main/java/de/ii/xtraplatform/cql/domain/Function.java @@ -61,11 +61,7 @@ public void serialize(Function value, JsonGenerator gen, SerializerProvider seri gen.writeFieldName("args"); gen.writeStartArray(); for (Operand operand : value.getArgs()) { - if (operand instanceof ScalarLiteral) { - gen.writeString(String.format("%s", ((ScalarLiteral) operand).getValue().toString())); - } else { - gen.writeObject(operand); - } + gen.writeObject(operand); } gen.writeEndArray(); gen.writeEndObject(); diff --git a/xtraplatform-cql/src/test/groovy/de/ii/xtraplatform/cql/app/CqlFunctionArgumentsSpec.groovy b/xtraplatform-cql/src/test/groovy/de/ii/xtraplatform/cql/app/CqlFunctionArgumentsSpec.groovy new file mode 100644 index 000000000..d82d5d6bc --- /dev/null +++ b/xtraplatform-cql/src/test/groovy/de/ii/xtraplatform/cql/app/CqlFunctionArgumentsSpec.groovy @@ -0,0 +1,91 @@ +/* + * Copyright 2026 interactive instruments GmbH + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package de.ii.xtraplatform.cql.app + +import de.ii.xtraplatform.cql.domain.Cql +import de.ii.xtraplatform.cql.domain.Cql2Expression +import de.ii.xtraplatform.cql.domain.CqlNode +import de.ii.xtraplatform.cql.domain.CqlVisitorCopy +import de.ii.xtraplatform.cql.domain.Eq +import de.ii.xtraplatform.cql.domain.Function +import de.ii.xtraplatform.cql.domain.Parameter +import de.ii.xtraplatform.cql.domain.Property +import de.ii.xtraplatform.cql.domain.ScalarLiteral +import de.ii.xtraplatform.jsonschema.domain.ImmutableJsonSchemaNumber +import spock.lang.Shared +import spock.lang.Specification + +class CqlFunctionArgumentsSpec extends Specification { + + @Shared + Cql cql + + def setupSpec() { + cql = new CqlImpl() + } + + def 'numeric arguments of a function in CQL2 JSON are numbers'() { + + given: + String cqlJson = """ + { + "op": "=", + "args": [ + {"function": {"name": "dwithin", "args": [{"property": "geometrie"}, ${literal}]}}, + true + ] + } + """ + + when: + Cql2Expression actual = cql.read(cqlJson, Cql.Format.JSON) + def argument = ((Function) ((Eq) actual).getArgs().get(0)).getArgs().get(1) + + then: + argument == expected + + where: + literal || expected + "300" || ScalarLiteral.of(300) + "300.5" || ScalarLiteral.of(300.5d) + } + + def 'literal arguments of a function keep their type when written to and read from CQL2 JSON'() { + + given: + def filter = Eq.ofFunction(Function.of("dwithin", [Property.of("geometrie"), literal]), ScalarLiteral.of(true)) + + when: + Cql2Expression actual = cql.read(cql.write(filter, Cql.Format.JSON), Cql.Format.JSON) + + then: + actual == filter + + where: + literal << [ScalarLiteral.of(300), ScalarLiteral.of(300.5d), ScalarLiteral.of("300")] + } + + def 'a copying visitor replaces parameters in the arguments of a function'() { + + given: + def parameter = Parameter.of("afstand", new ImmutableJsonSchemaNumber.Builder().build()) + def filter = Eq.ofFunction(Function.of("dwithin", [Property.of("geometrie"), parameter]), ScalarLiteral.of(true)) + def resolver = new CqlVisitorCopy() { + @Override + CqlNode visit(Parameter p, List children) { + return ScalarLiteral.of(300) + } + } + + when: + def actual = filter.accept(resolver) + + then: + actual == Eq.ofFunction(Function.of("dwithin", [Property.of("geometrie"), ScalarLiteral.of(300)]), ScalarLiteral.of(true)) + } +}