Repository: squashql/squashql
Pull request #277 (2024-02-20 10:51:33)

Add ParametrizedMeasure


Patch:
diff --git a/.gitignore b/.gitignore
index 5de0105..5a11c4b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -36,13 +36,7 @@ spark-warehouse
 node_modules
 
 # specific to the project
-build-from-querydto.json
-build-from-query.json
-build-from-query-merge.json
-build-from-query-pivot.json,
-build-from-query-merge-pivot.json
-build-from-query-join.json
-build-from-query-result.json
-build-from-pivot-query-result.json
+js/typescript-library/json
 js/typescript-library/dist
 js/client-typescript-library/build
+server/src/test/resources/json
diff --git a/core/src/main/java/io/squashql/jackson/JacksonUtil.java b/core/src/main/java/io/squashql/jackson/JacksonUtil.java
index 253e132..52d9747 100644
--- a/core/src/main/java/io/squashql/jackson/JacksonUtil.java
+++ b/core/src/main/java/io/squashql/jackson/JacksonUtil.java
@@ -7,6 +7,9 @@ import com.fasterxml.jackson.datatype.jsr310.JSR310Module;
 import io.squashql.query.Field;
 import io.squashql.query.dto.QueryDto;
 
+import java.util.LinkedHashMap;
+import java.util.Map;
+
 public class JacksonUtil {
 
   public static final ObjectMapper OBJECT_MAPPER;
@@ -16,6 +19,8 @@ public class JacksonUtil {
     var simpleModule = new SimpleModule();
     simpleModule.addKeyDeserializer(Field.class, new QueryDto.KeyFieldDeserializer());
     simpleModule.addKeySerializer(Field.class, new QueryDto.KeyFieldSerializer());
+    // Order can matter. For instance when we use Jackson to create a copy of QueryDto (cf. Pivot Table)
+    simpleModule.addAbstractTypeMapping(Map.class, LinkedHashMap.class);
     OBJECT_MAPPER.registerModule(simpleModule);
     OBJECT_MAPPER.registerModule(new JSR310Module());
   }
diff --git a/core/src/main/java/io/squashql/jackson/ParametrizedMeasureDeserializer.java b/core/src/main/java/io/squashql/jackson/ParametrizedMeasureDeserializer.java
new file mode 100644
index 0000000..ced7115
--- /dev/null
+++ b/core/src/main/java/io/squashql/jackson/ParametrizedMeasureDeserializer.java
@@ -0,0 +1,37 @@
+package io.squashql.jackson;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.*;
+import io.squashql.query.measure.ParametrizedMeasure;
+import io.squashql.query.measure.Repository;
+import lombok.NoArgsConstructor;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+@NoArgsConstructor
+public class ParametrizedMeasureDeserializer extends JsonDeserializer<ParametrizedMeasure> {
+
+  @Override
+  public ParametrizedMeasure deserialize(JsonParser p, DeserializationContext dc) throws IOException {
+    JsonNode node = p.getCodec().readTree(p);
+
+    Map<String, JavaType> parameterTypes = new HashMap<>();
+    String key = node.get("key").asText();
+    Repository.getParameterTypes(key).forEach(pt -> parameterTypes.put(pt.getOne(), pt.getTwo()));
+
+    Map<String, Object> parameters = new HashMap<>();
+    for (Map.Entry<String, JsonNode> property : node.get("parameters").properties()) {
+      String propertyKey = property.getKey();
+      JavaType javaType = parameterTypes.get(propertyKey);
+      if (javaType == null) {
+        throw new IllegalArgumentException("Incorrect key " + propertyKey + ". Expected keys are: " + parameterTypes.keySet());
+      }
+      parameters.put(propertyKey, JacksonUtil.OBJECT_MAPPER.readerFor(javaType).readValue(property.getValue()));
+    }
+
+    String alias = node.get("alias").asText();
+    return new ParametrizedMeasure(alias, key, parameters);
+  }
+}
diff --git a/core/src/main/java/io/squashql/jackson/SquashQLTypeFactory.java b/core/src/main/java/io/squashql/jackson/SquashQLTypeFactory.java
new file mode 100644
index 0000000..05b2ac3
--- /dev/null
+++ b/core/src/main/java/io/squashql/jackson/SquashQLTypeFactory.java
@@ -0,0 +1,17 @@
+package io.squashql.jackson;
+
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.type.TypeFactory;
+
+import java.util.List;
+
+public class SquashQLTypeFactory {
+
+  public static JavaType listOf(Class<?> clazz) {
+    return TypeFactory.defaultInstance().constructCollectionType(List.class, clazz);
+  }
+
+  public static JavaType of(Class<?> clazz) {
+    return TypeFactory.defaultInstance().constructType(clazz);
+  }
+}
diff --git a/core/src/main/java/io/squashql/query/QueryResolver.java b/core/src/main/java/io/squashql/query/QueryResolver.java
index bec32a4..e054d16 100644
--- a/core/src/main/java/io/squashql/query/QueryResolver.java
+++ b/core/src/main/java/io/squashql/query/QueryResolver.java
@@ -4,6 +4,8 @@ import io.squashql.query.compiled.*;
 import io.squashql.query.database.DatabaseQuery;
 import io.squashql.query.dto.*;
 import io.squashql.query.exception.FieldNotFoundException;
+import io.squashql.query.measure.ParametrizedMeasure;
+import io.squashql.query.measure.Repository;
 import io.squashql.store.Store;
 import io.squashql.type.*;
 import lombok.Data;
@@ -314,6 +316,8 @@ public class QueryResolver {
         compiledMeasure = compileVectorAggMeasure(v);
       } else if (m instanceof VectorTupleAggMeasure v) {
         compiledMeasure = compileVectorTupleAggMeasure(v);
+      } else if (m instanceof ParametrizedMeasure pm) {
+        compiledMeasure = compileMeasure(Repository.create(pm), topMeasure);
       } else {
         throw new IllegalArgumentException("Unknown type of measure " + m.getClass().getSimpleName());
       }
diff --git a/core/src/main/java/io/squashql/query/VectorTupleAggMeasure.java b/core/src/main/java/io/squashql/query/VectorTupleAggMeasure.java
index 5f170c8..8d2b823 100644
--- a/core/src/main/java/io/squashql/query/VectorTupleAggMeasure.java
+++ b/core/src/main/java/io/squashql/query/VectorTupleAggMeasure.java
@@ -17,6 +17,9 @@ public class VectorTupleAggMeasure implements Measure {
   public String alias;
   public List<FieldAndAggFunc> fieldToAggregateAndAggFunc;
   public Field vectorAxis;
+  /**
+   * Internal attribute that is not supposed to be exposed.
+   */
   public Function<List<Object>, Object> transformer;
   @With
   public String expression;
diff --git a/core/src/main/java/io/squashql/query/measure/ParametrizedMeasure.java b/core/src/main/java/io/squashql/query/measure/ParametrizedMeasure.java
new file mode 100644
index 0000000..8fce624
--- /dev/null
+++ b/core/src/main/java/io/squashql/query/measure/ParametrizedMeasure.java
@@ -0,0 +1,40 @@
+package io.squashql.query.measure;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import io.squashql.jackson.ParametrizedMeasureDeserializer;
+import io.squashql.query.Measure;
+import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.ToString;
+
+import java.util.Map;
+
+@ToString
+@EqualsAndHashCode
+@NoArgsConstructor // For Jackson
+@AllArgsConstructor
+@JsonDeserialize(using = ParametrizedMeasureDeserializer.class)
+public class ParametrizedMeasure implements Measure {
+
+  public String alias;
+
+  public String key;
+
+  public Map<String, Object> parameters;
+
+  @Override
+  public String alias() {
+    return this.alias;
+  }
+
+  @Override
+  public String expression() {
+    return this.key;
+  }
+
+  @Override
+  public Measure withExpression(String __) {
+    return this;
+  }
+}
diff --git a/core/src/main/java/io/squashql/query/measure/ParametrizedMeasureFactory.java b/core/src/main/java/io/squashql/query/measure/ParametrizedMeasureFactory.java
new file mode 100644
index 0000000..ffc8a30
--- /dev/null
+++ b/core/src/main/java/io/squashql/query/measure/ParametrizedMeasureFactory.java
@@ -0,0 +1,82 @@
+package io.squashql.query.measure;
+
+import io.squashql.list.Lists;
+import io.squashql.query.*;
+import io.squashql.util.MultipleColumnsSorter;
+import org.eclipse.collections.api.tuple.Pair;
+import org.eclipse.collections.impl.tuple.Tuples;
+
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import static io.squashql.query.agg.AggregationFunction.ANY_VALUE;
+import static io.squashql.query.agg.AggregationFunction.SUM;
+import static io.squashql.util.ListUtils.reorder;
+import static java.util.Comparator.naturalOrder;
+
+public final class ParametrizedMeasureFactory {
+
+  private ParametrizedMeasureFactory() {
+  }
+
+  /**
+   * Called by reflection.
+   */
+  public static Measure var(String alias, Field value, Field date, double quantile) {
+    Function<List<Object>, Object> transformer = (tuple) -> {
+      Lists.DoubleList prices = (Lists.DoubleList) tuple.get(0);
+      Lists.LocalDateList dates = (Lists.LocalDateList) tuple.get(1);
+      int[] sort = MultipleColumnsSorter.sort(Collections.singletonList(dates), Collections.singletonList(naturalOrder()), new int[0]);
+
+      List<Double> orderedPrices = reorder(prices, sort);
+      List<LocalDate> orderedDates = reorder(dates, sort);
+      var index = (int) Math.floor(orderedPrices.size() * (1 - quantile));
+      var quantileDate = orderedDates.get(index);
+      var quantilePnL = orderedPrices.get(index);
+
+      return List.of(quantileDate, quantilePnL);
+    };
+    return new VectorTupleAggMeasure(alias, List.of(new FieldAndAggFunc(value, SUM), new FieldAndAggFunc(date, ANY_VALUE)), date, transformer);
+  }
+
+  /**
+   * Called by reflection.
+   */
+  public static Measure incrementalVar(String alias, Field value, Field date, double quantile, List<Field> ancestors) {
+    Measure vector = new VectorTupleAggMeasure("__vector__",
+            List.of(new FieldAndAggFunc(value, SUM),
+                    new FieldAndAggFunc(date, ANY_VALUE)),
+            date,
+            null);
+
+    BiFunction<Object, Object, Object> comparisonOperator = (currentValue, parentValue) -> {
+      List<Double> current = orderTupleOfList(currentValue).getOne();
+      List<Double> parent = orderTupleOfList(parentValue).getOne();
+
+      int size = parent.size();
+      var index = (int) Math.floor(size * (1 - quantile));
+      var varParentWithCurrent = parent.get(index);
+      List<Double> minus = new ArrayList<>(size);
+      for (int i = 0; i < size; i++) {
+        minus.add(parent.get(0) - current.get(0));
+      }
+
+      Collections.sort(minus);
+      var varParentWithoutCurrent = minus.get(index);
+
+      return varParentWithCurrent - varParentWithoutCurrent;
+    };
+    return new ComparisonMeasureReferencePosition(alias, comparisonOperator, vector, ancestors);
+  }
+
+  private static Pair<List<Double>, List<LocalDate>> orderTupleOfList(Object value) {
+    Lists.DoubleList prices = (Lists.DoubleList) ((List) value).get(0);
+    Lists.LocalDateList dates = (Lists.LocalDateList) ((List) value).get(1);
+    int[] sort = MultipleColumnsSorter.sort(Collections.singletonList(dates), Collections.singletonList(naturalOrder()), new int[0]);
+    return Tuples.pair(reorder(prices, sort), reorder(dates, sort));
+  }
+}
diff --git a/core/src/main/java/io/squashql/query/measure/Repository.java b/core/src/main/java/io/squashql/query/measure/Repository.java
new file mode 100644
index 0000000..8cf09da
--- /dev/null
+++ b/core/src/main/java/io/squashql/query/measure/Repository.java
@@ -0,0 +1,58 @@
+package io.squashql.query.measure;
+
+import com.fasterxml.jackson.databind.JavaType;
+import io.squashql.jackson.SquashQLTypeFactory;
+import io.squashql.query.Field;
+import io.squashql.query.Measure;
+import org.eclipse.collections.api.tuple.Pair;
+import org.eclipse.collections.impl.tuple.Tuples;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public final class Repository {
+
+  public static final String VAR = "VAR";
+  public static final String INCREMENTAL_VAR = "INCREMENTAL_VAR";
+
+  private Repository() {
+  }
+
+  public static Measure create(ParametrizedMeasure m) {
+    String methodName = switch (m.key) {
+      case VAR -> "var";
+      case INCREMENTAL_VAR -> "incrementalVar";
+      default -> throw new IllegalArgumentException("unknown " + ParametrizedMeasure.class + ": " + m);
+    };
+
+    try {
+      Method var = Arrays.stream(ParametrizedMeasureFactory.class.getDeclaredMethods()).filter(method -> method.getName().equals(methodName)).findFirst().get();
+      List<String> parameterNames = getParameterTypes(m.key).stream().map(Pair::getOne).toList();
+      List<Object> args = new ArrayList<>();
+      args.add(m.alias);
+      for (String paramName : parameterNames) {
+        args.add(m.parameters.get(paramName));
+      }
+      return (Measure) var.invoke(null, args.toArray(new Object[0]));
+    } catch (IllegalAccessException |
+             InvocationTargetException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static List<Pair<String, JavaType>> getParameterTypes(String key) {
+    JavaType fieldJT = SquashQLTypeFactory.of(Field.class);
+    JavaType doubleJT = SquashQLTypeFactory.of(double.class);
+    JavaType listOfFieldsJT = SquashQLTypeFactory.listOf(Field.class);
+    if (key.equals(VAR)) {
+      return List.of(Tuples.pair("value", fieldJT), Tuples.pair("date", fieldJT), Tuples.pair("quantile", doubleJT));
+    } else if (key.equals(INCREMENTAL_VAR)) {
+      return List.of(Tuples.pair("value", fieldJT), Tuples.pair("date", fieldJT), Tuples.pair("quantile", doubleJT), Tuples.pair("ancestors", listOfFieldsJT));
+    } else {
+      throw new IllegalArgumentException("unknown key: " + key);
+    }
+  }
+}
diff --git a/js/typescript-library/build-from-query-pivot.json b/js/typescript-library/build-from-query-pivot.json
deleted file mode 100644
index 57eda13..0000000
--- a/js/typescript-library/build-from-query-pivot.json
+++ /dev/null
@@ -1,1 +0,0 @@
-{"query":{"table":{"name":"myTable","joins":[]},"virtualTableDtos":[],"columns":[{"@class":"io.squashql.query.TableField","fullName":"a","fieldName":"a"},{"@class":"io.squashql.query.TableField","fullName":"b","fieldName":"b"}],"rollupColumns":[],"columnSets":{},"parameters":{},"measures":[{"@class":"io.squashql.query.AggregatedMeasure","field":{"@class":"io.squashql.query.TableField","fullName":"f1","fieldName":"f1"},"aggregationFunction":"avg","alias":"sum"}],"orders":{},"limit":-1,"minify":true},"rows":[{"@class":"io.squashql.query.TableField","fullName":"a","fieldName":"a"}],"columns":[{"@class":"io.squashql.query.TableField","fullName":"b","fieldName":"b"}]}
\ No newline at end of file
diff --git a/js/typescript-library/package-lock.json b/js/typescript-library/package-lock.json
index 3b2251b..8917bb2 100644
--- a/js/typescript-library/package-lock.json
+++ b/js/typescript-library/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "@squashql/squashql-js",
-  "version": "1.11.0",
+  "version": "1.12.0",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "@squashql/squashql-js",
-      "version": "1.11.0",
+      "version": "1.12.0",
       "license": "Apache-2.0",
       "dependencies": {
         "axios": "^1.6.7"
diff --git a/js/typescript-library/pom.xml b/js/typescript-library/pom.xml
index bbe3999..7cdf496 100644
--- a/js/typescript-library/pom.xml
+++ b/js/typescript-library/pom.xml
@@ -86,11 +86,25 @@
         <version>3.1.0</version>
         <executions>
           <execution>
+            <id>clean old files</id>
+            <phase>clean</phase>
+            <configuration>
+              <target>
+                <delete dir="${basedir}/json"/>
+                <mkdir dir="${basedir}/json"/>
+              </target>
+            </configuration>
+            <goals>
+              <goal>run</goal>
+            </goals>
+          </execution>
+          <execution>
+            <id>copy new files</id>
             <phase>package</phase>
             <configuration>
               <target>
-                <copy todir="${basedir}/../../server/src/test/resources/">
-                  <fileset dir="${basedir}" includes="
+                <copy todir="${basedir}/../../server/src/test/resources/json/">
+                  <fileset dir="${basedir}/json" includes="
                       build-from-querydto.json,
                       build-from-query.json,
                       build-from-query-merge.json,
diff --git a/js/typescript-library/src/generate-json-query-join.ts b/js/typescript-library/src/generate-json-query-join.ts
index 3072a6b..9acce68 100644
--- a/js/typescript-library/src/generate-json-query-join.ts
+++ b/js/typescript-library/src/generate-json-query-join.ts
@@ -38,5 +38,5 @@ export function generateFromQueryJoin() {
           .limit(12)
   q.minify = true
   const data = JSON.stringify(q)
-  fs.writeFileSync('build-from-query-join.json', data)
+  fs.writeFileSync('json/build-from-query-join.json', data)
 }
diff --git a/js/typescript-library/src/generate-json-query-merge-pivot.ts b/js/typescript-library/src/generate-json-query-merge-pivot.ts
index 74048b9..1262d2b 100644
--- a/js/typescript-library/src/generate-json-query-merge-pivot.ts
+++ b/js/typescript-library/src/generate-json-query-merge-pivot.ts
@@ -20,5 +20,5 @@ export function generateFromQueryMergePivot() {
   q.minify = false
   const pivotQuery = createPivotTableQueryMerge(q, {rows: [tableField("a")], columns: [tableField("b")]})
   const data = JSON.stringify(pivotQuery)
-  fs.writeFileSync('build-from-query-merge-pivot.json', data)
+  fs.writeFileSync('json/build-from-query-merge-pivot.json', data)
 }
diff --git a/js/typescript-library/src/generate-json-query-merge.ts b/js/typescript-library/src/generate-json-query-merge.ts
index 8ccda0f..6c1e75c 100644
--- a/js/typescript-library/src/generate-json-query-merge.ts
+++ b/js/typescript-library/src/generate-json-query-merge.ts
@@ -22,5 +22,5 @@ export function generateFromQueryMerge() {
 
   const q = new QueryMerge(query1).join(query2, JoinType.LEFT).join(query3, JoinType.INNER).withParameter(new QueryCacheParameter(Action.NOT_USE))
   const data = JSON.stringify(q)
-  fs.writeFileSync('build-from-query-merge.json', data)
+  fs.writeFileSync('json/build-from-query-merge.json', data)
 }
diff --git a/js/typescript-library/src/generate-json-query-pivot.ts b/js/typescript-library/src/generate-json-query-pivot.ts
index 9e3916d..aab71e8 100644
--- a/js/typescript-library/src/generate-json-query-pivot.ts
+++ b/js/typescript-library/src/generate-json-query-pivot.ts
@@ -15,5 +15,5 @@ export function generateFromQueryPivot() {
   q.minify = true
   const pivotQuery = createPivotTableQuery(q, {rows: [tableField("a")], columns: [tableField("b")]})
   const data = JSON.stringify(pivotQuery)
-  fs.writeFileSync('build-from-query-pivot.json', data)
+  fs.writeFileSync('json/build-from-query-pivot.json', data)
 }
diff --git a/js/typescript-library/src/generate-json-query-results.ts b/js/typescript-library/src/generate-json-query-results.ts
index 022ea67..607bb70 100644
--- a/js/typescript-library/src/generate-json-query-results.ts
+++ b/js/typescript-library/src/generate-json-query-results.ts
@@ -19,7 +19,7 @@ function generateFromQueryResult() {
     }],
     debug: undefined,
   }
-  fs.writeFileSync('build-from-query-result.json', JSON.stringify(r))
+  fs.writeFileSync('json/build-from-query-result.json', JSON.stringify(r))
 }
 
 function generateFromPivotQueryResult() {
@@ -32,7 +32,7 @@ function generateFromPivotQueryResult() {
     columns: ["key3"],
     values: ["key4"]
   }
-  fs.writeFileSync('build-from-pivot-query-result.json', JSON.stringify(r))
+  fs.writeFileSync('json/build-from-pivot-query-result.json', JSON.stringify(r))
 }
 
 
diff --git a/js/typescript-library/src/generate-json-query.ts b/js/typescript-library/src/generate-json-query.ts
index e022bb6..983d908 100644
--- a/js/typescript-library/src/generate-json-query.ts
+++ b/js/typescript-library/src/generate-json-query.ts
@@ -56,5 +56,5 @@ export function generateFromQuery() {
   q.withParameter(new QueryCacheParameter(Action.NOT_USE))
 
   const data = JSON.stringify(q)
-  fs.writeFileSync('build-from-query.json', data)
+  fs.writeFileSync('json/build-from-query.json', data)
 }
diff --git a/js/typescript-library/src/generate-json-queryDto.ts b/js/typescript-library/src/generate-json-queryDto.ts
index f6f8b8a..565ccce 100644
--- a/js/typescript-library/src/generate-json-queryDto.ts
+++ b/js/typescript-library/src/generate-json-queryDto.ts
@@ -12,7 +12,7 @@ import {
   ComparisonMethod,
   decimal,
   ExpressionMeasure,
-  integer,
+  integer, ParametrizedMeasure,
   sum,
   totalCount,
 } from "./measure"
@@ -79,6 +79,17 @@ export function generateFromQueryDto() {
   q.withMeasure(comparisonMeasureWithParent("parent", ComparisonMethod.DIVIDE, price, [tableField("Year"), tableField("Month")]))
   q.withMeasure(comparisonMeasureWithGrandTotalAlongAncestors("grandTotalAlongAncestors", ComparisonMethod.DIVIDE, price, [tableField("Year"), tableField("Month")]))
   q.withMeasure(comparisonMeasureWithGrandTotal("grandTotal", ComparisonMethod.DIVIDE, price))
+  q.withMeasure(new ParametrizedMeasure("var measure", "VAR", {
+    "value": tableField("price"),
+    "date": tableField("date"),
+    "quantile": 0.95
+  }))
+  q.withMeasure(new ParametrizedMeasure("incr var measure", "INCREMENTAL_VAR", {
+    "value": tableField("price"),
+    "date": tableField("date"),
+    "quantile": 0.95,
+    "ancestors": [tableField("f1"), tableField("f2"), tableField("f3")],
+  }))
 
   const queryCondition = or(or(and(eq("a"), eq("b")), lt(5)), like("a%"))
   q.withWhereCriteria(all([
@@ -114,5 +125,5 @@ export function generateFromQueryDto() {
   q.onSubQuery(subQ)
 
   const data = JSON.stringify(q)
-  fs.writeFileSync('build-from-querydto.json', data)
+  fs.writeFileSync('json/build-from-querydto.json', data)
 }
diff --git a/js/typescript-library/src/index.ts b/js/typescript-library/src/index.ts
index b0e0445..d4fa7fc 100644
--- a/js/typescript-library/src/index.ts
+++ b/js/typescript-library/src/index.ts
@@ -11,7 +11,8 @@ export {
 } from './order'
 
 export {
-  Measure, BasicMeasure, AggregatedMeasure, ExpressionMeasure, sum, min, max, avg, count, countDistinct,
+  Measure, BasicMeasure, AggregatedMeasure, ExpressionMeasure, ParametrizedMeasure,
+  sum, min, max, avg, count, countDistinct,
   sumIf, minIf, maxIf, avgIf, countIf, countDistinctIf,
   plus, minus, multiply, divide,
   integer, decimal,
diff --git a/js/typescript-library/src/measure.ts b/js/typescript-library/src/measure.ts
index 59dbb34..6585fa4 100644
--- a/js/typescript-library/src/measure.ts
+++ b/js/typescript-library/src/measure.ts
@@ -131,6 +131,25 @@ export class ComparisonMeasureGrandTotal implements Measure {
   }
 }
 
+export class ParametrizedMeasure implements Measure {
+  readonly class: string = PACKAGE + "measure.ParametrizedMeasure"
+  readonly expression?: string
+
+  constructor(readonly alias: string,
+              readonly key: string,
+              readonly parameters: Record<string, any>) {
+  }
+
+  toJSON() {
+    return {
+      "@class": this.class,
+      "alias": this.alias,
+      "key": this.key,
+      "parameters": this.parameters,
+    }
+  }
+}
+
 export enum ComparisonMethod {
   ABSOLUTE_DIFFERENCE = "ABSOLUTE_DIFFERENCE",
   RELATIVE_DIFFERENCE = "RELATIVE_DIFFERENCE",
diff --git a/js/typescript-library/src/util.ts b/js/typescript-library/src/util.ts
index 7b43c4c..9cffd04 100644
--- a/js/typescript-library/src/util.ts
+++ b/js/typescript-library/src/util.ts
@@ -5,3 +5,11 @@ export function serializeMap(map: Map<any, any>): Map<string, any> {
   }
   return m
 }
+
+export function serializeRecord(r: Record<string, any>): Record<string, any> {
+  const m = new Map()
+  for (const [key, value] of Object.entries(r)) {
+    m.set(key, JSON.stringify(value))
+  }
+  return m
+}


Test patch:
diff --git a/core/src/test/java/io/squashql/query/ATestVectorOperation.java b/core/src/test/java/io/squashql/query/ATestVectorOperation.java
index afb7808..18b1734 100644
--- a/core/src/test/java/io/squashql/query/ATestVectorOperation.java
+++ b/core/src/test/java/io/squashql/query/ATestVectorOperation.java
@@ -7,6 +7,8 @@ import io.squashql.query.builder.Query;
 import io.squashql.query.compiled.CompiledExpressionMeasure;
 import io.squashql.query.database.SqlUtils;
 import io.squashql.query.dto.QueryDto;
+import io.squashql.query.measure.ParametrizedMeasure;
+import io.squashql.query.measure.Repository;
 import io.squashql.table.ColumnarTable;
 import io.squashql.table.Table;
 import io.squashql.type.TableTypedField;
@@ -24,6 +26,7 @@ import static io.squashql.query.agg.AggregationFunction.ANY_VALUE;
 import static io.squashql.query.agg.AggregationFunction.SUM;
 import static io.squashql.query.database.QueryEngine.GRAND_TOTAL;
 import static io.squashql.query.database.QueryEngine.TOTAL;
+import static io.squashql.query.measure.Repository.VAR;
 import static io.squashql.util.ListUtils.reorder;
 import static java.util.Comparator.naturalOrder;
 
@@ -37,7 +40,7 @@ public abstract class ATestVectorOperation extends ABaseTestQuery {
   static final String competitorZ = "Z";
   static final int day = 5;
   static final int month = 4;
-  final String storeName = "mystore" + getClass().getSimpleName().toLowerCase();
+  final String storeName = "mystore";// + getClass().getSimpleName().toLowerCase();
   final Field competitor = new TableField(this.storeName, "competitor");
   final Field ean = new TableField(this.storeName, "ean");
   final Field price = new TableField(this.storeName, "price");
@@ -82,6 +85,66 @@ public abstract class ATestVectorOperation extends ABaseTestQuery {
     assertVectorTuples(result, vector);
   }
 
+  @Test
+  void testVar() {
+    Measure var = new ParametrizedMeasure(
+            "var 95",
+            VAR,
+            Map.of(
+                    "value", this.price,
+                    "date", this.date,
+                    "quantile", 0.95
+            ));
+    QueryDto query = Query
+            .from(this.storeName)
+            .select(List.of(this.competitor, this.ean), List.of(var))
+            .rollup(List.of(this.competitor, this.ean))
+            .build();
+    Table result = this.executor.executeQuery(query);
+    Assertions.assertThat(result).containsExactly(
+            List.of(GRAND_TOTAL, GRAND_TOTAL, List.of(LocalDate.of(2023, 1, 1), 6d)),
+            List.of(competitorX, TOTAL, List.of(LocalDate.of(2023, 1, 1), 2d)),
+            List.of(competitorX, productA, List.of(LocalDate.of(2023, 1, 1), 1d)),
+            List.of(competitorX, productB, List.of(LocalDate.of(2023, 1, 1), 1d)),
+            List.of(competitorY, TOTAL, List.of(LocalDate.of(2023, 1, 1), 2d)),
+            List.of(competitorY, productA, List.of(LocalDate.of(2023, 1, 1), 1d)),
+            List.of(competitorY, productB, List.of(LocalDate.of(2023, 1, 1), 1d)),
+            List.of(competitorZ, TOTAL, List.of(LocalDate.of(2023, 1, 1), 2d)),
+            List.of(competitorZ, productA, List.of(LocalDate.of(2023, 1, 1), 1d)),
+            List.of(competitorZ, productB, List.of(LocalDate.of(2023, 1, 1), 1d)));
+  }
+
+  // FIXME this will not work if where clause because in case of inc. var, filters should not be cleared
+  @Test
+  void testIncrementalVar() {
+    List<Field> fields = List.of(this.competitor, this.ean);
+
+    Measure incrementalVar = new ParametrizedMeasure("inc var",
+            Repository.INCREMENTAL_VAR, Map.of(
+            "value", this.price,
+            "date", this.date,
+            "quantile", 0.95,
+            "ancestors", fields
+    ));
+    QueryDto query = Query
+            .from(this.storeName)
+            .select(fields, List.of(incrementalVar))
+            .rollup(fields)
+            .build();
+    Table result = this.executor.executeQuery(query);
+    Assertions.assertThat(result).containsExactly(
+            List.of(GRAND_TOTAL, GRAND_TOTAL, 6d),
+            List.of(competitorX, TOTAL, 2d),
+            List.of(competitorX, productA, 1d),
+            List.of(competitorX, productB, 1d),
+            List.of(competitorY, TOTAL, 2d),
+            List.of(competitorY, productA, 1d),
+            List.of(competitorY, productB, 1d),
+            List.of(competitorZ, TOTAL, 2d),
+            List.of(competitorZ, productA, 1d),
+            List.of(competitorZ, productB, 1d));
+  }
+
   @Test
   void testTotals() {
     Measure vector = new VectorTupleAggMeasure("vector", List.of(new FieldAndAggFunc(this.price, SUM), new FieldAndAggFunc(this.date, ANY_VALUE)), this.date, null);
diff --git a/server/src/test/java/io/squashql/js/TestJavascriptLibrary.java b/server/src/test/java/io/squashql/js/TestJavascriptLibrary.java
index 35efd55..42e5c22 100644
--- a/server/src/test/java/io/squashql/js/TestJavascriptLibrary.java
+++ b/server/src/test/java/io/squashql/js/TestJavascriptLibrary.java
@@ -4,6 +4,8 @@ import io.squashql.jackson.JacksonUtil;
 import io.squashql.query.*;
 import io.squashql.query.builder.Query;
 import io.squashql.query.dto.*;
+import io.squashql.query.measure.ParametrizedMeasure;
+import io.squashql.query.measure.Repository;
 import io.squashql.query.parameter.QueryCacheParameter;
 import io.squashql.util.TestUtil;
 import org.assertj.core.api.Assertions;
@@ -80,6 +82,17 @@ public class TestJavascriptLibrary {
     q.withMeasure(new ComparisonMeasureGrandTotal("grandTotal",
             ComparisonMethod.DIVIDE,
             price));
+    q.withMeasure(new ParametrizedMeasure("var measure", Repository.VAR, Map.of(
+            "value", tableField("price"),
+            "date", tableField("date"),
+            "quantile", 0.95
+    )));
+    q.withMeasure(new ParametrizedMeasure("incr var measure", Repository.INCREMENTAL_VAR, Map.of(
+            "value", tableField("price"),
+            "date", tableField("date"),
+            "quantile", 0.95,
+            "ancestors", tableFields(List.of("f1", "f2", "f3"))
+    )));
 
     var queryCondition = or(and(eq("a"), eq("b")), lt(5), like("a%"));
     q.withCondition(tableField("f1"), queryCondition);
@@ -105,7 +118,7 @@ public class TestJavascriptLibrary {
             .withMeasure(sum("sum_aa", "f"));
     q.table(subQuery);
 
-    String name = "build-from-querydto.json"; // The content of this file is generated by the js code.
+    String name = "json/build-from-querydto.json"; // The content of this file is generated by the js code.
     QueryDto qjs = JacksonUtil.deserialize(TestUtil.readAllLines(name), QueryDto.class);
     Assertions.assertThat(q.columnSets).isEqualTo(qjs.columnSets);
     Assertions.assertThat(q.columns).isEqualTo(qjs.columns);
@@ -119,7 +132,6 @@ public class TestJavascriptLibrary {
     Assertions.assertThat(q).isEqualTo(qjs);
   }
 
-
   /**
    * This is test is building the {@link QueryDto} <b>with</b> by using the query builder {@link Query}.
    */
@@ -158,7 +170,7 @@ public class TestJavascriptLibrary {
     q.withParameter(QueryCacheParameter.KEY, new QueryCacheParameter(QueryCacheParameter.Action.NOT_USE));
     q.minify = false;
 
-    String name = "build-from-query.json"; // The content of this file is generated by the js code.
+    String name = "json/build-from-query.json"; // The content of this file is generated by the js code.
     QueryDto qjs = JacksonUtil.deserialize(TestUtil.readAllLines(name), QueryDto.class);
     Assertions.assertThat(q.columnSets).isEqualTo(qjs.columnSets);
     Assertions.assertThat(q.columns).isEqualTo(qjs.columns);
@@ -193,7 +205,7 @@ public class TestJavascriptLibrary {
             .build();
 
     QueryMergeDto query = QueryMergeDto.from(q1).join(q2, JoinType.LEFT).join(q3, JoinType.INNER).withParameter(new QueryCacheParameter(QueryCacheParameter.Action.NOT_USE));
-    String name = "build-from-query-merge.json"; // The content of this file is generated by the js code.
+    String name = "json/build-from-query-merge.json"; // The content of this file is generated by the js code.
     QueryMergeDto qjs = JacksonUtil.deserialize(TestUtil.readAllLines(name), QueryMergeDto.class);
     Assertions.assertThat(qjs).isEqualTo(query);
   }
@@ -207,7 +219,7 @@ public class TestJavascriptLibrary {
             .build();
     q.minify = true;
 
-    String name = "build-from-query-pivot.json"; // The content of this file is generated by the js code.
+    String name = "json/build-from-query-pivot.json"; // The content of this file is generated by the js code.
     PivotTableQueryDto qjs = JacksonUtil.deserialize(TestUtil.readAllLines(name), PivotTableQueryDto.class);
     Assertions.assertThat(qjs).isEqualTo(new PivotTableQueryDto(q, tableFields(List.of("a")), tableFields(List.of("b"))));
   }
@@ -228,7 +240,7 @@ public class TestJavascriptLibrary {
     query.minify = false;
     PivotTableQueryMergeDto pivotTableQueryMergeDto = new PivotTableQueryMergeDto(query, tableFields(List.of("a")), tableFields(List.of("b")));
 
-    String name = "build-from-query-merge-pivot.json"; // The content of this file is generated by the js code.
+    String name = "json/build-from-query-merge-pivot.json"; // The content of this file is generated by the js code.
     PivotTableQueryMergeDto qjs = JacksonUtil.deserialize(TestUtil.readAllLines(name), PivotTableQueryMergeDto.class);
     Assertions.assertThat(qjs).isEqualTo(pivotTableQueryMergeDto);
   }
@@ -266,14 +278,14 @@ public class TestJavascriptLibrary {
     q.current = 0;
     q.minify = true;
 
-    String name = "build-from-query-join.json"; // The content of this file is generated by the js code.
+    String name = "json/build-from-query-join.json"; // The content of this file is generated by the js code.
     QueryJoinDto qjs = JacksonUtil.deserialize(TestUtil.readAllLines(name), QueryJoinDto.class);
     Assertions.assertThat(qjs).isEqualTo(q);
   }
 
   @Test
   void testQueryResult() {
-    String name = "build-from-query-result.json"; // The content of this file is generated by the js code.
+    String name = "json/build-from-query-result.json"; // The content of this file is generated by the js code.
     QueryResultDto expected = new QueryResultDto();
     expected.columns = List.of("key1", "key2");
     expected.cells = List.of(
@@ -290,7 +302,7 @@ public class TestJavascriptLibrary {
 
   @Test
   void testPivotQueryResult() {
-    String name = "build-from-pivot-query-result.json"; // The content of this file is generated by the js code.
+    String name = "json/build-from-pivot-query-result.json"; // The content of this file is generated by the js code.
     PivotTableQueryResultDto expected = new PivotTableQueryResultDto();
     expected.cells = List.of(
             Map.of("key1", 1, "key2", "sthg", "key3", true, "key4", 123.5),
diff --git a/server/src/test/resources/build-from-query-pivot.json b/server/src/test/resources/build-from-query-pivot.json
deleted file mode 100644
index 57eda13..0000000
--- a/server/src/test/resources/build-from-query-pivot.json
+++ /dev/null
@@ -1,1 +0,0 @@
-{"query":{"table":{"name":"myTable","joins":[]},"virtualTableDtos":[],"columns":[{"@class":"io.squashql.query.TableField","fullName":"a","fieldName":"a"},{"@class":"io.squashql.query.TableField","fullName":"b","fieldName":"b"}],"rollupColumns":[],"columnSets":{},"parameters":{},"measures":[{"@class":"io.squashql.query.AggregatedMeasure","field":{"@class":"io.squashql.query.TableField","fullName":"f1","fieldName":"f1"},"aggregationFunction":"avg","alias":"sum"}],"orders":{},"limit":-1,"minify":true},"rows":[{"@class":"io.squashql.query.TableField","fullName":"a","fieldName":"a"}],"columns":[{"@class":"io.squashql.query.TableField","fullName":"b","fieldName":"b"}]}
\ No newline at end of file